From 3e88fe35d70ae80b4aaf8521e04fd7fac2eaf3b7 Mon Sep 17 00:00:00 2001 From: Marco Caselli Date: Tue, 22 Jun 2021 08:18:56 +0200 Subject: [PATCH 001/428] new: update to handle network connection objects --- app/Lib/Export/NidsExport.php | 235 +++++++++++++++++++++++++--------- 1 file changed, 172 insertions(+), 63 deletions(-) diff --git a/app/Lib/Export/NidsExport.php b/app/Lib/Export/NidsExport.php index 2b691314d..dd881693f 100644 --- a/app/Lib/Export/NidsExport.php +++ b/app/Lib/Export/NidsExport.php @@ -16,11 +16,14 @@ class NidsExport 'fields' => array('threat_level_id') ) ), - 'flatten' => 1 + #'flatten' => 1 ); public function handler($data, $options = array()) { + + //NOTES: Here the scope "Object" should be probably checked + $continue = empty($format); $this->checkWhitelist = false; if ($options['scope'] === 'Attribute') { @@ -38,15 +41,16 @@ class NidsExport $this->__convertFromEventFormat($data['Attribute'], $data, $options, $continue); } if (!empty($data['Object'])) { - foreach ($data['Object'] as $object) { - $this->__convertFromEventFormat($object['Attribute'], $data, $options, $continue); - } + #foreach ($data['Object'] as $object) { + $this->__convertFromEventFormatObject($data['Object'], $data, $options, $continue); + #} } } return ''; } private function __convertFromEventFormat($attributes, $event, $options = array(), $continue = false) { + $rearranged = array(); foreach ($attributes as $attribute) { $attributeTag = array(); @@ -69,6 +73,44 @@ class NidsExport return true; } + + private function __convertFromEventFormatObject($objects, $event, $options = array(), $continue = false) { + + #CakeLog::debug("ConvertFromEventFormatObject"); + #CakeLog::debug(json_encode($event)); + + $rearranged = array(); + foreach ($objects as $object) { + + $objectTag = array(); + + foreach($object['Attribute'] as $attribute) { + + if (!empty($attribute['AttributeTag'])) { + $objectTag = array_merge($objectTag, $attribute['AttributeTag']); + unset($attribute['AttributeTag']); + } + + } + + $rearranged[] = array( + 'Attribute' => $object, //NOTES: Using 'Attribute' instead of 'Object' to comply with function export + 'AttributeTag' => $objectTag, //NOTES: Using 'AttributeTag' instead of 'ObjectTag' to comply with function export + 'Event' => $event['Event'] + ); + + } + + $this->export( + $rearranged, + $options['user']['nids_sid'], + $options['returnFormat'], + $continue + + ); + return true; + + } public function header($options = array()) { @@ -142,68 +184,135 @@ class NidsExport $sid = $startSid + ($item['Attribute']['id'] * 10); // leave 9 possible rules per attribute type $sid++; - switch ($item['Attribute']['type']) { - // LATER nids - test all the snort attributes - // LATER nids - add the tag keyword in the rules to capture network traffic - // LATER nids - sanitize every $attribute['value'] to not conflict with snort - case 'ip-dst': - $this->ipDstRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'ip-src': - $this->ipSrcRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'ip-dst|port': - $this->ipDstRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'ip-src|port': - $this->ipSrcRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'email': - $this->emailSrcRule($ruleFormat, $item['Attribute'], $sid); - $this->emailDstRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'email-src': - $this->emailSrcRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'email-dst': - $this->emailDstRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'email-subject': - $this->emailSubjectRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'email-attachment': - $this->emailAttachmentRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'domain': - $this->domainRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'domain|ip': - $this->domainIpRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'hostname': - $this->hostnameRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'url': - $this->urlRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'user-agent': - $this->userAgentRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'ja3-fingerprint-md5': - $this->ja3Rule($ruleFormat, $item['Attribute'], $sid); - break; - case 'ja3s-fingerprint-md5': // Atribute type doesn't exists yet (2020-12-10) but ready when created. - $this->ja3sRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'snort': - $this->snortRule($ruleFormat, $item['Attribute'], $sid, $ruleFormatMsg, $ruleFormatReference); - // no break - default: - break; - } + + if(!empty($item['Attribute']['type'])) { //NOTES: Item is an 'Attribute' + + switch ($item['Attribute']['type']) { + // LATER nids - test all the snort attributes + // LATER nids - add the tag keyword in the rules to capture network traffic + // LATER nids - sanitize every $attribute['value'] to not conflict with snort + case 'ip-dst': + $this->ipDstRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'ip-src': + $this->ipSrcRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'ip-dst|port': + $this->ipDstRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'ip-src|port': + $this->ipSrcRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'email': + $this->emailSrcRule($ruleFormat, $item['Attribute'], $sid); + $this->emailDstRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'email-src': + $this->emailSrcRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'email-dst': + $this->emailDstRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'email-subject': + $this->emailSubjectRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'email-attachment': + $this->emailAttachmentRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'domain': + $this->domainRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'domain|ip': + $this->domainIpRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'hostname': + $this->hostnameRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'url': + $this->urlRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'user-agent': + $this->userAgentRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'ja3-fingerprint-md5': + $this->ja3Rule($ruleFormat, $item['Attribute'], $sid); + break; + case 'ja3s-fingerprint-md5': // Atribute type doesn't exists yet (2020-12-10) but ready when created. + $this->ja3sRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'snort': + $this->snortRule($ruleFormat, $item['Attribute'], $sid, $ruleFormatMsg, $ruleFormatReference); + // no break + default: + break; + } + + } else if(!empty($item['Attribute']['name'])) { //NOTES: Item is an 'Object' + + switch ($item['Attribute']['name']) { + case 'network-connection': + $this->networkConnectionRule($ruleFormat, $item['Attribute'], $sid); + break; + default: + break; + } + + } + } return $this->rules; } + + public function networkConnectionRule($ruleFormat, $object, &$sid) + { + + $attributes = NidsExport::getObjectAttributes($object); + + if(!array_key_exists('layer4-protocol', $attributes)){ + $attributes['layer4-protocol'] = 'IP'; // If layer-4 protocol is unknown, we roll-back to layer-3 ('IP') + } + if(!array_key_exists('ip-src', $attributes)){ + $attributes['ip-src'] = '$HOME_NET'; // If ip-src is unknown, we roll-back to $HOME_NET + } + if(!array_key_exists('ip-dst', $attributes)){ + $attributes['ip-dst'] = '$HOME_NET'; // If ip-dst is unknown, we roll-back to $HOME_NET + } + if(!array_key_exists('src-port', $attributes)){ + $attributes['src-port'] = 'any'; // If src-port is unknown, we roll-back to 'any' + } + if(!array_key_exists('dst-port', $attributes)){ + $attributes['dst-port'] = 'any'; // If dst-port is unknown, we roll-back to 'any' + } + + $this->rules[] = sprintf( + $ruleFormat, + false, + $attributes['layer4-protocol'], // proto + $attributes['ip-src'], // src_ip + $attributes['src-port'], // src_port + '->', // direction + $attributes['ip-dst'], // dst_ip + $attributes['dst-port'], // dst_port + 'Network connection between ' . $attributes['ip-src'] . ' and ' . $attributes['ip-dst'], // msg + '', // rule_content + '', // tag + $sid, // sid + 1 // rev + ); + + } + + public static function getObjectAttributes($object) + { + + $attributes = array(); + + foreach ($object['Attribute'] as $attribute) { + $attributes[$attribute['object_relation']] = $attribute['value']; + } + + return $attributes; + } public function domainIpRule($ruleFormat, $attribute, &$sid) { From e71e46c11845f62c73f7cb0d79c2fdf77dae7522 Mon Sep 17 00:00:00 2001 From: Marco Caselli Date: Wed, 15 Sep 2021 12:34:26 +0200 Subject: [PATCH 002/428] fixes + ddos object handling --- app/Lib/Export/NidsExport.php | 80 +++++++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 13 deletions(-) diff --git a/app/Lib/Export/NidsExport.php b/app/Lib/Export/NidsExport.php index dd881693f..20dcff784 100644 --- a/app/Lib/Export/NidsExport.php +++ b/app/Lib/Export/NidsExport.php @@ -7,6 +7,8 @@ class NidsExport public $classtype = 'trojan-activity'; public $format = ""; // suricata (default), snort + + public $supportedObjects = array('network-connection', 'ddos'); public $checkWhitelist = true; @@ -82,22 +84,32 @@ class NidsExport $rearranged = array(); foreach ($objects as $object) { - $objectTag = array(); + #CakeLog::debug("Checking Object"); - foreach($object['Attribute'] as $attribute) { + if(in_array($object['name'], $this->supportedObjects)){ //NOTES: Checking if this is an object supported for the custom export - if (!empty($attribute['AttributeTag'])) { - $objectTag = array_merge($objectTag, $attribute['AttributeTag']); - unset($attribute['AttributeTag']); + $objectTag = array(); + + foreach($object['Attribute'] as $attribute) { + + if (!empty($attribute['AttributeTag'])) { + $objectTag = array_merge($objectTag, $attribute['AttributeTag']); + unset($attribute['AttributeTag']); + } + } - } + $rearranged[] = array( + 'Attribute' => $object, //NOTES: Using 'Attribute' instead of 'Object' to comply with function export + 'AttributeTag' => $objectTag, //NOTES: Using 'AttributeTag' instead of 'ObjectTag' to comply with function export + 'Event' => $event['Event'] + ); + + } else { //NOTES: In case the object is not supported for the custom export, the approach falls back to the attribute case - $rearranged[] = array( - 'Attribute' => $object, //NOTES: Using 'Attribute' instead of 'Object' to comply with function export - 'AttributeTag' => $objectTag, //NOTES: Using 'AttributeTag' instead of 'ObjectTag' to comply with function export - 'Event' => $event['Event'] - ); + $this->__convertFromEventFormat($object['Attribute'], $data, $options, $continue); + + } } @@ -253,6 +265,9 @@ class NidsExport case 'network-connection': $this->networkConnectionRule($ruleFormat, $item['Attribute'], $sid); break; + case 'ddos': + $this->ddosRule($ruleFormat, $item['Attribute'], $sid); + break; default: break; } @@ -269,7 +284,7 @@ class NidsExport $attributes = NidsExport::getObjectAttributes($object); if(!array_key_exists('layer4-protocol', $attributes)){ - $attributes['layer4-protocol'] = 'IP'; // If layer-4 protocol is unknown, we roll-back to layer-3 ('IP') + $attributes['layer4-protocol'] = 'ip'; // If layer-4 protocol is unknown, we roll-back to layer-3 ('ip') } if(!array_key_exists('ip-src', $attributes)){ $attributes['ip-src'] = '$HOME_NET'; // If ip-src is unknown, we roll-back to $HOME_NET @@ -302,6 +317,45 @@ class NidsExport } + public function ddosRule($ruleFormat, $object, &$sid) + { + + $attributes = NidsExport::getObjectAttributes($object); + + if(!array_key_exists('protocol', $attributes)){ + $attributes['protocol'] = 'ip'; // If protocol is unknown, we roll-back to 'ip' + } + if(!array_key_exists('ip-src', $attributes)){ + $attributes['ip-src'] = '$HOME_NET'; // If ip-src is unknown, we roll-back to $HOME_NET + } + if(!array_key_exists('ip-dst', $attributes)){ + $attributes['ip-dst'] = '$HOME_NET'; // If ip-dst is unknown, we roll-back to $HOME_NET + } + if(!array_key_exists('src-port', $attributes)){ + $attributes['src-port'] = 'any'; // If src-port is unknown, we roll-back to 'any' + } + if(!array_key_exists('dst-port', $attributes)){ + $attributes['dst-port'] = 'any'; // If dst-port is unknown, we roll-back to 'any' + } + + $this->rules[] = sprintf( + $ruleFormat, + false, + $attributes['protocol'], // proto + $attributes['ip-src'], // src_ip + $attributes['src-port'], // src_port + '->', // direction + $attributes['ip-dst'], // dst_ip + $attributes['dst-port'], // dst_port + 'DDOS attack detected between ' . $attributes['ip-src'] . ' and ' . $attributes['ip-dst'], // msg + '', // rule_content + '', // tag + $sid, // sid + 1 // rev + ); + + } + public static function getObjectAttributes($object) { @@ -826,4 +880,4 @@ class NidsExport } return $ipport; } -} +} \ No newline at end of file From a04694a5b4db552cb2a8e65ce88a3ec148f3aede Mon Sep 17 00:00:00 2001 From: Marco Caselli Date: Wed, 15 Sep 2021 12:59:19 +0200 Subject: [PATCH 003/428] Code polishing --- app/Lib/Export/NidsExport.php | 841 +++++++++++++++++----------------- 1 file changed, 414 insertions(+), 427 deletions(-) diff --git a/app/Lib/Export/NidsExport.php b/app/Lib/Export/NidsExport.php index 20dcff784..05d448109 100644 --- a/app/Lib/Export/NidsExport.php +++ b/app/Lib/Export/NidsExport.php @@ -10,135 +10,122 @@ class NidsExport public $supportedObjects = array('network-connection', 'ddos'); - public $checkWhitelist = true; + public $checkWhitelist = true; - public $additional_params = array( - 'contain' => array( - 'Event' => array( - 'fields' => array('threat_level_id') - ) - ), - #'flatten' => 1 - ); + public $additional_params = array( + 'contain' => array( + 'Event' => array( + 'fields' => array('threat_level_id') + ) + ), - public function handler($data, $options = array()) - { - - //NOTES: Here the scope "Object" should be probably checked - - $continue = empty($format); - $this->checkWhitelist = false; - if ($options['scope'] === 'Attribute') { - $this->export( - array($data), - $options['user']['nids_sid'], - $options['returnFormat'], - $continue - ); - } else if ($options['scope'] === 'Event') { - if (!empty($data['EventTag'])) { - $data['Event']['EventTag'] = $data['EventTag']; - } - if (!empty($data['Attribute'])) { - $this->__convertFromEventFormat($data['Attribute'], $data, $options, $continue); - } - if (!empty($data['Object'])) { - #foreach ($data['Object'] as $object) { - $this->__convertFromEventFormatObject($data['Object'], $data, $options, $continue); - #} - } - } - return ''; - } + ); - private function __convertFromEventFormat($attributes, $event, $options = array(), $continue = false) { - - $rearranged = array(); - foreach ($attributes as $attribute) { - $attributeTag = array(); - if (!empty($attribute['AttributeTag'])) { - $attributeTag = $attribute['AttributeTag']; - unset($attribute['AttributeTag']); - } - $rearranged[] = array( - 'Attribute' => $attribute, - 'AttributeTag' => $attributeTag, - 'Event' => $event['Event'] - ); - } - $this->export( - $rearranged, - $options['user']['nids_sid'], - $options['returnFormat'], - $continue - ); - return true; + public function handler($data, $options = array()) + { + $continue = empty($format); + $this->checkWhitelist = false; + if ($options['scope'] === 'Attribute') { + $this->export( + array($data), + $options['user']['nids_sid'], + $options['returnFormat'], + $continue + ); + } else if ($options['scope'] === 'Event') { + if (!empty($data['EventTag'])) { + $data['Event']['EventTag'] = $data['EventTag']; + } + if (!empty($data['Attribute'])) { + $this->__convertFromEventFormat($data['Attribute'], $data, $options, $continue); + } + if (!empty($data['Object'])) { + $this->__convertFromEventFormatObject($data['Object'], $data, $options, $continue); + } + } + return ''; + } - } - - private function __convertFromEventFormatObject($objects, $event, $options = array(), $continue = false) { - - #CakeLog::debug("ConvertFromEventFormatObject"); - #CakeLog::debug(json_encode($event)); - - $rearranged = array(); - foreach ($objects as $object) { - - #CakeLog::debug("Checking Object"); - - if(in_array($object['name'], $this->supportedObjects)){ //NOTES: Checking if this is an object supported for the custom export - - $objectTag = array(); - - foreach($object['Attribute'] as $attribute) { - - if (!empty($attribute['AttributeTag'])) { - $objectTag = array_merge($objectTag, $attribute['AttributeTag']); - unset($attribute['AttributeTag']); - } - - } - - $rearranged[] = array( - 'Attribute' => $object, //NOTES: Using 'Attribute' instead of 'Object' to comply with function export - 'AttributeTag' => $objectTag, //NOTES: Using 'AttributeTag' instead of 'ObjectTag' to comply with function export - 'Event' => $event['Event'] - ); - - } else { //NOTES: In case the object is not supported for the custom export, the approach falls back to the attribute case - - $this->__convertFromEventFormat($object['Attribute'], $data, $options, $continue); - - } - - } - - $this->export( - $rearranged, - $options['user']['nids_sid'], - $options['returnFormat'], - $continue - - ); - return true; + private function __convertFromEventFormat($attributes, $event, $options = array(), $continue = false) { - } + $rearranged = array(); + foreach ($attributes as $attribute) { + $attributeTag = array(); + if (!empty($attribute['AttributeTag'])) { + $attributeTag = $attribute['AttributeTag']; + unset($attribute['AttributeTag']); + } + $rearranged[] = array( + 'Attribute' => $attribute, + 'AttributeTag' => $attributeTag, + 'Event' => $event['Event'] + ); + } + $this->export( + $rearranged, + $options['user']['nids_sid'], + $options['returnFormat'], + $continue + ); + return true; - public function header($options = array()) - { - $this->explain(); - return ''; - } + } - public function footer() - { - return implode ("\n", $this->rules); - } + private function __convertFromEventFormatObject($objects, $event, $options = array(), $continue = false) { - public function separator() - { - return ''; - } + $rearranged = array(); + foreach ($objects as $object) { + + if(in_array($object['name'], $this->supportedObjects)){ + + $objectTag = array(); + + foreach($object['Attribute'] as $attribute) { + + if (!empty($attribute['AttributeTag'])) { + $objectTag = array_merge($objectTag, $attribute['AttributeTag']); + unset($attribute['AttributeTag']); + } + + } + + $rearranged[] = array( + 'Attribute' => $object, // Using 'Attribute' instead of 'Object' to comply with function export + 'AttributeTag' => $objectTag, // Using 'AttributeTag' instead of 'ObjectTag' to comply with function export + 'Event' => $event['Event'] + ); + + } else { // In case no custom export exists for the object, the approach falls back to the attribute case + $this->__convertFromEventFormat($object['Attribute'], $data, $options, $continue); + } + + } + + $this->export( + $rearranged, + $options['user']['nids_sid'], + $options['returnFormat'], + $continue + ); + return true; + + } + + public function header($options = array()) + { + $this->explain(); + return ''; + } + + public function footer() + { + return implode ("\n", $this->rules); + } + + public function separator() + { + return ''; + } public function explain() { @@ -147,7 +134,7 @@ class NidsExport $this->rules[] = '# These NIDS rules contain some variables that need to exist in your configuration.'; $this->rules[] = '# Make sure you have set:'; $this->rules[] = '#'; - $this->rules[] = '# $HOME_NET - Your internal network range'; + $this->rules[] = '# $HOME_NET - Your internal network range'; $this->rules[] = '# $EXTERNAL_NET - The network considered as outside'; $this->rules[] = '# $SMTP_SERVERS - All your internal SMTP servers'; $this->rules[] = '# $HTTP_PORTS - The ports used to contain HTTP traffic (not required with suricata export)'; @@ -160,10 +147,10 @@ class NidsExport public function export($items, $startSid, $format="suricata", $continue = false) { $this->format = $format; - if ($this->checkWhitelist && !isset($this->Whitelist)) { - $this->Whitelist = ClassRegistry::init('Whitelist'); - $this->whitelist = $this->Whitelist->getBlockedValues(); - } + if ($this->checkWhitelist && !isset($this->Whitelist)) { + $this->Whitelist = ClassRegistry::init('Whitelist'); + $this->whitelist = $this->Whitelist->getBlockedValues(); + } // output a short explanation if (!$continue) { @@ -173,20 +160,20 @@ class NidsExport foreach ($items as $item) { // retrieve all tags for this item to add them to the msg $tagsArray = []; - if (!empty($item['AttributeTag'])) { - foreach ($item['AttributeTag'] as $tag_attr) { - if (array_key_exists('name', $tag_attr['Tag'])) { - array_push($tagsArray, $tag_attr['Tag']['name']); - } - } - } - if (!empty($item['Event']['EventTag'])) { - foreach ($item['Event']['EventTag'] as $tag_event) { - if (array_key_exists('name', $tag_event['Tag'])) { - array_push($tagsArray, $tag_event['Tag']['name']); - } - } - } + if (!empty($item['AttributeTag'])) { + foreach ($item['AttributeTag'] as $tag_attr) { + if (array_key_exists('name', $tag_attr['Tag'])) { + array_push($tagsArray, $tag_attr['Tag']['name']); + } + } + } + if (!empty($item['Event']['EventTag'])) { + foreach ($item['Event']['EventTag'] as $tag_event) { + if (array_key_exists('name', $tag_event['Tag'])) { + array_push($tagsArray, $tag_event['Tag']['name']); + } + } + } $ruleFormatMsgTags = implode(",", $tagsArray); # proto src_ip src_port direction dst_ip dst_port msg rule_content tag sid rev @@ -197,122 +184,122 @@ class NidsExport $sid = $startSid + ($item['Attribute']['id'] * 10); // leave 9 possible rules per attribute type $sid++; - if(!empty($item['Attribute']['type'])) { //NOTES: Item is an 'Attribute' + if(!empty($item['Attribute']['type'])) { // item is an 'Attribute' - switch ($item['Attribute']['type']) { - // LATER nids - test all the snort attributes - // LATER nids - add the tag keyword in the rules to capture network traffic - // LATER nids - sanitize every $attribute['value'] to not conflict with snort - case 'ip-dst': - $this->ipDstRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'ip-src': - $this->ipSrcRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'ip-dst|port': - $this->ipDstRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'ip-src|port': - $this->ipSrcRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'email': - $this->emailSrcRule($ruleFormat, $item['Attribute'], $sid); - $this->emailDstRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'email-src': - $this->emailSrcRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'email-dst': - $this->emailDstRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'email-subject': - $this->emailSubjectRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'email-attachment': - $this->emailAttachmentRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'domain': - $this->domainRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'domain|ip': - $this->domainIpRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'hostname': - $this->hostnameRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'url': - $this->urlRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'user-agent': - $this->userAgentRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'ja3-fingerprint-md5': - $this->ja3Rule($ruleFormat, $item['Attribute'], $sid); - break; - case 'ja3s-fingerprint-md5': // Atribute type doesn't exists yet (2020-12-10) but ready when created. - $this->ja3sRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'snort': - $this->snortRule($ruleFormat, $item['Attribute'], $sid, $ruleFormatMsg, $ruleFormatReference); - // no break - default: - break; - } - - } else if(!empty($item['Attribute']['name'])) { //NOTES: Item is an 'Object' - - switch ($item['Attribute']['name']) { - case 'network-connection': - $this->networkConnectionRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'ddos': - $this->ddosRule($ruleFormat, $item['Attribute'], $sid); - break; - default: - break; - } - - } - + switch ($item['Attribute']['type']) { + // LATER nids - test all the snort attributes + // LATER nids - add the tag keyword in the rules to capture network traffic + // LATER nids - sanitize every $attribute['value'] to not conflict with snort + case 'ip-dst': + $this->ipDstRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'ip-src': + $this->ipSrcRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'ip-dst|port': + $this->ipDstRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'ip-src|port': + $this->ipSrcRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'email': + $this->emailSrcRule($ruleFormat, $item['Attribute'], $sid); + $this->emailDstRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'email-src': + $this->emailSrcRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'email-dst': + $this->emailDstRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'email-subject': + $this->emailSubjectRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'email-attachment': + $this->emailAttachmentRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'domain': + $this->domainRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'domain|ip': + $this->domainIpRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'hostname': + $this->hostnameRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'url': + $this->urlRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'user-agent': + $this->userAgentRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'ja3-fingerprint-md5': + $this->ja3Rule($ruleFormat, $item['Attribute'], $sid); + break; + case 'ja3s-fingerprint-md5': // Atribute type doesn't exists yet (2020-12-10) but ready when created. + $this->ja3sRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'snort': + $this->snortRule($ruleFormat, $item['Attribute'], $sid, $ruleFormatMsg, $ruleFormatReference); + // no break + default: + break; + } + + } else if(!empty($item['Attribute']['name'])) { // Item is an 'Object' + + switch ($item['Attribute']['name']) { + case 'network-connection': + $this->networkConnectionRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'ddos': + $this->ddosRule($ruleFormat, $item['Attribute'], $sid); + break; + default: + break; + } + + } + } return $this->rules; } - + public function networkConnectionRule($ruleFormat, $object, &$sid) { - $attributes = NidsExport::getObjectAttributes($object); + $attributes = NidsExport::getObjectAttributes($object); if(!array_key_exists('layer4-protocol', $attributes)){ - $attributes['layer4-protocol'] = 'ip'; // If layer-4 protocol is unknown, we roll-back to layer-3 ('ip') + $attributes['layer4-protocol'] = 'ip'; // If layer-4 protocol is unknown, we roll-back to layer-3 ('ip') } if(!array_key_exists('ip-src', $attributes)){ - $attributes['ip-src'] = '$HOME_NET'; // If ip-src is unknown, we roll-back to $HOME_NET + $attributes['ip-src'] = '$HOME_NET'; // If ip-src is unknown, we roll-back to $HOME_NET } if(!array_key_exists('ip-dst', $attributes)){ - $attributes['ip-dst'] = '$HOME_NET'; // If ip-dst is unknown, we roll-back to $HOME_NET + $attributes['ip-dst'] = '$HOME_NET'; // If ip-dst is unknown, we roll-back to $HOME_NET } if(!array_key_exists('src-port', $attributes)){ - $attributes['src-port'] = 'any'; // If src-port is unknown, we roll-back to 'any' + $attributes['src-port'] = 'any'; // If src-port is unknown, we roll-back to 'any' } if(!array_key_exists('dst-port', $attributes)){ - $attributes['dst-port'] = 'any'; // If dst-port is unknown, we roll-back to 'any' + $attributes['dst-port'] = 'any'; // If dst-port is unknown, we roll-back to 'any' } - $this->rules[] = sprintf( + $this->rules[] = sprintf( $ruleFormat, false, - $attributes['layer4-protocol'], // proto - $attributes['ip-src'], // src_ip - $attributes['src-port'], // src_port - '->', // direction - $attributes['ip-dst'], // dst_ip - $attributes['dst-port'], // dst_port - 'Network connection between ' . $attributes['ip-src'] . ' and ' . $attributes['ip-dst'], // msg - '', // rule_content - '', // tag - $sid, // sid - 1 // rev + $attributes['layer4-protocol'], // proto + $attributes['ip-src'], // src_ip + $attributes['src-port'], // src_port + '->', // direction + $attributes['ip-dst'], // dst_ip + $attributes['dst-port'], // dst_port + 'Network connection between ' . $attributes['ip-src'] . ' and ' . $attributes['ip-dst'], // msg + '', // rule_content + '', // tag + $sid, // sid + 1 // rev ); } @@ -320,49 +307,49 @@ class NidsExport public function ddosRule($ruleFormat, $object, &$sid) { - $attributes = NidsExport::getObjectAttributes($object); + $attributes = NidsExport::getObjectAttributes($object); if(!array_key_exists('protocol', $attributes)){ - $attributes['protocol'] = 'ip'; // If protocol is unknown, we roll-back to 'ip' + $attributes['protocol'] = 'ip'; // If protocol is unknown, we roll-back to 'ip' } if(!array_key_exists('ip-src', $attributes)){ - $attributes['ip-src'] = '$HOME_NET'; // If ip-src is unknown, we roll-back to $HOME_NET + $attributes['ip-src'] = '$HOME_NET'; // If ip-src is unknown, we roll-back to $HOME_NET } if(!array_key_exists('ip-dst', $attributes)){ - $attributes['ip-dst'] = '$HOME_NET'; // If ip-dst is unknown, we roll-back to $HOME_NET + $attributes['ip-dst'] = '$HOME_NET'; // If ip-dst is unknown, we roll-back to $HOME_NET } if(!array_key_exists('src-port', $attributes)){ - $attributes['src-port'] = 'any'; // If src-port is unknown, we roll-back to 'any' + $attributes['src-port'] = 'any'; // If src-port is unknown, we roll-back to 'any' } if(!array_key_exists('dst-port', $attributes)){ - $attributes['dst-port'] = 'any'; // If dst-port is unknown, we roll-back to 'any' + $attributes['dst-port'] = 'any'; // If dst-port is unknown, we roll-back to 'any' } - $this->rules[] = sprintf( + $this->rules[] = sprintf( $ruleFormat, false, - $attributes['protocol'], // proto - $attributes['ip-src'], // src_ip - $attributes['src-port'], // src_port - '->', // direction - $attributes['ip-dst'], // dst_ip - $attributes['dst-port'], // dst_port - 'DDOS attack detected between ' . $attributes['ip-src'] . ' and ' . $attributes['ip-dst'], // msg - '', // rule_content - '', // tag - $sid, // sid - 1 // rev + $attributes['protocol'], // proto + $attributes['ip-src'], // src_ip + $attributes['src-port'], // src_port + '->', // direction + $attributes['ip-dst'], // dst_ip + $attributes['dst-port'], // dst_port + 'DDOS attack detected between ' . $attributes['ip-src'] . ' and ' . $attributes['ip-dst'], // msg + '', // rule_content + '', // tag + $sid, // sid + 1 // rev ); } public static function getObjectAttributes($object) { - - $attributes = array(); + + $attributes = array(); foreach ($object['Attribute'] as $attribute) { - $attributes[$attribute['object_relation']] = $attribute['value']; + $attributes[$attribute['object_relation']] = $attribute['value']; } return $attributes; @@ -388,17 +375,17 @@ class NidsExport $this->rules[] = sprintf( $ruleFormat, ($overruled) ? '#OVERRULED BY WHITELIST# ' : '', - 'ip', // proto - '$HOME_NET', // src_ip - 'any', // src_port - '->', // direction - $ipport[0], // dst_ip - $ipport[1], // dst_port - 'Outgoing To IP: ' . $attribute['value'], // msg - '', // rule_content - '', // tag - $sid, // sid - 1 // rev + 'ip', // proto + '$HOME_NET', // src_ip + 'any', // src_port + '->', // direction + $ipport[0], // dst_ip + $ipport[1], // dst_port + 'Outgoing To IP: ' . $attribute['value'], // msg + '', // rule_content + '', // tag + $sid, // sid + 1 // rev ); } @@ -409,17 +396,17 @@ class NidsExport $this->rules[] = sprintf( $ruleFormat, ($overruled) ? '#OVERRULED BY WHITELIST# ' : '', - 'ip', // proto - $ipport[0], // src_ip - $ipport[1], // src_port - '->', // direction - '$HOME_NET', // dst_ip - 'any', // dst_port - 'Incoming From IP: ' . $attribute['value'], // msg - '', // rule_content - '', // tag - $sid, // sid - 1 // rev + 'ip', // proto + $ipport[0], // src_ip + $ipport[1], // src_port + '->', // direction + '$HOME_NET', // dst_ip + 'any', // dst_port + 'Incoming From IP: ' . $attribute['value'], // msg + '', // rule_content + '', // tag + $sid, // sid + 1 // rev ); } @@ -431,17 +418,17 @@ class NidsExport $this->rules[] = sprintf( $ruleFormat, ($overruled) ? '#OVERRULED BY WHITELIST# ' : '', - 'tcp', // proto - '$EXTERNAL_NET', // src_ip - 'any', // src_port - '->', // direction - '$SMTP_SERVERS', // dst_ip - '25', // dst_port - 'Source Email Address: ' . $attribute['value'], // msg - $content, // rule_content - 'tag:session,600,seconds;', // tag - $sid, // sid - 1 // rev + 'tcp', // proto + '$EXTERNAL_NET', // src_ip + 'any', // src_port + '->', // direction + '$SMTP_SERVERS', // dst_ip + '25', // dst_port + 'Source Email Address: ' . $attribute['value'], // msg + $content, // rule_content + 'tag:session,600,seconds;', // tag + $sid, // sid + 1 // rev ); } @@ -453,17 +440,17 @@ class NidsExport $this->rules[] = sprintf( $ruleFormat, ($overruled) ? '#OVERRULED BY WHITELIST# ' : '', - 'tcp', // proto - '$EXTERNAL_NET', // src_ip - 'any', // src_port - '->', // direction - '$SMTP_SERVERS', // dst_ip - '25', // dst_port - 'Destination Email Address: ' . $attribute['value'], // msg - $content, // rule_content - 'tag:session,600,seconds;', // tag - $sid, // sid - 1 // rev + 'tcp', // proto + '$EXTERNAL_NET', // src_ip + 'any', // src_port + '->', // direction + '$SMTP_SERVERS', // dst_ip + '25', // dst_port + 'Destination Email Address: ' . $attribute['value'], // msg + $content, // rule_content + 'tag:session,600,seconds;', // tag + $sid, // sid + 1 // rev ); } @@ -476,17 +463,17 @@ class NidsExport $this->rules[] = sprintf( $ruleFormat, ($overruled) ? '#OVERRULED BY WHITELIST# ' : '', - 'tcp', // proto - '$EXTERNAL_NET', // src_ip - 'any', // src_port - '->', // direction - '$SMTP_SERVERS', // dst_ip - '25', // dst_port - 'Bad Email Subject', // msg - $content, // rule_content - 'tag:session,600,seconds;', // tag - $sid, // sid - 1 // rev + 'tcp', // proto + '$EXTERNAL_NET', // src_ip + 'any', // src_port + '->', // direction + '$SMTP_SERVERS', // dst_ip + '25', // dst_port + 'Bad Email Subject', // msg + $content, // rule_content + 'tag:session,600,seconds;', // tag + $sid, // sid + 1 // rev ); } @@ -499,17 +486,17 @@ class NidsExport $this->rules[] = sprintf( $ruleFormat, ($overruled) ? '#OVERRULED BY WHITELIST# ' : '', - 'tcp', // proto - '$EXTERNAL_NET', // src_ip - 'any', // src_port - '->', // direction - '$SMTP_SERVERS', // dst_ip - '25', // dst_port - 'Bad Email Attachment', // msg - $content, // rule_content // LATER nids - test and finetune this snort rule https://secure.wikimedia.org/wikipedia/en/wiki/MIME#Content-Disposition - 'tag:session,600,seconds;', // tag - $sid, // sid - 1 // rev + 'tcp', // proto + '$EXTERNAL_NET', // src_ip + 'any', // src_port + '->', // direction + '$SMTP_SERVERS', // dst_ip + '25', // dst_port + 'Bad Email Attachment', // msg + $content, // rule_content // LATER nids - test and finetune this snort rule https://secure.wikimedia.org/wikipedia/en/wiki/MIME#Content-Disposition + 'tag:session,600,seconds;', // tag + $sid, // sid + 1 // rev ); } @@ -521,33 +508,33 @@ class NidsExport $this->rules[] = sprintf( $ruleFormat, ($overruled) ? '#OVERRULED BY WHITELIST# ' : '', - 'udp', // proto - 'any', // src_ip - 'any', // src_port - '->', // direction - 'any', // dst_ip - '53', // dst_port - 'Hostname: ' . $attribute['value'], // msg - $content, // rule_content - '', // tag - $sid, // sid - 1 // rev + 'udp', // proto + 'any', // src_ip + 'any', // src_port + '->', // direction + 'any', // dst_ip + '53', // dst_port + 'Hostname: ' . $attribute['value'], // msg + $content, // rule_content + '', // tag + $sid, // sid + 1 // rev ); $sid++; $this->rules[] = sprintf( $ruleFormat, ($overruled) ? '#OVERRULED BY WHITELIST# ' : '', - 'tcp', // proto - 'any', // src_ip - 'any', // src_port - '->', // direction - 'any', // dst_ip - '53', // dst_port - 'Hostname: ' . $attribute['value'], // msg - $content. ' flow:established;', // rule_content - '', // tag - $sid, // sid - 1 // rev + 'tcp', // proto + 'any', // src_ip + 'any', // src_port + '->', // direction + 'any', // dst_ip + '53', // dst_port + 'Hostname: ' . $attribute['value'], // msg + $content. ' flow:established;', // rule_content + '', // tag + $sid, // sid + 1 // rev ); $sid++; // also do http requests @@ -555,17 +542,17 @@ class NidsExport $this->rules[] = sprintf( $ruleFormat, ($overruled) ? '#OVERRULED BY WHITELIST# ' : '', - 'tcp', // proto - '$HOME_NET', // src_ip - 'any', // src_port - '->', // direction - '$EXTERNAL_NET', // dst_ip - '$HTTP_PORTS', // dst_port - 'Outgoing HTTP Hostname: ' . $attribute['value'], // msg - $content, // rule_content - 'tag:session,600,seconds;', // tag - $sid, // sid - 1 // rev + 'tcp', // proto + '$HOME_NET', // src_ip + 'any', // src_port + '->', // direction + '$EXTERNAL_NET', // dst_ip + '$HTTP_PORTS', // dst_port + 'Outgoing HTTP Hostname: ' . $attribute['value'], // msg + $content, // rule_content + 'tag:session,600,seconds;', // tag + $sid, // sid + 1 // rev ); } @@ -577,33 +564,33 @@ class NidsExport $this->rules[] = sprintf( $ruleFormat, ($overruled) ? '#OVERRULED BY WHITELIST# ' : '', - 'udp', // proto - 'any', // src_ip - 'any', // src_port - '->', // direction - 'any', // dst_ip - '53', // dst_port - 'Domain: ' . $attribute['value'], // msg - $content, // rule_content - '', // tag - $sid, // sid - 1 // rev + 'udp', // proto + 'any', // src_ip + 'any', // src_port + '->', // direction + 'any', // dst_ip + '53', // dst_port + 'Domain: ' . $attribute['value'], // msg + $content, // rule_content + '', // tag + $sid, // sid + 1 // rev ); $sid++; $this->rules[] = sprintf( $ruleFormat, ($overruled) ? '#OVERRULED BY WHITELIST# ' : '', - 'tcp', // proto - 'any', // src_ip - 'any', // src_port - '->', // direction - 'any', // dst_ip - '53', // dst_port - 'Domain: ' . $attribute['value'], // msg - $content. ' flow:established;', // rule_content - '', // tag - $sid, // sid - 1 // rev + 'tcp', // proto + 'any', // src_ip + 'any', // src_port + '->', // direction + 'any', // dst_ip + '53', // dst_port + 'Domain: ' . $attribute['value'], // msg + $content. ' flow:established;', // rule_content + '', // tag + $sid, // sid + 1 // rev ); $sid++; // also do http requests, @@ -611,17 +598,17 @@ class NidsExport $this->rules[] = sprintf( $ruleFormat, ($overruled) ? '#OVERRULED BY WHITELIST# ' : '', - 'tcp', // proto - '$HOME_NET', // src_ip - 'any', // src_port - '->', // direction - '$EXTERNAL_NET', // dst_ip - '$HTTP_PORTS', // dst_port - 'Outgoing HTTP Domain: ' . $attribute['value'], // msg - $content, // rule_content - 'tag:session,600,seconds;', // tag - $sid, // sid - 1 // rev + 'tcp', // proto + '$HOME_NET', // src_ip + 'any', // src_port + '->', // direction + '$EXTERNAL_NET', // dst_ip + '$HTTP_PORTS', // dst_port + 'Outgoing HTTP Domain: ' . $attribute['value'], // msg + $content, // rule_content + 'tag:session,600,seconds;', // tag + $sid, // sid + 1 // rev ); } @@ -636,17 +623,17 @@ class NidsExport $this->rules[] = sprintf( $ruleFormat, ($overruled) ? '#OVERRULED BY WHITELIST# ' : '', - 'tcp', // proto - '$HOME_NET', // src_ip - 'any', // src_port - '->', // direction - '$EXTERNAL_NET', // dst_ip - '$HTTP_PORTS', // dst_port - 'Outgoing HTTP URL: ' . $attribute['value'], // msg - $content, // rule_content - 'tag:session,600,seconds;', // tag - $sid, // sid - 1 // rev + 'tcp', // proto + '$HOME_NET', // src_ip + 'any', // src_port + '->', // direction + '$EXTERNAL_NET', // dst_ip + '$HTTP_PORTS', // dst_port + 'Outgoing HTTP URL: ' . $attribute['value'], // msg + $content, // rule_content + 'tag:session,600,seconds;', // tag + $sid, // sid + 1 // rev ); } @@ -658,17 +645,17 @@ class NidsExport $this->rules[] = sprintf( $ruleFormat, ($overruled) ? '#OVERRULED BY WHITELIST# ' : '', - 'tcp', // proto - '$HOME_NET', // src_ip - 'any', // src_port - '->', // direction - '$EXTERNAL_NET', // dst_ip - '$HTTP_PORTS', // dst_port - 'Outgoing User-Agent: ' . $attribute['value'], // msg - $content, // rule_content - 'tag:session,600,seconds;', // tag - $sid, // sid - 1 // rev + 'tcp', // proto + '$HOME_NET', // src_ip + 'any', // src_port + '->', // direction + '$EXTERNAL_NET', // dst_ip + '$HTTP_PORTS', // dst_port + 'Outgoing User-Agent: ' . $attribute['value'], // msg + $content, // rule_content + 'tag:session,600,seconds;', // tag + $sid, // sid + 1 // rev ); } @@ -690,37 +677,37 @@ class NidsExport $tmpRule = str_replace(array("\r","\n"), " ", $attribute['value']); // rebuild the rule by overwriting the different keywords using preg_replace() - // sid - '/sid\s*:\s*[0-9]+\s*;/' - // rev - '/rev\s*:\s*[0-9]+\s*;/' + // sid - '/sid\s*:\s*[0-9]+\s*;/' + // rev - '/rev\s*:\s*[0-9]+\s*;/' // classtype - '/classtype:[a-zA-Z_-]+;/' - // msg - '/msg\s*:\s*".*?"\s*;/' + // msg - '/msg\s*:\s*".*?"\s*;/' // reference - '/reference\s*:\s*.+?;/' - // tag - '/tag\s*:\s*.+?;/' + // tag - '/tag\s*:\s*.+?;/' $replaceCount = array(); $tmpRule = preg_replace('/sid\s*:\s*[0-9]+\s*;/', 'sid:' . $sid . ';', $tmpRule, -1, $replaceCount['sid']); if (null == $tmpRule) { return false; - } // don't output the rule on error with the regex + } // don't output the rule on error with the regex $tmpRule = preg_replace('/rev\s*:\s*[0-9]+\s*;/', 'rev:1;', $tmpRule, -1, $replaceCount['rev']); if (null == $tmpRule) { return false; - } // don't output the rule on error with the regex + } // don't output the rule on error with the regex $tmpRule = preg_replace('/classtype:[a-zA-Z_-]+;/', 'classtype:' . $this->classtype . ';', $tmpRule, -1, $replaceCount['classtype']); if (null == $tmpRule) { return false; - } // don't output the rule on error with the regex + } // don't output the rule on error with the regex $tmpRule = preg_replace('/msg\s*:\s*"(.*?)"\s*;/', sprintf($ruleFormatMsg, 'snort-rule | $1') . ';', $tmpRule, -1, $replaceCount['msg']); if (null == $tmpRule) { return false; - } // don't output the rule on error with the regex + } // don't output the rule on error with the regex $tmpRule = preg_replace('/reference\s*:\s*.+?;/', $ruleFormatReference . ';', $tmpRule, -1, $replaceCount['reference']); if (null == $tmpRule) { return false; - } // don't output the rule on error with the regex + } // don't output the rule on error with the regex $tmpRule = preg_replace('/reference\s*:\s*.+?;/', $ruleFormatReference . ';', $tmpRule, -1, $replaceCount['reference']); if (null == $tmpRule) { return false; - } // don't output the rule on error with the regex + } // don't output the rule on error with the regex // FIXME nids - implement priority overwriting // some values were not replaced, so we need to add them ourselves, and insert them in the rule @@ -830,13 +817,13 @@ class NidsExport public function checkWhitelist($value) { - if ($this->checkWhitelist && is_array($this->whitelist)) { - foreach ($this->whitelist as $wlitem) { - if (preg_match($wlitem, $value)) { - return true; - } - } - } + if ($this->checkWhitelist && is_array($this->whitelist)) { + foreach ($this->whitelist as $wlitem) { + if (preg_match($wlitem, $value)) { + return true; + } + } + } return false; } From 0180da6b576f81d70db90aa9083e7d07b3e1319b Mon Sep 17 00:00:00 2001 From: Marco Caselli Date: Fri, 1 Oct 2021 11:17:02 +0200 Subject: [PATCH 004/428] Fixing mistake ("data" -> "event") --- app/Lib/Export/NidsExport.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Lib/Export/NidsExport.php b/app/Lib/Export/NidsExport.php index 05d448109..c8cabe89b 100644 --- a/app/Lib/Export/NidsExport.php +++ b/app/Lib/Export/NidsExport.php @@ -96,7 +96,7 @@ class NidsExport ); } else { // In case no custom export exists for the object, the approach falls back to the attribute case - $this->__convertFromEventFormat($object['Attribute'], $data, $options, $continue); + $this->__convertFromEventFormat($object['Attribute'], $event, $options, $continue); } } From 653fe1c9016bd89971140ec04376c1ca26fa21b4 Mon Sep 17 00:00:00 2001 From: Marco Caselli Date: Fri, 1 Oct 2021 11:21:49 +0200 Subject: [PATCH 005/428] Fixed indentation --- app/Lib/Export/NidsExport.php | 147 +++++++++++++++++----------------- 1 file changed, 74 insertions(+), 73 deletions(-) diff --git a/app/Lib/Export/NidsExport.php b/app/Lib/Export/NidsExport.php index c8cabe89b..a381556ab 100644 --- a/app/Lib/Export/NidsExport.php +++ b/app/Lib/Export/NidsExport.php @@ -186,82 +186,83 @@ class NidsExport if(!empty($item['Attribute']['type'])) { // item is an 'Attribute' - switch ($item['Attribute']['type']) { - // LATER nids - test all the snort attributes - // LATER nids - add the tag keyword in the rules to capture network traffic - // LATER nids - sanitize every $attribute['value'] to not conflict with snort - case 'ip-dst': - $this->ipDstRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'ip-src': - $this->ipSrcRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'ip-dst|port': - $this->ipDstRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'ip-src|port': - $this->ipSrcRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'email': - $this->emailSrcRule($ruleFormat, $item['Attribute'], $sid); - $this->emailDstRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'email-src': - $this->emailSrcRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'email-dst': - $this->emailDstRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'email-subject': - $this->emailSubjectRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'email-attachment': - $this->emailAttachmentRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'domain': - $this->domainRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'domain|ip': - $this->domainIpRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'hostname': - $this->hostnameRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'url': - $this->urlRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'user-agent': - $this->userAgentRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'ja3-fingerprint-md5': - $this->ja3Rule($ruleFormat, $item['Attribute'], $sid); - break; - case 'ja3s-fingerprint-md5': // Atribute type doesn't exists yet (2020-12-10) but ready when created. - $this->ja3sRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'snort': - $this->snortRule($ruleFormat, $item['Attribute'], $sid, $ruleFormatMsg, $ruleFormatReference); - // no break - default: - break; + switch ($item['Attribute']['type']) { + // LATER nids - test all the snort attributes + // LATER nids - add the tag keyword in the rules to capture network traffic + // LATER nids - sanitize every $attribute['value'] to not conflict with snort + case 'ip-dst': + $this->ipDstRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'ip-src': + $this->ipSrcRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'ip-dst|port': + $this->ipDstRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'ip-src|port': + $this->ipSrcRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'email': + $this->emailSrcRule($ruleFormat, $item['Attribute'], $sid); + $this->emailDstRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'email-src': + $this->emailSrcRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'email-dst': + $this->emailDstRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'email-subject': + $this->emailSubjectRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'email-attachment': + $this->emailAttachmentRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'domain': + $this->domainRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'domain|ip': + $this->domainIpRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'hostname': + $this->hostnameRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'url': + $this->urlRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'user-agent': + $this->userAgentRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'ja3-fingerprint-md5': + $this->ja3Rule($ruleFormat, $item['Attribute'], $sid); + break; + case 'ja3s-fingerprint-md5': // Atribute type doesn't exists yet (2020-12-10) but ready when created. + $this->ja3sRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'snort': + $this->snortRule($ruleFormat, $item['Attribute'], $sid, $ruleFormatMsg, $ruleFormatReference); + // no break + default: + break; + } + + } else if(!empty($item['Attribute']['name'])) { // Item is an 'Object' + + switch ($item['Attribute']['name']) { + case 'network-connection': + $this->networkConnectionRule($ruleFormat, $item['Attribute'], $sid); + break; + case 'ddos': + $this->ddosRule($ruleFormat, $item['Attribute'], $sid); + break; + default: + break; + } + } - } else if(!empty($item['Attribute']['name'])) { // Item is an 'Object' - - switch ($item['Attribute']['name']) { - case 'network-connection': - $this->networkConnectionRule($ruleFormat, $item['Attribute'], $sid); - break; - case 'ddos': - $this->ddosRule($ruleFormat, $item['Attribute'], $sid); - break; - default: - break; - } - - } - } + return $this->rules; } From 5698b91e285c2bc6a650df7588435c499a153551 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Tue, 3 May 2022 22:57:59 +0200 Subject: [PATCH 006/428] chg: [ui:main] Fixed overflowing UI creating a useless X scrollbar --- app/webroot/css/main.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/webroot/css/main.css b/app/webroot/css/main.css index ef074b902..0aecaa7f6 100644 --- a/app/webroot/css/main.css +++ b/app/webroot/css/main.css @@ -640,7 +640,7 @@ dd { .footerCenterText { padding-top:12px; position: absolute; - width:100%; + width: calc(100% - 21px); text-align:center; } From f6213e86f8cf99bc87eca4e42c78d7da3ee3d38b Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Tue, 3 May 2022 22:59:42 +0200 Subject: [PATCH 007/428] new: [workflow:editor] Initial work on the workflow editor --- app/Controller/WorkflowsController.php | 33 ++ app/View/Elements/Workflows/sidebar-block.ctp | 11 + app/View/Workflows/add.ctp | 0 app/View/Workflows/delete.ctp | 0 app/View/Workflows/editor.ctp | 266 ++++++++++++ app/View/Workflows/index.ctp | 0 app/webroot/css/drawflow.min.css | 1 + app/webroot/css/workflows-editor.css | 392 ++++++++++++++++++ app/webroot/js/drawflow.min.js | 1 + .../js/workflows-editor/workflows-editor.js | 192 +++++++++ 10 files changed, 896 insertions(+) create mode 100644 app/Controller/WorkflowsController.php create mode 100644 app/View/Elements/Workflows/sidebar-block.ctp create mode 100644 app/View/Workflows/add.ctp create mode 100644 app/View/Workflows/delete.ctp create mode 100644 app/View/Workflows/editor.ctp create mode 100644 app/View/Workflows/index.ctp create mode 100644 app/webroot/css/drawflow.min.css create mode 100644 app/webroot/css/workflows-editor.css create mode 100644 app/webroot/js/drawflow.min.js create mode 100644 app/webroot/js/workflows-editor/workflows-editor.js diff --git a/app/Controller/WorkflowsController.php b/app/Controller/WorkflowsController.php new file mode 100644 index 000000000..b371af17f --- /dev/null +++ b/app/Controller/WorkflowsController.php @@ -0,0 +1,33 @@ +RestResponse->viewData([]); + } + + public function add() + { + + } + + public function edit() + { + + } + + public function delete() + { + + } + + public function editor() + { + + } +} diff --git a/app/View/Elements/Workflows/sidebar-block.ctp b/app/View/Elements/Workflows/sidebar-block.ctp new file mode 100644 index 000000000..185d98b69 --- /dev/null +++ b/app/View/Elements/Workflows/sidebar-block.ctp @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/app/View/Workflows/add.ctp b/app/View/Workflows/add.ctp new file mode 100644 index 000000000..e69de29bb diff --git a/app/View/Workflows/delete.ctp b/app/View/Workflows/delete.ctp new file mode 100644 index 000000000..e69de29bb diff --git a/app/View/Workflows/editor.ctp b/app/View/Workflows/editor.ctp new file mode 100644 index 000000000..132c91c79 --- /dev/null +++ b/app/View/Workflows/editor.ctp @@ -0,0 +1,266 @@ + 'publish', + 'name' => 'Publish', + 'icon' => 'upload', + 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', + 'inputs' => 0, + ], + [ + 'id' => 'new-attribute', + 'name' => 'New Attribute', + 'icon' => 'cube', + 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', + 'inputs' => 0, + 'disabled' => true, + ], + [ + 'id' => 'new-object', + 'name' => 'New Object', + 'icon' => 'cubes', + 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', + 'inputs' => 0, + 'disabled' => true, + ], + [ + 'id' => 'email-sent', + 'name' => 'Email sent', + 'icon' => 'envelope', + 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', + 'inputs' => 0, + 'disabled' => true, + ], + [ + 'id' => 'user-new', + 'name' => 'New User', + 'icon' => 'user-plus', + 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', + 'inputs' => 0, + 'disabled' => true, + ], + [ + 'id' => 'feed-pull', + 'name' => 'Feed pull', + 'icon' => 'arrow-alt-circle-down', + 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', + 'inputs' => 0, + 'disabled' => true, + ], +]; + +$blocks_condition = [ + [ + 'id' => 'if', + 'name' => 'IF', + 'icon' => 'code-branch', + 'description' => 'IF conditions', + 'outputs' => 2, + 'html_template' => 'IF', + ], +]; + +$blocks_action = [ + [ + 'id' => 'add-tag', + 'name' => 'Add Tag', + 'icon' => 'tag', + 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', + 'params' => [ + [ + 'type' => 'input', + 'label' => 'Tag name', + 'default' => 'tlp:red', + 'placeholder' => __('Enter tag name') + ], + ], + 'outputs' => 0, + ], + [ + 'id' => 'enrich-attribute', + 'name' => 'Enrich Attribute', + 'icon' => 'asterisk', + 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', + 'outputs' => 0, + ], + [ + 'id' => 'slack-message', + 'name' => 'Slack Message', + 'icon' => 'slack', + 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', + 'params' => [ + [ + 'type' => 'select', + 'label' => 'Channel name', + 'default' => 'team-4_3_misp', + 'options' => [ + 'team-4_3_misp', + 'team-4_0_elite_as_one', + ], + ], + ], + 'outputs' => 0, + ], + [ + 'id' => 'send-email', + 'name' => 'Send Email', + 'icon' => 'envelope', + 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', + 'params' => [ + [ + 'type' => 'select', + 'label' => 'Email template', + 'default' => 'default', + 'options' => [ + 'default', + 'TLP marking', + ], + ], + ], + 'outputs' => 0, + ], + [ + 'name' => 'Do nothing', + 'id' => 'dev-null', + 'icon' => 'ban', + 'description' => 'Essentially a /dev/null', + 'outputs' => 0, + ], + [ + 'name' => 'Push to ZMQ', + 'id' => 'push-zmq', + 'icon' => 'wifi', + 'icon_class' => 'fa-rotate-90', + 'description' => 'Push to the ZMQ channel', + 'params' => [ + [ + 'type' => 'input', + 'label' => 'ZMQ Topic', + 'default' => 'from-misp-workflow', + ], + ], + 'outputs' => 0, + ], +]; + +$blocks_all = array_merge($blocks_trigger, $blocks_condition, $blocks_action); +$workflows = [ + ['id' => 1, 'name' => 'Publish workflow', 'data' => []], + ['id' => 2, 'name' => 'My test worklow1', 'data' => []], +]; +?> + +
+
+
+

