vendor/pimcore/translations-provider-interfaces/src/TranslationsProvider/XPlanation.php line 109

Open in your IDE?
  1. <?php
  2. /**
  3.  * Pimcore
  4.  *
  5.  * This source file is available under following license:
  6.  * - Pimcore Commercial License (PCL)
  7.  *
  8.  *  @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  9.  *  @license    http://www.pimcore.org/license     PCL
  10.  */
  11. namespace Pimcore\TranslationsProviderInterfaceBundle\TranslationsProvider;
  12. use Pimcore\Log\ApplicationLogger;
  13. use Pimcore\Model\DataObject\Concrete;
  14. use Pimcore\Model\Document;
  15. use Pimcore\Model\Notification\Service\NotificationService;
  16. use Pimcore\Translation\ExportDataExtractorService\ExportDataExtractorServiceInterface;
  17. use Pimcore\Translation\ImportDataExtractor\ImportDataExtractorInterface;
  18. use Pimcore\Translation\ImporterService\ImporterServiceInterface;
  19. use Pimcore\Translation\TranslationItemCollection\TranslationItem;
  20. use Pimcore\Translation\Translator;
  21. use Pimcore\TranslationsProviderInterfaceBundle\Authenticator\XPlanationAuthenticator;
  22. use Pimcore\TranslationsProviderInterfaceBundle\Constant\JobState;
  23. use Pimcore\TranslationsProviderInterfaceBundle\Constant\JobTransition;
  24. use Pimcore\TranslationsProviderInterfaceBundle\Constant\LogContext;
  25. use Pimcore\TranslationsProviderInterfaceBundle\Constant\Xml\NodeAttribute;
  26. use Pimcore\TranslationsProviderInterfaceBundle\Constant\XPlanation\JobStatusCode;
  27. use Pimcore\TranslationsProviderInterfaceBundle\Constant\XPlanation\ResponseStatusCode;
  28. use Pimcore\TranslationsProviderInterfaceBundle\Entity\Job;
  29. use Pimcore\TranslationsProviderInterfaceBundle\Event\JobEvents;
  30. use Pimcore\TranslationsProviderInterfaceBundle\PimcoreTranslationsProviderInterfaceBundle;
  31. use Pimcore\TranslationsProviderInterfaceBundle\Service\ConfigurationService;
  32. use Pimcore\TranslationsProviderInterfaceBundle\Service\Rest\XPlanationRestService;
  33. use Pimcore\TranslationsProviderInterfaceBundle\Service\Xml\XmlParserService;
  34. use Pimcore\TranslationsProviderInterfaceBundle\Service\Zip\ZipService;
  35. use Pimcore\TranslationsProviderInterfaceBundle\Translation\ExportService\Exporter\XPlanationExporter;
  36. use Pimcore\TranslationsProviderInterfaceBundle\Translation\ImportDataExtractor\XPlanationDataExtractor;
  37. use Pimcore\TranslationsProviderInterfaceBundle\Workflow\Enum\JobWorkflow;
  38. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  39. use Symfony\Component\EventDispatcher\GenericEvent;
  40. use Symfony\Component\HttpFoundation\Response;
  41. use Symfony\Component\Workflow\StateMachine;
  42. class XPlanation extends AbstractTranslationsProvider
  43. {
  44.     const MAX_COMPLETED_TARGETS_LIMIT 100000;
  45.     /**
  46.      * @var string
  47.      */
  48.     protected $projectShortCode '';
  49.     /**
  50.      * @var ExportDataExtractorServiceInterface
  51.      */
  52.     protected $translationDataExtractorService;
  53.     /**
  54.      * @var XPlanationDataExtractor
  55.      */
  56.     protected $importDataExtractor;
  57.     /**
  58.      * @var ImporterServiceInterface
  59.      */
  60.     protected $importerService;
  61.     /**
  62.      * @var StateMachine
  63.      */
  64.     protected $workflow;
  65.     /**
  66.      * @var EventDispatcherInterface
  67.      */
  68.     protected $eventDispatcher;
  69.     /** @var XPlanationRestService $restService */
  70.     protected $restService;
  71.     /** @var XPlanationExporter $exporter */
  72.     protected $exporter;
  73.     /** @var XmlParserService $xmlParserService */
  74.     protected $xmlParserService;
  75.     /** @var ZipService $zipService */
  76.     protected $zipService;
  77.     /** @var XPlanationAuthenticator $authenticator */
  78.     protected $authenticator;
  79.     /** @var bool */
  80.     protected $authenticated;
  81.     public function __construct(string $shortcutEventDispatcherInterface $eventDispatcherXPlanationAuthenticator $authenticatorXPlanationRestService $restServiceXPlanationExporter $exporterXmlParserService $xmlParserServiceNotificationService $notificationServiceTranslator $translatorConfigurationService $configurationServiceApplicationLogger $logger)
  82.     {
  83.         parent::__construct($shortcut$notificationService$translator$configurationService$logger);
  84.         if (\Pimcore::getContainer()->hasParameter('XPlanation.project-shortcode')) {
  85.             trigger_deprecation('pimcore/translations-provider-interfaces''2.4''Using parameter `XPlanation.project-shortcode` is deprecated, use config instead. Will be removed in version 3.0');
  86.             $this->projectShortCode \Pimcore::getContainer()->getParameter('translations_com.project-shortcode');
  87.         } else {
  88.             $this->projectShortCode $configurationService->getProjectShortcode();
  89.         }
  90.         $this->workflow \Pimcore::getContainer()->get('state_machine.' 'translation_job_default');
  91.         $this->eventDispatcher $eventDispatcher;
  92.         $this->restService $restService;
  93.         $this->exporter $exporter;
  94.         $this->xmlParserService $xmlParserService;
  95.         $this->authenticator $authenticator;
  96.     }
  97.     /**
  98.      * @throws \Unirest\Exception
  99.      */
  100.     protected function authenticate()
  101.     {
  102.         if (!$this->authenticated) {
  103.             $this->authenticator->authenticate();
  104.             $this->authenticated true;
  105.         }
  106.     }
  107.     /**
  108.      * @inheritdoc
  109.      */
  110.     public function submitJob(Job $job): bool
  111.     {
  112.         try {
  113.             $this->resetError($this->translationsJobService$job);
  114.             if (\Pimcore::inDebugMode()) {
  115.                 if ($simulatedError PimcoreTranslationsProviderInterfaceBundle::getSimulatedError()) {
  116.                     throw new \Exception($simulatedError ' (submit)');
  117.                 }
  118.             }
  119.             $this->authenticate();
  120.             $exportData = [];
  121.             $exportId uniqid();
  122.             $translationItems $this->translationsJobService->getTranslationItems($job)->getItems();
  123.             $result null;
  124.             foreach ($translationItems as $counter => $translationItem) {
  125.                 // For documents it is expected that the content master is saved, but the documents referring to the saved
  126.                 // document as content master are set as elements in the job
  127.                 // Thus, in order to find the proper source texts, we need to fetch the master content document
  128.                 // If no master content document is given, we have to skip the document, otherwise the document will
  129.                 // translate itself to the target language
  130.                 $element $translationItem->getElement();
  131.                 $isDocument $element instanceof Document\PageSnippet;
  132.                 if ($isDocument) {
  133.                     $contentMasterDocument $this->getContentMaster($element);
  134.                     if ($contentMasterDocument) {
  135.                         $type $translationItem->getType();
  136.                         $id $translationItem->getId();
  137.                         $translationItem = new TranslationItem($type$id$contentMasterDocument);
  138.                     } else {    // Do not translate the document itself
  139.                         $message '[Skipping] The document did not have a master document set. Thus, the document is skipped, as otherwise the document would translate itself.';
  140.                         $this->applicationLogger->error($message, ['component' => LogContext::TRANSLATION_PROVIDER_XPLANATION]);
  141.                         continue;
  142.                     }
  143.                 }
  144.                 $result $this->getTranslationDataExtractorService()->extract($translationItem$job->getSourceLanguage(), $job->getTargetLanguages());
  145.                 if (!$result->isEmpty()) {
  146.                     $customJobAttributes $job->getCustomAttributes();
  147.                     $pimcoreLanguage $customJobAttributes[NodeAttribute::PIMCORE_LANGUAGE];
  148.                     $this->exporter->setPimcoreLanguage($pimcoreLanguage);
  149.                     $exportFilePath $this->exporter->export($result$exportId);
  150.                     $exportFileContent $this->exporter->getFileContentFromPath($exportFilePath);
  151.                     $exportData $this->computeExportData($exportId$exportFileContent$translationItem);
  152.                 }
  153.             }
  154.             if (!$exportData) {
  155.                 $this->eventDispatcher->dispatch(new GenericEvent($job, ['result' => $result]), JobEvents::NO_EXPORT_DATA);
  156.                 return false;
  157.             }
  158.             $job->setExportDataFromObject($exportData);
  159.             $this->translationsJobService->persist($job);
  160.             return $this->sendJobToProvider($job$exportData);
  161.         } catch (\Exception $e) {
  162.             $this->sendErrorNotification($job$e);
  163.             $this->recordError($this->translationsJobService$job$eLogContext::TRANSLATION_PROVIDER_XPLANATION);
  164.             throw $e;
  165.         }
  166.     }
  167.     /**
  168.      *
  169.      * @return bool
  170.      *
  171.      * @throws \Unirest\Exception
  172.      */
  173.     private function sendJobToProvider(Job $job, array $exportDatabool $restart false)
  174.     {
  175.         $jobId $job->getId();
  176.         $jobName $job->getName();
  177.         $source $job->getSourceLanguage();
  178.         $targets $job->getTargetLanguages();
  179.         $fileContent $exportData['xml'];
  180.         $fileContent base64_encode($fileContent);
  181.         $fileName $exportData['ticketId'] . XPlanationExporter::XLIFF_FILE_EXTENSION;
  182.         $dueDate $job->getDueDate() ?: '';
  183.         $body $this->restService->createJob([
  184.             $jobId,
  185.             $jobName,
  186.             $source,
  187.             $targets,
  188.             $fileContent,
  189.             $fileName,
  190.             $dueDate,
  191.             $restart
  192.         ]);
  193.         $response $this->restService->getLastResponse();
  194.         if ($wasSubmitted $this->restService->isOkay($response)) {
  195.             $this->workflow->apply($jobJobWorkflow::TRANSITION_SUBMIT);
  196.             $remoteId $body->jobid;
  197.             $fileId $body->item->file_id;
  198.             $job->setRemoteId($remoteId);
  199.             $job->setFileId($fileId);
  200.             // If the source language matches the target language (e.g. de-de => de-de), the translation is already sent in the response.
  201.             // This is, because the data is already available and simply used to create the same format of the response
  202.             // However, the job will not be created, thus, there is no remote ID and the job cannot be fetched from XPlanation.
  203.             if (in_array($job->getSourceLanguage(), $job->getTargetLanguages())) {
  204.                 $customJobAttributes $job->getCustomAttributes() ?: [];
  205.                 $customJobAttributes['xml'] = $body->item->final;
  206.                 $job->setCustomAttributes($customJobAttributes);
  207.             }
  208.             $this->translationsJobService->persist($job);
  209.         }
  210.         $event = new GenericEvent($job, ['response' => $response'restarted' => $restart]);
  211.         $this->eventDispatcher->dispatch($event$wasSubmitted JobEvents::JOB_SUBMITTED JobEvents::JOB_NOT_SUBMITTED);
  212.         if ($wasSubmitted) {
  213.             $this->receiveJob($job);
  214.         } else {
  215.             // Chances are the underlying tool (CAT-Tool) of XPlanation has an error
  216.             // In this case, XPlanation recommends to re-send the job with the "restart" flag to continue creating jobs
  217.             if ($this->restService->getStatusCode($response) === ResponseStatusCode::NOT_ACCEPTABLE) {
  218.                 if (!$restart) { // Avoid endless loop
  219.                     $wasSubmitted $this->sendJobToProvider($job$exportDatatrue);
  220.                 }
  221.             }
  222.         }
  223.         return $wasSubmitted;
  224.     }
  225.     /**
  226.      * @param null|string $ticketId the ID of the ticket that is created (used to identify the file, after a response was received)
  227.      * @param null|string $xml the XML's content
  228.      *
  229.      * @return array
  230.      */
  231.     private function computeExportData(?string $ticketId, ?string $xmlTranslationItem $translationItem)
  232.     {
  233.         return [
  234.             'ticketId' => $ticketId,
  235.             'xml' => $xml,
  236.             'ctype' => $translationItem->getType(),
  237.             'cid' => $translationItem->getId()
  238.         ];
  239.     }
  240.     /**
  241.      * @param int|null $length the file size in bytes
  242.      * @param null|string $checksum the checksum of the file
  243.      * @param null|string $xml the file content
  244.      *
  245.      * @return array
  246.      */
  247.     private function computeResultData(?int $length, ?string $checksum, ?string $xml)
  248.     {
  249.         return [
  250.             'xml' => $xml,
  251.             'length' => $length,
  252.             'checksum' => $checksum
  253.         ];
  254.     }
  255.     /**
  256.      * @inheritdoc
  257.      */
  258.     public function receiveJob(Job $job$isRedeliveryCheck false): bool
  259.     {
  260.         try {
  261.             $this->resetError($this->translationsJobService$job);
  262.             if (\Pimcore::inDebugMode()) {
  263.                 if ($simulatedError PimcoreTranslationsProviderInterfaceBundle::getSimulatedError()) {
  264.                     throw new \Exception($simulatedError ' (receive)');
  265.                 }
  266.             }
  267.             $this->authenticate();
  268.             $remoteId $job->getRemoteId();
  269.             $jobFinished false;
  270.             if ($remoteId) {
  271.                 $body $this->restService->getJobStatus($remoteId);
  272.                 $response $this->restService->getLastResponse();
  273.                 if ($this->restService->isOkay($response)) {
  274.                     $code $body->code;
  275.                     if ($code === JobStatusCode::END) {
  276.                         $body $this->restService->getJobTranslations($remoteId);
  277.                         $response $this->restService->getLastResponse();
  278.                         if ($this->restService->isOkay($response)) {
  279.                             $binaryZipData base64_decode($body->data);
  280.                             $xmlContent $this->zipService->createAndReadTempZip($binaryZipData);
  281.                             $targetLanguages $job->getTargetLanguages();
  282.                             $resultData = [];
  283.                             $relevantXmlContent = [];
  284.                             // Pre-filter files based on target language
  285.                             foreach ($xmlContent as $availableFile => $content) {
  286.                                 $resultData['availableFiles'] = $resultData['availableFiles'] ?: [];
  287.                                 $resultData['availableFiles'][] = $availableFile;
  288.                                 foreach ($targetLanguages as $currentTargetLanguage) {
  289.                                     if (strpos(strtolower($availableFile), $currentTargetLanguage) === 0) {
  290.                                         $relevantXmlContent[$availableFile] = $content;
  291.                                         break;
  292.                                     }
  293.                                 }
  294.                             }
  295.                             foreach ($relevantXmlContent as $availableFile => $content) {
  296.                                 // Only file with the highest version is relevant
  297.                                 $version $this->getVersionFromFileName($availableFile);
  298.                                 foreach ($relevantXmlContent as $currentFile => $currentContent) {
  299.                                     $currentVersion $this->getVersionFromFileName($currentFile);
  300.                                     if ($version $currentVersion) {
  301.                                         continue 2;
  302.                                     }
  303.                                 }
  304.                                 $xml $this->xmlParserService->loadFromString($content);
  305.                                 $resultData = [];
  306.                                 foreach ($this->xmlParserService->getFileNodes($xml) as $fileNode) {
  307.                                     if ($fileNode) {
  308.                                         $targetLanguage $this->xmlParserService->getTargetLanguage($fileNode);
  309.                                         $id $this->xmlParserService->getOriginal($fileNode);
  310.                                         if ($this->xmlParserService->isValidId($id) === false) {
  311.                                             $message "[Error] A part of the translation could not be updated, as the ID '$id' is invalid.";
  312.                                             $this->applicationLogger->error($message, ['component' => LogContext::TRANSLATION_PROVIDER_XPLANATION]);
  313.                                         }
  314.                                         $resultData = empty($resultData) ? $this->computeResultData($body->length$body->checksum$content) : $resultData;
  315.                                         $resultData[$id][$targetLanguage]['state'] = JobState::RECEIVED;
  316.                                         $resultData[$id][$targetLanguage]['importedFile'] = $availableFile;
  317.                                     } else {
  318.                                         $message '[Error] No file node available.';
  319.                                         $this->applicationLogger->error($message, ['component' => LogContext::TRANSLATION_PROVIDER_XPLANATION]);
  320.                                     }
  321.                                 }
  322.                             }
  323.                             $job->setResultDataFromObject($resultData);
  324.                             $jobFinished true;
  325.                             $event = new GenericEvent($job);
  326.                             $this->eventDispatcher->dispatch($eventJobEvents::JOB_RECEIVED);
  327.                         }
  328.                     } else {
  329.                         $event = new GenericEvent($job);
  330.                         $this->eventDispatcher->dispatch($eventJobEvents::JOB_IN_PROGRESS);
  331.                     }
  332.                 }
  333.                 if ($jobFinished) {
  334.                     if ($this->workflow->can($jobJobTransition::RECEIVE)) {
  335.                         $this->workflow->apply($jobJobTransition::RECEIVE);
  336.                     }// Was finished before --> do nothing
  337.                 } else {
  338.                     $resultData $this->restService->getBody($responsetrue);
  339.                     $job->setResultData($resultData);
  340.                 }
  341.                 $this->translationsJobService->persist($job);
  342.             } else {
  343.                 // Handle the case where source and target language equals (see submitJob method for details)
  344.                 if (in_array($job->getSourceLanguage(), $job->getTargetLanguages())) {
  345.                     $xmlString = ($job->getCustomAttributes()['xml']);
  346.                     $xmlString base64_decode($xmlString);
  347.                     $xml $this->xmlParserService->loadFromString($xmlString);
  348.                     $resultData = [];
  349.                     foreach ($this->xmlParserService->getFileNodes($xml) as $fileNode) {
  350.                         if ($fileNode) {
  351.                             $targetLanguage $this->xmlParserService->getTargetLanguage($fileNode);
  352.                             $id $this->xmlParserService->getOriginal($fileNode);
  353.                             if ($this->xmlParserService->isValidId($id) === false) {
  354.                                 $message "[Error] A part of the translation could not be updated, as the ID '$id' is invalid.";
  355.                                 $this->applicationLogger->error($message, ['component' => LogContext::TRANSLATION_PROVIDER_XPLANATION]);
  356.                             }
  357.                             $resultData = empty($resultData) ? $this->computeResultData(strlen($xmlString), '-1'$xmlString) : $resultData;
  358.                             $resultData[$id][$targetLanguage]['state'] = JobState::RECEIVED;
  359.                         } else {
  360.                             $message '[Error] No file node available.';
  361.                             $this->applicationLogger->error($message, ['component' => LogContext::TRANSLATION_PROVIDER_XPLANATION]);
  362.                         }
  363.                     }
  364.                     $job->setResultDataFromObject($resultData);
  365.                     $jobFinished true;
  366.                     $event = new GenericEvent($job);
  367.                     $this->eventDispatcher->dispatch($eventJobEvents::JOB_RECEIVED);
  368.                     if ($this->workflow->can($jobJobTransition::RECEIVE)) {
  369.                         $this->workflow->apply($jobJobTransition::RECEIVE);
  370.                     }// Was finished before --> do nothing
  371.                     $this->translationsJobService->persist($job);
  372.                 }
  373.             }
  374.             return $jobFinished;
  375.         } catch (\Exception $e) {
  376.             $this->sendErrorNotification($job$e);
  377.             $this->recordError($this->translationsJobService$job$eLogContext::TRANSLATION_PROVIDER_XPLANATION);
  378.             throw $e;
  379.         }
  380.     }
  381.     /**
  382.      *
  383.      * @return int the determined version, based on the file name.
  384.      */
  385.     private function getVersionFromFileName(string $file)
  386.     {
  387.         $parts explode('\\'$file);
  388.         $filename end($parts);
  389.         $parts explode('_'$filename);
  390.         $version reset($parts);
  391.         $version ltrim($version'0');
  392.         $version = (int)$version;
  393.         return $version ?: 0;
  394.     }
  395.     /**
  396.      *
  397.      * @throws \Exception
  398.      */
  399.     public function processReceived(Job $job): bool
  400.     {
  401.         try {
  402.             $this->resetError($this->translationsJobService$job);
  403.             if ($this->hasReceivedState($job)) {
  404.                 if (\Pimcore::inDebugMode()) {
  405.                     if ($simulatedError PimcoreTranslationsProviderInterfaceBundle::getSimulatedError()) {
  406.                         throw new \Exception($simulatedError ' (process)');
  407.                     }
  408.                 }
  409.                 $resultData $job->getResultDataAsArray();
  410.                 $processedItems $job->getProcessedTranslationItems();
  411.                 $changedTranslationItems = [];
  412.                 $importDataExtractor $this->getImportDataExtractor();
  413.                 $importId uniqid();
  414.                 // We need to create a temporary file, in order to be able to extract a single element
  415.                 $xml $resultData['xml'];
  416.                 $importFile $importDataExtractor->getImportFilePath($importId);
  417.                 file_put_contents($importFile$xml);
  418.                 foreach ($resultData as $id => $entry) {
  419.                     if (is_array($entry)) {   // Only object specific information is available as array
  420.                         foreach ($entry as $info) {
  421.                             $state $info['state'];
  422.                             if ($state !== JobState::CANCELLED) {
  423.                                 $attributeSet $importDataExtractor->extractElement($importId$id);
  424.                                 if ($attributeSet) {
  425.                                     $translationItem $attributeSet->getTranslationItem();
  426.                                     $type $translationItem->getType();
  427.                                     $translationItemId $translationItem->getId();
  428.                                     $targetLanguage current($attributeSet->getTargetLanguages());
  429.                                     $event = new GenericEvent($attributeSet->getTranslationItem()->getElement(), ['attributeSet' => $attributeSet'job' => $job]);
  430.                                     if (!$this->isAlreadyProcessed($type$translationItemId$targetLanguage$processedItems)) {
  431.                                         $this->getImporterService()->import($attributeSetfalse);
  432.                                         $changedTranslationItems[] = $attributeSet->getTranslationItem();
  433.                                         $this->markAsProcessed($type$translationItemId$targetLanguage$processedItems);
  434.                                         $this->eventDispatcher->dispatch($eventJobEvents::TRANSLATION_ITEM_PROCESSED);
  435.                                     } else {
  436.                                         $this->eventDispatcher->dispatch($eventJobEvents::TRANSLATION_ITEM_ALREADY_PROCESSED);
  437.                                     }
  438.                                 }
  439.                             }
  440.                         }
  441.                     }
  442.                 }
  443.                 $this->saveTranslationItems($changedTranslationItems);
  444.                 if (json_encode($job->getProcessedTranslationItems()) != json_encode($processedItems)) {
  445.                     $job->setProcessedTranslationItems($processedItems);
  446.                     if ($job->getState() === JobState::RECEIVED) {
  447.                         $this->workflow->apply($jobJobTransition::PROCESS);
  448.                         $event = new GenericEvent($job);
  449.                         $this->eventDispatcher->dispatch($eventJobEvents::FINISHED_JOB);
  450.                     }
  451.                     $this->translationsJobService->persist($job);
  452.                     return true;
  453.                 }
  454.             }
  455.             return false;
  456.         } catch (\Exception $e) {
  457.             $this->sendErrorNotification($job$e);
  458.             $this->recordError($this->translationsJobService$job$eLogContext::TRANSLATION_PROVIDER_XPLANATION);
  459.             throw $e;
  460.         }
  461.     }
  462.     /**
  463.      * @param Job $job the job to check the status for
  464.      *
  465.      * @return bool whether the job has any of the received states.
  466.      */
  467.     private function hasReceivedState(Job $job)
  468.     {
  469.         $status $job->getState();
  470.         $requiredStates = [JobState::RECEIVED_PARTIALLYJobState::RECEIVED];
  471.         return in_array($status$requiredStates);
  472.     }
  473.     /**
  474.      * @param array $processedItems
  475.      */
  476.     private function isAlreadyProcessed(string $typestring $idstring $targetLanguage$processedItems): bool
  477.     {
  478.         return isset($processedItems[$type]) && isset($processedItems[$type][$id]) && in_array($targetLanguage$processedItems[$type][$id]);
  479.     }
  480.     /**
  481.      * @param array $processedItems
  482.      */
  483.     private function markAsProcessed(string $typestring $idstring $targetLanguage, &$processedItems)
  484.     {
  485.         $processedItems[$type] = $processedItems[$type] ?? [];
  486.         $processedItems[$type][$id] = $processedItems[$type][$id] ?? [];
  487.         $processedItems[$type][$id][] = $targetLanguage;
  488.     }
  489.     private function saveTranslationItems(array $translationItems)
  490.     {
  491.         $savedItems = [];
  492.         foreach ($translationItems as $item) {
  493.             $itemKey $item->getType() . '_' $item->getId();
  494.             if (!in_array($itemKey$savedItems)) {
  495.                 if ($item->getElement() instanceof Concrete) {
  496.                     $item->getElement()->setOmitMandatoryCheck(true);
  497.                 }
  498.                 $item->getElement()->save();
  499.                 $savedItems[] = $itemKey;
  500.             }
  501.         }
  502.     }
  503.     public function getExportDataResponse(string $exportData): Response
  504.     {
  505.         $response = new Response($exportData);
  506.         $response->headers->set('Content-Type''text/json');
  507.         return $response;
  508.     }
  509.     public function getResultDataResponse(string $resultData): Response
  510.     {
  511.         $response = new Response($resultData);
  512.         $response->headers->set('Content-Type''text/json');
  513.         return $response;
  514.     }
  515.     public function getTranslationDataExtractorService(): ExportDataExtractorServiceInterface
  516.     {
  517.         return $this->translationDataExtractorService;
  518.     }
  519.     /**
  520.      * @required
  521.      */
  522.     public function setTranslationDataExtractorService(ExportDataExtractorServiceInterface $translationDataExtractorService): self
  523.     {
  524.         $this->translationDataExtractorService $translationDataExtractorService;
  525.         return $this;
  526.     }
  527.     public function getImportDataExtractor(): XPlanationDataExtractor
  528.     {
  529.         return $this->importDataExtractor;
  530.     }
  531.     /**
  532.      * @required
  533.      */
  534.     public function setImportDataExtractor(ImportDataExtractorInterface $importDataExtractor): void
  535.     {
  536.         $this->importDataExtractor $importDataExtractor;
  537.     }
  538.     public function getImporterService(): ImporterServiceInterface
  539.     {
  540.         return $this->importerService;
  541.     }
  542.     /**
  543.      * @required
  544.      */
  545.     public function setImporterService(ImporterServiceInterface $importerService): self
  546.     {
  547.         $this->importerService $importerService;
  548.         return $this;
  549.     }
  550.     public function getProjectShortCode(): string
  551.     {
  552.         return $this->projectShortCode;
  553.     }
  554.     /**
  555.      * @return string
  556.      */
  557.     protected function getSubmissionName(Job $job)
  558.     {
  559.         return 'Job ID ' $job->getId();
  560.     }
  561.     /**
  562.      * @return string
  563.      */
  564.     protected function getSubmissionFilename(Job $jobstring $suffix)
  565.     {
  566.         return 'submission_' $job->getId() . '_' $suffix '.xml';
  567.     }
  568.     /**
  569.      * @return bool whether the job was successfully cancelled.
  570.      *
  571.      * @throws \Exception
  572.      */
  573.     public function cancelJob(Job $job): bool
  574.     {
  575.         try {
  576.             $this->resetError($this->translationsJobService$job);
  577.             if (\Pimcore::inDebugMode()) {
  578.                 if ($simulatedError PimcoreTranslationsProviderInterfaceBundle::getSimulatedError()) {
  579.                     throw new \Exception($simulatedError ' (cancel)');
  580.                 }
  581.             }
  582.             $this->authenticate();
  583.             $remoteId $job->getRemoteId();
  584.             $fileId $job->getFileId();
  585.             $body $remoteId $this->restService->cancelJob($remoteId$fileId) : false;
  586.             if ($body == 1) {
  587.                 $this->workflow->apply($jobJobTransition::CANCEL);
  588.                 $this->translationsJobService->persist($job);
  589.                 return true;
  590.             }
  591.             return false;
  592.         } catch (\Exception $e) {
  593.             $this->sendErrorNotification($job$e);
  594.             $this->recordError($this->translationsJobService$job$eLogContext::TRANSLATION_PROVIDER_XPLANATION);
  595.             throw $e;
  596.         }
  597.     }
  598.     /**
  599.      * @required
  600.      */
  601.     public function setZipService(ZipService $zipService)
  602.     {
  603.         $this->zipService $zipService;
  604.     }
  605. }