diff --git a/ProcessMaker/Http/Controllers/Api/ProcessController.php b/ProcessMaker/Http/Controllers/Api/ProcessController.php index 0c223f70da..6ebd6a4916 100644 --- a/ProcessMaker/Http/Controllers/Api/ProcessController.php +++ b/ProcessMaker/Http/Controllers/Api/ProcessController.php @@ -37,6 +37,7 @@ use ProcessMaker\Models\Script; use ProcessMaker\Models\Template; use ProcessMaker\Nayra\Exceptions\ElementNotFoundException; +use ProcessMaker\Nayra\Services\FixBpmnSchemaService; use ProcessMaker\Nayra\Storage\BpmnDocument; use ProcessMaker\Package\Translations\Models\Language; use ProcessMaker\Package\WebEntry\Models\WebentryRoute; @@ -379,6 +380,12 @@ public function store(Request $request) // Validate if exists file bpmn if ($request->has('file')) { $data['bpmn'] = $request->file('file')->get(); + + // Some BPMN definitions created in others modelers doesn't comply BPMN 2.0 + // So, should be fixed + $fixBpmnSchemaService = app(FixBpmnSchemaService::class); + $data['bpmn'] = $fixBpmnSchemaService->fix($data['bpmn']); + $request->merge(['bpmn' => $data['bpmn']]); $request->offsetUnset('file'); unset($data['file']); diff --git a/ProcessMaker/Nayra/Services/FixBpmnSchemaService.php b/ProcessMaker/Nayra/Services/FixBpmnSchemaService.php new file mode 100644 index 0000000000..4c3cf3ffff --- /dev/null +++ b/ProcessMaker/Nayra/Services/FixBpmnSchemaService.php @@ -0,0 +1,138 @@ +preserveWhiteSpace = false; + $document->loadXml($bpmn); + + // Get root node + $root = $document->documentElement; + + // Set "targetNamespace" attribute always + $root->setAttribute('targetNamespace', 'http://www.omg.org/spec/BPMN/20100524/MODEL'); + + // Create XPath object + $xpath = new DOMXPath($document); + + // Register all namespaces, including default + foreach ($document->documentElement->attributes as $attr) { + if (strpos($attr->nodeName, 'xmlns:') === 0) { + $prefix = substr($attr->nodeName, 6); + $xpath->registerNamespace($prefix, $attr->nodeValue); + } elseif ($attr->nodeName === 'xmlns') { + // Register default namespace as "def" + $xpath->registerNamespace('def', $attr->nodeValue); + } + } + + // Find all task nodes + $taskNodes = $xpath->query('//*[local-name()="task"]'); + + foreach ($taskNodes as $task) { + $taskId = $task->getAttribute("id"); + $taskPrefix = !empty($task->prefix) ? "$task->prefix:" : ''; + $taskNS = $task->namespaceURI; + + $dataInputAssociation = $xpath->query('./*[local-name()="dataInputAssociation"]', $task)->item(0); + if (!$dataInputAssociation) { + continue; + } + + // Skip if targetRef already exists + $targetRefExists = $xpath->query('./*[local-name()="targetRef"]', $dataInputAssociation)->length > 0; + if ($targetRefExists) { + continue; + } + + // Extract sourceRef + $sourceRef = $xpath->query('./*[local-name()="sourceRef"]', $dataInputAssociation)->item(0); + if (!$sourceRef) { + throw new Exception("sourceRef not found in dataInputAssociation for task $taskId"); + } + $sourceId = $sourceRef->nodeValue; + + // Create ioSpecification and children + $ioSpec = $document->createElementNS($taskNS, "{$taskPrefix}ioSpecification"); + $ioSpecId = "{$taskId}_inner_" . round(microtime(true) * 1000); + $ioSpec->setAttribute("id", $ioSpecId); + + $dataInputId = "data_input_{$sourceId}"; + $dataInput = $document->createElementNS($taskNS, "{$taskPrefix}dataInput"); + $dataInput->setAttribute("id", $dataInputId); + $dataInput->setAttribute("name", "Template for protocol"); + $ioSpec->appendChild($dataInput); + + $inputSet = $document->createElementNS($taskNS, "{$taskPrefix}inputSet"); + $inputSet->setAttribute("id", "{$taskId}_inner_" . (round(microtime(true) * 1000) + 2)); + $dataInputRefs = $document->createElementNS($taskNS, "{$taskPrefix}dataInputRefs", $dataInputId); + $inputSet->appendChild($dataInputRefs); + $ioSpec->appendChild($inputSet); + + $outputSet = $document->createElementNS($taskNS, "{$taskPrefix}outputSet"); + $outputSet->setAttribute("id", "{$taskId}_inner_" . (round(microtime(true) * 1000) + 3)); + $ioSpec->appendChild($outputSet); + + $task->insertBefore($ioSpec, $dataInputAssociation); + + // Add targetRef + $targetRef = $document->createElementNS($taskNS, "{$taskPrefix}targetRef", $dataInputId); + $dataInputAssociation->appendChild($targetRef); + + // Add BPMNEdge to BPMNDiagram + $diagramNodes = $xpath->query('//*[local-name() = "BPMNDiagram"]'); + if ($diagramNodes->length === 0) { + throw new Exception("No BPMNDiagram node found in the BPMN file."); + } + $bpmnDiagram = $diagramNodes->item(0); + $diagramPrefix = $bpmnDiagram->prefix; + $diagramNS = $bpmnDiagram->namespaceURI; + + $bpmnPlaneNodes = $xpath->query('.//*[local-name() = "BPMNPlane"]', $bpmnDiagram); + if ($bpmnPlaneNodes->length === 0) { + throw new Exception("No BPMNPlane found inside BPMNDiagram."); + } + $bpmnPlane = $bpmnPlaneNodes->item(0); + + $edgeId = 'BPMNEdge_' . $dataInputAssociation->getAttribute("id"); + $bpmnEdge = $document->createElementNS($diagramNS, "{$diagramPrefix}:BPMNEdge"); + $bpmnEdge->setAttribute("id", $edgeId); + $bpmnEdge->setAttribute("bpmnElement", $dataInputAssociation->getAttribute("id")); + + $diNS = 'http://www.omg.org/spec/DD/20100524/DI'; + $waypoint1 = $document->createElementNS($diNS, "di:waypoint"); + $waypoint1->setAttribute("x", "100"); + $waypoint1->setAttribute("y", "100"); + + $waypoint2 = $document->createElementNS($diNS, "di:waypoint"); + $waypoint2->setAttribute("x", "200"); + $waypoint2->setAttribute("y", "200"); + + $bpmnEdge->appendChild($waypoint1); + $bpmnEdge->appendChild($waypoint2); + $bpmnPlane->appendChild($bpmnEdge); + } + + return $document->saveXml(); + } catch (Exception $e) { + throw $e; + } + } +} diff --git a/tests/Fixtures/process_data_input_generated_in_pm4.bpmn b/tests/Fixtures/process_data_input_generated_in_pm4.bpmn new file mode 100644 index 0000000000..4a4d0839c8 --- /dev/null +++ b/tests/Fixtures/process_data_input_generated_in_pm4.bpmn @@ -0,0 +1,33 @@ + + + + + + + + data_input_node_16 + + + + + node_16 + data_input_node_16 + + + + + + + + + + + + + + + + + + + diff --git a/tests/Fixtures/process_data_input_without_targetref.bpmn b/tests/Fixtures/process_data_input_without_targetref.bpmn new file mode 100644 index 0000000000..e6c346b2bf --- /dev/null +++ b/tests/Fixtures/process_data_input_without_targetref.bpmn @@ -0,0 +1,110 @@ + + + + + + _1446983_5 + _1446983_8 + _1446983_20 + _1446983_29 + + + _1446983_12 + _1446983_14 + + + + + + + + + + dor_1446983_36 + + + + + + + + dor_1446983_38 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/unit/ProcessMaker/Nayra/Services/FixBpmnSchemaServiceTest.php b/tests/unit/ProcessMaker/Nayra/Services/FixBpmnSchemaServiceTest.php new file mode 100644 index 0000000000..2b3ec5adaf --- /dev/null +++ b/tests/unit/ProcessMaker/Nayra/Services/FixBpmnSchemaServiceTest.php @@ -0,0 +1,84 @@ +loadXML($bpmn); + + $this->expectException(Exception::class); + $validation = $document->validateBPMNSchema( + public_path("definitions/ProcessMaker.xsd") + ); + } + + /** + * Test fixing incomplete BPMN definition created in another modeler + * + * @return void + */ + public function testFixIncompleteProcess() + { + $bpmn = file_get_contents( + __DIR__ . + "/../../../../Fixtures/process_data_input_without_targetref.bpmn" + ); + + $fixBpmnSchemaService = app(FixBpmnSchemaService::class); + $bpmn = $fixBpmnSchemaService->fix($bpmn); + + $document = new BpmnDocument(); + $document->loadXML($bpmn); + $validation = $document->validateBPMNSchema( + public_path("definitions/ProcessMaker.xsd") + ); + + $this->assertTrue($validation); + } + + /** + * Test using a BPMN definition created in PM4 + * + * @return void + */ + public function testFixPm4Process() + { + $bpmn = file_get_contents( + __DIR__ . + "/../../../../Fixtures/process_data_input_generated_in_pm4.bpmn" + ); + + $fixBpmnSchemaService = app(FixBpmnSchemaService::class); + $bpmn = $fixBpmnSchemaService->fix($bpmn); + + $document = new BpmnDocument(); + $document->loadXML($bpmn); + $validation = $document->validateBPMNSchema( + public_path("definitions/ProcessMaker.xsd") + ); + + $this->assertTrue($validation); + } +}