Workflows

+
+ +
+ + + +
+
+
+ + +
+ +

Blocks

+ + + + +
+
+ + element('Workflows/sidebar-block', ['block' => $block]) ?> + +
+
+ + element('Workflows/sidebar-block', ['block' => $block]) ?> + +
+
+ + element('Workflows/sidebar-block', ['block' => $block]) ?> + +
+
+ +
+
+
+
+
+
+ +
+
+ + + + +element('genericElements/assetLoader', [ + 'css' => ['drawflow.min'], + 'js' => ['jquery-ui', 'drawflow.min', 'doT'], +]); +echo $this->element('genericElements/assetLoader', [ + 'css' => ['workflows-editor'], + 'js' => ['workflows-editor/workflows-editor'], +]); +?> + + \ No newline at end of file diff --git a/app/View/Workflows/index.ctp b/app/View/Workflows/index.ctp new file mode 100644 index 000000000..e69de29bb diff --git a/app/webroot/css/drawflow.min.css b/app/webroot/css/drawflow.min.css new file mode 100644 index 000000000..c9c944248 --- /dev/null +++ b/app/webroot/css/drawflow.min.css @@ -0,0 +1 @@ +.drawflow,.drawflow .parent-node{position:relative}.parent-drawflow{display:flex;overflow:hidden;touch-action:none;outline:0}.drawflow{width:100%;height:100%;user-select:none;perspective:0}.drawflow .drawflow-node{display:flex;align-items:center;position:absolute;background:#0ff;width:160px;min-height:40px;border-radius:4px;border:2px solid #000;color:#000;z-index:2;padding:15px}.drawflow .drawflow-node.selected{background:red}.drawflow .drawflow-node:hover{cursor:move}.drawflow .drawflow-node .inputs,.drawflow .drawflow-node .outputs{width:0}.drawflow .drawflow-node .drawflow_content_node{width:100%;display:block}.drawflow .drawflow-node .input,.drawflow .drawflow-node .output{position:relative;width:20px;height:20px;background:#fff;border-radius:50%;border:2px solid #000;cursor:crosshair;z-index:1;margin-bottom:5px}.drawflow .drawflow-node .input{left:-27px;top:2px;background:#ff0}.drawflow .drawflow-node .output{right:-3px;top:2px}.drawflow svg{z-index:0;position:absolute;overflow:visible!important}.drawflow .connection{position:absolute;pointer-events:none;aspect-ratio:1/1}.drawflow .connection .main-path{fill:none;stroke-width:5px;stroke:#4682b4;pointer-events:all}.drawflow .connection .main-path:hover{stroke:#1266ab;cursor:pointer}.drawflow .connection .main-path.selected{stroke:#43b993}.drawflow .connection .point{cursor:move;stroke:#000;stroke-width:2;fill:#fff;pointer-events:all}.drawflow .connection .point.selected,.drawflow .connection .point:hover{fill:#1266ab}.drawflow .main-path{fill:none;stroke-width:5px;stroke:#4682b4}.drawflow-delete{position:absolute;display:block;width:30px;height:30px;background:#000;color:#fff;z-index:4;border:2px solid #fff;line-height:30px;font-weight:700;text-align:center;border-radius:50%;font-family:monospace;cursor:pointer}.drawflow>.drawflow-delete{margin-left:-15px;margin-top:15px}.parent-node .drawflow-delete{right:-15px;top:-15px} \ No newline at end of file diff --git a/app/webroot/css/workflows-editor.css b/app/webroot/css/workflows-editor.css new file mode 100644 index 000000000..8a31c5505 --- /dev/null +++ b/app/webroot/css/workflows-editor.css @@ -0,0 +1,392 @@ +.root-container { + display: flex; + height: calc(100vh - 2*42px); /* total space available minus header and footer */ + margin-top: -20px; +} + +.main-container { + display: flex; + height: 100%; + width: 100%; +} + +.side-panel { + z-index: 18; + position: absolute; + box-sizing: border-box; + height: calc(100% - 2*42px); + width: var(--editor-sidepanel-width); + box-shadow: 2px 0px 6px #cecece; + background-color: #fff; + padding: 0.5em 0.25em; + display: flex; + flex-direction: column; +} + +.side-panel #block-tabs { + margin-bottom: 10px; +} + +.side-panel .workflow-selector-container { + display: flex; +} + +.side-panel .workflow-selector-container > select { + flex-grow: 1; +} +.side-panel .workflow-selector-container > button { + margin-left: 0.25em; +} + +.side-panel .chosen-container.workflows { + margin-bottom: 0; +} + +.side-panel .chosen-container.blocks { + width: 100%; + margin-bottom: 1.25em; +} + +.side-panel #block-tabs { + margin-top: 1.25em; +} + +.side-panel .tab-pane.active { + overflow-y: auto; +} + +.sidebar-workflow-block { + display: flex; + width: 300px; + background-color: #fff; + border-radius: 5px; + margin: 0.75em 0.5em; + padding: 0.25em; + transition-property: box-shadow; + transition-duration: .1s; + box-shadow: 0px 3px 6px 2px #33333311; +} + +.sidebar-workflow-block:hover { + box-shadow: 0px 3px 6px 2px #33333333; + cursor: pointer; +} + +.sidebar-workflow-block.disabled { + cursor: not-allowed !important; + filter: blur(1px); +} + +.sidebar-workflow-block > .icon { + width: 1.25em; + align-items: flex-start; + display: flex; + font-size: large; + padding: 0 0.25em; +} + +.canvas { + height: 100%; + width: calc(100% - var(--editor-sidepanel-width)); + margin-left: var(--editor-sidepanel-width); +} + +.canvas-workflow-block { + display: flex; + width: 300px; + background-color: #fff; + border-radius: 5px; + padding: 0.25em 0.75em; + box-shadow: 0px 3px 6px 2px #33333333; +} + +.canvas-workflow-block.small { + height: 50px; +} + +.canvas-workflow-block br { + width: 100%; + height: 1px; + background-color: #e9e9e9; +} + +.canvas-workflow-block>.icon { + width: 1.25em; + align-items: flex-start; + display: flex; + font-size: large; + padding: 0 0.25em; +} + +#drawflow { + height: 100%; + width: 100%; +} + +/* Canvas blocks */ +.default-main-container { + display: flex; + font-size: large; + padding: 0.25em 0; + border-bottom: 1px solid #efefef; +} + +.canvas-workflow-block.small .default-main-container-small { + display: flex; + font-size: large; + padding: 0.25em; + height: 100%; + box-sizing: border-box; + align-items: center; +} + +.default-main-container-small .then-else-container { + font-size: small; + display: flex; + flex-direction: column; + margin-left: auto; + font-family: monospace; + margin-top: -1px; + margin-right: 2px; + +} + +.default-main-container .description { + user-select: none; + margin: 0.25em 0.5em; +} + + + +/* Drawflow CSS */ + +:root { + --editor-sidepanel-width: 340px; + + --dfBackgroundColor: #ffffff; + --dfBackgroundSize: 15px; + --dfBackgroundImage: linear-gradient(to right, #66666615 1px, transparent 1px), + linear-gradient(to bottom, #66666615 1px, transparent 1px); + + --dfNodeType: flex; + --dfNodeTypeFloat: none; + --dfNodeBackgroundColor: #ffffff; + --dfNodeTextColor: #000000; + --dfNodeBorderSize: 0px; + --dfNodeBorderColor: #000000; + --dfNodeBorderRadius: 4px; + --dfNodeMinHeight: 40px; + --dfNodeMinWidth: 160px; + --dfNodePadding: 0px; + --dfNodePaddingTop: 15px; + --dfNodePaddingBottom: 15px; + + --dfNodeHoverBackgroundColor: #ffffff; + --dfNodeHoverTextColor: #000000; + + --dfNodeHoverBoxShadowHL: 0px; + --dfNodeHoverBoxShadowVL: 2px; + --dfNodeHoverBoxShadowBR: 6px; + --dfNodeHoverBoxShadowS: 0px; + --dfNodeHoverBoxShadowColor: #0088cc99; + + --dfNodeSelectedBackgroundColor: #ffffff; + --dfNodeSelectedTextColor: #000000; + + --dfNodeSelectedBoxShadowHL: 0px; + --dfNodeSelectedBoxShadowVL: 2px; + --dfNodeSelectedBoxShadowBR: 6px; + --dfNodeSelectedBoxShadowS: 0px; + --dfNodeSelectedBoxShadowColor: #0088cc99; + + --dfInputBackgroundColor: #ffffff; + --dfInputBorderSize: 2px; + --dfInputBorderColor: #5a5a5a; + --dfInputBorderRadius: 50px; + --dfInputLeft: -7px; + --dfInputHeight: 10px; + --dfInputWidth: 10px; + + --dfInputHeightHover: 16px; + --dfInputWidthHover: 16px; + --dfInputLeftHover: -10px; + + --dfInputHoverBackgroundColor: #ffffff; + --dfInputHoverBorderSize: 2px; + --dfInputHoverBorderColor: #5a5a5a; + --dfInputHoverBorderRadius: 50px; + + --dfOutputBackgroundColor: #ffffff; + --dfOutputBorderSize: 2px; + --dfOutputBorderColor: #5a5a5a; + --dfOutputBorderRadius: 50px; + --dfOutputRight: 6px; + --dfOutputHeight: 10px; + --dfOutputWidth: 10px; + + --dfOutputHoverBackgroundColor: #ffffff; + --dfOutputHoverBorderSize: 2px; + --dfOutputHoverBorderColor: #5a5a5a; + --dfOutputHoverBorderRadius: 50px; + + --dfOutputHeightHover: 16px; + --dfOutputWidthHover: 16px; + --dfOutputRightHover: 9px; + + --dfLineWidth: 5px; + --dfLineColor: #4682b4; + --dfLineHoverColor: #4682b4; + --dfLineSelectedColor: #43b993; + + --dfRerouteBorderWidth: 2px; + --dfRerouteBorderColor: #000000; + --dfRerouteBackgroundColor: #ffffff; + + --dfRerouteHoverBorderWidth: 2px; + --dfRerouteHoverBorderColor: #000000; + --dfRerouteHoverBackgroundColor: #ffffff; + + --dfDeleteDisplay: block; + --dfDeleteColor: #ffffff; + --dfDeleteBackgroundColor: #e37b78; + --dfDeleteBorderSize: 2px; + --dfDeleteBorderColor: #ffffff; + --dfDeleteBorderRadius: 50px; + --dfDeleteTop: -11px; + --dfDeleteRight: -11px; + --dfDeleteHeight: 15px; + --dfDeleteWidth: 15px; + --dfDeleteLineHeight: 16px; + + --dfDeleteHoverColor: #ffffff; + --dfDeleteHoverBackgroundColor: #da4e49; + --dfDeleteHoverBorderSize: 2px; + --dfDeleteHoverBorderColor: #ffffff; + --dfDeleteHoverBorderRadius: 50px; +} + +#drawflow { + background: var(--dfBackgroundColor); + background-size: var(--dfBackgroundSize) var(--dfBackgroundSize); + background-image: var(--dfBackgroundImage); +} + +.drawflow .drawflow-node { + display: var(--dfNodeType); + background: var(--dfNodeBackgroundColor); + color: var(--dfNodeTextColor); + border: var(--dfNodeBorderSize) solid var(--dfNodeBorderColor); + border-radius: var(--dfNodeBorderRadius); + min-height: var(--dfNodeMinHeight); + width: auto; + min-width: var(--dfNodeMinWidth); + padding-top: var(--dfNodePaddingTop); + padding-bottom: var(--dfNodePaddingBottom); + padding: var(--dfNodePadding); + -webkit-box-shadow: var(--dfNodeBoxShadowHL) var(--dfNodeBoxShadowVL) var(--dfNodeBoxShadowBR) var(--dfNodeBoxShadowS) var(--dfNodeBoxShadowColor); + box-shadow: var(--dfNodeBoxShadowHL) var(--dfNodeBoxShadowVL) var(--dfNodeBoxShadowBR) var(--dfNodeBoxShadowS) var(--dfNodeBoxShadowColor); +} + +.drawflow .drawflow-node:hover { + -webkit-box-shadow: var(--dfNodeHoverBoxShadowHL) var(--dfNodeHoverBoxShadowVL) var(--dfNodeHoverBoxShadowBR) var(--dfNodeHoverBoxShadowS) var(--dfNodeHoverBoxShadowColor); + box-shadow: var(--dfNodeHoverBoxShadowHL) var(--dfNodeHoverBoxShadowVL) var(--dfNodeHoverBoxShadowBR) var(--dfNodeHoverBoxShadowS) var(--dfNodeHoverBoxShadowColor); +} + +.drawflow .drawflow-node.selected { + background: var(--dfNodeSelectedBackgroundColor); + color: var(--dfNodeSelectedTextColor); + -webkit-box-shadow: var(--dfNodeSelectedBoxShadowHL) var(--dfNodeSelectedBoxShadowVL) var(--dfNodeSelectedBoxShadowBR) var(--dfNodeSelectedBoxShadowS) var(--dfNodeSelectedBoxShadowColor); + box-shadow: var(--dfNodeSelectedBoxShadowHL) var(--dfNodeSelectedBoxShadowVL) var(--dfNodeSelectedBoxShadowBR) var(--dfNodeSelectedBoxShadowS) var(--dfNodeSelectedBoxShadowColor); +} + +.drawflow .drawflow-node .input { + left: var(--dfInputLeft); + background: var(--dfInputBackgroundColor); + border: var(--dfInputBorderSize) solid var(--dfInputBorderColor); + border-radius: var(--dfInputBorderRadius); + height: var(--dfInputHeight); + width: var(--dfInputWidth); + transition-property: height,width,left; + transition-duration: .1s; +} + +.drawflow .drawflow-node .input:hover { + background: var(--dfInputHoverBackgroundColor); + border: var(--dfInputHoverBorderSize) solid var(--dfInputHoverBorderColor); + border-radius: var(--dfInputHoverBorderRadius); + height: var(--dfInputHeightHover); + width: var(--dfInputWidthHover); + left: var(--dfInputLeftHover); +} + +.drawflow .drawflow-node .outputs { + float: var(--dfNodeTypeFloat); +} + +.drawflow .drawflow-node .output { + right: var(--dfOutputRight); + background: var(--dfOutputBackgroundColor); + border: var(--dfOutputBorderSize) solid var(--dfOutputBorderColor); + border-radius: var(--dfOutputBorderRadius); + height: var(--dfOutputHeight); + width: var(--dfOutputWidth); + transition-property: height,width,right; + transition-duration: .1s; +} + +.drawflow .drawflow-node .output:hover { + background: var(--dfOutputHoverBackgroundColor); + border: var(--dfOutputHoverBorderSize) solid var(--dfOutputHoverBorderColor); + border-radius: var(--dfOutputHoverBorderRadius); + height: var(--dfOutputHeightHover); + width: var(--dfOutputWidthHover); + right: var(--dfOutputRightHover); +} + +.drawflow .connection .main-path { + stroke-width: var(--dfLineWidth); + stroke: var(--dfLineColor); +} + +.drawflow .connection .main-path:hover { + stroke: var(--dfLineHoverColor); +} + +.drawflow .connection .main-path.selected { + stroke: var(--dfLineSelectedColor); +} + +.drawflow .connection .point { + stroke: var(--dfRerouteBorderColor); + stroke-width: var(--dfRerouteBorderWidth); + fill: var(--dfRerouteBackgroundColor); +} + +.drawflow .connection .point:hover { + stroke: var(--dfRerouteHoverBorderColor); + stroke-width: var(--dfRerouteHoverBorderWidth); + fill: var(--dfRerouteHoverBackgroundColor); +} + +.drawflow-delete { + display: var(--dfDeleteDisplay); + color: var(--dfDeleteColor); + background: var(--dfDeleteBackgroundColor); + border: var(--dfDeleteBorderSize) solid var(--dfDeleteBorderColor); + border-radius: var(--dfDeleteBorderRadius); +} + +.parent-node .drawflow-delete { + top: var(--dfDeleteTop); + right: var(--dfDeleteRight); + height: var(--dfDeleteHeight); + width: var(--dfDeleteWidth); + line-height: var(--dfDeleteLineHeight); +} + +.drawflow-delete:hover { + color: var(--dfDeleteHoverColor); + background: var(--dfDeleteHoverBackgroundColor); + border: var(--dfDeleteHoverBorderSize) solid var(--dfDeleteHoverBorderColor); + border-radius: var(--dfDeleteHoverBorderRadius); +} \ No newline at end of file diff --git a/app/webroot/js/drawflow.min.js b/app/webroot/js/drawflow.min.js new file mode 100644 index 000000000..922aebc31 --- /dev/null +++ b/app/webroot/js/drawflow.min.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Drawflow=t():e.Drawflow=t()}("undefined"!=typeof self?self:this,(function(){return function(e){var t={};function n(i){if(t[i])return t[i].exports;var s=t[i]={i:i,l:!1,exports:{}};return e[i].call(s.exports,s,s.exports,n),s.l=!0,s.exports}return n.m=e,n.c=t,n.d=function(e,t,i){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var s in e)n.d(i,s,function(t){return e[t]}.bind(null,s));return i},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";n.r(t),n.d(t,"default",(function(){return i}));class i{constructor(e,t=null,n=null){this.events={},this.container=e,this.precanvas=null,this.nodeId=1,this.ele_selected=null,this.node_selected=null,this.drag=!1,this.reroute=!1,this.reroute_fix_curvature=!1,this.curvature=.5,this.reroute_curvature_start_end=.5,this.reroute_curvature=.5,this.reroute_width=6,this.drag_point=!1,this.editor_selected=!1,this.connection=!1,this.connection_ele=null,this.connection_selected=null,this.canvas_x=0,this.canvas_y=0,this.pos_x=0,this.pos_x_start=0,this.pos_y=0,this.pos_y_start=0,this.mouse_x=0,this.mouse_y=0,this.line_path=5,this.first_click=null,this.force_first_input=!1,this.draggable_inputs=!0,this.useuuid=!1,this.parent=n,this.noderegister={},this.render=t,this.drawflow={drawflow:{Home:{data:{}}}},this.module="Home",this.editor_mode="edit",this.zoom=1,this.zoom_max=1.6,this.zoom_min=.5,this.zoom_value=.1,this.zoom_last_value=1,this.evCache=new Array,this.prevDiff=-1}start(){this.container.classList.add("parent-drawflow"),this.container.tabIndex=0,this.precanvas=document.createElement("div"),this.precanvas.classList.add("drawflow"),this.container.appendChild(this.precanvas),this.container.addEventListener("mouseup",this.dragEnd.bind(this)),this.container.addEventListener("mousemove",this.position.bind(this)),this.container.addEventListener("mousedown",this.click.bind(this)),this.container.addEventListener("touchend",this.dragEnd.bind(this)),this.container.addEventListener("touchmove",this.position.bind(this)),this.container.addEventListener("touchstart",this.click.bind(this)),this.container.addEventListener("contextmenu",this.contextmenu.bind(this)),this.container.addEventListener("keydown",this.key.bind(this)),this.container.addEventListener("wheel",this.zoom_enter.bind(this)),this.container.addEventListener("input",this.updateNodeValue.bind(this)),this.container.addEventListener("dblclick",this.dblclick.bind(this)),this.container.onpointerdown=this.pointerdown_handler.bind(this),this.container.onpointermove=this.pointermove_handler.bind(this),this.container.onpointerup=this.pointerup_handler.bind(this),this.container.onpointercancel=this.pointerup_handler.bind(this),this.container.onpointerout=this.pointerup_handler.bind(this),this.container.onpointerleave=this.pointerup_handler.bind(this),this.load()}pointerdown_handler(e){this.evCache.push(e)}pointermove_handler(e){for(var t=0;t100&&(n>this.prevDiff&&this.zoom_in(),n=n&&(n=parseInt(e)+1)}))})),this.nodeId=n}removeReouteConnectionSelected(){this.dispatch("connectionUnselected",!0),this.reroute_fix_curvature&&this.connection_selected.parentElement.querySelectorAll(".main-path").forEach((e,t)=>{e.classList.remove("selected")})}click(e){if(this.dispatch("click",e),"fixed"===this.editor_mode){if(e.preventDefault(),"parent-drawflow"!==e.target.classList[0]&&"drawflow"!==e.target.classList[0])return!1;this.ele_selected=e.target.closest(".parent-drawflow")}else"view"===this.editor_mode?(null!=e.target.closest(".drawflow")||e.target.matches(".parent-drawflow"))&&(this.ele_selected=e.target.closest(".parent-drawflow"),e.preventDefault()):(this.first_click=e.target,this.ele_selected=e.target,0===e.button&&this.contextmenuDel(),null!=e.target.closest(".drawflow_content_node")&&(this.ele_selected=e.target.closest(".drawflow_content_node").parentElement));switch(this.ele_selected.classList[0]){case"drawflow-node":null!=this.node_selected&&(this.node_selected.classList.remove("selected"),this.node_selected!=this.ele_selected&&this.dispatch("nodeUnselected",!0)),null!=this.connection_selected&&(this.connection_selected.classList.remove("selected"),this.removeReouteConnectionSelected(),this.connection_selected=null),this.node_selected!=this.ele_selected&&this.dispatch("nodeSelected",this.ele_selected.id.slice(5)),this.node_selected=this.ele_selected,this.node_selected.classList.add("selected"),this.draggable_inputs?"SELECT"!==e.target.tagName&&(this.drag=!0):"INPUT"!==e.target.tagName&&"TEXTAREA"!==e.target.tagName&&"SELECT"!==e.target.tagName&&!0!==e.target.hasAttribute("contenteditable")&&(this.drag=!0);break;case"output":this.connection=!0,null!=this.node_selected&&(this.node_selected.classList.remove("selected"),this.node_selected=null,this.dispatch("nodeUnselected",!0)),null!=this.connection_selected&&(this.connection_selected.classList.remove("selected"),this.removeReouteConnectionSelected(),this.connection_selected=null),this.drawConnection(e.target);break;case"parent-drawflow":case"drawflow":null!=this.node_selected&&(this.node_selected.classList.remove("selected"),this.node_selected=null,this.dispatch("nodeUnselected",!0)),null!=this.connection_selected&&(this.connection_selected.classList.remove("selected"),this.removeReouteConnectionSelected(),this.connection_selected=null),this.editor_selected=!0;break;case"main-path":null!=this.node_selected&&(this.node_selected.classList.remove("selected"),this.node_selected=null,this.dispatch("nodeUnselected",!0)),null!=this.connection_selected&&(this.connection_selected.classList.remove("selected"),this.removeReouteConnectionSelected(),this.connection_selected=null),this.connection_selected=this.ele_selected,this.connection_selected.classList.add("selected");const t=this.connection_selected.parentElement.classList;this.dispatch("connectionSelected",{output_id:t[2].slice(14),input_id:t[1].slice(13),output_class:t[3],input_class:t[4]}),this.reroute_fix_curvature&&this.connection_selected.parentElement.querySelectorAll(".main-path").forEach((e,t)=>{e.classList.add("selected")});break;case"point":this.drag_point=!0,this.ele_selected.classList.add("selected");break;case"drawflow-delete":this.node_selected&&this.removeNodeId(this.node_selected.id),this.connection_selected&&this.removeConnection(),null!=this.node_selected&&(this.node_selected.classList.remove("selected"),this.node_selected=null,this.dispatch("nodeUnselected",!0)),null!=this.connection_selected&&(this.connection_selected.classList.remove("selected"),this.removeReouteConnectionSelected(),this.connection_selected=null)}"touchstart"===e.type?(this.pos_x=e.touches[0].clientX,this.pos_x_start=e.touches[0].clientX,this.pos_y=e.touches[0].clientY,this.pos_y_start=e.touches[0].clientY):(this.pos_x=e.clientX,this.pos_x_start=e.clientX,this.pos_y=e.clientY,this.pos_y_start=e.clientY),(this.drag||["input","output","main-path"].includes(this.ele_selected.classList[0]))&&e.preventDefault(),this.dispatch("clickEnd",e)}position(e){if("touchmove"===e.type)var t=e.touches[0].clientX,n=e.touches[0].clientY;else t=e.clientX,n=e.clientY;if(this.connection&&this.updateConnection(t,n),this.editor_selected&&(i=this.canvas_x+-(this.pos_x-t),s=this.canvas_y+-(this.pos_y-n),this.dispatch("translate",{x:i,y:s}),this.precanvas.style.transform="translate("+i+"px, "+s+"px) scale("+this.zoom+")"),this.drag){var i=(this.pos_x-t)*this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom),s=(this.pos_y-n)*this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom);this.pos_x=t,this.pos_y=n,this.ele_selected.style.top=this.ele_selected.offsetTop-s+"px",this.ele_selected.style.left=this.ele_selected.offsetLeft-i+"px",this.drawflow.drawflow[this.module].data[this.ele_selected.id.slice(5)].pos_x=this.ele_selected.offsetLeft-i,this.drawflow.drawflow[this.module].data[this.ele_selected.id.slice(5)].pos_y=this.ele_selected.offsetTop-s,this.updateConnectionNodes(this.ele_selected.id)}if(this.drag_point){i=(this.pos_x-t)*this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom),s=(this.pos_y-n)*this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom);this.pos_x=t,this.pos_y=n;var o=this.pos_x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom))-this.precanvas.getBoundingClientRect().x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom)),l=this.pos_y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom))-this.precanvas.getBoundingClientRect().y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom));this.ele_selected.setAttributeNS(null,"cx",o),this.ele_selected.setAttributeNS(null,"cy",l);const e=this.ele_selected.parentElement.classList[2].slice(9),c=this.ele_selected.parentElement.classList[1].slice(13),d=this.ele_selected.parentElement.classList[3],a=this.ele_selected.parentElement.classList[4];let r=Array.from(this.ele_selected.parentElement.children).indexOf(this.ele_selected)-1;if(this.reroute_fix_curvature){r-=this.ele_selected.parentElement.querySelectorAll(".main-path").length-1,r<0&&(r=0)}const h=e.slice(5),u=this.drawflow.drawflow[this.module].data[h].outputs[d].connections.findIndex((function(e,t){return e.node===c&&e.output===a}));this.drawflow.drawflow[this.module].data[h].outputs[d].connections[u].points[r]={pos_x:o,pos_y:l};const p=this.ele_selected.parentElement.classList[2].slice(9);this.updateConnectionNodes(p)}"touchmove"===e.type&&(this.mouse_x=t,this.mouse_y=n),this.dispatch("mouseMove",{x:t,y:n})}dragEnd(e){if("touchend"===e.type)var t=this.mouse_x,n=this.mouse_y,i=document.elementFromPoint(t,n);else t=e.clientX,n=e.clientY,i=e.target;if(this.drag&&(this.pos_x_start==t&&this.pos_y_start==n||this.dispatch("nodeMoved",this.ele_selected.id.slice(5))),this.drag_point&&(this.ele_selected.classList.remove("selected"),this.pos_x_start==t&&this.pos_y_start==n||this.dispatch("rerouteMoved",this.ele_selected.parentElement.classList[2].slice(14))),this.editor_selected&&(this.canvas_x=this.canvas_x+-(this.pos_x-t),this.canvas_y=this.canvas_y+-(this.pos_y-n),this.editor_selected=!1),!0===this.connection)if("input"===i.classList[0]||this.force_first_input&&(null!=i.closest(".drawflow_content_node")||"drawflow-node"===i.classList[0])){if(!this.force_first_input||null==i.closest(".drawflow_content_node")&&"drawflow-node"!==i.classList[0])s=i.parentElement.parentElement.id,o=i.classList[1];else{if(null!=i.closest(".drawflow_content_node"))var s=i.closest(".drawflow_content_node").parentElement.id;else var s=i.id;if(0===Object.keys(this.getNodeFromId(s.slice(5)).inputs).length)var o=!1;else var o="input_1"}var l=this.ele_selected.parentElement.parentElement.id,c=this.ele_selected.classList[1];if(l!==s&&!1!==o){if(0===this.container.querySelectorAll(".connection.node_in_"+s+".node_out_"+l+"."+c+"."+o).length){this.connection_ele.classList.add("node_in_"+s),this.connection_ele.classList.add("node_out_"+l),this.connection_ele.classList.add(c),this.connection_ele.classList.add(o);var d=s.slice(5),a=l.slice(5);this.drawflow.drawflow[this.module].data[a].outputs[c].connections.push({node:d,output:o}),this.drawflow.drawflow[this.module].data[d].inputs[o].connections.push({node:a,input:c}),this.updateConnectionNodes("node-"+a),this.updateConnectionNodes("node-"+d),this.dispatch("connectionCreated",{output_id:a,input_id:d,output_class:c,input_class:o})}else this.dispatch("connectionCancel",!0),this.connection_ele.remove();this.connection_ele=null}else this.dispatch("connectionCancel",!0),this.connection_ele.remove(),this.connection_ele=null}else this.dispatch("connectionCancel",!0),this.connection_ele.remove(),this.connection_ele=null;this.drag=!1,this.drag_point=!1,this.connection=!1,this.ele_selected=null,this.editor_selected=!1,this.dispatch("mouseUp",e)}contextmenu(e){if(this.dispatch("contextmenu",e),e.preventDefault(),"fixed"===this.editor_mode||"view"===this.editor_mode)return!1;if(this.precanvas.getElementsByClassName("drawflow-delete").length&&this.precanvas.getElementsByClassName("drawflow-delete")[0].remove(),this.node_selected||this.connection_selected){var t=document.createElement("div");t.classList.add("drawflow-delete"),t.innerHTML="x",this.node_selected&&this.node_selected.appendChild(t),this.connection_selected&&(t.style.top=e.clientY*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom))-this.precanvas.getBoundingClientRect().y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom))+"px",t.style.left=e.clientX*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom))-this.precanvas.getBoundingClientRect().x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom))+"px",this.precanvas.appendChild(t))}}contextmenuDel(){this.precanvas.getElementsByClassName("drawflow-delete").length&&this.precanvas.getElementsByClassName("drawflow-delete")[0].remove()}key(e){if(this.dispatch("keydown",e),"fixed"===this.editor_mode||"view"===this.editor_mode)return!1;("Delete"===e.key||"Backspace"===e.key&&e.metaKey)&&(null!=this.node_selected&&"INPUT"!==this.first_click.tagName&&"TEXTAREA"!==this.first_click.tagName&&!0!==this.first_click.hasAttribute("contenteditable")&&this.removeNodeId(this.node_selected.id),null!=this.connection_selected&&this.removeConnection())}zoom_enter(e,t){e.ctrlKey&&(e.preventDefault(),e.deltaY>0?this.zoom_out():this.zoom_in())}zoom_refresh(){this.dispatch("zoom",this.zoom),this.canvas_x=this.canvas_x/this.zoom_last_value*this.zoom,this.canvas_y=this.canvas_y/this.zoom_last_value*this.zoom,this.zoom_last_value=this.zoom,this.precanvas.style.transform="translate("+this.canvas_x+"px, "+this.canvas_y+"px) scale("+this.zoom+")"}zoom_in(){this.zoomthis.zoom_min&&(this.zoom-=this.zoom_value,this.zoom_refresh())}zoom_reset(){1!=this.zoom&&(this.zoom=1,this.zoom_refresh())}createCurvature(e,t,n,i,s,o){var l=e,c=t,d=n,a=i,r=s;switch(o){case"open":if(e>=n)var h=l+Math.abs(d-l)*r,u=d-Math.abs(d-l)*(-1*r);else h=l+Math.abs(d-l)*r,u=d-Math.abs(d-l)*r;return" M "+l+" "+c+" C "+h+" "+c+" "+u+" "+a+" "+d+" "+a;case"close":if(e>=n)h=l+Math.abs(d-l)*(-1*r),u=d-Math.abs(d-l)*r;else h=l+Math.abs(d-l)*r,u=d-Math.abs(d-l)*r;return" M "+l+" "+c+" C "+h+" "+c+" "+u+" "+a+" "+d+" "+a;case"other":if(e>=n)h=l+Math.abs(d-l)*(-1*r),u=d-Math.abs(d-l)*(-1*r);else h=l+Math.abs(d-l)*r,u=d-Math.abs(d-l)*r;return" M "+l+" "+c+" C "+h+" "+c+" "+u+" "+a+" "+d+" "+a;default:return" M "+l+" "+c+" C "+(h=l+Math.abs(d-l)*r)+" "+c+" "+(u=d-Math.abs(d-l)*r)+" "+a+" "+d+" "+a}}drawConnection(e){var t=document.createElementNS("http://www.w3.org/2000/svg","svg");this.connection_ele=t;var n=document.createElementNS("http://www.w3.org/2000/svg","path");n.classList.add("main-path"),n.setAttributeNS(null,"d",""),t.classList.add("connection"),t.appendChild(n),this.precanvas.appendChild(t);var i=e.parentElement.parentElement.id.slice(5),s=e.classList[1];this.dispatch("connectionStart",{output_id:i,output_class:s})}updateConnection(e,t){const n=this.precanvas,i=this.zoom;let s=n.clientWidth/(n.clientWidth*i);s=s||0;let o=n.clientHeight/(n.clientHeight*i);o=o||0;var l=this.connection_ele.children[0],c=this.ele_selected.offsetWidth/2+(this.ele_selected.getBoundingClientRect().x-n.getBoundingClientRect().x)*s,d=this.ele_selected.offsetHeight/2+(this.ele_selected.getBoundingClientRect().y-n.getBoundingClientRect().y)*o,a=e*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom))-this.precanvas.getBoundingClientRect().x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom)),r=t*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom))-this.precanvas.getBoundingClientRect().y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom)),h=this.curvature,u=this.createCurvature(c,d,a,r,h,"openclose");l.setAttributeNS(null,"d",u)}addConnection(e,t,n,i){var s=this.getModuleFromNodeId(e);if(s===this.getModuleFromNodeId(t)){var o=this.getNodeFromId(e),l=!1;for(var c in o.outputs[n].connections){var d=o.outputs[n].connections[c];d.node==t&&d.output==i&&(l=!0)}if(!1===l){if(this.drawflow.drawflow[s].data[e].outputs[n].connections.push({node:t.toString(),output:i}),this.drawflow.drawflow[s].data[t].inputs[i].connections.push({node:e.toString(),input:n}),this.module===s){var a=document.createElementNS("http://www.w3.org/2000/svg","svg"),r=document.createElementNS("http://www.w3.org/2000/svg","path");r.classList.add("main-path"),r.setAttributeNS(null,"d",""),a.classList.add("connection"),a.classList.add("node_in_node-"+t),a.classList.add("node_out_node-"+e),a.classList.add(n),a.classList.add(i),a.appendChild(r),this.precanvas.appendChild(a),this.updateConnectionNodes("node-"+e),this.updateConnectionNodes("node-"+t)}this.dispatch("connectionCreated",{output_id:e,input_id:t,output_class:n,input_class:i})}}}updateConnectionNodes(e){const t="node_in_"+e,n="node_out_"+e;this.line_path;const i=this.container,s=this.precanvas,o=this.curvature,l=this.createCurvature,c=this.reroute_curvature,d=this.reroute_curvature_start_end,a=this.reroute_fix_curvature,r=this.reroute_width,h=this.zoom;let u=s.clientWidth/(s.clientWidth*h);u=u||0;let p=s.clientHeight/(s.clientHeight*h);p=p||0;const f=i.querySelectorAll("."+n);Object.keys(f).map((function(t,n){if(null===f[t].querySelector(".point")){var g=i.querySelector("#"+e),m=f[t].classList[1].replace("node_in_",""),_=i.querySelector("#"+m).querySelectorAll("."+f[t].classList[4])[0],w=_.offsetWidth/2+(_.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,v=_.offsetHeight/2+(_.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,y=g.querySelectorAll("."+f[t].classList[3])[0],C=y.offsetWidth/2+(y.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,x=y.offsetHeight/2+(y.getBoundingClientRect().y-s.getBoundingClientRect().y)*p;const n=l(C,x,w,v,o,"openclose");f[t].children[0].setAttributeNS(null,"d",n)}else{const n=f[t].querySelectorAll(".point");let o="";const g=[];n.forEach((t,a)=>{if(0===a&&n.length-1==0){var f=i.querySelector("#"+e),m=((x=t).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,_=(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,w=(L=f.querySelectorAll("."+t.parentElement.classList[3])[0]).offsetWidth/2+(L.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,v=L.offsetHeight/2+(L.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,y=l(w,v,m,_,d,"open");o+=y,g.push(y);f=t;var C=t.parentElement.classList[1].replace("node_in_",""),x=(E=i.querySelector("#"+C)).querySelectorAll("."+t.parentElement.classList[4])[0];m=(R=E.querySelectorAll("."+t.parentElement.classList[4])[0]).offsetWidth/2+(R.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,_=R.offsetHeight/2+(R.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,w=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,v=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,y=l(w,v,m,_,d,"close");o+=y,g.push(y)}else if(0===a){var L;f=i.querySelector("#"+e),m=((x=t).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,_=(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,w=(L=f.querySelectorAll("."+t.parentElement.classList[3])[0]).offsetWidth/2+(L.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,v=L.offsetHeight/2+(L.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,y=l(w,v,m,_,d,"open");o+=y,g.push(y);f=t,m=((x=n[a+1]).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,_=(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,w=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,v=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,y=l(w,v,m,_,c,"other");o+=y,g.push(y)}else if(a===n.length-1){var E,R;f=t,C=t.parentElement.classList[1].replace("node_in_",""),x=(E=i.querySelector("#"+C)).querySelectorAll("."+t.parentElement.classList[4])[0],m=(R=E.querySelectorAll("."+t.parentElement.classList[4])[0]).offsetWidth/2+(R.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,_=R.offsetHeight/2+(R.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,w=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*(s.clientWidth/(s.clientWidth*h))+r,v=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*(s.clientHeight/(s.clientHeight*h))+r,y=l(w,v,m,_,d,"close");o+=y,g.push(y)}else{f=t,m=((x=n[a+1]).getBoundingClientRect().x-s.getBoundingClientRect().x)*(s.clientWidth/(s.clientWidth*h))+r,_=(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*(s.clientHeight/(s.clientHeight*h))+r,w=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*(s.clientWidth/(s.clientWidth*h))+r,v=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*(s.clientHeight/(s.clientHeight*h))+r,y=l(w,v,m,_,c,"other");o+=y,g.push(y)}}),a?g.forEach((e,n)=>{f[t].children[n].setAttributeNS(null,"d",e)}):f[t].children[0].setAttributeNS(null,"d",o)}}));const g=i.querySelectorAll("."+t);Object.keys(g).map((function(t,n){if(null===g[t].querySelector(".point")){var h=i.querySelector("#"+e),f=g[t].classList[2].replace("node_out_",""),m=i.querySelector("#"+f).querySelectorAll("."+g[t].classList[3])[0],_=m.offsetWidth/2+(m.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,w=m.offsetHeight/2+(m.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,v=(h=h.querySelectorAll("."+g[t].classList[4])[0]).offsetWidth/2+(h.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,y=h.offsetHeight/2+(h.getBoundingClientRect().y-s.getBoundingClientRect().y)*p;const n=l(_,w,v,y,o,"openclose");g[t].children[0].setAttributeNS(null,"d",n)}else{const n=g[t].querySelectorAll(".point");let o="";const h=[];n.forEach((t,a)=>{if(0===a&&n.length-1==0){var f=i.querySelector("#"+e),g=((C=t).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,m=(C.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,_=(E=f.querySelectorAll("."+t.parentElement.classList[4])[0]).offsetWidth/2+(E.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,w=E.offsetHeight/2+(E.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,v=l(g,m,_,w,d,"close");o+=v,h.push(v);f=t;var y=t.parentElement.classList[2].replace("node_out_",""),C=(L=i.querySelector("#"+y)).querySelectorAll("."+t.parentElement.classList[3])[0];g=(x=L.querySelectorAll("."+t.parentElement.classList[3])[0]).offsetWidth/2+(x.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,m=x.offsetHeight/2+(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,_=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,w=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,v=l(g,m,_,w,d,"open");o+=v,h.push(v)}else if(0===a){var x;f=t,y=t.parentElement.classList[2].replace("node_out_",""),C=(L=i.querySelector("#"+y)).querySelectorAll("."+t.parentElement.classList[3])[0],g=(x=L.querySelectorAll("."+t.parentElement.classList[3])[0]).offsetWidth/2+(x.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,m=x.offsetHeight/2+(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,_=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,w=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,v=l(g,m,_,w,d,"open");o+=v,h.push(v);f=t,_=((C=n[a+1]).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,w=(C.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,g=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,m=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,v=l(g,m,_,w,c,"other");o+=v,h.push(v)}else if(a===n.length-1){var L,E;f=t,y=t.parentElement.classList[1].replace("node_in_",""),C=(L=i.querySelector("#"+y)).querySelectorAll("."+t.parentElement.classList[4])[0],_=(E=L.querySelectorAll("."+t.parentElement.classList[4])[0]).offsetWidth/2+(E.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,w=E.offsetHeight/2+(E.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,g=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,m=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,v=l(g,m,_,w,d,"close");o+=v,h.push(v)}else{f=t,_=((C=n[a+1]).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,w=(C.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,g=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,m=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,v=l(g,m,_,w,c,"other");o+=v,h.push(v)}}),a?h.forEach((e,n)=>{g[t].children[n].setAttributeNS(null,"d",e)}):g[t].children[0].setAttributeNS(null,"d",o)}}))}dblclick(e){null!=this.connection_selected&&this.reroute&&this.createReroutePoint(this.connection_selected),"point"===e.target.classList[0]&&this.removeReroutePoint(e.target)}createReroutePoint(e){this.connection_selected.classList.remove("selected");const t=this.connection_selected.parentElement.classList[2].slice(9),n=this.connection_selected.parentElement.classList[1].slice(13),i=this.connection_selected.parentElement.classList[3],s=this.connection_selected.parentElement.classList[4];this.connection_selected=null;const o=document.createElementNS("http://www.w3.org/2000/svg","circle");o.classList.add("point");var l=this.pos_x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom))-this.precanvas.getBoundingClientRect().x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom)),c=this.pos_y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom))-this.precanvas.getBoundingClientRect().y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom));o.setAttributeNS(null,"cx",l),o.setAttributeNS(null,"cy",c),o.setAttributeNS(null,"r",this.reroute_width);let d=0;if(this.reroute_fix_curvature){const t=e.parentElement.querySelectorAll(".main-path").length;var a=document.createElementNS("http://www.w3.org/2000/svg","path");if(a.classList.add("main-path"),a.setAttributeNS(null,"d",""),e.parentElement.insertBefore(a,e.parentElement.children[t]),1===t)e.parentElement.appendChild(o);else{const n=Array.from(e.parentElement.children).indexOf(e);d=n,e.parentElement.insertBefore(o,e.parentElement.children[n+t+1])}}else e.parentElement.appendChild(o);const r=t.slice(5),h=this.drawflow.drawflow[this.module].data[r].outputs[i].connections.findIndex((function(e,t){return e.node===n&&e.output===s}));void 0===this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points&&(this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points=[]),this.reroute_fix_curvature?(d>0||this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points!==[]?this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points.splice(d,0,{pos_x:l,pos_y:c}):this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points.push({pos_x:l,pos_y:c}),e.parentElement.querySelectorAll(".main-path").forEach((e,t)=>{e.classList.remove("selected")})):this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points.push({pos_x:l,pos_y:c}),this.dispatch("addReroute",r),this.updateConnectionNodes(t)}removeReroutePoint(e){const t=e.parentElement.classList[2].slice(9),n=e.parentElement.classList[1].slice(13),i=e.parentElement.classList[3],s=e.parentElement.classList[4];let o=Array.from(e.parentElement.children).indexOf(e);const l=t.slice(5),c=this.drawflow.drawflow[this.module].data[l].outputs[i].connections.findIndex((function(e,t){return e.node===n&&e.output===s}));if(this.reroute_fix_curvature){const t=e.parentElement.querySelectorAll(".main-path").length;e.parentElement.children[t-1].remove(),o-=t,o<0&&(o=0)}else o--;this.drawflow.drawflow[this.module].data[l].outputs[i].connections[c].points.splice(o,1),e.remove(),this.dispatch("removeReroute",l),this.updateConnectionNodes(t)}registerNode(e,t,n=null,i=null){this.noderegister[e]={html:t,props:n,options:i}}getNodeFromId(e){var t=this.getModuleFromNodeId(e);return JSON.parse(JSON.stringify(this.drawflow.drawflow[t].data[e]))}getNodesFromName(e){var t=[];const n=this.drawflow.drawflow;return Object.keys(n).map((function(i,s){for(var o in n[i].data)n[i].data[o].name==e&&t.push(n[i].data[o].id)})),t}addNode(e,t,n,i,s,o,l,c,d=!1){if(this.useuuid)var a=this.getUuid();else a=this.nodeId;const r=document.createElement("div");r.classList.add("parent-node");const h=document.createElement("div");h.innerHTML="",h.setAttribute("id","node-"+a),h.classList.add("drawflow-node"),""!=o&&h.classList.add(...o.split(" "));const u=document.createElement("div");u.classList.add("inputs");const p=document.createElement("div");p.classList.add("outputs");const f={};for(var g=0;ge(this.noderegister[c].html,{props:this.noderegister[c].props}),...this.noderegister[c].options}).$mount();_.appendChild(e.$el)}Object.entries(l).forEach((function(e,t){if("object"==typeof e[1])!function e(t,n,i){if(null===t)t=l[n];else t=t[n];null!==t&&Object.entries(t).forEach((function(n,s){if("object"==typeof n[1])e(t,n[0],i+"-"+n[0]);else for(var o=_.querySelectorAll("[df-"+i+"-"+n[0]+"]"),l=0;lt(this.noderegister[e.html].html,{props:this.noderegister[e.html].props}),...this.noderegister[e.html].options}).$mount();c.appendChild(t.$el)}Object.entries(e.data).forEach((function(t,n){if("object"==typeof t[1])!function t(n,i,s){if(null===n)n=e.data[i];else n=n[i];null!==n&&Object.entries(n).forEach((function(e,i){if("object"==typeof e[1])t(n,e[0],s+"-"+e[0]);else for(var o=c.querySelectorAll("[df-"+s+"-"+e[0]+"]"),l=0;l{const a=e.outputs[s].connections[o].node,r=e.outputs[s].connections[o].output,h=i.querySelector(".connection.node_in_node-"+a+".node_out_node-"+e.id+"."+s+"."+r);if(n&&0===d)for(var u=0;u{this.removeSingleConnection(e.id_output,e.id,e.output_class,e.input_class)}),delete this.drawflow.drawflow[n].data[e].inputs[t];const o=[],l=this.drawflow.drawflow[n].data[e].inputs;Object.keys(l).map((function(e,t){o.push(l[e])})),this.drawflow.drawflow[n].data[e].inputs={};const c=t.slice(6);let d=[];if(o.forEach((t,i)=>{t.connections.forEach((e,t)=>{d.push(e)}),this.drawflow.drawflow[n].data[e].inputs["input_"+(i+1)]=t}),d=new Set(d.map(e=>JSON.stringify(e))),d=Array.from(d).map(e=>JSON.parse(e)),this.module===n){this.container.querySelectorAll("#node-"+e+" .inputs .input").forEach((e,t)=>{const n=e.classList[1].slice(6);parseInt(c){this.drawflow.drawflow[n].data[t.node].outputs[t.input].connections.forEach((i,s)=>{if(i.node==e){const o=i.output.slice(6);if(parseInt(c){this.removeSingleConnection(e.id,e.id_input,e.output_class,e.input_class)}),delete this.drawflow.drawflow[n].data[e].outputs[t];const o=[],l=this.drawflow.drawflow[n].data[e].outputs;Object.keys(l).map((function(e,t){o.push(l[e])})),this.drawflow.drawflow[n].data[e].outputs={};const c=t.slice(7);let d=[];if(o.forEach((t,i)=>{t.connections.forEach((e,t)=>{d.push({node:e.node,output:e.output})}),this.drawflow.drawflow[n].data[e].outputs["output_"+(i+1)]=t}),d=new Set(d.map(e=>JSON.stringify(e))),d=Array.from(d).map(e=>JSON.parse(e)),this.module===n){this.container.querySelectorAll("#node-"+e+" .outputs .output").forEach((e,t)=>{const n=e.classList[1].slice(7);parseInt(c){this.drawflow.drawflow[n].data[t.node].inputs[t.output].connections.forEach((i,s)=>{if(i.node==e){const o=i.input.slice(7);if(parseInt(c)-1){this.module===s&&this.container.querySelector(".connection.node_in_node-"+t+".node_out_node-"+e+"."+n+"."+i).remove();var o=this.drawflow.drawflow[s].data[e].outputs[n].connections.findIndex((function(e,n){return e.node==t&&e.output===i}));this.drawflow.drawflow[s].data[e].outputs[n].connections.splice(o,1);var l=this.drawflow.drawflow[s].data[t].inputs[i].connections.findIndex((function(t,i){return t.node==e&&t.input===n}));return this.drawflow.drawflow[s].data[t].inputs[i].connections.splice(l,1),this.dispatch("connectionRemoved",{output_id:e,input_id:t,output_class:n,input_class:i}),!0}return!1}return!1}removeConnectionNodeId(e){const t="node_in_"+e,n="node_out_"+e,i=this.container.querySelectorAll("."+n);for(var s=i.length-1;s>=0;s--){var o=i[s].classList,l=this.drawflow.drawflow[this.module].data[o[1].slice(13)].inputs[o[4]].connections.findIndex((function(e,t){return e.node===o[2].slice(14)&&e.input===o[3]}));this.drawflow.drawflow[this.module].data[o[1].slice(13)].inputs[o[4]].connections.splice(l,1);var c=this.drawflow.drawflow[this.module].data[o[2].slice(14)].outputs[o[3]].connections.findIndex((function(e,t){return e.node===o[1].slice(13)&&e.output===o[4]}));this.drawflow.drawflow[this.module].data[o[2].slice(14)].outputs[o[3]].connections.splice(c,1),i[s].remove(),this.dispatch("connectionRemoved",{output_id:o[2].slice(14),input_id:o[1].slice(13),output_class:o[3],input_class:o[4]})}const d=this.container.querySelectorAll("."+t);for(s=d.length-1;s>=0;s--){o=d[s].classList,c=this.drawflow.drawflow[this.module].data[o[2].slice(14)].outputs[o[3]].connections.findIndex((function(e,t){return e.node===o[1].slice(13)&&e.output===o[4]}));this.drawflow.drawflow[this.module].data[o[2].slice(14)].outputs[o[3]].connections.splice(c,1);l=this.drawflow.drawflow[this.module].data[o[1].slice(13)].inputs[o[4]].connections.findIndex((function(e,t){return e.node===o[2].slice(14)&&e.input===o[3]}));this.drawflow.drawflow[this.module].data[o[1].slice(13)].inputs[o[4]].connections.splice(l,1),d[s].remove(),this.dispatch("connectionRemoved",{output_id:o[2].slice(14),input_id:o[1].slice(13),output_class:o[3],input_class:o[4]})}}getModuleFromNodeId(e){var t;const n=this.drawflow.drawflow;return Object.keys(n).map((function(i,s){Object.keys(n[i].data).map((function(n,s){n==e&&(t=i)}))})),t}addModule(e){this.drawflow.drawflow[e]={data:{}},this.dispatch("moduleCreated",e)}changeModule(e){this.dispatch("moduleChanged",e),this.module=e,this.precanvas.innerHTML="",this.canvas_x=0,this.canvas_y=0,this.pos_x=0,this.pos_y=0,this.mouse_x=0,this.mouse_y=0,this.zoom=1,this.zoom_last_value=1,this.precanvas.style.transform="",this.import(this.drawflow,!1)}removeModule(e){this.module===e&&this.changeModule("Home"),delete this.drawflow.drawflow[e],this.dispatch("moduleRemoved",e)}clearModuleSelected(){this.precanvas.innerHTML="",this.drawflow.drawflow[this.module]={data:{}}}clear(){this.precanvas.innerHTML="",this.drawflow={drawflow:{Home:{data:{}}}}}export(){const e=JSON.parse(JSON.stringify(this.drawflow));return this.dispatch("export",e),e}import(e,t=!0){this.clear(),this.drawflow=JSON.parse(JSON.stringify(e)),this.load(),t&&this.dispatch("import","import")}on(e,t){return"function"!=typeof t?(console.error("The listener callback must be a function, the given type is "+typeof t),!1):"string"!=typeof e?(console.error("The event name must be a string, the given type is "+typeof e),!1):(void 0===this.events[e]&&(this.events[e]={listeners:[]}),void this.events[e].listeners.push(t))}removeListener(e,t){if(!this.events[e])return!1;const n=this.events[e].listeners,i=n.indexOf(t);i>-1&&n.splice(i,1)}dispatch(e,t){if(void 0===this.events[e])return!1;this.events[e].listeners.forEach(e=>{e(t)})}getUuid(){for(var e=[],t=0;t<36;t++)e[t]="0123456789abcdef".substr(Math.floor(16*Math.random()),1);return e[14]="4",e[19]="0123456789abcdef".substr(3&e[19]|8,1),e[8]=e[13]=e[18]=e[23]="-",e.join("")}}}]).default})); \ No newline at end of file diff --git a/app/webroot/js/workflows-editor/workflows-editor.js b/app/webroot/js/workflows-editor/workflows-editor.js new file mode 100644 index 000000000..05fa604d1 --- /dev/null +++ b/app/webroot/js/workflows-editor/workflows-editor.js @@ -0,0 +1,192 @@ +var dotBlockDefault = doT.template(' \ +
\ +
\ +
\ + \ + \ + {{=it.name}} \ + \ + \ + \ + \ +
\ +
{{=it.description}}
\ + {{=it.block_param_html}} \ +
\ +
') + +var dotIF = doT.template(' \ +
\ +
\ +
\ + \ + \ + {{=it.name}} \ + \ +
\ + then \ + else \ +
\ +
\ +
\ +
') + +function sanitizeObject(obj) { + var newObj = {} + for (var key of Object.keys(obj)) { + var newVal = $('

').text(obj[key]).html() + newObj[key] = newVal + } + return newObj +} + + +function initDrawflow() { + editor = new Drawflow($drawflow[0]); + editor.start(); + + $('#block-tabs a').click(function (e) { + e.preventDefault(); + $(this).tab('show'); + }) + + $chosenWorkflows.chosen() + .on('change', function (evt, param) { + // console.log(param); + }); + $chosenBlocks.chosen() + .on('change', function (evt, param) { + // console.log(param); + }); + + $('.sidebar-workflow-block').each(function () { + var $block = $(this) + all_blocks.forEach(function (block) { + if ($block[0].id == block['id']) { + $block.data('block', block) + } + }); + }) + + $('.sidebar-workflow-block').each(function() { + if ($(this).data('block').disabled) { + $(this).addClass('disabled') + } + $(this).draggable({ + helper: "clone", + scroll: false, + disabled: $(this).data('block').disabled, + start: function (event, ui) { + $(this).addClass('disabled') + }, + stop: function (event, ui) { + $(this).removeClass('disabled') + // addNode($(this).data('block'), ui.position) + } + }); + }) + + $canvas.droppable({ + drop: function (event, ui) { + // console.log(event) + // console.log(ui) + addNode(ui.draggable.data('block'), ui.position) + } + }); + +} + +function getTemplateForBlock(block) { + var html = block.name + if (block.html !== undefined) { + html = block.html + } else { + if (block.html_template !== undefined) { + html = window['dot' + block.html_template](block) + } else { + html = dotBlockDefault(block) + } + } + return html +} + +function genBlockParamHtml(block) { + if (!block.params) { + return '' + } + var html = '' + block.params.forEach(function(param) { + paramHtml = '' + switch (param.type) { + case 'input': + paramHtml = genInput(param)[0].outerHTML + break; + case 'select': + paramHtml = genSelect(param)[0].outerHTML + break; + default: + break; + } + html += paramHtml + }) + return html +} + +function genSelect(options) { + var $select = $('') + $input.attr('type', 'text') + if (options.default !== undefined) { + // $input.val(options.default) + $input.attr('value', options.default) // wtf why does it not work?! + } + if (options.placeholder !== undefined) { + $input.attr('placeholder', options.placeholder) + } + return $input +} + + +function addNode(block, position) { + var canvasPosition = $canvas[0].getBoundingClientRect() + var adjsutedPosition = { + left: position.left - canvasPosition.left, + top: position.top, + } + block['block_param_html'] = genBlockParamHtml(block) + var html = getTemplateForBlock(block) + editor.addNode( + block.name, + block.inputs === undefined ? 1 : block.inputs, + block.outputs === undefined ? 1 : block.outputs, + adjsutedPosition.left, + adjsutedPosition.top, + block.class === undefined ? '' : block.class, + block, + html + ); +} \ No newline at end of file From 6670333f10172321fef8dbf0e2d1549462f28b48 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Wed, 4 May 2022 00:00:19 +0200 Subject: [PATCH 008/428] chg: [workflow] Added database migration --- app/Model/AppModel.php | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/Model/AppModel.php b/app/Model/AppModel.php index 5d21e9b29..2e8fbac18 100644 --- a/app/Model/AppModel.php +++ b/app/Model/AppModel.php @@ -89,7 +89,7 @@ class AppModel extends Model 63 => true, 64 => false, 65 => false, 66 => false, 67 => false, 68 => false, 69 => false, 70 => false, 71 => true, 72 => true, 73 => false, 74 => false, 75 => false, 76 => true, 77 => false, 78 => false, 79 => false, 80 => false, - 81 => false, 82 => false, 83 => false, 84 => false, 85 => false, + 81 => false, 82 => false, 83 => false, 84 => false, 85 => false, 86 => false, ); public $advanced_updates_description = array( @@ -1684,6 +1684,23 @@ class AppModel extends Model $this->__addIndex('cryptographic_keys', 'parent_type'); $this->__addIndex('cryptographic_keys', 'fingerprint'); break; + case 86: + $sqlArray[] = "CREATE TABLE IF NOT EXISTS `workflows` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `uuid` varchar(40) COLLATE utf8_bin NOT NULL , + `name` varchar(191) NOT NULL, + `description` varchar(191) NOT NULL, + `timestamp` int(11) NOT NULL DEFAULT 0, + `user_id` int(11) NOT NULL, + `org_id` int(11) NOT NULL, + `data` text, + PRIMARY KEY (`id`), + INDEX `uuid` (`uuid`), + INDEX `name` (`name`), + INDEX `user_id` (`user_id`), + INDEX `org_id` (`org_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"; + break; case 'fixNonEmptySharingGroupID': $sqlArray[] = 'UPDATE `events` SET `sharing_group_id` = 0 WHERE `distribution` != 4;'; $sqlArray[] = 'UPDATE `attributes` SET `sharing_group_id` = 0 WHERE `distribution` != 4;'; From 45e5be415b3a7378c915504d648d23dff9be7e4a Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Wed, 4 May 2022 00:00:39 +0200 Subject: [PATCH 009/428] chg: [workflow:editor] Added TODO --- app/webroot/js/workflows-editor/workflows-editor.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/webroot/js/workflows-editor/workflows-editor.js b/app/webroot/js/workflows-editor/workflows-editor.js index 05fa604d1..86397e95f 100644 --- a/app/webroot/js/workflows-editor/workflows-editor.js +++ b/app/webroot/js/workflows-editor/workflows-editor.js @@ -173,10 +173,16 @@ function genInput(options) { function addNode(block, position) { var canvasPosition = $canvas[0].getBoundingClientRect() + var adjsutedPosition = { left: position.left - canvasPosition.left, top: position.top, } + + // TODO: Take into account zoon level + // pos_x = pos_x * (editor.precanvas.clientWidth / (editor.precanvas.clientWidth * editor.zoom)) - (editor.precanvas.getBoundingClientRect().x * (editor.precanvas.clientWidth / (editor.precanvas.clientWidth * editor.zoom))); + // pos_y = pos_y * (editor.precanvas.clientHeight / (editor.precanvas.clientHeight * editor.zoom)) - (editor.precanvas.getBoundingClientRect().y * (editor.precanvas.clientHeight / (editor.precanvas.clientHeight * editor.zoom))); + block['block_param_html'] = genBlockParamHtml(block) var html = getTemplateForBlock(block) editor.addNode( From 2f7bf4fe49105d3599b2dbaf5b0c4884a2468249 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Wed, 4 May 2022 00:01:02 +0200 Subject: [PATCH 010/428] chg: [workflow] Started CRUD --- app/Controller/WorkflowsController.php | 112 ++++++++++++++++++++++++- app/Model/Workflow.php | 36 ++++++++ app/View/Workflows/add.ctp | 34 ++++++++ app/View/Workflows/index.ctp | 85 +++++++++++++++++++ 4 files changed, 263 insertions(+), 4 deletions(-) create mode 100644 app/Model/Workflow.php diff --git a/app/Controller/WorkflowsController.php b/app/Controller/WorkflowsController.php index b371af17f..98d5e1091 100644 --- a/app/Controller/WorkflowsController.php +++ b/app/Controller/WorkflowsController.php @@ -8,26 +8,130 @@ class WorkflowsController extends AppController public function index() { - return $this->RestResponse->viewData([]); + $params = [ + 'filters' => ['name', 'uuid'], + 'quickFilters' => ['name', 'uuid'] + ]; + $this->CRUD->index($params); + if ($this->IndexFilter->isRest()) { + return $this->restResponsePayload; + } + $this->set('menuData', array('menuList' => 'workflows', 'menuItem' => 'index')); } public function add() { - + $currentUser = $this->Auth->user(); + $params = [ + 'beforeSave' => function ($data) use ($currentUser) { + $data['Workflow']['uuid'] = CakeText::uuid(); + $data['Workflow']['user_id'] = $currentUser['id']; + $data['Workflow']['org_id'] = $currentUser['org_id']; + if (!isset($data['Workflow']['description'])) { + $data['Workflow']['description'] = ''; + } + if (empty($data['Workflow']['data'])) { + $data['Workflow']['data'] = '{}'; + } + return $data; + }, + 'redirect' => [ + 'action' => 'index', + ] + ]; + $this->CRUD->add($params); + if ($this->restResponsePayload) { + return $this->restResponsePayload; + } + $this->set('menuData', array('menuList' => 'workflows', 'menuItem' => 'add')); } - public function edit() + public function edit($id) { + $this->set('id', $id); + $params = [ + 'fields' => ['name', 'description', 'data'], + 'redirect' => [ + 'action' => 'index', + ] + ]; + $workflow = $this->Workflow->fetchWorkflow($this->Auth->user(), $id); + if (empty($workflow)) { + throw new NotFoundException(__('Invalid workflow')); + } + $this->CRUD->edit($id, $params); + if ($this->IndexFilter->isRest()) { + return $this->restResponsePayload; + } + $this->set('menuData', array('menuList' => 'workflows', 'menuItem' => 'edit')); + $this->render('add'); } - public function delete() + public function delete($id) { + $params = [ + 'redirect' => [ + 'action' => 'index', + ] + ]; + $this->CRUD->delete($id, $params); + if ($this->IndexFilter->isRest()) { + return $this->restResponsePayload; + } + } + public function view($id) + { + $this->CRUD->view($id, [ + 'contain' => ['Organisation', 'User'] + ]); + if ($this->IndexFilter->isRest()) { + return $this->restResponsePayload; + } + $this->set('id', $id); + $this->set('menuData', array('menuList' => 'workflows', 'menuItem' => 'view')); } public function editor() { + $modules = $this->Workflow->getModules(); + $this->set('modules', $modules); + } + private function __getSuccessResponseBasedOnContext($message, $data = null, $action = '', $id = false, $redirect = array()) + { + if ($this->_isRest()) { + if (!is_null($data)) { + return $this->RestResponse->viewData($data, $this->response->type()); + } else { + return $this->RestResponse->saveSuccessResponse($this->EventReport->alias, $action, $id, false, $message); + } + } elseif ($this->request->is('ajax')) { + return $this->RestResponse->saveSuccessResponse($this->EventReport->alias, $action, $id, false, $message, $data); + } else { + $this->Flash->success($message); + $this->redirect($redirect); + } + return; + } + + private function __getFailResponseBasedOnContext($message, $data = null, $action = '', $id = false, $redirect = array()) + { + if (is_array($message)) { + $message = implode(', ', $message); + } + if ($this->_isRest()) { + if ($data !== null) { + return $this->RestResponse->viewData($data, $this->response->type()); + } else { + return $this->RestResponse->saveFailResponse('EventReport', $action, $id, $message); + } + } elseif ($this->request->is('ajax')) { + return $this->RestResponse->saveFailResponse('EventReport', $action, $id, $message, false, $data); + } else { + $this->Flash->error($message); + $this->redirect($this->referer()); + } } } diff --git a/app/Model/Workflow.php b/app/Model/Workflow.php new file mode 100644 index 000000000..e80a7d85b --- /dev/null +++ b/app/Model/Workflow.php @@ -0,0 +1,36 @@ + [ + 'className' => 'User', + 'foreignKey' => 'user_id', + ], + 'Organisation' => [ + 'className' => 'Organisation', + 'foreignKey' => 'org_id' + ] + ]; + + public $validate = array( + ); + + public function beforeValidate($options = array()) + { + parent::beforeValidate(); + return true; + } + + public function getModules(): array + { + return []; + } +} \ No newline at end of file diff --git a/app/View/Workflows/add.ctp b/app/View/Workflows/add.ctp index e69de29bb..bd6a68922 100644 --- a/app/View/Workflows/add.ctp +++ b/app/View/Workflows/add.ctp @@ -0,0 +1,34 @@ +request->params['action'] === 'edit' ? true : false; +$fields = [ + [ + 'field' => 'name', + 'class' => 'span6', + ], + [ + 'field' => 'description', + 'type' => 'textarea', + 'class' => 'input span6', + ], + [ + 'field' => 'data', + 'type' => 'textarea', + 'class' => 'input span6' + ] +]; +echo $this->element('genericElements/Form/genericForm', [ + 'data' => [ + 'description' => false, + 'model' => 'Workflow', + 'title' => $edit ? __('Edit Workflow') : __('Add Workflow'), + 'fields' => $fields, + 'submit' => [ + 'action' => $this->request->params['action'], + 'ajaxSubmit' => 'submitGenericFormInPlace();' + ] + ] +]); + +if (!$ajax) { + echo $this->element('/genericElements/SideMenu/side_menu', $menuData); +} diff --git a/app/View/Workflows/index.ctp b/app/View/Workflows/index.ctp index e69de29bb..6f6689cb6 100644 --- a/app/View/Workflows/index.ctp +++ b/app/View/Workflows/index.ctp @@ -0,0 +1,85 @@ + __('ID'), + 'sort' => 'Workflow.id', + 'data_path' => 'Workflow.id' + ], + [ + 'name' => __('Owner Org'), + 'sort' => 'Organisation', + 'data_path' => 'Organisation', + 'element' => 'org' + ], + [ + 'name' => __('Name'), + 'sort' => 'Workflow.name', + 'data_path' => 'Workflow.name' + ], + [ + 'name' => __('Description'), + 'sort' => 'Workflow.description', + 'data_path' => 'Workflow.description' + ], + ]; + + + echo $this->element('genericElements/IndexTable/scaffold', [ + 'scaffold_data' => [ + 'data' => [ + 'data' => $data, + 'top_bar' => [ + 'pull' => 'right', + 'children' => [ + [ + 'type' => 'simple', + 'children' => [ + 'data' => [ + 'type' => 'simple', + 'text' => __('Add Workflow'), + 'class' => 'btn btn-primary', + 'onClick' => 'openGenericModal', + 'onClickParams' => [ + sprintf( + '%s/workflows/add', + $baseurl + ) + ] + ] + ] + ], + [ + 'type' => 'search', + 'button' => __('Filter'), + 'placeholder' => __('Enter value to search'), + 'data' => '', + 'searchKey' => 'quickFilter' + ] + ] + ], + 'fields' => $fields, + 'title' => __('Workflows'), + 'description' => __('You can create workflows relying on pipeline hooks to that can listen to triggers and then perform actions depending on some conditions'), + 'actions' => [ + [ + 'url' => $baseurl . '/workflows/editor/', + 'url_params_data_paths' => ['Workflow.id'], + 'icon' => 'code' + ], + [ + 'url' => $baseurl . '/workflows/edit', + 'url_params_data_paths' => ['Workflow.id'], + 'icon' => 'edit' + ], + [ + 'onclick' => sprintf( + 'openGenericModal(\'%s/workflows/delete/[onclick_params_data_path]\');', + $baseurl + ), + 'onclick_params_data_path' => 'Workflow.id', + 'icon' => 'trash' + ] + ] + ] + ] + ]); From a4cba3fdc6f1d34c8c47ec27466e78c12c1ad4fa Mon Sep 17 00:00:00 2001 From: iglocska Date: Wed, 4 May 2022 01:23:13 +0200 Subject: [PATCH 011/428] new: [modules] action module type added - hooking function type - add a hooking point via `$this->Module->executeActions($hook_name, $user, $input, $logging_options, $error)` - will execute the enabled modules for the hook name and depending on the module's type (blocking/not blocking) allow for breaking the execution when false is returned. - For a sample skeleton, see the misp-modules project --- app/Controller/EventsController.php | 41 ++++++---- app/Controller/ServersController.php | 9 +- app/Model/Event.php | 118 +++++++++++++++++---------- app/Model/Module.php | 99 +++++++++++++++++++++- app/Model/Server.php | 47 +++++++++-- 5 files changed, 246 insertions(+), 68 deletions(-) diff --git a/app/Controller/EventsController.php b/app/Controller/EventsController.php index 8f12bfe03..f99eeec41 100644 --- a/app/Controller/EventsController.php +++ b/app/Controller/EventsController.php @@ -2961,8 +2961,12 @@ class EventsController extends AppController $result = $this->Event->publishRouter($event['Event']['id'], null, $this->Auth->user()); if (!Configure::read('MISP.background_jobs')) { if (!is_array($result)) { - // redirect to the view event page - $message = __('Event published without alerts'); + if ($result === true) { + $message = __('Event published without alerts'); + } else { + $message = __('Event publishing failed due to a blocking module failing. The reason for the failure: %s', $result); + $errors['Module'] = 'Module failure.'; + } } else { $lastResult = array_pop($result); $resultString = (count($result) > 0) ? implode(', ', $result) . ' and ' . $lastResult : $lastResult; @@ -2970,11 +2974,6 @@ class EventsController extends AppController $message = __('Event published but not pushed to %s, re-try later. If the issue persists, make sure that the correct sync user credentials are used for the server link and that the sync user on the remote server has authentication privileges.', $resultString); } } else { - // update the DB to set the published flag - // for background jobs, this should be done already - $event['Event']['published'] = 1; - $event['Event']['publish_timestamp'] = time(); - $this->Event->save($event, true, ['id', 'published', 'publish_timestamp', 'info']); // info field is required because of SysLogLogableBehavior $message = 'Job queued'; } if ($this->_isRest()) { @@ -2984,7 +2983,11 @@ class EventsController extends AppController return $this->RestResponse->saveSuccessResponse('Events', 'publish', $event['Event']['id'], false, $message); } } else { - $this->Flash->success($message); + if (!empty($errors)) { + $this->Flash->error($message); + } else { + $this->Flash->success($message); + } $this->redirect(array('action' => 'view', $event['Event']['id'])); } } else { @@ -3009,11 +3012,16 @@ class EventsController extends AppController // Performs all the actions required to publish an event $result = $this->Event->publishRouter($event['Event']['id'], null, $this->Auth->user()); if (!is_array($result)) { - // redirect to the view event page - if (Configure::read('MISP.background_jobs')) { - $message = 'Job queued.'; + if ($result === true) { + // redirect to the view event page + if (Configure::read('MISP.background_jobs')) { + $message = 'Job queued.'; + } else { + $message = 'Email sent to all participants.'; + } } else { - $message = 'Email sent to all participants.'; + $message = $result; + $errors['Module'] = 'Module failure.'; } } else { $lastResult = array_pop($result); @@ -3025,9 +3033,14 @@ class EventsController extends AppController // Performs all the actions required to publish an event $result = $this->Event->publishRouter($event['Event']['id'], null, $this->Auth->user()); if (!is_array($result)) { + if ($result === true) { + $message = __('Published but no email sent given GnuPG is not configured.'); + $errors['GnuPG'] = 'GnuPG not set up.'; + } else { + $message = $result; + $errors['Module'] = 'Module failure.'; + } // redirect to the view event page - $message = __('Published but no email sent given GnuPG is not configured.'); - $errors['GnuPG'] = 'GnuPG not set up.'; } else { $lastResult = array_pop($result); $resultString = (count($result) > 0) ? implode(', ', $result) . ' and ' . $lastResult : $lastResult; diff --git a/app/Controller/ServersController.php b/app/Controller/ServersController.php index bd38bd082..ff157fd3a 100644 --- a/app/Controller/ServersController.php +++ b/app/Controller/ServersController.php @@ -948,7 +948,13 @@ class ServersController extends AppController public function serverSettingsReloadSetting($setting, $id) { $pathToSetting = explode('.', $setting); - if (strpos($setting, 'Plugin.Enrichment') !== false || strpos($setting, 'Plugin.Import') !== false || strpos($setting, 'Plugin.Export') !== false || strpos($setting, 'Plugin.Cortex') !== false) { + if ( + strpos($setting, 'Plugin.Enrichment') !== false || + strpos($setting, 'Plugin.Import') !== false || + strpos($setting, 'Plugin.Export') !== false || + strpos($setting, 'Plugin.Cortex') !== false || + strpos($setting, 'Plugin.Action') !== false + ) { $settingObject = $this->Server->getCurrentServerSettings(); } else { $settingObject = $this->Server->serverSettings; @@ -1440,7 +1446,6 @@ class ServersController extends AppController } $this->set('id', $id); } - $setting = $this->Server->getSettingData($settingName); if ($setting === false) { throw new NotFoundException(__('Setting %s is invalid.', $settingName)); diff --git a/app/Model/Event.php b/app/Model/Event.php index 8728be925..a120e258f 100755 --- a/app/Model/Event.php +++ b/app/Model/Event.php @@ -4502,6 +4502,25 @@ class Event extends AppModel if (empty($event)) { return false; } + $hostOrg = $this->Org->getHostOrg(); + $user = [ + 'id' => 0, + 'org_id' => $hostOrg['Org']['id'], + 'Role' => array('perm_sync' => 0, 'perm_audit' => 0, 'perm_site_admin' => 0), + 'Organisation' => $hostOrg['Org'] + ]; + $fullEvent = null; + if (Configure::read('Plugin.Action_services_enable')) { + $fullEvent = $this->fetchEvent($user, [ + 'eventid' => $id, + 'includeAttachments' => 1 + ]); + $this->Module = ClassRegistry::init('Module'); + $error = null; + if (!$this->Module->executeActions('publish', $user, $fullEvent, ['model' => 'Event', 'id' => $id], $error)) { + return $error; + } + } if ($jobId) { $this->Behaviors->unload('SysLogLogable.SysLogLogable'); } else { @@ -4514,50 +4533,16 @@ class Event extends AppModel $event['Event']['skip_kafka'] = 1; $this->save($event, array('fieldList' => $fieldList)); } - $pubToZmq = Configure::read('Plugin.ZeroMQ_enable'); $kafkaTopic = Configure::read('Plugin.Kafka_event_publish_notifications_topic'); - $pubToKafka = Configure::read('Plugin.Kafka_enable') && Configure::read('Plugin.Kafka_event_publish_notifications_enable') && !empty($kafkaTopic); - if ($pubToZmq || $pubToKafka) { - $hostOrgId = Configure::read('MISP.host_org_id'); - if (!empty($hostOrgId)) { - $hostOrg = $this->Org->find('first', [ - 'recursive' => -1, - 'conditions' => [ - 'id' => $hostOrgId - ] - ] - ); - } - if (empty($hostOrg)) { - $hostOrg = $this->Org->find('first', [ - 'recursive' => -1, - 'order' => ['id ASC'] - ]); - $hostOrgId = $hostOrg['Org']['id']; - } - $user = array('org_id' => $hostOrgId, 'Role' => array('perm_sync' => 0, 'perm_audit' => 0, 'perm_site_admin' => 0), 'Organisation' => $hostOrg['Org']); - if ($pubToZmq) { - $params = array('eventid' => $id); - if (Configure::read('Plugin.ZeroMQ_include_attachments')) { - $params['includeAttachments'] = 1; - } - $fullEvent = $this->fetchEvent($user, $params); - if (!empty($fullEvent)) { - $pubSubTool = $this->getPubSubTool(); - $pubSubTool->publishEvent($fullEvent[0], 'publish'); - } - } - if ($pubToKafka) { - $params = array('eventid' => $id); - if (Configure::read('Plugin.Kafka_include_attachments')) { - $params['includeAttachments'] = 1; - } - $fullEvent = $this->fetchEvent($user, $params); - if (!empty($fullEvent)) { - $kafkaPubTool = $this->getKafkaPubTool(); - $kafkaPubTool->publishJson($kafkaTopic, $fullEvent[0], 'publish'); - } - } + if (Configure::read('Plugin.ZeroMQ_enable')) { + $this->publishEventToZmq($id, $user, $fullEvent); + } + if ( + Configure::read('Plugin.Kafka_enable') && + Configure::read('Plugin.Kafka_event_publish_notifications_enable') && + !empty($kafkaTopic) + ) { + $this->publishEventToKafka($id, $user, $fullEvent, $kafkaTopic); } return $this->uploadEventToServersRouter($id, $passAlong); } @@ -7594,4 +7579,51 @@ class Event extends AppModel ), ); } + + private function __prepareEventForPubSub($id, $user, &$fullEvent) + { + if ($fullEvent) { + if (empty(Configure::read('Plugin.ZeroMQ_include_attachments'))) { + foreach ($fullEvent[0]['Attribute'] as $k => $attribute) { + if (isset($attribute['data'])) { + unset($fullEvent[0]['Attribute'][$k]['data']); + } + } + foreach ($fullEvent[0]['Object'] as $k => $object) { + foreach ($object['Attribute'] as $k2 => $attribute) { + if (isset($attribute['data'])) { + unset($fullEvent[0]['Object'][$k]['Attribute'][$k2]['data']); + } + } + } + } + } else { + $params = [ + 'eventid' => $id + ]; + if (Configure::read('Plugin.ZeroMQ_include_attachments')) { + $params['includeAttachments'] = 1; + } + $fullEvent = $this->fetchEvent($user, $params); + } + return $fullEvent; + } + + public function publishEventToZmq($id, $user, &$fullEvent) + { + $fullEvent = $this->__prepareEventForPubSub($id, $user, $fullEvent); + if (!empty($fullEvent)) { + $pubSubTool = $this->getPubSubTool(); + $pubSubTool->publishEvent($fullEvent[0], 'publish'); + } + } + + public function publishEventToKafka($id, $user, &$fullEvent, $kafkaTopic) + { + $fullEvent = $this->__prepareEventForPubSub($id, $user, $fullEvent); + if (!empty($fullEvent)) { + $kafkaPubTool = $this->getKafkaPubTool(); + $kafkaPubTool->publishJson($kafkaTopic, $fullEvent[0], 'publish'); + } + } } diff --git a/app/Model/Module.php b/app/Model/Module.php index 003e050cd..c5a23f03f 100644 --- a/app/Model/Module.php +++ b/app/Model/Module.php @@ -10,12 +10,14 @@ class Module extends AppModel 'Enrichment' => array('hover', 'expansion'), 'Import' => array('import'), 'Export' => array('export'), + 'Action' => array('action'), 'Cortex' => array('cortex') ); private $__typeToFamily = array( 'Import' => 'Import', 'Export' => 'Export', + 'Action' => 'Action', 'hover' => 'Enrichment', 'expansion' => 'Enrichment', 'Cortex' => 'Cortex' @@ -131,6 +133,8 @@ class Module extends AppModel $output['Import'] = $temp['name']; } elseif (isset($temp['meta']['module-type']) && in_array('export', $temp['meta']['module-type'])) { $output['Export'] = $temp['name']; + } elseif (isset($temp['meta']['module-type']) && in_array('action', $temp['meta']['module-type'])) { + $output['Action'] = $temp['name']; } else { foreach ($temp['mispattributes']['input'] as $input) { if (!isset($temp['meta']['module-type']) || (in_array('expansion', $temp['meta']['module-type']) || in_array('cortex', $temp['meta']['module-type']))) { @@ -290,13 +294,49 @@ class Module extends AppModel foreach ($modules as $module) { if (array_intersect($this->__validTypes[$moduleFamily], $module['meta']['module-type'])) { $moduleSettings = [ - array('name' => 'enabled', 'type' => 'boolean'), - array('name' => 'restrict', 'type' => 'orgs') + [ + 'name' => 'enabled', + 'type' => 'boolean', + 'description' => empty($module['meta']['description']) ? '' : $module['meta']['description'] + ] ]; + if ($moduleFamily === 'Action') { + $moduleSettings[] = [ + 'name' => 'weight', + 'type' => 'numeric', + 'value' => 0, + 'description' => __('Set a weight (via an integer) for the module, determining its position in the order of executed modules. Higher values are taken first.'), + 'test' => 'testForNumeric', + 'null' => true + ]; + } else { + $moduleSettings[] = [ + 'name' => 'restrict', + 'type' => 'orgs', + 'description' => __('Restrict the use of this module to an organisation.') + ]; + } if (isset($module['meta']['config'])) { foreach ($module['meta']['config'] as $key => $value) { - if (is_string($key)) { - $moduleSettings[] = array('name' => $key, 'type' => 'string', 'description' => $value); + if (is_array($value)) { + $name = is_string($key) ? $key : $value['name']; + $moduleSettings[] = [ + 'name' => $name, + 'type' => isset($value['type']) ? $value['type'] : 'string', + 'test' => isset($value['test']) ? $value['test'] : null, + 'description' => isset($value['description']) ? $value['description'] : null, + 'null' => isset($value['null']) ? $value['null'] : null, + 'test' => isset($value['test']) ? $value['test'] : null, + 'bigField' => isset($value['bigField']) ? $value['bigField'] : false, + 'cli_only' => isset($value['cli_only']) ? $value['cli_only'] : false, + 'redacted' => isset($value['redacted']) ? $value['redacted'] : false + ]; + } else if (is_string($key)) { + $moduleSettings[] = [ + 'name' => $key, + 'type' => 'string', + 'description' => $value + ]; } else { $moduleSettings[] = array('name' => $value, 'type' => 'string'); } @@ -308,4 +348,55 @@ class Module extends AppModel } return $result; } + + public function executeActions($type, $user, $input, $logData, &$error = null) + { + $modules = $this->getEnabledModules($user, null, $moduleFamily = 'Action'); + $sorted_modules = []; + foreach ($modules['modules'] as $k => &$module) { + if (!in_array($type, $module['mispattributes']['hooks'])) { + //unset($modules['modules'][$k]); + continue; + } + $settingPath = 'Plugin.' . $module['name'] . '_'; + $module['weight'] = Configure::check($settingPath . 'weight') ? Configure::read($settingPath . 'weight') : 0; + $module['filters'] = Configure::check($settingPath . 'filters') ? json_decode(Configure::read($settingPath . 'filters'), true) : []; + foreach ($module['meta']['config'] as $settingName => $settingData) { + $module['config'][$settingName] = Configure::check($settingPath . $settingName) ? Configure::read($settingPath . $settingName) : $settingData['value']; + } + $sorted_modules[$module['weight']][] = $module; + } + krsort($sorted_modules); + foreach ($sorted_modules as $weight => $modules) { + foreach ($modules as $module) { + $data = [ + 'module' => $module['name'], + 'config' => empty($module['config']) ? [] : $module['config'], + 'data' => $input + ]; + $result = $this->queryModuleServer($data, false, 'Action'); + if (!empty($result['error'])) { + $this->loadLog()->createLogEntry( + 'SYSTEM', + 'warning', + empty($logData['model']) ? 'Module' : $logData['model'], + empty($logData['id']) ? 0 : $logData['id'], + sprintf( + 'Executing %s action module on failed.', + $type + ), + sprintf( + 'Returned error: %s', + $result['error'] + ) + ); + } + if (!empty($module['mispattributes']['blocking']) && (empty($result['data']) || !empty($result['error']))) { + $error = empty($result['error']) ? __('Execution failed for module %s.', $module['name']) : $result['error']; + return false; + } + } + } + return true; + } } diff --git a/app/Model/Server.php b/app/Model/Server.php index 463ffff47..08f878eff 100644 --- a/app/Model/Server.php +++ b/app/Model/Server.php @@ -1226,7 +1226,7 @@ class Server extends AppModel public function getCurrentServerSettings() { $serverSettings = $this->serverSettings; - $moduleTypes = array('Enrichment', 'Import', 'Export', 'Cortex'); + $moduleTypes = array('Enrichment', 'Import', 'Export', 'Action', 'Cortex'); return $this->readModuleSettings($serverSettings, $moduleTypes); } @@ -1248,6 +1248,15 @@ class Server extends AppModel $setting['test'] = 'testBool'; $setting['type'] = 'boolean'; $setting['description'] = __('Enable or disable the %s module.', $module); + if (!empty($result['description'])) { + $setting['description'] = sprintf( + "[%s%s%s] %s", + '', + $setting['description'], + '', + $result['description'] + ); + } $setting['value'] = false; } elseif ($result['type'] === 'orgs') { $setting['description'] = __('Restrict the %s module to the given organisation.', $module); @@ -1258,10 +1267,10 @@ class Server extends AppModel return $this->loadLocalOrganisations(); }; } else { - $setting['test'] = 'testForEmpty'; - $setting['type'] = 'string'; + $setting['test'] = isset($result['test']) ? $result['test'] : 'testForEmpty'; + $setting['type'] = isset($result['type']) ? $result['type'] : 'string'; $setting['description'] = isset($result['description']) ? $result['description'] : __('Set this required module specific setting.'); - $setting['value'] = ''; + $setting['value'] = isset($result['value']) ? $result['value'] : ''; } $serverSettings['Plugin'][$moduleType . '_' . $module . '_' . $result['name']] = $setting; } @@ -2096,7 +2105,7 @@ class Server extends AppModel // This is just hack to reset opcache, so for next request cache will be reloaded. $this->opcacheResetConfig(); - if (strpos($settingName, 'Plugin.Enrichment') !== false || strpos($settingName, 'Plugin.Import') !== false || strpos($settingName, 'Plugin.Export') !== false || strpos($settingName, 'Plugin.Cortex') !== false) { + if (strpos($settingName, 'Plugin.Enrichment') !== false || strpos($settingName, 'Plugin.Import') !== false || strpos($settingName, 'Plugin.Export') !== false || strpos($settingName, 'Plugin.Cortex') !== false || strpos($settingName, 'Plugin.Action') !== false) { $serverSettings = $this->getCurrentServerSettings(); } else { $serverSettings = $this->serverSettings; @@ -6924,6 +6933,34 @@ class Server extends AppModel 'test' => 'testForEmpty', 'type' => 'numeric' ), + 'Action_services_url' => array( + 'level' => 1, + 'description' => __('The url used to access the action services. By default, it is accessible at http://127.0.0.1:6666'), + 'value' => 'http://127.0.0.1', + 'test' => 'testForEmpty', + 'type' => 'string' + ), + 'Action_services_port' => array( + 'level' => 1, + 'description' => __('The port used to access the action services. By default, it is accessible at 127.0.0.1:6666'), + 'value' => '6666', + 'test' => 'testForPortNumber', + 'type' => 'numeric' + ), + 'Action_services_enable' => array( + 'level' => 0, + 'description' => __('Enable/disable the action services'), + 'value' => false, + 'test' => 'testBool', + 'type' => 'boolean' + ), + 'Action_timeout' => array( + 'level' => 1, + 'description' => __('Set a timeout for the action services'), + 'value' => 10, + 'test' => 'testForEmpty', + 'type' => 'numeric' + ), 'Enrichment_hover_enable' => array( 'level' => 0, 'description' => __('Enable/disable the hover over information retrieved from the enrichment modules'), From a77d7ef34e5ed8123ce9d7fd83cb263c55f40a5a Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Wed, 4 May 2022 10:43:44 +0200 Subject: [PATCH 012/428] chg: [side_menu:workflows] Added workflow quick links --- .../genericElements/SideMenu/side_menu.ctp | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/app/View/Elements/genericElements/SideMenu/side_menu.ctp b/app/View/Elements/genericElements/SideMenu/side_menu.ctp index 7ba3d61f8..85f8f00c9 100644 --- a/app/View/Elements/genericElements/SideMenu/side_menu.ctp +++ b/app/View/Elements/genericElements/SideMenu/side_menu.ctp @@ -1596,7 +1596,7 @@ $divider = $this->element('/genericElements/SideMenu/side_menu_divider'); } break; - case 'api': + case 'api': echo $this->element('/genericElements/SideMenu/side_menu_link', array( 'element_id' => 'openapi', 'url' => $baseurl . '/api/openapi', @@ -1610,6 +1610,39 @@ $divider = $this->element('/genericElements/SideMenu/side_menu_divider'); )); } break; + + case 'workflows': + echo $this->element('/genericElements/SideMenu/side_menu_link', array( + 'element_id' => 'index', + 'url' => '/workflows/index', + 'text' => __('List Worflows') + )); + if ($isAclAdd) { + echo $this->element('/genericElements/SideMenu/side_menu_link', array( + 'element_id' => 'add', + 'text' => __('Add Workflow'), + 'url' => '/workflows/add', + )); + } + if ($menuItem === 'view' || $menuItem === 'edit') { + echo $this->element('/genericElements/SideMenu/side_menu_link', array( + 'element_id' => 'view', + 'url' => '/workflows/view/' . h($id), + 'text' => __('View Workflow') + )); + echo $this->element('/genericElements/SideMenu/side_menu_link', array( + 'element_id' => 'edit', + 'url' => '/workflows/edit/' . h($id), + 'text' => __('Edit Workflow') + )); + echo $this->element('/genericElements/SideMenu/side_menu_link', array( + 'url' => '/admin/audit_logs/index/model:Workflows/model_id:' . h($id), + 'text' => __('View worflow history'), + 'requirement' => Configure::read('MISP.log_new_audit') && $canAccess('auditLogs', 'admin_index'), + )); + } + break; + } ?> From 503f2aa08d59f7ab4145906497c762a2c249b7b2 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Wed, 4 May 2022 10:44:18 +0200 Subject: [PATCH 013/428] chg: [workflows] Added more endpoints for CRUD --- app/Controller/WorkflowsController.php | 19 ++-- app/Model/Workflow.php | 125 ++++++++++++++++++++++++- app/View/Workflows/view.ctp | 42 +++++++++ 3 files changed, 173 insertions(+), 13 deletions(-) create mode 100644 app/View/Workflows/view.ctp diff --git a/app/Controller/WorkflowsController.php b/app/Controller/WorkflowsController.php index 98d5e1091..1f6b15718 100644 --- a/app/Controller/WorkflowsController.php +++ b/app/Controller/WorkflowsController.php @@ -4,13 +4,15 @@ App::uses('AppController', 'Controller'); class WorkflowsController extends AppController { public $components = array( + 'RequestHandler' ); public function index() { $params = [ 'filters' => ['name', 'uuid'], - 'quickFilters' => ['name', 'uuid'] + 'quickFilters' => ['name', 'uuid'], + 'contain' => ['Organisation'] ]; $this->CRUD->index($params); if ($this->IndexFilter->isRest()) { @@ -50,15 +52,10 @@ class WorkflowsController extends AppController { $this->set('id', $id); $params = [ + 'conditions' => $this->Workflow->buildACLConditions($this->Auth->user()), 'fields' => ['name', 'description', 'data'], - 'redirect' => [ - 'action' => 'index', - ] + 'redirect' => ['action' => 'view', $id], ]; - $workflow = $this->Workflow->fetchWorkflow($this->Auth->user(), $id); - if (empty($workflow)) { - throw new NotFoundException(__('Invalid workflow')); - } $this->CRUD->edit($id, $params); if ($this->IndexFilter->isRest()) { return $this->restResponsePayload; @@ -71,9 +68,8 @@ class WorkflowsController extends AppController public function delete($id) { $params = [ - 'redirect' => [ - 'action' => 'index', - ] + 'conditions' => $this->Workflow->buildACLConditions($this->Auth->user()), + 'redirect' => ['action' => 'index'] ]; $this->CRUD->delete($id, $params); if ($this->IndexFilter->isRest()) { @@ -84,6 +80,7 @@ class WorkflowsController extends AppController public function view($id) { $this->CRUD->view($id, [ + 'conditions' => $this->Workflow->buildACLConditions($this->Auth->user()), 'contain' => ['Organisation', 'User'] ]); if ($this->IndexFilter->isRest()) { diff --git a/app/Model/Workflow.php b/app/Model/Workflow.php index e80a7d85b..d608b814f 100644 --- a/app/Model/Workflow.php +++ b/app/Model/Workflow.php @@ -20,12 +20,77 @@ class Workflow extends AppModel ] ]; - public $validate = array( - ); + public $validate = [ + 'value' => [ + 'stringNotEmpty' => [ + 'rule' => ['stringNotEmpty'] + ] + ], + 'uuid' => [ + 'uuid' => [ + 'rule' => 'uuid', + 'message' => 'Please provide a valid RFC 4122 UUID' + ], + 'unique' => [ + 'rule' => 'isUnique', + 'message' => 'The UUID provided is not unique', + 'required' => 'create' + ] + ], + ]; + + public $defaultContain = [ + 'Organisation', + 'User' + ]; public function beforeValidate($options = array()) { parent::beforeValidate(); + if (empty($this->data['Workflow']['data'])) { + $this->data['Workflow']['data'] = []; + } + $this->data['Workflow']['data'] = JsonTool::encode($this->data['Workflow']['data']); + return true; + } + + public function afterFind($results, $primary = false) + { + foreach ($results as $k => $result) { + if (empty($result['Workflow']['data'])) { + $result['Workflow']['data'] = '[]'; + } + $results[$k]['Workflow']['data'] = JsonTool::decode($result['Workflow']['data']); + } + return $results; + } + + /** + * buildACLConditions Generate ACL conditions for viewing the report + * + * @param array $user + * @return array + */ + public function buildACLConditions(array $user) + { + $conditions = []; + if (!$user['Role']['perm_site_admin']) { + $conditions['Workflow.org_id'] = $user['org_id']; + } + return $conditions; + } + + public function canEdit(array $user, array $workflow) + { + if ($user['Role']['perm_site_admin']) { + return true; + } + if (empty($workflow['Workflow'])) { + return __('Could not find associated workflow'); + } + if ($workflow['Workflow']['user_id'] != $user['id']) { + return __('Only the creator user of the workflow can modify it'); + } return true; } @@ -33,4 +98,60 @@ class Workflow extends AppModel { return []; } + + /** + * fetchReports ACL-aware method. Basically find with ACL + * + * @param array $user + * @param array $options + * @param bool $full + * @return array + */ + public function fetchWorkflows(array $user, array $options = array(), $full = false) + { + $params = array( + 'conditions' => $this->buildACLConditions($user), + 'contain' => $this->defaultContain, + 'recursive' => -1 + ); + if ($full) { + $params['recursive'] = 1; + } + if (isset($options['fields'])) { + $params['fields'] = $options['fields']; + } + if (isset($options['conditions'])) { + $params['conditions']['AND'][] = $options['conditions']; + } + if (isset($options['group'])) { + $params['group'] = empty($options['group']) ? $options['group'] : false; + } + $workflows = $this->find('all', $params); + return $workflows; + } + + /** + * fetchReports ACL-aware method. Basically find with ACL + * + * @param array $user + * @param int|string $id + * @param bool $throwErrors + * @return array + */ + public function fetchWorkflow(array $user, $id, bool $throwErrors = true) + { + $options = []; + if (is_numeric($id)) { + $options = ['conditions' => ["Workflow.id" => $id]]; + } elseif (Validation::uuid($id)) { + $options = ['conditions' => ["Workflow.uuid" => $id]]; + } else { + if ($throwErrors) { + throw new NotFoundException(__('Invalid workflow')); + } + return []; + } + $workflow = $this->fetchWorkflows($user, $options); + return $workflow; + } } \ No newline at end of file diff --git a/app/View/Workflows/view.ctp b/app/View/Workflows/view.ctp new file mode 100644 index 000000000..06ae0efb2 --- /dev/null +++ b/app/View/Workflows/view.ctp @@ -0,0 +1,42 @@ +element( + 'genericElements/SingleViews/single_view', + [ + 'title' => 'Workflow view', + 'data' => $data, + 'fields' => [ + [ + 'key' => __('Name'), + 'path' => 'Workflow.name' + ], + [ + 'key' => __('ID'), + 'path' => 'Workflow.id' + ], + [ + 'key' => __('UUID'), + 'path' => 'Workflow.uuid' + ], + [ + 'key' => __('Timestamp'), + 'path' => 'Workflow.url', + ], + [ + 'key' => __('Owner Organisation'), + 'path' => 'Workflow.org_id', + 'pathName' => 'Organisation.name', + 'type' => 'model', + 'model' => 'organisations' + ], + [ + 'key' => __('Description'), + 'path' => 'Workflow.description' + ], + [ + 'key' => __('Data'), + 'path' => 'Workflow.data', + 'type' => 'json', + ], + ], + ] +); From 43acefed2c23e61abfa276dc4bf4c5a7e76eb8b9 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Wed, 4 May 2022 10:46:06 +0200 Subject: [PATCH 014/428] chg: [workflows] Added view and editor links --- app/View/Elements/genericElements/SideMenu/side_menu.ctp | 5 +++++ app/View/Workflows/index.ctp | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/app/View/Elements/genericElements/SideMenu/side_menu.ctp b/app/View/Elements/genericElements/SideMenu/side_menu.ctp index 85f8f00c9..06a175f9b 100644 --- a/app/View/Elements/genericElements/SideMenu/side_menu.ctp +++ b/app/View/Elements/genericElements/SideMenu/side_menu.ctp @@ -1635,6 +1635,11 @@ $divider = $this->element('/genericElements/SideMenu/side_menu_divider'); 'url' => '/workflows/edit/' . h($id), 'text' => __('Edit Workflow') )); + echo $this->element('/genericElements/SideMenu/side_menu_link', array( + 'element_id' => 'editor', + 'url' => '/workflows/editor/' . h($id), + 'text' => __('Edit Workflow in editor') + )); echo $this->element('/genericElements/SideMenu/side_menu_link', array( 'url' => '/admin/audit_logs/index/model:Workflows/model_id:' . h($id), 'text' => __('View worflow history'), diff --git a/app/View/Workflows/index.ctp b/app/View/Workflows/index.ctp index 6f6689cb6..b5fe93a42 100644 --- a/app/View/Workflows/index.ctp +++ b/app/View/Workflows/index.ctp @@ -61,6 +61,11 @@ 'title' => __('Workflows'), 'description' => __('You can create workflows relying on pipeline hooks to that can listen to triggers and then perform actions depending on some conditions'), 'actions' => [ + [ + 'url' => $baseurl . '/workflows/view/', + 'url_params_data_paths' => ['Workflow.id'], + 'icon' => 'eye' + ], [ 'url' => $baseurl . '/workflows/editor/', 'url_params_data_paths' => ['Workflow.id'], From d30a47d7eb0b5c9647460006cbb5914ed98f62e0 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Wed, 4 May 2022 13:54:55 +0200 Subject: [PATCH 015/428] chg: [workflow:editor] Better handling of save / loading of workflows --- app/Controller/WorkflowsController.php | 52 +++-- app/Model/Workflow.php | 207 +++++++++++++++++- app/View/Workflows/editor.ctp | 188 ++-------------- app/View/Workflows/view.ctp | 2 +- app/webroot/css/workflows-editor.css | 31 ++- .../js/workflows-editor/workflows-editor.js | 185 +++++++++++++++- 6 files changed, 473 insertions(+), 192 deletions(-) diff --git a/app/Controller/WorkflowsController.php b/app/Controller/WorkflowsController.php index 1f6b15718..92507e7cc 100644 --- a/app/Controller/WorkflowsController.php +++ b/app/Controller/WorkflowsController.php @@ -51,14 +51,21 @@ class WorkflowsController extends AppController public function edit($id) { $this->set('id', $id); - $params = [ - 'conditions' => $this->Workflow->buildACLConditions($this->Auth->user()), - 'fields' => ['name', 'description', 'data'], - 'redirect' => ['action' => 'view', $id], - ]; - $this->CRUD->edit($id, $params); - if ($this->IndexFilter->isRest()) { - return $this->restResponsePayload; + $savedWorkflow = $this->Workflow->fetchWorkflow($this->Auth->user(), $id); + if ($this->request->is('post') || $this->request->is('put')) { + $newWorkflow = $this->request->data; + $newWorkflow = $this->__applyDataFromSavedWorkflow($newWorkflow, $savedWorkflow); + $errors = $this->Workflow->editWorkflow($this->Auth->user(), $newWorkflow); + $redirectTarget = ['action' => 'view', $id]; + if (!empty($errors)) { + return $this->__getFailResponseBasedOnContext($errors, null, 'edit', $this->Workflow->id, $redirectTarget); + } else { + $successMessage = __('Workflow saved.'); + $savedWorkflow =$this->Workflow->fetchWorkflow($this->Auth->user(), $id); + return $this->__getSuccessResponseBasedOnContext($successMessage, $savedWorkflow, 'edit', false, $redirectTarget); + } + } else { + $this->request->data = $savedWorkflow; } $this->set('menuData', array('menuList' => 'workflows', 'menuItem' => 'edit')); @@ -90,9 +97,14 @@ class WorkflowsController extends AppController $this->set('menuData', array('menuList' => 'workflows', 'menuItem' => 'view')); } - public function editor() + public function editor($id = false) { $modules = $this->Workflow->getModules(); + $workflow = $this->Workflow->fetchWorkflow($this->Auth->user(), $id); + $workflows = $this->Workflow->fetchWorkflows($this->Auth->user()); + $modules = $this->Workflow->getModules(); + $this->set('selectedWorklow', $workflow); + $this->set('workflows', $workflows); $this->set('modules', $modules); } @@ -102,10 +114,10 @@ class WorkflowsController extends AppController if (!is_null($data)) { return $this->RestResponse->viewData($data, $this->response->type()); } else { - return $this->RestResponse->saveSuccessResponse($this->EventReport->alias, $action, $id, false, $message); + return $this->RestResponse->saveSuccessResponse('Workflow', $action, $id, false, $message); } } elseif ($this->request->is('ajax')) { - return $this->RestResponse->saveSuccessResponse($this->EventReport->alias, $action, $id, false, $message, $data); + return $this->RestResponse->saveSuccessResponse('Workflow', $action, $id, false, $message, $data); } else { $this->Flash->success($message); $this->redirect($redirect); @@ -122,13 +134,27 @@ class WorkflowsController extends AppController if ($data !== null) { return $this->RestResponse->viewData($data, $this->response->type()); } else { - return $this->RestResponse->saveFailResponse('EventReport', $action, $id, $message); + return $this->RestResponse->saveFailResponse('Workflow', $action, $id, $message); } } elseif ($this->request->is('ajax')) { - return $this->RestResponse->saveFailResponse('EventReport', $action, $id, $message, false, $data); + return $this->RestResponse->saveFailResponse('Workflow', $action, $id, $message, false, $data); } else { $this->Flash->error($message); $this->redirect($this->referer()); } } + + private function __applyDataFromSavedWorkflow($newWorkflow, $savedWorkflow) + { + if (!isset($newReport['Workflow'])) { + $newReport = ['Workflow' => $newWorkflow]; + } + $ignoreFieldList = ['id', 'uuid', 'org_id', 'user_id']; + foreach (Workflow::CAPTURE_FIELDS as $field) { + if (!in_array($field, $ignoreFieldList) && isset($newWorkflow['Workflow'][$field])) { + $savedWorkflow['Workflow'][$field] = $newWorkflow['Workflow'][$field]; + } + } + return $savedWorkflow; + } } diff --git a/app/Model/Workflow.php b/app/Model/Workflow.php index d608b814f..3eaf846a6 100644 --- a/app/Model/Workflow.php +++ b/app/Model/Workflow.php @@ -40,16 +40,21 @@ class Workflow extends AppModel ]; public $defaultContain = [ - 'Organisation', - 'User' + // 'Organisation', + // 'User' ]; + const CAPTURE_FIELDS = ['name', 'description', 'timestamp', 'data']; + public function beforeValidate($options = array()) { parent::beforeValidate(); if (empty($this->data['Workflow']['data'])) { $this->data['Workflow']['data'] = []; } + if (empty($this->data['Workflow']['timestamp'])) { + $this->data['Workflow']['timestamp'] = time(); + } $this->data['Workflow']['data'] = JsonTool::encode($this->data['Workflow']['data']); return true; } @@ -66,7 +71,7 @@ class Workflow extends AppModel } /** - * buildACLConditions Generate ACL conditions for viewing the report + * buildACLConditions Generate ACL conditions for viewing the workflow * * @param array $user * @return array @@ -96,11 +101,164 @@ class Workflow extends AppModel public function getModules(): array { - return []; + $blocks_trigger = [ + [ + 'id' => 'publish', + 'name' => 'Publish', + 'icon' => 'upload', + 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', + 'inputs' => 0, + ], + [ + 'id' => 'new-attribute', + 'name' => 'New Attribute', + 'icon' => 'cube', + 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', + 'inputs' => 0, + 'disabled' => true, + ], + [ + 'id' => 'new-object', + 'name' => 'New Object', + 'icon' => 'cubes', + 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', + 'inputs' => 0, + 'disabled' => true, + ], + [ + 'id' => 'email-sent', + 'name' => 'Email sent', + 'icon' => 'envelope', + 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', + 'inputs' => 0, + 'disabled' => true, + ], + [ + 'id' => 'user-new', + 'name' => 'New User', + 'icon' => 'user-plus', + 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', + 'inputs' => 0, + 'disabled' => true, + ], + [ + 'id' => 'feed-pull', + 'name' => 'Feed pull', + 'icon' => 'arrow-alt-circle-down', + 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', + 'inputs' => 0, + 'disabled' => true, + ], + ]; + + $blocks_condition = [ + [ + 'id' => 'if', + 'name' => 'IF', + 'icon' => 'code-branch', + 'description' => 'IF conditions', + 'outputs' => 2, + 'html_template' => 'IF', + 'disabled' => true, + ], + ]; + + $blocks_action = [ + [ + 'id' => 'add-tag', + 'name' => 'Add Tag', + 'icon' => 'tag', + 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', + 'params' => [ + [ + 'type' => 'input', + 'label' => 'Tag name', + 'default' => 'tlp:red', + 'placeholder' => __('Enter tag name') + ], + ], + 'outputs' => 0, + 'disabled' => true, + ], + [ + 'id' => 'enrich-attribute', + 'name' => 'Enrich Attribute', + 'icon' => 'asterisk', + 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', + 'outputs' => 0, + 'disabled' => true, + ], + [ + 'id' => 'slack-message', + 'name' => 'Slack Message', + 'icon' => 'slack', + 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', + 'params' => [ + [ + 'type' => 'select', + 'label' => 'Channel name', + 'default' => 'team-4_3_misp', + 'options' => [ + 'team-4_3_misp', + 'team-4_0_elite_as_one', + ], + ], + ], + 'outputs' => 0, + ], + [ + 'id' => 'send-email', + 'name' => 'Send Email', + 'icon' => 'envelope', + 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', + 'params' => [ + [ + 'type' => 'select', + 'label' => 'Email template', + 'default' => 'default', + 'options' => [ + 'default', + 'TLP marking', + ], + ], + ], + 'outputs' => 0, + 'disabled' => true, + ], + [ + 'name' => 'Do nothing', + 'id' => 'dev-null', + 'icon' => 'ban', + 'description' => 'Essentially a /dev/null', + 'outputs' => 0, + ], + [ + 'name' => 'Push to ZMQ', + 'id' => 'push-zmq', + 'icon' => 'wifi', + 'icon_class' => 'fa-rotate-90', + 'description' => 'Push to the ZMQ channel', + 'params' => [ + [ + 'type' => 'input', + 'label' => 'ZMQ Topic', + 'default' => 'from-misp-workflow', + ], + ], + 'outputs' => 0, + 'disabled' => true, + ], + ]; + return [ + 'blocks_trigger' => $blocks_trigger, + 'blocks_condition' => $blocks_condition, + 'blocks_action' => $blocks_action, + 'blocks_all' => array_merge($blocks_trigger, $blocks_condition, $blocks_action), + ]; } /** - * fetchReports ACL-aware method. Basically find with ACL + * fetchWorkflows ACL-aware method. Basically find with ACL * * @param array $user * @param array $options @@ -131,7 +289,7 @@ class Workflow extends AppModel } /** - * fetchReports ACL-aware method. Basically find with ACL + * fetchWorkflow ACL-aware method. Basically find with ACL * * @param array $user * @param int|string $id @@ -152,6 +310,41 @@ class Workflow extends AppModel return []; } $workflow = $this->fetchWorkflows($user, $options); - return $workflow; + if (empty($workflow)) { + throw new NotFoundException(__('Invalid workflow')); + } + return $workflow[0]; + } + + /** + * editWorkflow Edit a worflow + * + * @param array $user + * @param array $workflow + * @return array Any errors preventing the edition + */ + public function editWorkflow(array $user, array $workflow) + { + $errors = array(); + if (!isset($workflow['Workflow']['uuid'])) { + $errors[] = __('Workflow doesn\'t have an UUID'); + return $errors; + } + $existingWorkflow = $this->fetchWorkflow($user, $workflow['Workflow']['id']); + $workflow['Workflow']['id'] = $existingWorkflow['Workflow']['id']; + unset($workflow['Workflow']['timestamp']); + $errors = $this->saveAndReturnErrors($workflow, ['fieldList' => self::CAPTURE_FIELDS], $errors); + return $errors; + } + + private function saveAndReturnErrors($data, $saveOptions = [], $errors = []) + { + $saveSuccess = $this->save($data, $saveOptions); + if (!$saveSuccess) { + foreach ($this->validationErrors as $validationError) { + $errors[] = $validationError[0]; + } + } + return $errors; } } \ No newline at end of file diff --git a/app/View/Workflows/editor.ctp b/app/View/Workflows/editor.ctp index 132c91c79..d579cb9a9 100644 --- a/app/View/Workflows/editor.ctp +++ b/app/View/Workflows/editor.ctp @@ -1,155 +1,3 @@ - 'publish', - 'name' => 'Publish', - 'icon' => 'upload', - 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', - 'inputs' => 0, - ], - [ - 'id' => 'new-attribute', - 'name' => 'New Attribute', - 'icon' => 'cube', - 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', - 'inputs' => 0, - 'disabled' => true, - ], - [ - 'id' => 'new-object', - 'name' => 'New Object', - 'icon' => 'cubes', - 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', - 'inputs' => 0, - 'disabled' => true, - ], - [ - 'id' => 'email-sent', - 'name' => 'Email sent', - 'icon' => 'envelope', - 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', - 'inputs' => 0, - 'disabled' => true, - ], - [ - 'id' => 'user-new', - 'name' => 'New User', - 'icon' => 'user-plus', - 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', - 'inputs' => 0, - 'disabled' => true, - ], - [ - 'id' => 'feed-pull', - 'name' => 'Feed pull', - 'icon' => 'arrow-alt-circle-down', - 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', - 'inputs' => 0, - 'disabled' => true, - ], -]; - -$blocks_condition = [ - [ - 'id' => 'if', - 'name' => 'IF', - 'icon' => 'code-branch', - 'description' => 'IF conditions', - 'outputs' => 2, - 'html_template' => 'IF', - ], -]; - -$blocks_action = [ - [ - 'id' => 'add-tag', - 'name' => 'Add Tag', - 'icon' => 'tag', - 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', - 'params' => [ - [ - 'type' => 'input', - 'label' => 'Tag name', - 'default' => 'tlp:red', - 'placeholder' => __('Enter tag name') - ], - ], - 'outputs' => 0, - ], - [ - 'id' => 'enrich-attribute', - 'name' => 'Enrich Attribute', - 'icon' => 'asterisk', - 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', - 'outputs' => 0, - ], - [ - 'id' => 'slack-message', - 'name' => 'Slack Message', - 'icon' => 'slack', - 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', - 'params' => [ - [ - 'type' => 'select', - 'label' => 'Channel name', - 'default' => 'team-4_3_misp', - 'options' => [ - 'team-4_3_misp', - 'team-4_0_elite_as_one', - ], - ], - ], - 'outputs' => 0, - ], - [ - 'id' => 'send-email', - 'name' => 'Send Email', - 'icon' => 'envelope', - 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', - 'params' => [ - [ - 'type' => 'select', - 'label' => 'Email template', - 'default' => 'default', - 'options' => [ - 'default', - 'TLP marking', - ], - ], - ], - 'outputs' => 0, - ], - [ - 'name' => 'Do nothing', - 'id' => 'dev-null', - 'icon' => 'ban', - 'description' => 'Essentially a /dev/null', - 'outputs' => 0, - ], - [ - 'name' => 'Push to ZMQ', - 'id' => 'push-zmq', - 'icon' => 'wifi', - 'icon_class' => 'fa-rotate-90', - 'description' => 'Push to the ZMQ channel', - 'params' => [ - [ - 'type' => 'input', - 'label' => 'ZMQ Topic', - 'default' => 'from-misp-workflow', - ], - ], - 'outputs' => 0, - ], -]; - -$blocks_all = array_merge($blocks_trigger, $blocks_condition, $blocks_action); -$workflows = [ - ['id' => 1, 'name' => 'Publish workflow', 'data' => []], - ['id' => 2, 'name' => 'My test worklow1', 'data' => []], -]; -?> -
@@ -157,7 +5,7 @@ $workflows = [
@@ -170,13 +18,17 @@ $workflows = [
- - + +
+ 2 days ago

Blocks

@@ -198,17 +50,17 @@ $workflows = [
- + element('Workflows/sidebar-block', ['block' => $block]) ?>
- + element('Workflows/sidebar-block', ['block' => $block]) ?>
- + element('Workflows/sidebar-block', ['block' => $block]) ?>
@@ -216,12 +68,10 @@ $workflows = [
-
+
+
-
- -
\
') +var workflow_id = 0 +var contentChanged = false +var lastModified = 0 + function sanitizeObject(obj) { var newObj = {} for (var key of Object.keys(obj)) { @@ -42,9 +46,17 @@ function sanitizeObject(obj) { function initDrawflow() { + workflow_id = $drawflow.data('workflowid') editor = new Drawflow($drawflow[0]); editor.start(); + editor.on('nodeCreated', invalidateContentCache) + editor.on('nodeRemoved', invalidateContentCache) + editor.on('nodeDataChanged', invalidateContentCache) + editor.on('nodeMoved', invalidateContentCache) + editor.on('connectionCreated', invalidateContentCache) + editor.on('connectionRemoved', invalidateContentCache) + $('#block-tabs a').click(function (e) { e.preventDefault(); $(this).tab('show'); @@ -81,19 +93,19 @@ function initDrawflow() { }, stop: function (event, ui) { $(this).removeClass('disabled') - // addNode($(this).data('block'), ui.position) } }); }) $canvas.droppable({ drop: function (event, ui) { - // console.log(event) - // console.log(ui) addNode(ui.draggable.data('block'), ui.position) } }); + loadWorkflow() + $saveWorkflowButton.click(saveWorkflow) + } function getTemplateForBlock(block) { @@ -170,6 +182,34 @@ function genInput(options) { return $input } +function invalidateContentCache() { + changeDetectedMessage1 = "[unsaved]" + changeDetectedMessage2 = " Last saved change: " + contentTimestamp = true + toggleSaveButton(true) + $lastModifiedField + .removeClass('label-success') + .addClass('label-important') + .text(changeDetectedMessage1 + changeDetectedMessage2 + moment(parseInt(lastModified)).fromNow()) +} + +function revalidateContentCache() { + changeDetectedMessage1 = "[saved]" + changeDetectedMessage2 = " Last saved change: " + contentChanged = false + toggleSaveButton(false) + $lastModifiedField + .removeClass('label-important') + .addClass('label-success') + .text(changeDetectedMessage1 + changeDetectedMessage2 + moment(parseInt(lastModified)).fromNow()) +} + +function refreshLastUpdatedField() { + // lastModifiedMessage = "Last modified: " + // $lastModifiedField.text(lastModifiedMessage + moment(parseInt(lastModified)).fromNow()) +} + + function addNode(block, position) { var canvasPosition = $canvas[0].getBoundingClientRect() @@ -195,4 +235,141 @@ function addNode(block, position) { block, html ); -} \ No newline at end of file +} + +function getEditorData() { + var data = editor.export().drawflow.Home.data + return data +} + +function loadWorkflow() { + fetchWorkflow(workflow_id, function(workflow) { + lastModified = workflow.timestamp + '000' + // refreshLastUpdatedField() + revalidateContentCache() + if (workflow.data !== undefined) { + workflow.data = JSON.parse(workflow.data) + var editor_data = { + drawflow: { + Home: { + data: workflow.data + } + } + } + editor.import(editor_data); + } + }) +} + + +/* API */ +function fetchWorkflow(id, callback) { + var url = '/workflows/view/' + id + '.json' + $.ajax({ + beforeSend: function () { + toggleLoadingInSaveButton(true) + }, + success: function (workflow, textStatus) { + if (workflow) { + workflow = workflow.Workflow + showMessage('success', 'Workflow fetched'); + if (callback !== undefined) { + callback(workflow) + } + } + }, + error: function (jqXHR, textStatus, errorThrown) { + showMessage('fail', saveFailedMessage + ': ' + errorThrown); + if (callback !== undefined) { + callback(false) + } + }, + complete: function () { + toggleLoadingInSaveButton(false) + }, + type: "post", + url: url + }) +} + +function saveWorkflow(confirmSave, callback) { + saveConfirmMessage = 'Confirm saving the current state of the workflow' + confirmSave = confirmSave === undefined ? true : confirmSave + if (confirmSave && !confirm(saveConfirmMessage)) { + return + } + var url = baseurl + "/workflows/edit/" + workflow_id + fetchFormDataAjax(url, function (formHTML) { + $('body').append($('
- +
\ No newline at end of file diff --git a/app/View/Elements/global_menu.ctp b/app/View/Elements/global_menu.ctp index de9b49f14..fa206cd17 100755 --- a/app/View/Elements/global_menu.ctp +++ b/app/View/Elements/global_menu.ctp @@ -422,6 +422,15 @@ 'type' => 'separator', 'requirement' => Configure::read('MISP.enableEventBlocklisting') !== false && $isSiteAdmin ), + array( + 'text' => __('Workflows'), + 'url' => $baseurl . '/workflows/index', + 'requirement' => $isSiteAdmin + ), + array( + 'type' => 'separator', + 'requirement' => Configure::read('MISP.enableEventBlocklisting') !== false && $isSiteAdmin + ), array( 'text' => __('Blocklist Event'), 'url' => $baseurl . '/eventBlocklists/add', diff --git a/app/View/Workflows/editor.ctp b/app/View/Workflows/editor.ctp index 6c7685f8b..d081879c0 100644 --- a/app/View/Workflows/editor.ctp +++ b/app/View/Workflows/editor.ctp @@ -5,7 +5,7 @@
diff --git a/app/View/Workflows/view.ctp b/app/View/Workflows/view.ctp index 575ac9fce..042d2cd1b 100644 --- a/app/View/Workflows/view.ctp +++ b/app/View/Workflows/view.ctp @@ -1,18 +1,19 @@ $node) { - $executionPathHTML .= sprintf('
%s%s', + $executionPathHTML .= sprintf( + '
%s%s', $inline ? 'inline-block' : 'block', - $first ? '' : sprintf('', $inline ? '' : 'fa-rotate-90' , $viewBuilder->FontAwesome->getClass($inline ? 'long-arrow-alt-right' : 'level-up-alt')), + $first ? '' : sprintf('', $inline ? '' : 'fa-rotate-90', $viewBuilder->FontAwesome->getClass($inline ? 'long-arrow-alt-right' : 'level-up-alt')), $viewBuilder->element('Workflows/executionPathNode', ['node' => $node]) ); if (!empty($node['next'])) { if (count($node['next']) == 1) { $executionPathHTML .= sprintf('%s', buildExecutionPathHTML($executionPathHTML, $node['next'], $viewBuilder, false, $depth + 1, true)); } else { - $executionPathHTML .= sprintf('
%s
', $depth*20, buildExecutionPathHTML($executionPathHTML, $node['next'], $viewBuilder, false, $depth + 1, false)); + $executionPathHTML .= sprintf('
%s
', $depth * 20, buildExecutionPathHTML($executionPathHTML, $node['next'], $viewBuilder, false, $depth + 1, false)); } } $executionPathHTML .= '
'; @@ -63,6 +64,7 @@ echo $this->element( ], [ 'key' => __('Data'), + 'class' => 'restrict-height', 'path' => 'Workflow.data', 'type' => 'json', ], @@ -80,4 +82,10 @@ echo $this->element( padding: 0.25em; box-shadow: 0px 3px 6px 2px #33333311; } + + .restrict-height>.json_container_Data { + height: 30vh; + overflow: auto; + resize: both; + } \ No newline at end of file diff --git a/app/webroot/js/workflows-editor/workflows-editor.js b/app/webroot/js/workflows-editor/workflows-editor.js index c4a3cd73f..dbdeaebb7 100644 --- a/app/webroot/js/workflows-editor/workflows-editor.js +++ b/app/webroot/js/workflows-editor/workflows-editor.js @@ -64,7 +64,8 @@ function initDrawflow() { $chosenWorkflows.chosen() .on('change', function (evt, param) { - // console.log(param); + var selection = param.selected + window.location = '/workflows/editor/' + selection }); $chosenBlocks.chosen() .on('change', function (evt, param) { From babcb651a9ba5f07f497e96828fd9ab8442933e8 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 5 May 2022 12:04:08 +0200 Subject: [PATCH 021/428] chg: [workflow:editor] Added support of brand icons --- app/Model/Workflow.php | 1 + app/webroot/js/workflows-editor/workflows-editor.js | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/Model/Workflow.php b/app/Model/Workflow.php index 024a5bb01..3870cae68 100644 --- a/app/Model/Workflow.php +++ b/app/Model/Workflow.php @@ -269,6 +269,7 @@ class Workflow extends AppModel 'id' => 'slack-message', 'name' => 'Slack Message', 'icon' => 'slack', + 'icon_class' => 'fab', 'description' => 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.', 'module_type' => 'action', 'params' => [ diff --git a/app/webroot/js/workflows-editor/workflows-editor.js b/app/webroot/js/workflows-editor/workflows-editor.js index dbdeaebb7..e6deec61e 100644 --- a/app/webroot/js/workflows-editor/workflows-editor.js +++ b/app/webroot/js/workflows-editor/workflows-editor.js @@ -2,7 +2,7 @@ var dotBlockDefault = doT.template(' \
\
\
\ - \ + \ \ {{=it.name}} \ \ @@ -19,7 +19,7 @@ var dotIF = doT.template(' \
\
\
\ - \ + \ \ {{=it.name}} \ \ @@ -320,6 +320,7 @@ function getTemplateForBlock(block) { if (block.html_template !== undefined) { html = window['dot' + block.html_template](block) } else { + block.icon_class = block.icon_class !== undefined ? block.icon_class : 'fas' html = dotBlockDefault(block) } } From bce383273c6fe5771324d924c5c1e8a64e7a05b7 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 5 May 2022 14:28:16 +0200 Subject: [PATCH 022/428] fix: [worflow:editor] Fix loading of saved state. - Save internal representing with node indexed by their IDs --- app/webroot/js/workflows-editor/workflows-editor.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/webroot/js/workflows-editor/workflows-editor.js b/app/webroot/js/workflows-editor/workflows-editor.js index e6deec61e..3bd290cb6 100644 --- a/app/webroot/js/workflows-editor/workflows-editor.js +++ b/app/webroot/js/workflows-editor/workflows-editor.js @@ -170,8 +170,12 @@ function addNode(block, position) { } function getEditorData() { - var data = editor.export().drawflow.Home.data - .filter(function (node) { return node !== null }) // for some reason, the editor create null nodes + var data = {} // Make sure nodes are index by their internal IDs + editor.export().drawflow.Home.data.forEach(function(node) { + if (node !== null) { // for some reason, the editor create null nodes + data[node.id] = node + } + }) return data } @@ -317,10 +321,10 @@ function getTemplateForBlock(block) { if (block.html !== undefined) { html = block.html } else { + block.icon_class = block.icon_class !== undefined ? block.icon_class : 'fas' if (block.html_template !== undefined) { html = window['dot' + block.html_template](block) } else { - block.icon_class = block.icon_class !== undefined ? block.icon_class : 'fas' html = dotBlockDefault(block) } } From 20f0963430987339dd986081bbdeaf698f8ffeaa Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 5 May 2022 15:28:38 +0200 Subject: [PATCH 023/428] chg: [js:drawflow] bumped to version 0.0.58 --- app/webroot/js/drawflow.min.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/webroot/js/drawflow.min.js b/app/webroot/js/drawflow.min.js index 922aebc31..85f5417e1 100644 --- a/app/webroot/js/drawflow.min.js +++ b/app/webroot/js/drawflow.min.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Drawflow=t():e.Drawflow=t()}("undefined"!=typeof self?self:this,(function(){return function(e){var t={};function n(i){if(t[i])return t[i].exports;var s=t[i]={i:i,l:!1,exports:{}};return e[i].call(s.exports,s,s.exports,n),s.l=!0,s.exports}return n.m=e,n.c=t,n.d=function(e,t,i){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var s in e)n.d(i,s,function(t){return e[t]}.bind(null,s));return i},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";n.r(t),n.d(t,"default",(function(){return i}));class i{constructor(e,t=null,n=null){this.events={},this.container=e,this.precanvas=null,this.nodeId=1,this.ele_selected=null,this.node_selected=null,this.drag=!1,this.reroute=!1,this.reroute_fix_curvature=!1,this.curvature=.5,this.reroute_curvature_start_end=.5,this.reroute_curvature=.5,this.reroute_width=6,this.drag_point=!1,this.editor_selected=!1,this.connection=!1,this.connection_ele=null,this.connection_selected=null,this.canvas_x=0,this.canvas_y=0,this.pos_x=0,this.pos_x_start=0,this.pos_y=0,this.pos_y_start=0,this.mouse_x=0,this.mouse_y=0,this.line_path=5,this.first_click=null,this.force_first_input=!1,this.draggable_inputs=!0,this.useuuid=!1,this.parent=n,this.noderegister={},this.render=t,this.drawflow={drawflow:{Home:{data:{}}}},this.module="Home",this.editor_mode="edit",this.zoom=1,this.zoom_max=1.6,this.zoom_min=.5,this.zoom_value=.1,this.zoom_last_value=1,this.evCache=new Array,this.prevDiff=-1}start(){this.container.classList.add("parent-drawflow"),this.container.tabIndex=0,this.precanvas=document.createElement("div"),this.precanvas.classList.add("drawflow"),this.container.appendChild(this.precanvas),this.container.addEventListener("mouseup",this.dragEnd.bind(this)),this.container.addEventListener("mousemove",this.position.bind(this)),this.container.addEventListener("mousedown",this.click.bind(this)),this.container.addEventListener("touchend",this.dragEnd.bind(this)),this.container.addEventListener("touchmove",this.position.bind(this)),this.container.addEventListener("touchstart",this.click.bind(this)),this.container.addEventListener("contextmenu",this.contextmenu.bind(this)),this.container.addEventListener("keydown",this.key.bind(this)),this.container.addEventListener("wheel",this.zoom_enter.bind(this)),this.container.addEventListener("input",this.updateNodeValue.bind(this)),this.container.addEventListener("dblclick",this.dblclick.bind(this)),this.container.onpointerdown=this.pointerdown_handler.bind(this),this.container.onpointermove=this.pointermove_handler.bind(this),this.container.onpointerup=this.pointerup_handler.bind(this),this.container.onpointercancel=this.pointerup_handler.bind(this),this.container.onpointerout=this.pointerup_handler.bind(this),this.container.onpointerleave=this.pointerup_handler.bind(this),this.load()}pointerdown_handler(e){this.evCache.push(e)}pointermove_handler(e){for(var t=0;t100&&(n>this.prevDiff&&this.zoom_in(),n=n&&(n=parseInt(e)+1)}))})),this.nodeId=n}removeReouteConnectionSelected(){this.dispatch("connectionUnselected",!0),this.reroute_fix_curvature&&this.connection_selected.parentElement.querySelectorAll(".main-path").forEach((e,t)=>{e.classList.remove("selected")})}click(e){if(this.dispatch("click",e),"fixed"===this.editor_mode){if(e.preventDefault(),"parent-drawflow"!==e.target.classList[0]&&"drawflow"!==e.target.classList[0])return!1;this.ele_selected=e.target.closest(".parent-drawflow")}else"view"===this.editor_mode?(null!=e.target.closest(".drawflow")||e.target.matches(".parent-drawflow"))&&(this.ele_selected=e.target.closest(".parent-drawflow"),e.preventDefault()):(this.first_click=e.target,this.ele_selected=e.target,0===e.button&&this.contextmenuDel(),null!=e.target.closest(".drawflow_content_node")&&(this.ele_selected=e.target.closest(".drawflow_content_node").parentElement));switch(this.ele_selected.classList[0]){case"drawflow-node":null!=this.node_selected&&(this.node_selected.classList.remove("selected"),this.node_selected!=this.ele_selected&&this.dispatch("nodeUnselected",!0)),null!=this.connection_selected&&(this.connection_selected.classList.remove("selected"),this.removeReouteConnectionSelected(),this.connection_selected=null),this.node_selected!=this.ele_selected&&this.dispatch("nodeSelected",this.ele_selected.id.slice(5)),this.node_selected=this.ele_selected,this.node_selected.classList.add("selected"),this.draggable_inputs?"SELECT"!==e.target.tagName&&(this.drag=!0):"INPUT"!==e.target.tagName&&"TEXTAREA"!==e.target.tagName&&"SELECT"!==e.target.tagName&&!0!==e.target.hasAttribute("contenteditable")&&(this.drag=!0);break;case"output":this.connection=!0,null!=this.node_selected&&(this.node_selected.classList.remove("selected"),this.node_selected=null,this.dispatch("nodeUnselected",!0)),null!=this.connection_selected&&(this.connection_selected.classList.remove("selected"),this.removeReouteConnectionSelected(),this.connection_selected=null),this.drawConnection(e.target);break;case"parent-drawflow":case"drawflow":null!=this.node_selected&&(this.node_selected.classList.remove("selected"),this.node_selected=null,this.dispatch("nodeUnselected",!0)),null!=this.connection_selected&&(this.connection_selected.classList.remove("selected"),this.removeReouteConnectionSelected(),this.connection_selected=null),this.editor_selected=!0;break;case"main-path":null!=this.node_selected&&(this.node_selected.classList.remove("selected"),this.node_selected=null,this.dispatch("nodeUnselected",!0)),null!=this.connection_selected&&(this.connection_selected.classList.remove("selected"),this.removeReouteConnectionSelected(),this.connection_selected=null),this.connection_selected=this.ele_selected,this.connection_selected.classList.add("selected");const t=this.connection_selected.parentElement.classList;this.dispatch("connectionSelected",{output_id:t[2].slice(14),input_id:t[1].slice(13),output_class:t[3],input_class:t[4]}),this.reroute_fix_curvature&&this.connection_selected.parentElement.querySelectorAll(".main-path").forEach((e,t)=>{e.classList.add("selected")});break;case"point":this.drag_point=!0,this.ele_selected.classList.add("selected");break;case"drawflow-delete":this.node_selected&&this.removeNodeId(this.node_selected.id),this.connection_selected&&this.removeConnection(),null!=this.node_selected&&(this.node_selected.classList.remove("selected"),this.node_selected=null,this.dispatch("nodeUnselected",!0)),null!=this.connection_selected&&(this.connection_selected.classList.remove("selected"),this.removeReouteConnectionSelected(),this.connection_selected=null)}"touchstart"===e.type?(this.pos_x=e.touches[0].clientX,this.pos_x_start=e.touches[0].clientX,this.pos_y=e.touches[0].clientY,this.pos_y_start=e.touches[0].clientY):(this.pos_x=e.clientX,this.pos_x_start=e.clientX,this.pos_y=e.clientY,this.pos_y_start=e.clientY),(this.drag||["input","output","main-path"].includes(this.ele_selected.classList[0]))&&e.preventDefault(),this.dispatch("clickEnd",e)}position(e){if("touchmove"===e.type)var t=e.touches[0].clientX,n=e.touches[0].clientY;else t=e.clientX,n=e.clientY;if(this.connection&&this.updateConnection(t,n),this.editor_selected&&(i=this.canvas_x+-(this.pos_x-t),s=this.canvas_y+-(this.pos_y-n),this.dispatch("translate",{x:i,y:s}),this.precanvas.style.transform="translate("+i+"px, "+s+"px) scale("+this.zoom+")"),this.drag){var i=(this.pos_x-t)*this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom),s=(this.pos_y-n)*this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom);this.pos_x=t,this.pos_y=n,this.ele_selected.style.top=this.ele_selected.offsetTop-s+"px",this.ele_selected.style.left=this.ele_selected.offsetLeft-i+"px",this.drawflow.drawflow[this.module].data[this.ele_selected.id.slice(5)].pos_x=this.ele_selected.offsetLeft-i,this.drawflow.drawflow[this.module].data[this.ele_selected.id.slice(5)].pos_y=this.ele_selected.offsetTop-s,this.updateConnectionNodes(this.ele_selected.id)}if(this.drag_point){i=(this.pos_x-t)*this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom),s=(this.pos_y-n)*this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom);this.pos_x=t,this.pos_y=n;var o=this.pos_x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom))-this.precanvas.getBoundingClientRect().x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom)),l=this.pos_y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom))-this.precanvas.getBoundingClientRect().y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom));this.ele_selected.setAttributeNS(null,"cx",o),this.ele_selected.setAttributeNS(null,"cy",l);const e=this.ele_selected.parentElement.classList[2].slice(9),c=this.ele_selected.parentElement.classList[1].slice(13),d=this.ele_selected.parentElement.classList[3],a=this.ele_selected.parentElement.classList[4];let r=Array.from(this.ele_selected.parentElement.children).indexOf(this.ele_selected)-1;if(this.reroute_fix_curvature){r-=this.ele_selected.parentElement.querySelectorAll(".main-path").length-1,r<0&&(r=0)}const h=e.slice(5),u=this.drawflow.drawflow[this.module].data[h].outputs[d].connections.findIndex((function(e,t){return e.node===c&&e.output===a}));this.drawflow.drawflow[this.module].data[h].outputs[d].connections[u].points[r]={pos_x:o,pos_y:l};const p=this.ele_selected.parentElement.classList[2].slice(9);this.updateConnectionNodes(p)}"touchmove"===e.type&&(this.mouse_x=t,this.mouse_y=n),this.dispatch("mouseMove",{x:t,y:n})}dragEnd(e){if("touchend"===e.type)var t=this.mouse_x,n=this.mouse_y,i=document.elementFromPoint(t,n);else t=e.clientX,n=e.clientY,i=e.target;if(this.drag&&(this.pos_x_start==t&&this.pos_y_start==n||this.dispatch("nodeMoved",this.ele_selected.id.slice(5))),this.drag_point&&(this.ele_selected.classList.remove("selected"),this.pos_x_start==t&&this.pos_y_start==n||this.dispatch("rerouteMoved",this.ele_selected.parentElement.classList[2].slice(14))),this.editor_selected&&(this.canvas_x=this.canvas_x+-(this.pos_x-t),this.canvas_y=this.canvas_y+-(this.pos_y-n),this.editor_selected=!1),!0===this.connection)if("input"===i.classList[0]||this.force_first_input&&(null!=i.closest(".drawflow_content_node")||"drawflow-node"===i.classList[0])){if(!this.force_first_input||null==i.closest(".drawflow_content_node")&&"drawflow-node"!==i.classList[0])s=i.parentElement.parentElement.id,o=i.classList[1];else{if(null!=i.closest(".drawflow_content_node"))var s=i.closest(".drawflow_content_node").parentElement.id;else var s=i.id;if(0===Object.keys(this.getNodeFromId(s.slice(5)).inputs).length)var o=!1;else var o="input_1"}var l=this.ele_selected.parentElement.parentElement.id,c=this.ele_selected.classList[1];if(l!==s&&!1!==o){if(0===this.container.querySelectorAll(".connection.node_in_"+s+".node_out_"+l+"."+c+"."+o).length){this.connection_ele.classList.add("node_in_"+s),this.connection_ele.classList.add("node_out_"+l),this.connection_ele.classList.add(c),this.connection_ele.classList.add(o);var d=s.slice(5),a=l.slice(5);this.drawflow.drawflow[this.module].data[a].outputs[c].connections.push({node:d,output:o}),this.drawflow.drawflow[this.module].data[d].inputs[o].connections.push({node:a,input:c}),this.updateConnectionNodes("node-"+a),this.updateConnectionNodes("node-"+d),this.dispatch("connectionCreated",{output_id:a,input_id:d,output_class:c,input_class:o})}else this.dispatch("connectionCancel",!0),this.connection_ele.remove();this.connection_ele=null}else this.dispatch("connectionCancel",!0),this.connection_ele.remove(),this.connection_ele=null}else this.dispatch("connectionCancel",!0),this.connection_ele.remove(),this.connection_ele=null;this.drag=!1,this.drag_point=!1,this.connection=!1,this.ele_selected=null,this.editor_selected=!1,this.dispatch("mouseUp",e)}contextmenu(e){if(this.dispatch("contextmenu",e),e.preventDefault(),"fixed"===this.editor_mode||"view"===this.editor_mode)return!1;if(this.precanvas.getElementsByClassName("drawflow-delete").length&&this.precanvas.getElementsByClassName("drawflow-delete")[0].remove(),this.node_selected||this.connection_selected){var t=document.createElement("div");t.classList.add("drawflow-delete"),t.innerHTML="x",this.node_selected&&this.node_selected.appendChild(t),this.connection_selected&&(t.style.top=e.clientY*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom))-this.precanvas.getBoundingClientRect().y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom))+"px",t.style.left=e.clientX*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom))-this.precanvas.getBoundingClientRect().x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom))+"px",this.precanvas.appendChild(t))}}contextmenuDel(){this.precanvas.getElementsByClassName("drawflow-delete").length&&this.precanvas.getElementsByClassName("drawflow-delete")[0].remove()}key(e){if(this.dispatch("keydown",e),"fixed"===this.editor_mode||"view"===this.editor_mode)return!1;("Delete"===e.key||"Backspace"===e.key&&e.metaKey)&&(null!=this.node_selected&&"INPUT"!==this.first_click.tagName&&"TEXTAREA"!==this.first_click.tagName&&!0!==this.first_click.hasAttribute("contenteditable")&&this.removeNodeId(this.node_selected.id),null!=this.connection_selected&&this.removeConnection())}zoom_enter(e,t){e.ctrlKey&&(e.preventDefault(),e.deltaY>0?this.zoom_out():this.zoom_in())}zoom_refresh(){this.dispatch("zoom",this.zoom),this.canvas_x=this.canvas_x/this.zoom_last_value*this.zoom,this.canvas_y=this.canvas_y/this.zoom_last_value*this.zoom,this.zoom_last_value=this.zoom,this.precanvas.style.transform="translate("+this.canvas_x+"px, "+this.canvas_y+"px) scale("+this.zoom+")"}zoom_in(){this.zoomthis.zoom_min&&(this.zoom-=this.zoom_value,this.zoom_refresh())}zoom_reset(){1!=this.zoom&&(this.zoom=1,this.zoom_refresh())}createCurvature(e,t,n,i,s,o){var l=e,c=t,d=n,a=i,r=s;switch(o){case"open":if(e>=n)var h=l+Math.abs(d-l)*r,u=d-Math.abs(d-l)*(-1*r);else h=l+Math.abs(d-l)*r,u=d-Math.abs(d-l)*r;return" M "+l+" "+c+" C "+h+" "+c+" "+u+" "+a+" "+d+" "+a;case"close":if(e>=n)h=l+Math.abs(d-l)*(-1*r),u=d-Math.abs(d-l)*r;else h=l+Math.abs(d-l)*r,u=d-Math.abs(d-l)*r;return" M "+l+" "+c+" C "+h+" "+c+" "+u+" "+a+" "+d+" "+a;case"other":if(e>=n)h=l+Math.abs(d-l)*(-1*r),u=d-Math.abs(d-l)*(-1*r);else h=l+Math.abs(d-l)*r,u=d-Math.abs(d-l)*r;return" M "+l+" "+c+" C "+h+" "+c+" "+u+" "+a+" "+d+" "+a;default:return" M "+l+" "+c+" C "+(h=l+Math.abs(d-l)*r)+" "+c+" "+(u=d-Math.abs(d-l)*r)+" "+a+" "+d+" "+a}}drawConnection(e){var t=document.createElementNS("http://www.w3.org/2000/svg","svg");this.connection_ele=t;var n=document.createElementNS("http://www.w3.org/2000/svg","path");n.classList.add("main-path"),n.setAttributeNS(null,"d",""),t.classList.add("connection"),t.appendChild(n),this.precanvas.appendChild(t);var i=e.parentElement.parentElement.id.slice(5),s=e.classList[1];this.dispatch("connectionStart",{output_id:i,output_class:s})}updateConnection(e,t){const n=this.precanvas,i=this.zoom;let s=n.clientWidth/(n.clientWidth*i);s=s||0;let o=n.clientHeight/(n.clientHeight*i);o=o||0;var l=this.connection_ele.children[0],c=this.ele_selected.offsetWidth/2+(this.ele_selected.getBoundingClientRect().x-n.getBoundingClientRect().x)*s,d=this.ele_selected.offsetHeight/2+(this.ele_selected.getBoundingClientRect().y-n.getBoundingClientRect().y)*o,a=e*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom))-this.precanvas.getBoundingClientRect().x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom)),r=t*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom))-this.precanvas.getBoundingClientRect().y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom)),h=this.curvature,u=this.createCurvature(c,d,a,r,h,"openclose");l.setAttributeNS(null,"d",u)}addConnection(e,t,n,i){var s=this.getModuleFromNodeId(e);if(s===this.getModuleFromNodeId(t)){var o=this.getNodeFromId(e),l=!1;for(var c in o.outputs[n].connections){var d=o.outputs[n].connections[c];d.node==t&&d.output==i&&(l=!0)}if(!1===l){if(this.drawflow.drawflow[s].data[e].outputs[n].connections.push({node:t.toString(),output:i}),this.drawflow.drawflow[s].data[t].inputs[i].connections.push({node:e.toString(),input:n}),this.module===s){var a=document.createElementNS("http://www.w3.org/2000/svg","svg"),r=document.createElementNS("http://www.w3.org/2000/svg","path");r.classList.add("main-path"),r.setAttributeNS(null,"d",""),a.classList.add("connection"),a.classList.add("node_in_node-"+t),a.classList.add("node_out_node-"+e),a.classList.add(n),a.classList.add(i),a.appendChild(r),this.precanvas.appendChild(a),this.updateConnectionNodes("node-"+e),this.updateConnectionNodes("node-"+t)}this.dispatch("connectionCreated",{output_id:e,input_id:t,output_class:n,input_class:i})}}}updateConnectionNodes(e){const t="node_in_"+e,n="node_out_"+e;this.line_path;const i=this.container,s=this.precanvas,o=this.curvature,l=this.createCurvature,c=this.reroute_curvature,d=this.reroute_curvature_start_end,a=this.reroute_fix_curvature,r=this.reroute_width,h=this.zoom;let u=s.clientWidth/(s.clientWidth*h);u=u||0;let p=s.clientHeight/(s.clientHeight*h);p=p||0;const f=i.querySelectorAll("."+n);Object.keys(f).map((function(t,n){if(null===f[t].querySelector(".point")){var g=i.querySelector("#"+e),m=f[t].classList[1].replace("node_in_",""),_=i.querySelector("#"+m).querySelectorAll("."+f[t].classList[4])[0],w=_.offsetWidth/2+(_.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,v=_.offsetHeight/2+(_.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,y=g.querySelectorAll("."+f[t].classList[3])[0],C=y.offsetWidth/2+(y.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,x=y.offsetHeight/2+(y.getBoundingClientRect().y-s.getBoundingClientRect().y)*p;const n=l(C,x,w,v,o,"openclose");f[t].children[0].setAttributeNS(null,"d",n)}else{const n=f[t].querySelectorAll(".point");let o="";const g=[];n.forEach((t,a)=>{if(0===a&&n.length-1==0){var f=i.querySelector("#"+e),m=((x=t).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,_=(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,w=(L=f.querySelectorAll("."+t.parentElement.classList[3])[0]).offsetWidth/2+(L.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,v=L.offsetHeight/2+(L.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,y=l(w,v,m,_,d,"open");o+=y,g.push(y);f=t;var C=t.parentElement.classList[1].replace("node_in_",""),x=(E=i.querySelector("#"+C)).querySelectorAll("."+t.parentElement.classList[4])[0];m=(R=E.querySelectorAll("."+t.parentElement.classList[4])[0]).offsetWidth/2+(R.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,_=R.offsetHeight/2+(R.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,w=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,v=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,y=l(w,v,m,_,d,"close");o+=y,g.push(y)}else if(0===a){var L;f=i.querySelector("#"+e),m=((x=t).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,_=(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,w=(L=f.querySelectorAll("."+t.parentElement.classList[3])[0]).offsetWidth/2+(L.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,v=L.offsetHeight/2+(L.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,y=l(w,v,m,_,d,"open");o+=y,g.push(y);f=t,m=((x=n[a+1]).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,_=(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,w=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,v=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,y=l(w,v,m,_,c,"other");o+=y,g.push(y)}else if(a===n.length-1){var E,R;f=t,C=t.parentElement.classList[1].replace("node_in_",""),x=(E=i.querySelector("#"+C)).querySelectorAll("."+t.parentElement.classList[4])[0],m=(R=E.querySelectorAll("."+t.parentElement.classList[4])[0]).offsetWidth/2+(R.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,_=R.offsetHeight/2+(R.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,w=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*(s.clientWidth/(s.clientWidth*h))+r,v=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*(s.clientHeight/(s.clientHeight*h))+r,y=l(w,v,m,_,d,"close");o+=y,g.push(y)}else{f=t,m=((x=n[a+1]).getBoundingClientRect().x-s.getBoundingClientRect().x)*(s.clientWidth/(s.clientWidth*h))+r,_=(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*(s.clientHeight/(s.clientHeight*h))+r,w=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*(s.clientWidth/(s.clientWidth*h))+r,v=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*(s.clientHeight/(s.clientHeight*h))+r,y=l(w,v,m,_,c,"other");o+=y,g.push(y)}}),a?g.forEach((e,n)=>{f[t].children[n].setAttributeNS(null,"d",e)}):f[t].children[0].setAttributeNS(null,"d",o)}}));const g=i.querySelectorAll("."+t);Object.keys(g).map((function(t,n){if(null===g[t].querySelector(".point")){var h=i.querySelector("#"+e),f=g[t].classList[2].replace("node_out_",""),m=i.querySelector("#"+f).querySelectorAll("."+g[t].classList[3])[0],_=m.offsetWidth/2+(m.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,w=m.offsetHeight/2+(m.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,v=(h=h.querySelectorAll("."+g[t].classList[4])[0]).offsetWidth/2+(h.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,y=h.offsetHeight/2+(h.getBoundingClientRect().y-s.getBoundingClientRect().y)*p;const n=l(_,w,v,y,o,"openclose");g[t].children[0].setAttributeNS(null,"d",n)}else{const n=g[t].querySelectorAll(".point");let o="";const h=[];n.forEach((t,a)=>{if(0===a&&n.length-1==0){var f=i.querySelector("#"+e),g=((C=t).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,m=(C.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,_=(E=f.querySelectorAll("."+t.parentElement.classList[4])[0]).offsetWidth/2+(E.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,w=E.offsetHeight/2+(E.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,v=l(g,m,_,w,d,"close");o+=v,h.push(v);f=t;var y=t.parentElement.classList[2].replace("node_out_",""),C=(L=i.querySelector("#"+y)).querySelectorAll("."+t.parentElement.classList[3])[0];g=(x=L.querySelectorAll("."+t.parentElement.classList[3])[0]).offsetWidth/2+(x.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,m=x.offsetHeight/2+(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,_=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,w=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,v=l(g,m,_,w,d,"open");o+=v,h.push(v)}else if(0===a){var x;f=t,y=t.parentElement.classList[2].replace("node_out_",""),C=(L=i.querySelector("#"+y)).querySelectorAll("."+t.parentElement.classList[3])[0],g=(x=L.querySelectorAll("."+t.parentElement.classList[3])[0]).offsetWidth/2+(x.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,m=x.offsetHeight/2+(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,_=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,w=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,v=l(g,m,_,w,d,"open");o+=v,h.push(v);f=t,_=((C=n[a+1]).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,w=(C.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,g=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,m=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,v=l(g,m,_,w,c,"other");o+=v,h.push(v)}else if(a===n.length-1){var L,E;f=t,y=t.parentElement.classList[1].replace("node_in_",""),C=(L=i.querySelector("#"+y)).querySelectorAll("."+t.parentElement.classList[4])[0],_=(E=L.querySelectorAll("."+t.parentElement.classList[4])[0]).offsetWidth/2+(E.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,w=E.offsetHeight/2+(E.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,g=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,m=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,v=l(g,m,_,w,d,"close");o+=v,h.push(v)}else{f=t,_=((C=n[a+1]).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,w=(C.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,g=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,m=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,v=l(g,m,_,w,c,"other");o+=v,h.push(v)}}),a?h.forEach((e,n)=>{g[t].children[n].setAttributeNS(null,"d",e)}):g[t].children[0].setAttributeNS(null,"d",o)}}))}dblclick(e){null!=this.connection_selected&&this.reroute&&this.createReroutePoint(this.connection_selected),"point"===e.target.classList[0]&&this.removeReroutePoint(e.target)}createReroutePoint(e){this.connection_selected.classList.remove("selected");const t=this.connection_selected.parentElement.classList[2].slice(9),n=this.connection_selected.parentElement.classList[1].slice(13),i=this.connection_selected.parentElement.classList[3],s=this.connection_selected.parentElement.classList[4];this.connection_selected=null;const o=document.createElementNS("http://www.w3.org/2000/svg","circle");o.classList.add("point");var l=this.pos_x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom))-this.precanvas.getBoundingClientRect().x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom)),c=this.pos_y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom))-this.precanvas.getBoundingClientRect().y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom));o.setAttributeNS(null,"cx",l),o.setAttributeNS(null,"cy",c),o.setAttributeNS(null,"r",this.reroute_width);let d=0;if(this.reroute_fix_curvature){const t=e.parentElement.querySelectorAll(".main-path").length;var a=document.createElementNS("http://www.w3.org/2000/svg","path");if(a.classList.add("main-path"),a.setAttributeNS(null,"d",""),e.parentElement.insertBefore(a,e.parentElement.children[t]),1===t)e.parentElement.appendChild(o);else{const n=Array.from(e.parentElement.children).indexOf(e);d=n,e.parentElement.insertBefore(o,e.parentElement.children[n+t+1])}}else e.parentElement.appendChild(o);const r=t.slice(5),h=this.drawflow.drawflow[this.module].data[r].outputs[i].connections.findIndex((function(e,t){return e.node===n&&e.output===s}));void 0===this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points&&(this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points=[]),this.reroute_fix_curvature?(d>0||this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points!==[]?this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points.splice(d,0,{pos_x:l,pos_y:c}):this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points.push({pos_x:l,pos_y:c}),e.parentElement.querySelectorAll(".main-path").forEach((e,t)=>{e.classList.remove("selected")})):this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points.push({pos_x:l,pos_y:c}),this.dispatch("addReroute",r),this.updateConnectionNodes(t)}removeReroutePoint(e){const t=e.parentElement.classList[2].slice(9),n=e.parentElement.classList[1].slice(13),i=e.parentElement.classList[3],s=e.parentElement.classList[4];let o=Array.from(e.parentElement.children).indexOf(e);const l=t.slice(5),c=this.drawflow.drawflow[this.module].data[l].outputs[i].connections.findIndex((function(e,t){return e.node===n&&e.output===s}));if(this.reroute_fix_curvature){const t=e.parentElement.querySelectorAll(".main-path").length;e.parentElement.children[t-1].remove(),o-=t,o<0&&(o=0)}else o--;this.drawflow.drawflow[this.module].data[l].outputs[i].connections[c].points.splice(o,1),e.remove(),this.dispatch("removeReroute",l),this.updateConnectionNodes(t)}registerNode(e,t,n=null,i=null){this.noderegister[e]={html:t,props:n,options:i}}getNodeFromId(e){var t=this.getModuleFromNodeId(e);return JSON.parse(JSON.stringify(this.drawflow.drawflow[t].data[e]))}getNodesFromName(e){var t=[];const n=this.drawflow.drawflow;return Object.keys(n).map((function(i,s){for(var o in n[i].data)n[i].data[o].name==e&&t.push(n[i].data[o].id)})),t}addNode(e,t,n,i,s,o,l,c,d=!1){if(this.useuuid)var a=this.getUuid();else a=this.nodeId;const r=document.createElement("div");r.classList.add("parent-node");const h=document.createElement("div");h.innerHTML="",h.setAttribute("id","node-"+a),h.classList.add("drawflow-node"),""!=o&&h.classList.add(...o.split(" "));const u=document.createElement("div");u.classList.add("inputs");const p=document.createElement("div");p.classList.add("outputs");const f={};for(var g=0;ge(this.noderegister[c].html,{props:this.noderegister[c].props}),...this.noderegister[c].options}).$mount();_.appendChild(e.$el)}Object.entries(l).forEach((function(e,t){if("object"==typeof e[1])!function e(t,n,i){if(null===t)t=l[n];else t=t[n];null!==t&&Object.entries(t).forEach((function(n,s){if("object"==typeof n[1])e(t,n[0],i+"-"+n[0]);else for(var o=_.querySelectorAll("[df-"+i+"-"+n[0]+"]"),l=0;lt(this.noderegister[e.html].html,{props:this.noderegister[e.html].props}),...this.noderegister[e.html].options}).$mount();c.appendChild(t.$el)}Object.entries(e.data).forEach((function(t,n){if("object"==typeof t[1])!function t(n,i,s){if(null===n)n=e.data[i];else n=n[i];null!==n&&Object.entries(n).forEach((function(e,i){if("object"==typeof e[1])t(n,e[0],s+"-"+e[0]);else for(var o=c.querySelectorAll("[df-"+s+"-"+e[0]+"]"),l=0;l{const a=e.outputs[s].connections[o].node,r=e.outputs[s].connections[o].output,h=i.querySelector(".connection.node_in_node-"+a+".node_out_node-"+e.id+"."+s+"."+r);if(n&&0===d)for(var u=0;u{this.removeSingleConnection(e.id_output,e.id,e.output_class,e.input_class)}),delete this.drawflow.drawflow[n].data[e].inputs[t];const o=[],l=this.drawflow.drawflow[n].data[e].inputs;Object.keys(l).map((function(e,t){o.push(l[e])})),this.drawflow.drawflow[n].data[e].inputs={};const c=t.slice(6);let d=[];if(o.forEach((t,i)=>{t.connections.forEach((e,t)=>{d.push(e)}),this.drawflow.drawflow[n].data[e].inputs["input_"+(i+1)]=t}),d=new Set(d.map(e=>JSON.stringify(e))),d=Array.from(d).map(e=>JSON.parse(e)),this.module===n){this.container.querySelectorAll("#node-"+e+" .inputs .input").forEach((e,t)=>{const n=e.classList[1].slice(6);parseInt(c){this.drawflow.drawflow[n].data[t.node].outputs[t.input].connections.forEach((i,s)=>{if(i.node==e){const o=i.output.slice(6);if(parseInt(c){this.removeSingleConnection(e.id,e.id_input,e.output_class,e.input_class)}),delete this.drawflow.drawflow[n].data[e].outputs[t];const o=[],l=this.drawflow.drawflow[n].data[e].outputs;Object.keys(l).map((function(e,t){o.push(l[e])})),this.drawflow.drawflow[n].data[e].outputs={};const c=t.slice(7);let d=[];if(o.forEach((t,i)=>{t.connections.forEach((e,t)=>{d.push({node:e.node,output:e.output})}),this.drawflow.drawflow[n].data[e].outputs["output_"+(i+1)]=t}),d=new Set(d.map(e=>JSON.stringify(e))),d=Array.from(d).map(e=>JSON.parse(e)),this.module===n){this.container.querySelectorAll("#node-"+e+" .outputs .output").forEach((e,t)=>{const n=e.classList[1].slice(7);parseInt(c){this.drawflow.drawflow[n].data[t.node].inputs[t.output].connections.forEach((i,s)=>{if(i.node==e){const o=i.input.slice(7);if(parseInt(c)-1){this.module===s&&this.container.querySelector(".connection.node_in_node-"+t+".node_out_node-"+e+"."+n+"."+i).remove();var o=this.drawflow.drawflow[s].data[e].outputs[n].connections.findIndex((function(e,n){return e.node==t&&e.output===i}));this.drawflow.drawflow[s].data[e].outputs[n].connections.splice(o,1);var l=this.drawflow.drawflow[s].data[t].inputs[i].connections.findIndex((function(t,i){return t.node==e&&t.input===n}));return this.drawflow.drawflow[s].data[t].inputs[i].connections.splice(l,1),this.dispatch("connectionRemoved",{output_id:e,input_id:t,output_class:n,input_class:i}),!0}return!1}return!1}removeConnectionNodeId(e){const t="node_in_"+e,n="node_out_"+e,i=this.container.querySelectorAll("."+n);for(var s=i.length-1;s>=0;s--){var o=i[s].classList,l=this.drawflow.drawflow[this.module].data[o[1].slice(13)].inputs[o[4]].connections.findIndex((function(e,t){return e.node===o[2].slice(14)&&e.input===o[3]}));this.drawflow.drawflow[this.module].data[o[1].slice(13)].inputs[o[4]].connections.splice(l,1);var c=this.drawflow.drawflow[this.module].data[o[2].slice(14)].outputs[o[3]].connections.findIndex((function(e,t){return e.node===o[1].slice(13)&&e.output===o[4]}));this.drawflow.drawflow[this.module].data[o[2].slice(14)].outputs[o[3]].connections.splice(c,1),i[s].remove(),this.dispatch("connectionRemoved",{output_id:o[2].slice(14),input_id:o[1].slice(13),output_class:o[3],input_class:o[4]})}const d=this.container.querySelectorAll("."+t);for(s=d.length-1;s>=0;s--){o=d[s].classList,c=this.drawflow.drawflow[this.module].data[o[2].slice(14)].outputs[o[3]].connections.findIndex((function(e,t){return e.node===o[1].slice(13)&&e.output===o[4]}));this.drawflow.drawflow[this.module].data[o[2].slice(14)].outputs[o[3]].connections.splice(c,1);l=this.drawflow.drawflow[this.module].data[o[1].slice(13)].inputs[o[4]].connections.findIndex((function(e,t){return e.node===o[2].slice(14)&&e.input===o[3]}));this.drawflow.drawflow[this.module].data[o[1].slice(13)].inputs[o[4]].connections.splice(l,1),d[s].remove(),this.dispatch("connectionRemoved",{output_id:o[2].slice(14),input_id:o[1].slice(13),output_class:o[3],input_class:o[4]})}}getModuleFromNodeId(e){var t;const n=this.drawflow.drawflow;return Object.keys(n).map((function(i,s){Object.keys(n[i].data).map((function(n,s){n==e&&(t=i)}))})),t}addModule(e){this.drawflow.drawflow[e]={data:{}},this.dispatch("moduleCreated",e)}changeModule(e){this.dispatch("moduleChanged",e),this.module=e,this.precanvas.innerHTML="",this.canvas_x=0,this.canvas_y=0,this.pos_x=0,this.pos_y=0,this.mouse_x=0,this.mouse_y=0,this.zoom=1,this.zoom_last_value=1,this.precanvas.style.transform="",this.import(this.drawflow,!1)}removeModule(e){this.module===e&&this.changeModule("Home"),delete this.drawflow.drawflow[e],this.dispatch("moduleRemoved",e)}clearModuleSelected(){this.precanvas.innerHTML="",this.drawflow.drawflow[this.module]={data:{}}}clear(){this.precanvas.innerHTML="",this.drawflow={drawflow:{Home:{data:{}}}}}export(){const e=JSON.parse(JSON.stringify(this.drawflow));return this.dispatch("export",e),e}import(e,t=!0){this.clear(),this.drawflow=JSON.parse(JSON.stringify(e)),this.load(),t&&this.dispatch("import","import")}on(e,t){return"function"!=typeof t?(console.error("The listener callback must be a function, the given type is "+typeof t),!1):"string"!=typeof e?(console.error("The event name must be a string, the given type is "+typeof e),!1):(void 0===this.events[e]&&(this.events[e]={listeners:[]}),void this.events[e].listeners.push(t))}removeListener(e,t){if(!this.events[e])return!1;const n=this.events[e].listeners,i=n.indexOf(t);i>-1&&n.splice(i,1)}dispatch(e,t){if(void 0===this.events[e])return!1;this.events[e].listeners.forEach(e=>{e(t)})}getUuid(){for(var e=[],t=0;t<36;t++)e[t]="0123456789abcdef".substr(Math.floor(16*Math.random()),1);return e[14]="4",e[19]="0123456789abcdef".substr(3&e[19]|8,1),e[8]=e[13]=e[18]=e[23]="-",e.join("")}}}]).default})); \ No newline at end of file +!function (e, t) { "object" == typeof exports && "object" == typeof module ? module.exports = t() : "function" == typeof define && define.amd ? define([], t) : "object" == typeof exports ? exports.Drawflow = t() : e.Drawflow = t() }("undefined" != typeof self ? self : this, (function () { return function (e) { var t = {}; function n(i) { if (t[i]) return t[i].exports; var s = t[i] = { i: i, l: !1, exports: {} }; return e[i].call(s.exports, s, s.exports, n), s.l = !0, s.exports } return n.m = e, n.c = t, n.d = function (e, t, i) { n.o(e, t) || Object.defineProperty(e, t, { enumerable: !0, get: i }) }, n.r = function (e) { "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, { value: "Module" }), Object.defineProperty(e, "__esModule", { value: !0 }) }, n.t = function (e, t) { if (1 & t && (e = n(e)), 8 & t) return e; if (4 & t && "object" == typeof e && e && e.__esModule) return e; var i = Object.create(null); if (n.r(i), Object.defineProperty(i, "default", { enumerable: !0, value: e }), 2 & t && "string" != typeof e) for (var s in e) n.d(i, s, function (t) { return e[t] }.bind(null, s)); return i }, n.n = function (e) { var t = e && e.__esModule ? function () { return e.default } : function () { return e }; return n.d(t, "a", t), t }, n.o = function (e, t) { return Object.prototype.hasOwnProperty.call(e, t) }, n.p = "", n(n.s = 0) }([function (e, t, n) { "use strict"; n.r(t), n.d(t, "default", (function () { return i })); class i { constructor(e, t = null, n = null) { this.events = {}, this.container = e, this.precanvas = null, this.nodeId = 1, this.ele_selected = null, this.node_selected = null, this.drag = !1, this.reroute = !1, this.reroute_fix_curvature = !1, this.curvature = .5, this.reroute_curvature_start_end = .5, this.reroute_curvature = .5, this.reroute_width = 6, this.drag_point = !1, this.editor_selected = !1, this.connection = !1, this.connection_ele = null, this.connection_selected = null, this.canvas_x = 0, this.canvas_y = 0, this.pos_x = 0, this.pos_x_start = 0, this.pos_y = 0, this.pos_y_start = 0, this.mouse_x = 0, this.mouse_y = 0, this.line_path = 5, this.first_click = null, this.force_first_input = !1, this.draggable_inputs = !0, this.useuuid = !1, this.parent = n, this.noderegister = {}, this.render = t, this.drawflow = { drawflow: { Home: { data: {} } } }, this.module = "Home", this.editor_mode = "edit", this.zoom = 1, this.zoom_max = 1.6, this.zoom_min = .5, this.zoom_value = .1, this.zoom_last_value = 1, this.evCache = new Array, this.prevDiff = -1 } start() { this.container.classList.add("parent-drawflow"), this.container.tabIndex = 0, this.precanvas = document.createElement("div"), this.precanvas.classList.add("drawflow"), this.container.appendChild(this.precanvas), this.container.addEventListener("mouseup", this.dragEnd.bind(this)), this.container.addEventListener("mousemove", this.position.bind(this)), this.container.addEventListener("mousedown", this.click.bind(this)), this.container.addEventListener("touchend", this.dragEnd.bind(this)), this.container.addEventListener("touchmove", this.position.bind(this)), this.container.addEventListener("touchstart", this.click.bind(this)), this.container.addEventListener("contextmenu", this.contextmenu.bind(this)), this.container.addEventListener("keydown", this.key.bind(this)), this.container.addEventListener("wheel", this.zoom_enter.bind(this)), this.container.addEventListener("input", this.updateNodeValue.bind(this)), this.container.addEventListener("dblclick", this.dblclick.bind(this)), this.container.onpointerdown = this.pointerdown_handler.bind(this), this.container.onpointermove = this.pointermove_handler.bind(this), this.container.onpointerup = this.pointerup_handler.bind(this), this.container.onpointercancel = this.pointerup_handler.bind(this), this.container.onpointerout = this.pointerup_handler.bind(this), this.container.onpointerleave = this.pointerup_handler.bind(this), this.load() } pointerdown_handler(e) { this.evCache.push(e) } pointermove_handler(e) { for (var t = 0; t < this.evCache.length; t++)if (e.pointerId == this.evCache[t].pointerId) { this.evCache[t] = e; break } if (2 == this.evCache.length) { var n = Math.abs(this.evCache[0].clientX - this.evCache[1].clientX); this.prevDiff > 100 && (n > this.prevDiff && this.zoom_in(), n < this.prevDiff && this.zoom_out()), this.prevDiff = n } } pointerup_handler(e) { this.remove_event(e), this.evCache.length < 2 && (this.prevDiff = -1) } remove_event(e) { for (var t = 0; t < this.evCache.length; t++)if (this.evCache[t].pointerId == e.pointerId) { this.evCache.splice(t, 1); break } } load() { for (var e in this.drawflow.drawflow[this.module].data) this.addNodeImport(this.drawflow.drawflow[this.module].data[e], this.precanvas); if (this.reroute) for (var e in this.drawflow.drawflow[this.module].data) this.addRerouteImport(this.drawflow.drawflow[this.module].data[e]); for (var e in this.drawflow.drawflow[this.module].data) this.updateConnectionNodes("node-" + e); const t = this.drawflow.drawflow; let n = 1; Object.keys(t).map((function (e, i) { Object.keys(t[e].data).map((function (e, t) { parseInt(e) >= n && (n = parseInt(e) + 1) })) })), this.nodeId = n } removeReouteConnectionSelected() { this.dispatch("connectionUnselected", !0), this.reroute_fix_curvature && this.connection_selected.parentElement.querySelectorAll(".main-path").forEach((e, t) => { e.classList.remove("selected") }) } click(e) { if (this.dispatch("click", e), "fixed" === this.editor_mode) { if (e.preventDefault(), "parent-drawflow" !== e.target.classList[0] && "drawflow" !== e.target.classList[0]) return !1; this.ele_selected = e.target.closest(".parent-drawflow") } else "view" === this.editor_mode ? (null != e.target.closest(".drawflow") || e.target.matches(".parent-drawflow")) && (this.ele_selected = e.target.closest(".parent-drawflow"), e.preventDefault()) : (this.first_click = e.target, this.ele_selected = e.target, 0 === e.button && this.contextmenuDel(), null != e.target.closest(".drawflow_content_node") && (this.ele_selected = e.target.closest(".drawflow_content_node").parentElement)); switch (this.ele_selected.classList[0]) { case "drawflow-node": null != this.node_selected && (this.node_selected.classList.remove("selected"), this.node_selected != this.ele_selected && this.dispatch("nodeUnselected", !0)), null != this.connection_selected && (this.connection_selected.classList.remove("selected"), this.removeReouteConnectionSelected(), this.connection_selected = null), this.node_selected != this.ele_selected && this.dispatch("nodeSelected", this.ele_selected.id.slice(5)), this.node_selected = this.ele_selected, this.node_selected.classList.add("selected"), this.draggable_inputs ? "SELECT" !== e.target.tagName && (this.drag = !0) : "INPUT" !== e.target.tagName && "TEXTAREA" !== e.target.tagName && "SELECT" !== e.target.tagName && !0 !== e.target.hasAttribute("contenteditable") && (this.drag = !0); break; case "output": this.connection = !0, null != this.node_selected && (this.node_selected.classList.remove("selected"), this.node_selected = null, this.dispatch("nodeUnselected", !0)), null != this.connection_selected && (this.connection_selected.classList.remove("selected"), this.removeReouteConnectionSelected(), this.connection_selected = null), this.drawConnection(e.target); break; case "parent-drawflow": case "drawflow": null != this.node_selected && (this.node_selected.classList.remove("selected"), this.node_selected = null, this.dispatch("nodeUnselected", !0)), null != this.connection_selected && (this.connection_selected.classList.remove("selected"), this.removeReouteConnectionSelected(), this.connection_selected = null), this.editor_selected = !0; break; case "main-path": null != this.node_selected && (this.node_selected.classList.remove("selected"), this.node_selected = null, this.dispatch("nodeUnselected", !0)), null != this.connection_selected && (this.connection_selected.classList.remove("selected"), this.removeReouteConnectionSelected(), this.connection_selected = null), this.connection_selected = this.ele_selected, this.connection_selected.classList.add("selected"); const t = this.connection_selected.parentElement.classList; t.length > 1 && (this.dispatch("connectionSelected", { output_id: t[2].slice(14), input_id: t[1].slice(13), output_class: t[3], input_class: t[4] }), this.reroute_fix_curvature && this.connection_selected.parentElement.querySelectorAll(".main-path").forEach((e, t) => { e.classList.add("selected") })); break; case "point": this.drag_point = !0, this.ele_selected.classList.add("selected"); break; case "drawflow-delete": this.node_selected && this.removeNodeId(this.node_selected.id), this.connection_selected && this.removeConnection(), null != this.node_selected && (this.node_selected.classList.remove("selected"), this.node_selected = null, this.dispatch("nodeUnselected", !0)), null != this.connection_selected && (this.connection_selected.classList.remove("selected"), this.removeReouteConnectionSelected(), this.connection_selected = null) }"touchstart" === e.type ? (this.pos_x = e.touches[0].clientX, this.pos_x_start = e.touches[0].clientX, this.pos_y = e.touches[0].clientY, this.pos_y_start = e.touches[0].clientY) : (this.pos_x = e.clientX, this.pos_x_start = e.clientX, this.pos_y = e.clientY, this.pos_y_start = e.clientY), ["input", "output", "main-path"].includes(this.ele_selected.classList[0]) && e.preventDefault(), this.dispatch("clickEnd", e) } position(e) { if ("touchmove" === e.type) var t = e.touches[0].clientX, n = e.touches[0].clientY; else t = e.clientX, n = e.clientY; if (this.connection && this.updateConnection(t, n), this.editor_selected && (i = this.canvas_x + -(this.pos_x - t), s = this.canvas_y + -(this.pos_y - n), this.dispatch("translate", { x: i, y: s }), this.precanvas.style.transform = "translate(" + i + "px, " + s + "px) scale(" + this.zoom + ")"), this.drag) { e.preventDefault(); var i = (this.pos_x - t) * this.precanvas.clientWidth / (this.precanvas.clientWidth * this.zoom), s = (this.pos_y - n) * this.precanvas.clientHeight / (this.precanvas.clientHeight * this.zoom); this.pos_x = t, this.pos_y = n, this.ele_selected.style.top = this.ele_selected.offsetTop - s + "px", this.ele_selected.style.left = this.ele_selected.offsetLeft - i + "px", this.drawflow.drawflow[this.module].data[this.ele_selected.id.slice(5)].pos_x = this.ele_selected.offsetLeft - i, this.drawflow.drawflow[this.module].data[this.ele_selected.id.slice(5)].pos_y = this.ele_selected.offsetTop - s, this.updateConnectionNodes(this.ele_selected.id) } if (this.drag_point) { i = (this.pos_x - t) * this.precanvas.clientWidth / (this.precanvas.clientWidth * this.zoom), s = (this.pos_y - n) * this.precanvas.clientHeight / (this.precanvas.clientHeight * this.zoom); this.pos_x = t, this.pos_y = n; var o = this.pos_x * (this.precanvas.clientWidth / (this.precanvas.clientWidth * this.zoom)) - this.precanvas.getBoundingClientRect().x * (this.precanvas.clientWidth / (this.precanvas.clientWidth * this.zoom)), l = this.pos_y * (this.precanvas.clientHeight / (this.precanvas.clientHeight * this.zoom)) - this.precanvas.getBoundingClientRect().y * (this.precanvas.clientHeight / (this.precanvas.clientHeight * this.zoom)); this.ele_selected.setAttributeNS(null, "cx", o), this.ele_selected.setAttributeNS(null, "cy", l); const e = this.ele_selected.parentElement.classList[2].slice(9), c = this.ele_selected.parentElement.classList[1].slice(13), d = this.ele_selected.parentElement.classList[3], a = this.ele_selected.parentElement.classList[4]; let r = Array.from(this.ele_selected.parentElement.children).indexOf(this.ele_selected) - 1; if (this.reroute_fix_curvature) { r -= this.ele_selected.parentElement.querySelectorAll(".main-path").length - 1, r < 0 && (r = 0) } const h = e.slice(5), u = this.drawflow.drawflow[this.module].data[h].outputs[d].connections.findIndex((function (e, t) { return e.node === c && e.output === a })); this.drawflow.drawflow[this.module].data[h].outputs[d].connections[u].points[r] = { pos_x: o, pos_y: l }; const p = this.ele_selected.parentElement.classList[2].slice(9); this.updateConnectionNodes(p) } "touchmove" === e.type && (this.mouse_x = t, this.mouse_y = n), this.dispatch("mouseMove", { x: t, y: n }) } dragEnd(e) { if ("touchend" === e.type) var t = this.mouse_x, n = this.mouse_y, i = document.elementFromPoint(t, n); else t = e.clientX, n = e.clientY, i = e.target; if (this.drag && (this.pos_x_start == t && this.pos_y_start == n || this.dispatch("nodeMoved", this.ele_selected.id.slice(5))), this.drag_point && (this.ele_selected.classList.remove("selected"), this.pos_x_start == t && this.pos_y_start == n || this.dispatch("rerouteMoved", this.ele_selected.parentElement.classList[2].slice(14))), this.editor_selected && (this.canvas_x = this.canvas_x + -(this.pos_x - t), this.canvas_y = this.canvas_y + -(this.pos_y - n), this.editor_selected = !1), !0 === this.connection) if ("input" === i.classList[0] || this.force_first_input && (null != i.closest(".drawflow_content_node") || "drawflow-node" === i.classList[0])) { if (!this.force_first_input || null == i.closest(".drawflow_content_node") && "drawflow-node" !== i.classList[0]) s = i.parentElement.parentElement.id, o = i.classList[1]; else { if (null != i.closest(".drawflow_content_node")) var s = i.closest(".drawflow_content_node").parentElement.id; else var s = i.id; if (0 === Object.keys(this.getNodeFromId(s.slice(5)).inputs).length) var o = !1; else var o = "input_1" } var l = this.ele_selected.parentElement.parentElement.id, c = this.ele_selected.classList[1]; if (l !== s && !1 !== o) { if (0 === this.container.querySelectorAll(".connection.node_in_" + s + ".node_out_" + l + "." + c + "." + o).length) { this.connection_ele.classList.add("node_in_" + s), this.connection_ele.classList.add("node_out_" + l), this.connection_ele.classList.add(c), this.connection_ele.classList.add(o); var d = s.slice(5), a = l.slice(5); this.drawflow.drawflow[this.module].data[a].outputs[c].connections.push({ node: d, output: o }), this.drawflow.drawflow[this.module].data[d].inputs[o].connections.push({ node: a, input: c }), this.updateConnectionNodes("node-" + a), this.updateConnectionNodes("node-" + d), this.dispatch("connectionCreated", { output_id: a, input_id: d, output_class: c, input_class: o }) } else this.dispatch("connectionCancel", !0), this.connection_ele.remove(); this.connection_ele = null } else this.dispatch("connectionCancel", !0), this.connection_ele.remove(), this.connection_ele = null } else this.dispatch("connectionCancel", !0), this.connection_ele.remove(), this.connection_ele = null; this.drag = !1, this.drag_point = !1, this.connection = !1, this.ele_selected = null, this.editor_selected = !1, this.dispatch("mouseUp", e) } contextmenu(e) { if (this.dispatch("contextmenu", e), e.preventDefault(), "fixed" === this.editor_mode || "view" === this.editor_mode) return !1; if (this.precanvas.getElementsByClassName("drawflow-delete").length && this.precanvas.getElementsByClassName("drawflow-delete")[0].remove(), this.node_selected || this.connection_selected) { var t = document.createElement("div"); t.classList.add("drawflow-delete"), t.innerHTML = "x", this.node_selected && this.node_selected.appendChild(t), this.connection_selected && this.connection_selected.parentElement.classList.length > 1 && (t.style.top = e.clientY * (this.precanvas.clientHeight / (this.precanvas.clientHeight * this.zoom)) - this.precanvas.getBoundingClientRect().y * (this.precanvas.clientHeight / (this.precanvas.clientHeight * this.zoom)) + "px", t.style.left = e.clientX * (this.precanvas.clientWidth / (this.precanvas.clientWidth * this.zoom)) - this.precanvas.getBoundingClientRect().x * (this.precanvas.clientWidth / (this.precanvas.clientWidth * this.zoom)) + "px", this.precanvas.appendChild(t)) } } contextmenuDel() { this.precanvas.getElementsByClassName("drawflow-delete").length && this.precanvas.getElementsByClassName("drawflow-delete")[0].remove() } key(e) { if (this.dispatch("keydown", e), "fixed" === this.editor_mode || "view" === this.editor_mode) return !1; ("Delete" === e.key || "Backspace" === e.key && e.metaKey) && (null != this.node_selected && "INPUT" !== this.first_click.tagName && "TEXTAREA" !== this.first_click.tagName && !0 !== this.first_click.hasAttribute("contenteditable") && this.removeNodeId(this.node_selected.id), null != this.connection_selected && this.removeConnection()) } zoom_enter(e, t) { e.ctrlKey && (e.preventDefault(), e.deltaY > 0 ? this.zoom_out() : this.zoom_in()) } zoom_refresh() { this.dispatch("zoom", this.zoom), this.canvas_x = this.canvas_x / this.zoom_last_value * this.zoom, this.canvas_y = this.canvas_y / this.zoom_last_value * this.zoom, this.zoom_last_value = this.zoom, this.precanvas.style.transform = "translate(" + this.canvas_x + "px, " + this.canvas_y + "px) scale(" + this.zoom + ")" } zoom_in() { this.zoom < this.zoom_max && (this.zoom += this.zoom_value, this.zoom_refresh()) } zoom_out() { this.zoom > this.zoom_min && (this.zoom -= this.zoom_value, this.zoom_refresh()) } zoom_reset() { 1 != this.zoom && (this.zoom = 1, this.zoom_refresh()) } createCurvature(e, t, n, i, s, o) { var l = e, c = t, d = n, a = i, r = s; switch (o) { case "open": if (e >= n) var h = l + Math.abs(d - l) * r, u = d - Math.abs(d - l) * (-1 * r); else h = l + Math.abs(d - l) * r, u = d - Math.abs(d - l) * r; return " M " + l + " " + c + " C " + h + " " + c + " " + u + " " + a + " " + d + " " + a; case "close": if (e >= n) h = l + Math.abs(d - l) * (-1 * r), u = d - Math.abs(d - l) * r; else h = l + Math.abs(d - l) * r, u = d - Math.abs(d - l) * r; return " M " + l + " " + c + " C " + h + " " + c + " " + u + " " + a + " " + d + " " + a; case "other": if (e >= n) h = l + Math.abs(d - l) * (-1 * r), u = d - Math.abs(d - l) * (-1 * r); else h = l + Math.abs(d - l) * r, u = d - Math.abs(d - l) * r; return " M " + l + " " + c + " C " + h + " " + c + " " + u + " " + a + " " + d + " " + a; default: return " M " + l + " " + c + " C " + (h = l + Math.abs(d - l) * r) + " " + c + " " + (u = d - Math.abs(d - l) * r) + " " + a + " " + d + " " + a } } drawConnection(e) { var t = document.createElementNS("http://www.w3.org/2000/svg", "svg"); this.connection_ele = t; var n = document.createElementNS("http://www.w3.org/2000/svg", "path"); n.classList.add("main-path"), n.setAttributeNS(null, "d", ""), t.classList.add("connection"), t.appendChild(n), this.precanvas.appendChild(t); var i = e.parentElement.parentElement.id.slice(5), s = e.classList[1]; this.dispatch("connectionStart", { output_id: i, output_class: s }) } updateConnection(e, t) { const n = this.precanvas, i = this.zoom; let s = n.clientWidth / (n.clientWidth * i); s = s || 0; let o = n.clientHeight / (n.clientHeight * i); o = o || 0; var l = this.connection_ele.children[0], c = this.ele_selected.offsetWidth / 2 + (this.ele_selected.getBoundingClientRect().x - n.getBoundingClientRect().x) * s, d = this.ele_selected.offsetHeight / 2 + (this.ele_selected.getBoundingClientRect().y - n.getBoundingClientRect().y) * o, a = e * (this.precanvas.clientWidth / (this.precanvas.clientWidth * this.zoom)) - this.precanvas.getBoundingClientRect().x * (this.precanvas.clientWidth / (this.precanvas.clientWidth * this.zoom)), r = t * (this.precanvas.clientHeight / (this.precanvas.clientHeight * this.zoom)) - this.precanvas.getBoundingClientRect().y * (this.precanvas.clientHeight / (this.precanvas.clientHeight * this.zoom)), h = this.curvature, u = this.createCurvature(c, d, a, r, h, "openclose"); l.setAttributeNS(null, "d", u) } addConnection(e, t, n, i) { var s = this.getModuleFromNodeId(e); if (s === this.getModuleFromNodeId(t)) { var o = this.getNodeFromId(e), l = !1; for (var c in o.outputs[n].connections) { var d = o.outputs[n].connections[c]; d.node == t && d.output == i && (l = !0) } if (!1 === l) { if (this.drawflow.drawflow[s].data[e].outputs[n].connections.push({ node: t.toString(), output: i }), this.drawflow.drawflow[s].data[t].inputs[i].connections.push({ node: e.toString(), input: n }), this.module === s) { var a = document.createElementNS("http://www.w3.org/2000/svg", "svg"), r = document.createElementNS("http://www.w3.org/2000/svg", "path"); r.classList.add("main-path"), r.setAttributeNS(null, "d", ""), a.classList.add("connection"), a.classList.add("node_in_node-" + t), a.classList.add("node_out_node-" + e), a.classList.add(n), a.classList.add(i), a.appendChild(r), this.precanvas.appendChild(a), this.updateConnectionNodes("node-" + e), this.updateConnectionNodes("node-" + t) } this.dispatch("connectionCreated", { output_id: e, input_id: t, output_class: n, input_class: i }) } } } updateConnectionNodes(e) { const t = "node_in_" + e, n = "node_out_" + e; this.line_path; const i = this.container, s = this.precanvas, o = this.curvature, l = this.createCurvature, c = this.reroute_curvature, d = this.reroute_curvature_start_end, a = this.reroute_fix_curvature, r = this.reroute_width, h = this.zoom; let u = s.clientWidth / (s.clientWidth * h); u = u || 0; let p = s.clientHeight / (s.clientHeight * h); p = p || 0; const f = i.querySelectorAll("." + n); Object.keys(f).map((function (t, n) { if (null === f[t].querySelector(".point")) { var g = i.querySelector("#" + e), m = f[t].classList[1].replace("node_in_", ""), _ = i.querySelector("#" + m).querySelectorAll("." + f[t].classList[4])[0], w = _.offsetWidth / 2 + (_.getBoundingClientRect().x - s.getBoundingClientRect().x) * u, v = _.offsetHeight / 2 + (_.getBoundingClientRect().y - s.getBoundingClientRect().y) * p, y = g.querySelectorAll("." + f[t].classList[3])[0], C = y.offsetWidth / 2 + (y.getBoundingClientRect().x - s.getBoundingClientRect().x) * u, x = y.offsetHeight / 2 + (y.getBoundingClientRect().y - s.getBoundingClientRect().y) * p; const n = l(C, x, w, v, o, "openclose"); f[t].children[0].setAttributeNS(null, "d", n) } else { const n = f[t].querySelectorAll(".point"); let o = ""; const g = []; n.forEach((t, a) => { if (0 === a && n.length - 1 == 0) { var f = i.querySelector("#" + e), m = ((x = t).getBoundingClientRect().x - s.getBoundingClientRect().x) * u + r, _ = (x.getBoundingClientRect().y - s.getBoundingClientRect().y) * p + r, w = (L = f.querySelectorAll("." + t.parentElement.classList[3])[0]).offsetWidth / 2 + (L.getBoundingClientRect().x - s.getBoundingClientRect().x) * u, v = L.offsetHeight / 2 + (L.getBoundingClientRect().y - s.getBoundingClientRect().y) * p, y = l(w, v, m, _, d, "open"); o += y, g.push(y); f = t; var C = t.parentElement.classList[1].replace("node_in_", ""), x = (E = i.querySelector("#" + C)).querySelectorAll("." + t.parentElement.classList[4])[0]; m = (R = E.querySelectorAll("." + t.parentElement.classList[4])[0]).offsetWidth / 2 + (R.getBoundingClientRect().x - s.getBoundingClientRect().x) * u, _ = R.offsetHeight / 2 + (R.getBoundingClientRect().y - s.getBoundingClientRect().y) * p, w = (f.getBoundingClientRect().x - s.getBoundingClientRect().x) * u + r, v = (f.getBoundingClientRect().y - s.getBoundingClientRect().y) * p + r, y = l(w, v, m, _, d, "close"); o += y, g.push(y) } else if (0 === a) { var L; f = i.querySelector("#" + e), m = ((x = t).getBoundingClientRect().x - s.getBoundingClientRect().x) * u + r, _ = (x.getBoundingClientRect().y - s.getBoundingClientRect().y) * p + r, w = (L = f.querySelectorAll("." + t.parentElement.classList[3])[0]).offsetWidth / 2 + (L.getBoundingClientRect().x - s.getBoundingClientRect().x) * u, v = L.offsetHeight / 2 + (L.getBoundingClientRect().y - s.getBoundingClientRect().y) * p, y = l(w, v, m, _, d, "open"); o += y, g.push(y); f = t, m = ((x = n[a + 1]).getBoundingClientRect().x - s.getBoundingClientRect().x) * u + r, _ = (x.getBoundingClientRect().y - s.getBoundingClientRect().y) * p + r, w = (f.getBoundingClientRect().x - s.getBoundingClientRect().x) * u + r, v = (f.getBoundingClientRect().y - s.getBoundingClientRect().y) * p + r, y = l(w, v, m, _, c, "other"); o += y, g.push(y) } else if (a === n.length - 1) { var E, R; f = t, C = t.parentElement.classList[1].replace("node_in_", ""), x = (E = i.querySelector("#" + C)).querySelectorAll("." + t.parentElement.classList[4])[0], m = (R = E.querySelectorAll("." + t.parentElement.classList[4])[0]).offsetWidth / 2 + (R.getBoundingClientRect().x - s.getBoundingClientRect().x) * u, _ = R.offsetHeight / 2 + (R.getBoundingClientRect().y - s.getBoundingClientRect().y) * p, w = (f.getBoundingClientRect().x - s.getBoundingClientRect().x) * (s.clientWidth / (s.clientWidth * h)) + r, v = (f.getBoundingClientRect().y - s.getBoundingClientRect().y) * (s.clientHeight / (s.clientHeight * h)) + r, y = l(w, v, m, _, d, "close"); o += y, g.push(y) } else { f = t, m = ((x = n[a + 1]).getBoundingClientRect().x - s.getBoundingClientRect().x) * (s.clientWidth / (s.clientWidth * h)) + r, _ = (x.getBoundingClientRect().y - s.getBoundingClientRect().y) * (s.clientHeight / (s.clientHeight * h)) + r, w = (f.getBoundingClientRect().x - s.getBoundingClientRect().x) * (s.clientWidth / (s.clientWidth * h)) + r, v = (f.getBoundingClientRect().y - s.getBoundingClientRect().y) * (s.clientHeight / (s.clientHeight * h)) + r, y = l(w, v, m, _, c, "other"); o += y, g.push(y) } }), a ? g.forEach((e, n) => { f[t].children[n].setAttributeNS(null, "d", e) }) : f[t].children[0].setAttributeNS(null, "d", o) } })); const g = i.querySelectorAll("." + t); Object.keys(g).map((function (t, n) { if (null === g[t].querySelector(".point")) { var h = i.querySelector("#" + e), f = g[t].classList[2].replace("node_out_", ""), m = i.querySelector("#" + f).querySelectorAll("." + g[t].classList[3])[0], _ = m.offsetWidth / 2 + (m.getBoundingClientRect().x - s.getBoundingClientRect().x) * u, w = m.offsetHeight / 2 + (m.getBoundingClientRect().y - s.getBoundingClientRect().y) * p, v = (h = h.querySelectorAll("." + g[t].classList[4])[0]).offsetWidth / 2 + (h.getBoundingClientRect().x - s.getBoundingClientRect().x) * u, y = h.offsetHeight / 2 + (h.getBoundingClientRect().y - s.getBoundingClientRect().y) * p; const n = l(_, w, v, y, o, "openclose"); g[t].children[0].setAttributeNS(null, "d", n) } else { const n = g[t].querySelectorAll(".point"); let o = ""; const h = []; n.forEach((t, a) => { if (0 === a && n.length - 1 == 0) { var f = i.querySelector("#" + e), g = ((C = t).getBoundingClientRect().x - s.getBoundingClientRect().x) * u + r, m = (C.getBoundingClientRect().y - s.getBoundingClientRect().y) * p + r, _ = (E = f.querySelectorAll("." + t.parentElement.classList[4])[0]).offsetWidth / 2 + (E.getBoundingClientRect().x - s.getBoundingClientRect().x) * u, w = E.offsetHeight / 2 + (E.getBoundingClientRect().y - s.getBoundingClientRect().y) * p, v = l(g, m, _, w, d, "close"); o += v, h.push(v); f = t; var y = t.parentElement.classList[2].replace("node_out_", ""), C = (L = i.querySelector("#" + y)).querySelectorAll("." + t.parentElement.classList[3])[0]; g = (x = L.querySelectorAll("." + t.parentElement.classList[3])[0]).offsetWidth / 2 + (x.getBoundingClientRect().x - s.getBoundingClientRect().x) * u, m = x.offsetHeight / 2 + (x.getBoundingClientRect().y - s.getBoundingClientRect().y) * p, _ = (f.getBoundingClientRect().x - s.getBoundingClientRect().x) * u + r, w = (f.getBoundingClientRect().y - s.getBoundingClientRect().y) * p + r, v = l(g, m, _, w, d, "open"); o += v, h.push(v) } else if (0 === a) { var x; f = t, y = t.parentElement.classList[2].replace("node_out_", ""), C = (L = i.querySelector("#" + y)).querySelectorAll("." + t.parentElement.classList[3])[0], g = (x = L.querySelectorAll("." + t.parentElement.classList[3])[0]).offsetWidth / 2 + (x.getBoundingClientRect().x - s.getBoundingClientRect().x) * u, m = x.offsetHeight / 2 + (x.getBoundingClientRect().y - s.getBoundingClientRect().y) * p, _ = (f.getBoundingClientRect().x - s.getBoundingClientRect().x) * u + r, w = (f.getBoundingClientRect().y - s.getBoundingClientRect().y) * p + r, v = l(g, m, _, w, d, "open"); o += v, h.push(v); f = t, _ = ((C = n[a + 1]).getBoundingClientRect().x - s.getBoundingClientRect().x) * u + r, w = (C.getBoundingClientRect().y - s.getBoundingClientRect().y) * p + r, g = (f.getBoundingClientRect().x - s.getBoundingClientRect().x) * u + r, m = (f.getBoundingClientRect().y - s.getBoundingClientRect().y) * p + r, v = l(g, m, _, w, c, "other"); o += v, h.push(v) } else if (a === n.length - 1) { var L, E; f = t, y = t.parentElement.classList[1].replace("node_in_", ""), C = (L = i.querySelector("#" + y)).querySelectorAll("." + t.parentElement.classList[4])[0], _ = (E = L.querySelectorAll("." + t.parentElement.classList[4])[0]).offsetWidth / 2 + (E.getBoundingClientRect().x - s.getBoundingClientRect().x) * u, w = E.offsetHeight / 2 + (E.getBoundingClientRect().y - s.getBoundingClientRect().y) * p, g = (f.getBoundingClientRect().x - s.getBoundingClientRect().x) * u + r, m = (f.getBoundingClientRect().y - s.getBoundingClientRect().y) * p + r, v = l(g, m, _, w, d, "close"); o += v, h.push(v) } else { f = t, _ = ((C = n[a + 1]).getBoundingClientRect().x - s.getBoundingClientRect().x) * u + r, w = (C.getBoundingClientRect().y - s.getBoundingClientRect().y) * p + r, g = (f.getBoundingClientRect().x - s.getBoundingClientRect().x) * u + r, m = (f.getBoundingClientRect().y - s.getBoundingClientRect().y) * p + r, v = l(g, m, _, w, c, "other"); o += v, h.push(v) } }), a ? h.forEach((e, n) => { g[t].children[n].setAttributeNS(null, "d", e) }) : g[t].children[0].setAttributeNS(null, "d", o) } })) } dblclick(e) { null != this.connection_selected && this.reroute && this.createReroutePoint(this.connection_selected), "point" === e.target.classList[0] && this.removeReroutePoint(e.target) } createReroutePoint(e) { this.connection_selected.classList.remove("selected"); const t = this.connection_selected.parentElement.classList[2].slice(9), n = this.connection_selected.parentElement.classList[1].slice(13), i = this.connection_selected.parentElement.classList[3], s = this.connection_selected.parentElement.classList[4]; this.connection_selected = null; const o = document.createElementNS("http://www.w3.org/2000/svg", "circle"); o.classList.add("point"); var l = this.pos_x * (this.precanvas.clientWidth / (this.precanvas.clientWidth * this.zoom)) - this.precanvas.getBoundingClientRect().x * (this.precanvas.clientWidth / (this.precanvas.clientWidth * this.zoom)), c = this.pos_y * (this.precanvas.clientHeight / (this.precanvas.clientHeight * this.zoom)) - this.precanvas.getBoundingClientRect().y * (this.precanvas.clientHeight / (this.precanvas.clientHeight * this.zoom)); o.setAttributeNS(null, "cx", l), o.setAttributeNS(null, "cy", c), o.setAttributeNS(null, "r", this.reroute_width); let d = 0; if (this.reroute_fix_curvature) { const t = e.parentElement.querySelectorAll(".main-path").length; var a = document.createElementNS("http://www.w3.org/2000/svg", "path"); if (a.classList.add("main-path"), a.setAttributeNS(null, "d", ""), e.parentElement.insertBefore(a, e.parentElement.children[t]), 1 === t) e.parentElement.appendChild(o); else { const n = Array.from(e.parentElement.children).indexOf(e); d = n, e.parentElement.insertBefore(o, e.parentElement.children[n + t + 1]) } } else e.parentElement.appendChild(o); const r = t.slice(5), h = this.drawflow.drawflow[this.module].data[r].outputs[i].connections.findIndex((function (e, t) { return e.node === n && e.output === s })); void 0 === this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points && (this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points = []), this.reroute_fix_curvature ? (d > 0 || this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points !== [] ? this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points.splice(d, 0, { pos_x: l, pos_y: c }) : this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points.push({ pos_x: l, pos_y: c }), e.parentElement.querySelectorAll(".main-path").forEach((e, t) => { e.classList.remove("selected") })) : this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points.push({ pos_x: l, pos_y: c }), this.dispatch("addReroute", r), this.updateConnectionNodes(t) } removeReroutePoint(e) { const t = e.parentElement.classList[2].slice(9), n = e.parentElement.classList[1].slice(13), i = e.parentElement.classList[3], s = e.parentElement.classList[4]; let o = Array.from(e.parentElement.children).indexOf(e); const l = t.slice(5), c = this.drawflow.drawflow[this.module].data[l].outputs[i].connections.findIndex((function (e, t) { return e.node === n && e.output === s })); if (this.reroute_fix_curvature) { const t = e.parentElement.querySelectorAll(".main-path").length; e.parentElement.children[t - 1].remove(), o -= t, o < 0 && (o = 0) } else o--; this.drawflow.drawflow[this.module].data[l].outputs[i].connections[c].points.splice(o, 1), e.remove(), this.dispatch("removeReroute", l), this.updateConnectionNodes(t) } registerNode(e, t, n = null, i = null) { this.noderegister[e] = { html: t, props: n, options: i } } getNodeFromId(e) { var t = this.getModuleFromNodeId(e); return JSON.parse(JSON.stringify(this.drawflow.drawflow[t].data[e])) } getNodesFromName(e) { var t = []; const n = this.drawflow.drawflow; return Object.keys(n).map((function (i, s) { for (var o in n[i].data) n[i].data[o].name == e && t.push(n[i].data[o].id) })), t } addNode(e, t, n, i, s, o, l, c, d = !1) { if (this.useuuid) var a = this.getUuid(); else a = this.nodeId; const r = document.createElement("div"); r.classList.add("parent-node"); const h = document.createElement("div"); h.innerHTML = "", h.setAttribute("id", "node-" + a), h.classList.add("drawflow-node"), "" != o && h.classList.add(...o.split(" ")); const u = document.createElement("div"); u.classList.add("inputs"); const p = document.createElement("div"); p.classList.add("outputs"); const f = {}; for (var g = 0; g < t; g++) { const e = document.createElement("div"); e.classList.add("input"), e.classList.add("input_" + (g + 1)), f["input_" + (g + 1)] = { connections: [] }, u.appendChild(e) } const m = {}; for (g = 0; g < n; g++) { const e = document.createElement("div"); e.classList.add("output"), e.classList.add("output_" + (g + 1)), m["output_" + (g + 1)] = { connections: [] }, p.appendChild(e) } const _ = document.createElement("div"); if (_.classList.add("drawflow_content_node"), !1 === d) _.innerHTML = c; else if (!0 === d) _.appendChild(this.noderegister[c].html.cloneNode(!0)); else if (3 === parseInt(this.render.version)) { let e = this.render.h(this.noderegister[c].html, this.noderegister[c].props, this.noderegister[c].options); e.appContext = this.parent, this.render.render(e, _) } else { let e = new this.render({ parent: this.parent, render: e => e(this.noderegister[c].html, { props: this.noderegister[c].props }), ...this.noderegister[c].options }).$mount(); _.appendChild(e.$el) } Object.entries(l).forEach((function (e, t) { if ("object" == typeof e[1]) !function e(t, n, i) { if (null === t) t = l[n]; else t = t[n]; null !== t && Object.entries(t).forEach((function (n, s) { if ("object" == typeof n[1]) e(t, n[0], i + "-" + n[0]); else for (var o = _.querySelectorAll("[df-" + i + "-" + n[0] + "]"), l = 0; l < o.length; l++)o[l].value = n[1], o[l].isContentEditable && (o[l].innerText = n[1]) })) }(null, e[0], e[0]); else for (var n = _.querySelectorAll("[df-" + e[0] + "]"), i = 0; i < n.length; i++)n[i].value = e[1], n[i].isContentEditable && (n[i].innerText = e[1]) })), h.appendChild(u), h.appendChild(_), h.appendChild(p), h.style.top = s + "px", h.style.left = i + "px", r.appendChild(h), this.precanvas.appendChild(r); var w = { id: a, name: e, data: l, class: o, html: c, typenode: d, inputs: f, outputs: m, pos_x: i, pos_y: s }; return this.drawflow.drawflow[this.module].data[a] = w, this.dispatch("nodeCreated", a), this.useuuid || this.nodeId++, a } addNodeImport(e, t) { const n = document.createElement("div"); n.classList.add("parent-node"); const i = document.createElement("div"); i.innerHTML = "", i.setAttribute("id", "node-" + e.id), i.classList.add("drawflow-node"), "" != e.class && i.classList.add(...e.class.split(" ")); const s = document.createElement("div"); s.classList.add("inputs"); const o = document.createElement("div"); o.classList.add("outputs"), Object.keys(e.inputs).map((function (n, i) { const o = document.createElement("div"); o.classList.add("input"), o.classList.add(n), s.appendChild(o), Object.keys(e.inputs[n].connections).map((function (i, s) { var o = document.createElementNS("http://www.w3.org/2000/svg", "svg"), l = document.createElementNS("http://www.w3.org/2000/svg", "path"); l.classList.add("main-path"), l.setAttributeNS(null, "d", ""), o.classList.add("connection"), o.classList.add("node_in_node-" + e.id), o.classList.add("node_out_node-" + e.inputs[n].connections[i].node), o.classList.add(e.inputs[n].connections[i].input), o.classList.add(n), o.appendChild(l), t.appendChild(o) })) })); for (var l = 0; l < Object.keys(e.outputs).length; l++) { const e = document.createElement("div"); e.classList.add("output"), e.classList.add("output_" + (l + 1)), o.appendChild(e) } const c = document.createElement("div"); if (c.classList.add("drawflow_content_node"), !1 === e.typenode) c.innerHTML = e.html; else if (!0 === e.typenode) c.appendChild(this.noderegister[e.html].html.cloneNode(!0)); else if (3 === parseInt(this.render.version)) { let t = this.render.h(this.noderegister[e.html].html, this.noderegister[e.html].props, this.noderegister[e.html].options); t.appContext = this.parent, this.render.render(t, c) } else { let t = new this.render({ parent: this.parent, render: t => t(this.noderegister[e.html].html, { props: this.noderegister[e.html].props }), ...this.noderegister[e.html].options }).$mount(); c.appendChild(t.$el) } Object.entries(e.data).forEach((function (t, n) { if ("object" == typeof t[1]) !function t(n, i, s) { if (null === n) n = e.data[i]; else n = n[i]; null !== n && Object.entries(n).forEach((function (e, i) { if ("object" == typeof e[1]) t(n, e[0], s + "-" + e[0]); else for (var o = c.querySelectorAll("[df-" + s + "-" + e[0] + "]"), l = 0; l < o.length; l++)o[l].value = e[1], o[l].isContentEditable && (o[l].innerText = e[1]) })) }(null, t[0], t[0]); else for (var i = c.querySelectorAll("[df-" + t[0] + "]"), s = 0; s < i.length; s++)i[s].value = t[1], i[s].isContentEditable && (i[s].innerText = t[1]) })), i.appendChild(s), i.appendChild(c), i.appendChild(o), i.style.top = e.pos_y + "px", i.style.left = e.pos_x + "px", n.appendChild(i), this.precanvas.appendChild(n) } addRerouteImport(e) { const t = this.reroute_width, n = this.reroute_fix_curvature, i = this.container; Object.keys(e.outputs).map((function (s, o) { Object.keys(e.outputs[s].connections).map((function (o, l) { const c = e.outputs[s].connections[o].points; void 0 !== c && c.forEach((l, d) => { const a = e.outputs[s].connections[o].node, r = e.outputs[s].connections[o].output, h = i.querySelector(".connection.node_in_node-" + a + ".node_out_node-" + e.id + "." + s + "." + r); if (n && 0 === d) for (var u = 0; u < c.length; u++) { var p = document.createElementNS("http://www.w3.org/2000/svg", "path"); p.classList.add("main-path"), p.setAttributeNS(null, "d", ""), h.appendChild(p) } const f = document.createElementNS("http://www.w3.org/2000/svg", "circle"); f.classList.add("point"); var g = l.pos_x, m = l.pos_y; f.setAttributeNS(null, "cx", g), f.setAttributeNS(null, "cy", m), f.setAttributeNS(null, "r", t), h.appendChild(f) }) })) })) } updateNodeValue(e) { for (var t = e.target.attributes, n = 0; n < t.length; n++)if (t[n].nodeName.startsWith("df-")) { for (var i = t[n].nodeName.slice(3).split("-"), s = this.drawflow.drawflow[this.module].data[e.target.closest(".drawflow_content_node").parentElement.id.slice(5)].data, o = 0; o < i.length - 1; o += 1)null == s[i[o]] && (s[i[o]] = {}), s = s[i[o]]; s[i[i.length - 1]] = e.target.value, e.target.isContentEditable && (s[i[i.length - 1]] = e.target.innerText), this.dispatch("nodeDataChanged", e.target.closest(".drawflow_content_node").parentElement.id.slice(5)) } } updateNodeDataFromId(e, t) { var n = this.getModuleFromNodeId(e); if (this.drawflow.drawflow[n].data[e].data = t, this.module === n) { const n = this.container.querySelector("#node-" + e); Object.entries(t).forEach((function (e, i) { if ("object" == typeof e[1]) !function e(i, s, o) { if (null === i) i = t[s]; else i = i[s]; null !== i && Object.entries(i).forEach((function (t, s) { if ("object" == typeof t[1]) e(i, t[0], o + "-" + t[0]); else for (var l = n.querySelectorAll("[df-" + o + "-" + t[0] + "]"), c = 0; c < l.length; c++)l[c].value = t[1], l[c].isContentEditable && (l[c].innerText = t[1]) })) }(null, e[0], e[0]); else for (var s = n.querySelectorAll("[df-" + e[0] + "]"), o = 0; o < s.length; o++)s[o].value = e[1], s[o].isContentEditable && (s[o].innerText = e[1]) })) } } addNodeInput(e) { var t = this.getModuleFromNodeId(e); const n = this.getNodeFromId(e), i = Object.keys(n.inputs).length; if (this.module === t) { const t = document.createElement("div"); t.classList.add("input"), t.classList.add("input_" + (i + 1)), this.container.querySelector("#node-" + e + " .inputs").appendChild(t), this.updateConnectionNodes("node-" + e) } this.drawflow.drawflow[t].data[e].inputs["input_" + (i + 1)] = { connections: [] } } addNodeOutput(e) { var t = this.getModuleFromNodeId(e); const n = this.getNodeFromId(e), i = Object.keys(n.outputs).length; if (this.module === t) { const t = document.createElement("div"); t.classList.add("output"), t.classList.add("output_" + (i + 1)), this.container.querySelector("#node-" + e + " .outputs").appendChild(t), this.updateConnectionNodes("node-" + e) } this.drawflow.drawflow[t].data[e].outputs["output_" + (i + 1)] = { connections: [] } } removeNodeInput(e, t) { var n = this.getModuleFromNodeId(e); const i = this.getNodeFromId(e); this.module === n && this.container.querySelector("#node-" + e + " .inputs .input." + t).remove(); const s = []; Object.keys(i.inputs[t].connections).map((function (n, o) { const l = i.inputs[t].connections[o].node, c = i.inputs[t].connections[o].input; s.push({ id_output: l, id: e, output_class: c, input_class: t }) })), s.forEach((e, t) => { this.removeSingleConnection(e.id_output, e.id, e.output_class, e.input_class) }), delete this.drawflow.drawflow[n].data[e].inputs[t]; const o = [], l = this.drawflow.drawflow[n].data[e].inputs; Object.keys(l).map((function (e, t) { o.push(l[e]) })), this.drawflow.drawflow[n].data[e].inputs = {}; const c = t.slice(6); let d = []; if (o.forEach((t, i) => { t.connections.forEach((e, t) => { d.push(e) }), this.drawflow.drawflow[n].data[e].inputs["input_" + (i + 1)] = t }), d = new Set(d.map(e => JSON.stringify(e))), d = Array.from(d).map(e => JSON.parse(e)), this.module === n) { this.container.querySelectorAll("#node-" + e + " .inputs .input").forEach((e, t) => { const n = e.classList[1].slice(6); parseInt(c) < parseInt(n) && (e.classList.remove("input_" + n), e.classList.add("input_" + (n - 1))) }) } d.forEach((t, i) => { this.drawflow.drawflow[n].data[t.node].outputs[t.input].connections.forEach((i, s) => { if (i.node == e) { const o = i.output.slice(6); if (parseInt(c) < parseInt(o)) { if (this.module === n) { const n = this.container.querySelector(".connection.node_in_node-" + e + ".node_out_node-" + t.node + "." + t.input + ".input_" + o); n.classList.remove("input_" + o), n.classList.add("input_" + (o - 1)) } i.points ? this.drawflow.drawflow[n].data[t.node].outputs[t.input].connections[s] = { node: i.node, output: "input_" + (o - 1), points: i.points } : this.drawflow.drawflow[n].data[t.node].outputs[t.input].connections[s] = { node: i.node, output: "input_" + (o - 1) } } } }) }), this.updateConnectionNodes("node-" + e) } removeNodeOutput(e, t) { var n = this.getModuleFromNodeId(e); const i = this.getNodeFromId(e); this.module === n && this.container.querySelector("#node-" + e + " .outputs .output." + t).remove(); const s = []; Object.keys(i.outputs[t].connections).map((function (n, o) { const l = i.outputs[t].connections[o].node, c = i.outputs[t].connections[o].output; s.push({ id: e, id_input: l, output_class: t, input_class: c }) })), s.forEach((e, t) => { this.removeSingleConnection(e.id, e.id_input, e.output_class, e.input_class) }), delete this.drawflow.drawflow[n].data[e].outputs[t]; const o = [], l = this.drawflow.drawflow[n].data[e].outputs; Object.keys(l).map((function (e, t) { o.push(l[e]) })), this.drawflow.drawflow[n].data[e].outputs = {}; const c = t.slice(7); let d = []; if (o.forEach((t, i) => { t.connections.forEach((e, t) => { d.push({ node: e.node, output: e.output }) }), this.drawflow.drawflow[n].data[e].outputs["output_" + (i + 1)] = t }), d = new Set(d.map(e => JSON.stringify(e))), d = Array.from(d).map(e => JSON.parse(e)), this.module === n) { this.container.querySelectorAll("#node-" + e + " .outputs .output").forEach((e, t) => { const n = e.classList[1].slice(7); parseInt(c) < parseInt(n) && (e.classList.remove("output_" + n), e.classList.add("output_" + (n - 1))) }) } d.forEach((t, i) => { this.drawflow.drawflow[n].data[t.node].inputs[t.output].connections.forEach((i, s) => { if (i.node == e) { const o = i.input.slice(7); if (parseInt(c) < parseInt(o)) { if (this.module === n) { const n = this.container.querySelector(".connection.node_in_node-" + t.node + ".node_out_node-" + e + ".output_" + o + "." + t.output); n.classList.remove("output_" + o), n.classList.remove(t.output), n.classList.add("output_" + (o - 1)), n.classList.add(t.output) } i.points ? this.drawflow.drawflow[n].data[t.node].inputs[t.output].connections[s] = { node: i.node, input: "output_" + (o - 1), points: i.points } : this.drawflow.drawflow[n].data[t.node].inputs[t.output].connections[s] = { node: i.node, input: "output_" + (o - 1) } } } }) }), this.updateConnectionNodes("node-" + e) } removeNodeId(e) { this.removeConnectionNodeId(e); var t = this.getModuleFromNodeId(e.slice(5)); this.module === t && this.container.querySelector("#" + e).remove(), delete this.drawflow.drawflow[t].data[e.slice(5)], this.dispatch("nodeRemoved", e.slice(5)) } removeConnection() { if (null != this.connection_selected) { var e = this.connection_selected.parentElement.classList; this.connection_selected.parentElement.remove(); var t = this.drawflow.drawflow[this.module].data[e[2].slice(14)].outputs[e[3]].connections.findIndex((function (t, n) { return t.node === e[1].slice(13) && t.output === e[4] })); this.drawflow.drawflow[this.module].data[e[2].slice(14)].outputs[e[3]].connections.splice(t, 1); var n = this.drawflow.drawflow[this.module].data[e[1].slice(13)].inputs[e[4]].connections.findIndex((function (t, n) { return t.node === e[2].slice(14) && t.input === e[3] })); this.drawflow.drawflow[this.module].data[e[1].slice(13)].inputs[e[4]].connections.splice(n, 1), this.dispatch("connectionRemoved", { output_id: e[2].slice(14), input_id: e[1].slice(13), output_class: e[3], input_class: e[4] }), this.connection_selected = null } } removeSingleConnection(e, t, n, i) { var s = this.getModuleFromNodeId(e); if (s === this.getModuleFromNodeId(t)) { if (this.drawflow.drawflow[s].data[e].outputs[n].connections.findIndex((function (e, n) { return e.node == t && e.output === i })) > -1) { this.module === s && this.container.querySelector(".connection.node_in_node-" + t + ".node_out_node-" + e + "." + n + "." + i).remove(); var o = this.drawflow.drawflow[s].data[e].outputs[n].connections.findIndex((function (e, n) { return e.node == t && e.output === i })); this.drawflow.drawflow[s].data[e].outputs[n].connections.splice(o, 1); var l = this.drawflow.drawflow[s].data[t].inputs[i].connections.findIndex((function (t, i) { return t.node == e && t.input === n })); return this.drawflow.drawflow[s].data[t].inputs[i].connections.splice(l, 1), this.dispatch("connectionRemoved", { output_id: e, input_id: t, output_class: n, input_class: i }), !0 } return !1 } return !1 } removeConnectionNodeId(e) { const t = "node_in_" + e, n = "node_out_" + e, i = this.container.querySelectorAll("." + n); for (var s = i.length - 1; s >= 0; s--) { var o = i[s].classList, l = this.drawflow.drawflow[this.module].data[o[1].slice(13)].inputs[o[4]].connections.findIndex((function (e, t) { return e.node === o[2].slice(14) && e.input === o[3] })); this.drawflow.drawflow[this.module].data[o[1].slice(13)].inputs[o[4]].connections.splice(l, 1); var c = this.drawflow.drawflow[this.module].data[o[2].slice(14)].outputs[o[3]].connections.findIndex((function (e, t) { return e.node === o[1].slice(13) && e.output === o[4] })); this.drawflow.drawflow[this.module].data[o[2].slice(14)].outputs[o[3]].connections.splice(c, 1), i[s].remove(), this.dispatch("connectionRemoved", { output_id: o[2].slice(14), input_id: o[1].slice(13), output_class: o[3], input_class: o[4] }) } const d = this.container.querySelectorAll("." + t); for (s = d.length - 1; s >= 0; s--) { o = d[s].classList, c = this.drawflow.drawflow[this.module].data[o[2].slice(14)].outputs[o[3]].connections.findIndex((function (e, t) { return e.node === o[1].slice(13) && e.output === o[4] })); this.drawflow.drawflow[this.module].data[o[2].slice(14)].outputs[o[3]].connections.splice(c, 1); l = this.drawflow.drawflow[this.module].data[o[1].slice(13)].inputs[o[4]].connections.findIndex((function (e, t) { return e.node === o[2].slice(14) && e.input === o[3] })); this.drawflow.drawflow[this.module].data[o[1].slice(13)].inputs[o[4]].connections.splice(l, 1), d[s].remove(), this.dispatch("connectionRemoved", { output_id: o[2].slice(14), input_id: o[1].slice(13), output_class: o[3], input_class: o[4] }) } } getModuleFromNodeId(e) { var t; const n = this.drawflow.drawflow; return Object.keys(n).map((function (i, s) { Object.keys(n[i].data).map((function (n, s) { n == e && (t = i) })) })), t } addModule(e) { this.drawflow.drawflow[e] = { data: {} }, this.dispatch("moduleCreated", e) } changeModule(e) { this.dispatch("moduleChanged", e), this.module = e, this.precanvas.innerHTML = "", this.canvas_x = 0, this.canvas_y = 0, this.pos_x = 0, this.pos_y = 0, this.mouse_x = 0, this.mouse_y = 0, this.zoom = 1, this.zoom_last_value = 1, this.precanvas.style.transform = "", this.import(this.drawflow, !1) } removeModule(e) { this.module === e && this.changeModule("Home"), delete this.drawflow.drawflow[e], this.dispatch("moduleRemoved", e) } clearModuleSelected() { this.precanvas.innerHTML = "", this.drawflow.drawflow[this.module] = { data: {} } } clear() { this.precanvas.innerHTML = "", this.drawflow = { drawflow: { Home: { data: {} } } } } export() { const e = JSON.parse(JSON.stringify(this.drawflow)); return this.dispatch("export", e), e } import(e, t = !0) { this.clear(), this.drawflow = JSON.parse(JSON.stringify(e)), this.load(), t && this.dispatch("import", "import") } on(e, t) { return "function" != typeof t ? (console.error("The listener callback must be a function, the given type is " + typeof t), !1) : "string" != typeof e ? (console.error("The event name must be a string, the given type is " + typeof e), !1) : (void 0 === this.events[e] && (this.events[e] = { listeners: [] }), void this.events[e].listeners.push(t)) } removeListener(e, t) { if (!this.events[e]) return !1; const n = this.events[e].listeners, i = n.indexOf(t); i > -1 && n.splice(i, 1) } dispatch(e, t) { if (void 0 === this.events[e]) return !1; this.events[e].listeners.forEach(e => { e(t) }) } getUuid() { for (var e = [], t = 0; t < 36; t++)e[t] = "0123456789abcdef".substr(Math.floor(16 * Math.random()), 1); return e[14] = "4", e[19] = "0123456789abcdef".substr(3 & e[19] | 8, 1), e[8] = e[13] = e[18] = e[23] = "-", e.join("") } } }]).default })); \ No newline at end of file From e8ac7e1588cea1874d5747e6df33727576687133 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 5 May 2022 15:29:13 +0200 Subject: [PATCH 024/428] chg: [workflow:editor] Added more resilience on import/export --- app/webroot/js/workflows-editor/workflows-editor.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/webroot/js/workflows-editor/workflows-editor.js b/app/webroot/js/workflows-editor/workflows-editor.js index 3bd290cb6..c30fe1f94 100644 --- a/app/webroot/js/workflows-editor/workflows-editor.js +++ b/app/webroot/js/workflows-editor/workflows-editor.js @@ -171,7 +171,9 @@ function addNode(block, position) { function getEditorData() { var data = {} // Make sure nodes are index by their internal IDs - editor.export().drawflow.Home.data.forEach(function(node) { + var editorExport = editor.export().drawflow.Home.data + editorExport = Array.isArray(editorExport) ? editorExport : Object.values(editorExport) + editorExport.forEach(function(node) { if (node !== null) { // for some reason, the editor create null nodes data[node.id] = node } @@ -229,6 +231,7 @@ function fetchWorkflow(id, callback) { function saveWorkflow(confirmSave, callback) { saveConfirmMessage = 'Confirm saving the current state of the workflow' + saveFailedMessage = 'Failed to save the workflow' confirmSave = confirmSave === undefined ? true : confirmSave if (confirmSave && !confirm(saveConfirmMessage)) { return From 11676411af29dd9b35ced4d3e2f72e5f5babc2b4 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 5 May 2022 17:32:11 +0200 Subject: [PATCH 025/428] chg: [workflow:editor] Small UI improvement --- app/webroot/css/workflows-editor.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/webroot/css/workflows-editor.css b/app/webroot/css/workflows-editor.css index 2995644e0..01696be5d 100644 --- a/app/webroot/css/workflows-editor.css +++ b/app/webroot/css/workflows-editor.css @@ -27,6 +27,10 @@ margin-bottom: 10px; } +.side-panel #block-tabs i.fas { + margin-right: 2px; +} + .side-panel .workflow-selector-container { display: flex; } From be4555335438fd63983ffca0cdec9c95a1001605 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 5 May 2022 17:32:51 +0200 Subject: [PATCH 026/428] chg: [workflow:editor] Reflect editing of inputs in the editor state --- app/View/Workflows/editor.ctp | 6 +- .../js/workflows-editor/workflows-editor.js | 92 +++++++++++++++++-- 2 files changed, 88 insertions(+), 10 deletions(-) diff --git a/app/View/Workflows/editor.ctp b/app/View/Workflows/editor.ctp index d081879c0..1b237326e 100644 --- a/app/View/Workflows/editor.ctp +++ b/app/View/Workflows/editor.ctp @@ -82,11 +82,11 @@

Block options

@@ -110,6 +110,8 @@ echo $this->element('genericElements/assetLoader', [ var $chosenWorkflows = $('.root-container .side-panel .chosen-container.workflows') var $chosenBlocks = $('.root-container .side-panel .chosen-container.blocks') var $drawflow = $('#drawflow') + var $blockModal = $('#block-modal') + var $blockModalSave = $('#block-modal #block-modal-save-button') var $importWorkflowButton = $('#importWorkflow') var $exportWorkflowButton = $('#exportWorkflow') var $saveWorkflowButton = $('#saveWorkflow') diff --git a/app/webroot/js/workflows-editor/workflows-editor.js b/app/webroot/js/workflows-editor/workflows-editor.js index c30fe1f94..876a1294c 100644 --- a/app/webroot/js/workflows-editor/workflows-editor.js +++ b/app/webroot/js/workflows-editor/workflows-editor.js @@ -10,7 +10,7 @@ var dotBlockDefault = doT.template(' \ \ \
\ -
{{=it.description}}
\ +
{{=it.description}}
\ {{=it.block_param_html}} \
\
') @@ -117,7 +117,27 @@ function initDrawflow() { $saveWorkflowButton.click(saveWorkflow) $importWorkflowButton.click(importWorkflow) $exportWorkflowButton.click(exportWorkflow) + $blockModal.on('show', function (evt) { + var selectedBlock = editor.getNodeFromId(editor.node_selected.id.split('-')[1]) // Couldn't find a better way to get the selected node + buildModalForBlock(selectedBlock.data) + }) + $blockModalSave.click(function() { + saveBlockConfiguration() + }) + +} + +function saveBlockConfiguration() { + console.log( + $blockModal.data('selected-block') + ); +} + +function buildModalForBlock(block) { + var html = genBlockParamHtml(block) + $blockModal.data('selected-block', block) + $blockModal.find('.modal-body').empty().append(html) } function invalidateContentCache() { @@ -340,6 +360,7 @@ function genBlockParamHtml(block) { } var html = '' block.params.forEach(function (param) { + param['param_id'] = getIDForBlockParameter(param) paramHtml = '' switch (param.type) { case 'input': @@ -375,21 +396,76 @@ function genSelect(options) { .text(optionName) $select.append($option) }) - if (options.default !== undefined) { - $select.val(options.default) + if (options.value !== undefined) { + $select.attr('value', options.value) + } else { + $select.attr('value', options.default) } + $select + .attr('data-paramid', options.param_id) + .attr('onchange', 'handleSelectChange(this)') return $select } function genInput(options) { + var $label = $('
@@ -119,9 +119,9 @@ echo $this->element('genericElements/assetLoader', [ var $lastModifiedField = $('#lastModifiedField') var editor = false var all_blocks = ; - var worklow = false + var workflow = false - var worklow = ; + var workflow = ; $(document).ready(function() { diff --git a/app/webroot/js/workflows-editor/workflows-editor.js b/app/webroot/js/workflows-editor/workflows-editor.js index 876a1294c..d85358749 100644 --- a/app/webroot/js/workflows-editor/workflows-editor.js +++ b/app/webroot/js/workflows-editor/workflows-editor.js @@ -205,16 +205,40 @@ function loadWorkflow() { fetchWorkflow(workflow_id, function(workflow) { lastModified = workflow.timestamp + '000' revalidateContentCache() - if (workflow.data) { - var editor_data = { - drawflow: { - Home: { - data: workflow.data - } - } + // if (workflow.data) { + // var editor_data = { + // drawflow: { + // Home: { + // data: workflow.data + // } + // } + // } + // editor.import(editor_data); + // } + + // We cannot rely on the editor's import function as it recreates the nodes with the saved HTML instead of rebuilding them + // We have to manually add the nodes and their connections + Object.values(workflow.data).forEach(function(block) { + block.data['block_param_html'] = genBlockParamHtml(block.data) + var html = getTemplateForBlock(block.data) + editor.addNode( + block.name, + Object.values(block.inputs).length, + Object.values(block.outputs).length, + block.pos_x, + block.pos_y, + block.class, + block.data, + html + ); + }) + Object.values(workflow.data).forEach(function (block) { + for (var input_name in block.inputs) { + block.inputs[input_name].connections.forEach(function(connection) { + editor.addConnection(connection.node, block.id, connection.input, input_name) + }) } - editor.import(editor_data); - } + }) }) } @@ -340,16 +364,12 @@ function toggleEditorLoading(loading, message) { } function getTemplateForBlock(block) { - var html = block.name - if (block.html !== undefined) { - html = block.html + var html = '' + block.icon_class = block.icon_class !== undefined ? block.icon_class : 'fas' + if (block.html_template !== undefined) { + html = window['dot' + block.html_template](block) } else { - block.icon_class = block.icon_class !== undefined ? block.icon_class : 'fas' - if (block.html_template !== undefined) { - html = window['dot' + block.html_template](block) - } else { - html = dotBlockDefault(block) - } + html = dotBlockDefault(block) } return html } From f7ba87263f5fbc6435dfe71d16830ac27ee4837d Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Tue, 10 May 2022 15:34:53 +0200 Subject: [PATCH 028/428] chg: [workflow:editor] Added support of modal while browsing node --- app/View/Workflows/editor.ctp | 2 -- .../js/workflows-editor/workflows-editor.js | 33 +++++++++++-------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/app/View/Workflows/editor.ctp b/app/View/Workflows/editor.ctp index 0f4639fa1..0ee28b33f 100644 --- a/app/View/Workflows/editor.ctp +++ b/app/View/Workflows/editor.ctp @@ -86,7 +86,6 @@
@@ -111,7 +110,6 @@ echo $this->element('genericElements/assetLoader', [ var $chosenBlocks = $('.root-container .side-panel .chosen-container.blocks') var $drawflow = $('#drawflow') var $blockModal = $('#block-modal') - var $blockModalSave = $('#block-modal #block-modal-save-button') var $importWorkflowButton = $('#importWorkflow') var $exportWorkflowButton = $('#exportWorkflow') var $saveWorkflowButton = $('#saveWorkflow') diff --git a/app/webroot/js/workflows-editor/workflows-editor.js b/app/webroot/js/workflows-editor/workflows-editor.js index d85358749..65b3ec1b4 100644 --- a/app/webroot/js/workflows-editor/workflows-editor.js +++ b/app/webroot/js/workflows-editor/workflows-editor.js @@ -119,24 +119,16 @@ function initDrawflow() { $exportWorkflowButton.click(exportWorkflow) $blockModal.on('show', function (evt) { var selectedBlock = editor.getNodeFromId(editor.node_selected.id.split('-')[1]) // Couldn't find a better way to get the selected node - buildModalForBlock(selectedBlock.data) - }) - - $blockModalSave.click(function() { - saveBlockConfiguration() + buildModalForBlock(selectedBlock.id, selectedBlock.data) }) } -function saveBlockConfiguration() { - console.log( - $blockModal.data('selected-block') - ); -} - -function buildModalForBlock(block) { +function buildModalForBlock(node_id, block) { var html = genBlockParamHtml(block) - $blockModal.data('selected-block', block) + $blockModal + .data('selected-block', block) + .data('selected-node-id', node_id) $blockModal.find('.modal-body').empty().append(html) } @@ -475,7 +467,20 @@ function getIDForBlockParameter(param) { } function getNodeFromNodeInput($input) { - var node = editor.getNodeFromId($input.closest('.drawflow-node')[0].id.split('-')[1]) + var node_id = 0 + if ($input.closest('.modal').length > 0) { + node_id = $input.closest('.modal').data('selected-node-id') + // sync changes with node's content + // Need to handle the case where SELECT changes! + debugger; + $input.change(function() { + var $relatedInputInNode = $drawflow.find('#node-'+node_id).find('[data-paramid="' + $input.data('paramid') + '"]') + $relatedInputInNode.val($(this).val()) + }) + } else { + node_id = $input.closest('.drawflow-node')[0].id.split('-')[1] + } + var node = editor.getNodeFromId(node_id) return node } From a3e9c7041ac2141895f6e754732df80b0a7187d8 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Tue, 10 May 2022 15:55:17 +0200 Subject: [PATCH 029/428] chg: [workflow:editor] Added support of select in the modal --- app/webroot/js/workflows-editor/workflows-editor.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/webroot/js/workflows-editor/workflows-editor.js b/app/webroot/js/workflows-editor/workflows-editor.js index 65b3ec1b4..3f5dd9da1 100644 --- a/app/webroot/js/workflows-editor/workflows-editor.js +++ b/app/webroot/js/workflows-editor/workflows-editor.js @@ -470,13 +470,8 @@ function getNodeFromNodeInput($input) { var node_id = 0 if ($input.closest('.modal').length > 0) { node_id = $input.closest('.modal').data('selected-node-id') - // sync changes with node's content - // Need to handle the case where SELECT changes! - debugger; - $input.change(function() { - var $relatedInputInNode = $drawflow.find('#node-'+node_id).find('[data-paramid="' + $input.data('paramid') + '"]') - $relatedInputInNode.val($(this).val()) - }) + var $relatedInputInNode = $drawflow.find('#node-'+node_id).find('[data-paramid="' + $input.data('paramid') + '"]') + $relatedInputInNode.val($input.val()) } else { node_id = $input.closest('.drawflow-node')[0].id.split('-')[1] } From f7507ff934684f953875bc525c2c65b65d89fff7 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Tue, 10 May 2022 17:13:49 +0200 Subject: [PATCH 030/428] chg: [workflow:editor] Added support of keyed select value --- app/webroot/js/workflows-editor/workflows-editor.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/webroot/js/workflows-editor/workflows-editor.js b/app/webroot/js/workflows-editor/workflows-editor.js index 3f5dd9da1..508f1b372 100644 --- a/app/webroot/js/workflows-editor/workflows-editor.js +++ b/app/webroot/js/workflows-editor/workflows-editor.js @@ -393,7 +393,11 @@ function genSelect(options) { var $select = $('') + $input + .attr('type', 'checkbox') + .attr('oninput', 'handleInputChange(this)') + .attr('data-paramid', options.param_id) + if (options.value !== undefined) { + $input.attr('value', options.value) + } else { + $input.attr('value', options.default) + } + $label.append($input) + var $container = $('
') + .addClass('checkbox') + .append($label) + return $container +} + +function genRadio(options) { + var $container = $('
') + var selectOptions = options.options + if (!Array.isArray(selectOptions)) { + selectOptions = Object.keys(options.options).map((k) => { return { name: options.options[k], value: k } }) + } + var u_id = uid() + selectOptions.forEach(function (option) { + var optionValue = '' + var optionName = '' + if (typeof option === 'string') { + optionValue = option + optionName = option + } else { + optionValue = option.value + optionName = option.name + } + var $input = $('') + .attr('type', 'radio') + .attr('name', 'option-radio-' + u_id) + .val(optionValue) + .attr('data-paramid', options.param_id) + .attr('onchange', 'handleSelectChange(this)') + if (options.value !== undefined && optionValue == options.value) { + $input.prop('checked', true) + } + var $label = $('