diff --git a/composer.json b/composer.json index c5c09ab..726bb47 100644 --- a/composer.json +++ b/composer.json @@ -29,17 +29,18 @@ }, "require": { "php": ">=7.3", - "jms/serializer": "^3", - "goetas-webservices/xsd2php-runtime": "^0.2.13", "ext-simplexml": "*", - "symfony/validator": "^5|^6|^7", - "smalot/pdfparser": "^0|^2", + "goetas-webservices/xsd2php-runtime": "^0.2.13", + "horstoeko/mimedb": "^1", + "horstoeko/stringmanagement": "^1", + "jms/serializer": "^3", "setasign/fpdf": "^1", "setasign/fpdi": "^2", - "symfony/yaml": "^5|^6|^7", + "smalot/pdfparser": "^0|^2", + "symfony/finder": "^5.4", "symfony/process": "^5|^6|^7", - "horstoeko/stringmanagement": "^1", - "horstoeko/mimedb": "^1" + "symfony/validator": "^5|^6|^7", + "symfony/yaml": "^5|^6|^7" }, "require-dev": { "goetas-webservices/xsd2php": "^0", diff --git a/examples/PdfValidator.php b/examples/PdfValidator.php new file mode 100644 index 0000000..23d2058 --- /dev/null +++ b/examples/PdfValidator.php @@ -0,0 +1,73 @@ +getProcessOutput() as $output) { + //echo $output . PHP_EOL; + } + + if ($pdfValidator->hasProcessErrors()) { + echo "\033[01;31mProcess failed\e[0m\n"; + foreach ($pdfValidator->getProcessErrors() as $processError) { + echo " - " . $processError . PHP_EOL; + } + } elseif ($pdfValidator->hasValidationErrors()) { + echo "\033[01;31mValidation failed\e[0m\n"; + foreach ($pdfValidator->getValidationErrors() as $validationError) { + echo " - " . $validationError . PHP_EOL; + } + } else { + echo "\033[01;32mValidation passed\e[0m\n"; + } +} + + +/* ---------------------------------------------------------------------------------- + - Perform validation + ---------------------------------------------------------------------------------- */ + +$pdfValidator = ZugferdPdfValidator::fromContent(file_get_contents(dirname(__FILE__) . "/invoice_1.pdf"))->disableCleanup()->validate(); + +showValidationResult($pdfValidator); + +$pdfValidator = ZugferdPdfValidator::fromContent(file_get_contents(dirname(__FILE__) . "/invoice_1.pdf"))->disableCleanup()->setValidatorRuleset(ZugferdPdfValidator::RULESET_PDF_A_3B)->validate(); + +showValidationResult($pdfValidator); + +$pdfValidator = ZugferdPdfValidator::fromContent(file_get_contents(dirname(__FILE__) . "/../tests/assets/pdf_plain.pdf"))->disableCleanup()->setValidatorRuleset(ZugferdPdfValidator::RULESET_PDF_A_0)->validate(); + +showValidationResult($pdfValidator); + +$pdfValidator = ZugferdPdfValidator::fromContent(file_get_contents(dirname(__FILE__) . "/../tests/assets/pdf_plain.pdf"))->disableCleanup()->setValidatorRuleset(ZugferdPdfValidator::RULESET_PDF_A_3B)->validate(); + +showValidationResult($pdfValidator); + +$pdfValidator = ZugferdPdfValidator::fromContent(file_get_contents(dirname(__FILE__) . "/../tests/assets/pdf_zf_en16931_1.pdf"))->disableCleanup()->setValidatorRuleset(ZugferdPdfValidator::RULESET_PDF_A_3B)->validate(); + +showValidationResult($pdfValidator); + +$pdfValidator = ZugferdPdfValidator::fromContent(file_get_contents(dirname(__FILE__) . "/../tests/assets/pdf_zf_en16931_2.pdf"))->disableCleanup()->setValidatorRuleset(ZugferdPdfValidator::RULESET_PDF_A_3B)->validate(); + +showValidationResult($pdfValidator); + +$pdfValidator = ZugferdPdfValidator::fromContent(file_get_contents(dirname(__FILE__) . "/../tests/assets/pdf_zf_en16931_3.pdf"))->disableCleanup()->setValidatorRuleset(ZugferdPdfValidator::RULESET_PDF_A_3B)->validate(); + +showValidationResult($pdfValidator); + +$pdfValidator = ZugferdPdfValidator::fromContent(file_get_contents(dirname(__FILE__) . "/../tests/assets/pdf_zf_extended_1.pdf"))->disableCleanup()->setValidatorRuleset(ZugferdPdfValidator::RULESET_PDF_A_3B)->validate(); + +showValidationResult($pdfValidator); + +$pdfValidator = ZugferdPdfValidator::fromContent(file_get_contents(dirname(__FILE__) . "/../tests/assets/pdf_zf_xrechnung_1.pdf"))->disableCleanup()->setValidatorRuleset(ZugferdPdfValidator::RULESET_PDF_A_3B)->validate(); + +showValidationResult($pdfValidator); diff --git a/src/ZugferdPdfValidator.php b/src/ZugferdPdfValidator.php new file mode 100644 index 0000000..0964bb4 --- /dev/null +++ b/src/ZugferdPdfValidator.php @@ -0,0 +1,1078 @@ + + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/horstoeko/zugferd + */ +class ZugferdPdfValidator +{ + /** + * The PDF content + * + * @var string|null + */ + private $pdfContent; + + /** + * Internal message bag + * + * @var array + */ + private $messageBag = []; + + /** + * Base directory (download) + * + * @var string + */ + private $baseDirectory; + + /** + * VeraPDF Validator download url + * + * @var string + */ + private $validatorDownloadUrl = "https://software.verapdf.org/rel/verapdf-installer.zip"; + + /** + * The filename of the validation application zip archive + * + * @var string $validatorAppZipFilename + */ + private $validatorAppZipFilename = "verapdf-installer.zip"; + + /** + * The ruleset to use + * Allowed values are 0, 1a, 1b, 2a, 2b, 2u, 3a, 3b, 3u, 4, 4f, 4e, ua1, ua2 + * + * @var string + */ + private $validatorRuleset = "3a"; + + /** + * The temporary filename which contains the PDF data to validate + * + * @var string + */ + private $fileToValidateFilename = ""; + + /** + * Internal flag which indicates that the cleanup of the base directory is disables + * + * @var boolean + */ + private $cleanupBaseDirectoryIsDisabled = false; + + /** + * Message Type "Internal Error" + */ + protected const MSG_TYPE_INTERNALERROR = 'internalerror'; + + /** + * Message Type "Validation Error" + */ + protected const MSG_TYPE_VALIDATIONERROR = 'validationerror'; + + /** + * Message Type "Validation Warning" + */ + protected const MSG_TYPE_VALIDATIONWARNING = 'validationwarning'; + + /** + * Message Type "Validation info" + */ + protected const MSG_TYPE_VALIDATIONINFORMATION = 'validationinformation'; + + /** + * Message Type "Process Output" + */ + protected const MSG_TYPE_PROCESSOUTPUT = 'processoutput'; + + /** + * Ruleset for Automatic detection based on a file's metadata + */ + public const RULESET_PDF_A_0 = '0'; + + /** + * Ruleset PDF/A-1A validation profile + */ + public const RULESET_PDF_A_1A = '1a'; + + /** + * Ruleset PDF/A-1B validation profile + */ + public const RULESET_PDF_A_1B = '1b'; + + /** + * Ruleset PDF/A-2A validation profile + */ + public const RULESET_PDF_A_2A = '2a'; + + /** + * Ruleset PDF/A-2B validation profile + */ + public const RULESET_PDF_A_2B = '2b'; + + /** + * Ruleset PDF/A-2U validation profile + */ + public const RULESET_PDF_A_2U = '2u'; + + /** + * Ruleset PDF/A-3A validation profile + */ + public const RULESET_PDF_A_3A = '3a'; + + /** + * Ruleset PDF/A-3B validation profile + */ + public const RULESET_PDF_A_3B = '3b'; + + /** + * Ruleset PDF/A-3U validation profile + */ + public const RULESET_PDF_A_3U = '3u'; + + /** + * Ruleset PDF/A-4 validation profile + */ + public const RULESET_PDF_A_4 = '4'; + + /** + * Ruleset PDF/A-4F validation profile + */ + public const RULESET_PDF_A_4F = '4f'; + + /** + * Ruleset PDF/A-4E validation profile + */ + public const RULESET_PDF_A_4E = '4e'; + + /** + * Ruleset PDF/UA-1 validation profile + */ + public const RULESET_PDF_UA_1 = 'ua1'; + + /** + * Ruleset PDF/UA-2 + Tagged PDF validation profile + */ + public const RULESET_PDF_UA_2 = 'ua2'; + + /** + * Create a ZugferdPdfValidator-Instance by an existing PDF-File + * + * @param string $pdfFilename + * @return ZugferdPdfValidator + */ + public static function fromFile(string $pdfFilename): ZugferdPdfValidator + { + if (!file_exists($pdfFilename)) { + throw new ZugferdFileNotFoundException($pdfFilename); + } + + $pdfContent = file_get_contents($pdfFilename); + + if ($pdfContent === false) { + throw new ZugferdFileNotReadableException($pdfFilename); + } + + return ZugferdPdfValidator::fromContent($pdfContent); + } + + /** + * Create a ZugferdPdfValidator-Instance by a given content string + * + * @param string $pdfContent + * @return ZugferdPdfValidator + */ + public static function fromContent(string $pdfContent): ZugferdPdfValidator + { + return new ZugferdPdfValidator($pdfContent); + } + + /** + * Constructor + * + * @param string|null $pdfContent + */ + final protected function __construct(?string $pdfContent = null) + { + $this->setBaseDirectory(sys_get_temp_dir()); + $this->setPdfContent($pdfContent); + } + + /** + * Set the PDF content to validate + * + * @param string $pdfContent + * @return ZugferdPdfValidator + */ + public function setPdfContent(string $pdfContent): ZugferdPdfValidator + { + $this->pdfContent = $pdfContent; + + return $this; + } + + /** + * Setup the base directory. In the base directory all files will be downloaded + * and created + * + * @param string $newBaseDirectory + * @return ZugferdPdfValidator + */ + public function setBaseDirectory(string $newBaseDirectory): ZugferdPdfValidator + { + if (is_dir($newBaseDirectory)) { + $this->baseDirectory = $newBaseDirectory; + } + + return $this; + } + + /** + * Setup the VeraPDF validator application download url + * + * @param string $newValidatorDownloadUrl + * @return ZugferdPdfValidator + */ + public function setValidatorDownloadUrl(string $newValidatorDownloadUrl): ZugferdPdfValidator + { + if (filter_var($newValidatorDownloadUrl, FILTER_VALIDATE_URL) !== false) { + $this->validatorDownloadUrl = $newValidatorDownloadUrl; + } + + return $this; + } + + /** + * Set the filename of the ZIP file which contains the validation application + * + * @param string $newValidatorAppZipFilename + * @return ZugferdPdfValidator + */ + public function setValidatorAppZipFilename(string $newValidatorAppZipFilename): ZugferdPdfValidator + { + $this->validatorAppZipFilename = $newValidatorAppZipFilename; + + return $this; + } + + /** + * Set the Ruleset to use for validation. + * Allowed values are 0, 1a, 1b, 2a, 2b, 2u, 3a, 3b, 3u, 4, 4f, 4e, ua1, ua2 + * + * @param string $newVlidatorRuleset + * @return ZugferdPdfValidator + */ + public function setValidatorRuleset(string $newVlidatorRuleset): ZugferdPdfValidator + { + $newVlidatorRuleset = strtolower($newVlidatorRuleset); + + if (in_array($newVlidatorRuleset, ['0', '1a', '1b', '2a', '2b', '2u', '3a', '3b', '3u', '4', '4f', '4e', 'ua1', 'ua2'])) { + $this->validatorRuleset = $newVlidatorRuleset; + } + + return $this; + } + + /** + * Disable cleanup base directory + * + * @return ZugferdPdfValidator + */ + public function disableCleanup(): ZugferdPdfValidator + { + $this->cleanupBaseDirectoryIsDisabled = true; + + return $this; + } + + /** + * Enable cleanup base directory + * + * @return ZugferdPdfValidator + */ + public function enableCleanup(): ZugferdPdfValidator + { + $this->cleanupBaseDirectoryIsDisabled = false; + + return $this; + } + + /** + * Perform validation + * + * @return ZugferdPdfValidator + */ + public function validate(): ZugferdPdfValidator + { + $this->clearMessageBag(); + + if ($this->checkRequirements() === false) { + return $this; + } + + if ($this->downloadRequiredFiles() === false) { + $this->cleanupBaseDirectory(); + return $this; + } + + if ($this->unpackRequiredFiles() === false) { + $this->cleanupBaseDirectory(); + return $this; + } + + if ($this->installValidator() === false) { + $this->cleanupBaseDirectory(); + return $this; + } + + $this->performValidation(); + + $this->cleanupBaseDirectory(); + + return $this; + } + + /** + * Internal get the content of the PDF + * + * @return string + */ + private function getPdfContent(): string + { + return $this->pdfContent; + } + + /** + * Internal get (and create) the directory for downloads and file creation + * + * @return string + */ + private function resolveBaseDirectory(): string + { + $baseDirectorySuffix = md5($this->validatorDownloadUrl); + + $baseDirectory = PathUtils::combinePathWithPath($this->baseDirectory, sprintf("verapdf-%s", $baseDirectorySuffix)); + + if (!is_dir($baseDirectory)) { + @mkdir($baseDirectory); + } + + return $baseDirectory; + } + + /** + * Get the full filename of the archive to download which contains the Java validation application + * + * @return string + */ + private function resolveAppZipFilename(): string + { + return PathUtils::combinePathWithFile($this->resolveBaseDirectory(), $this->validatorAppZipFilename); + } + + /** + * Get the executable of the validator + * + * @return string + */ + private function resolveValidatorExecutable(): string + { + return PathUtils::combinePathWithFile($this->resolveBaseDirectory(), 'verapdf'); + } + + /** + * Reset the internal filename where data of the PDF to validate are stored + * + * @return void + */ + private function resetFileToValidateFilename(): void + { + $this->fileToValidateFilename = ""; + } + + /** + * Get the full filename which contains the PDF to validate + * + * @return string + */ + private function resolveFileToValidateFilename(): string + { + if (StringUtils::stringIsNullOrEmpty($this->fileToValidateFilename)) { + $this->fileToValidateFilename = PathUtils::combinePathWithFile($this->resolveBaseDirectory(), sprintf('filetovalidate-%s-%s.pdf', uniqid(), uniqid())); + } + + return $this->fileToValidateFilename; + } + + /** + * Clear the internal error bag + * + * @return void + */ + private function clearMessageBag(): void + { + $this->messageBag = []; + } + + /** + * Add message to error bag + * + * @param string|Throwable $error + * @return void + */ + private function addToMessageBag($error, string $messageType = ""): void + { + $messageType = StringUtils::stringIsNullOrEmpty($messageType) ? static::MSG_TYPE_INTERNALERROR : $messageType; + + if (is_string($error)) { + $this->messageBag[] = ["type" => $messageType, "message" => $error]; + } elseif ($error instanceof Throwable) { + $this->messageBag[] = ["type" => $messageType, "message" => $error->getMessage()]; + } + } + + /** + * Get messages from messagebag filtered by message type + * + * @param string $messageType + * @return array + */ + private function getMessageBagFiltered(string $messageType): array + { + return array_map( + function ($data) { + return $data["message"]; + }, + array_filter( + $this->messageBag, + function ($data) use ($messageType) { + return $data['type'] == $messageType; + } + ) + ); + } + + /** + * Returns an array of all validation errors + * + * @return array + */ + public function getValidationErrors(): array + { + return $this->getMessageBagFiltered(static::MSG_TYPE_VALIDATIONERROR); + } + + /** + * Returns true if __no__ validation errors are present otherwise false + * + * @return boolean + */ + public function hasNoValidationErrors(): bool + { + return $this->getValidationErrors() === []; + } + + /** + * Returns true if validation errors are present otherwise false + * + * @return boolean + */ + public function hasValidationErrors(): bool + { + return !$this->hasNoValidationErrors(); + } + + /** + * Returns an array of all validation warnings + * + * @return array + */ + public function getValidationWarnings(): array + { + return $this->getMessageBagFiltered(static::MSG_TYPE_VALIDATIONWARNING); + } + + /** + * Returns true if __no__ validation warnings are present otherwise false + * + * @return boolean + */ + public function hasNoValidationWarnings(): bool + { + return $this->getValidationWarnings() === []; + } + + /** + * Returns true if validation warnings are present otherwise false + * + * @return boolean + */ + public function hasValidationWarnings(): bool + { + return !$this->hasNoValidationWarnings(); + } + + /** + * Returns an array of all validation information + * + * @return array + */ + public function getValidationInformation(): array + { + return $this->getMessageBagFiltered(static::MSG_TYPE_VALIDATIONINFORMATION); + } + + /** + * Returns true if __no__ validation information are present otherwise false + * + * @return boolean + */ + public function hasNoValidationInformation(): bool + { + return $this->getValidationInformation() === []; + } + + /** + * Returns true if validation Information are present otherwise false + * + * @return boolean + */ + public function hasValidationInformation(): bool + { + return !$this->hasNoValidationInformation(); + } + + /** + * Return an array of all internal errors (such as download error or system exceptions) + * + * @return array + */ + public function getProcessErrors(): array + { + return $this->getMessageBagFiltered(static::MSG_TYPE_INTERNALERROR); + } + + /** + * Returns true if there are __no__ system errors (e.g. exceptions before the validation app was called) + * + * @return boolean + */ + public function hasNoProcessErrors(): bool + { + return $this->getProcessErrors() === []; + } + + /** + * Returns true if there are any system errors (e.g. exceptions before the validation app was called) + * + * @return boolean + */ + public function hasProcessErrors(): bool + { + return !$this->hasNoProcessErrors(); + } + + /** + * Returns an array of all messages from process system (calling external applications) + * + * @return array + */ + public function getProcessOutput(): array + { + return $this->getMessageBagFiltered(static::MSG_TYPE_PROCESSOUTPUT); + } + + /** + * Check Requirements + * + * @return boolean + */ + private function checkRequirements(): bool + { + if (is_null($this->pdfContent)) { + $this->addToMessageBag("You must specify the content or a filename of a PDF to validate"); + return false; + } + + if (!extension_loaded('zip')) { + $this->addToMessageBag("ZIP extension not installed"); + return false; + } + + $executableFinder = new ExecutableFinder(); + + if (is_null($executableFinder->find('java'))) { + $this->addToMessageBag("JAVA not installed on this machine"); + return false; + } + + return true; + } + + /** + * Download required files + * + * @return boolean + */ + private function downloadRequiredFiles(): bool + { + if (!$this->runFileDownload($this->validatorDownloadUrl, $this->resolveAppZipFilename())) { + $this->addToMessageBag(sprintf("Unable to download from %s containing the JAVA-Application", $this->validatorDownloadUrl)); + return false; + } + + return true; + } + + /** + * Unpack required files + * + * @return boolean + */ + private function unpackRequiredFiles(): bool + { + $validatorAppFile = $this->resolveAppZipFilename(); + + if (!$this->unpackRequiredFile($validatorAppFile, true)) { + $this->addToMessageBag(sprintf("Unable to unpack archive %s containing the JAVA-Application", $validatorAppFile)); + return false; + } + + return true; + } + + /** + * Unpack single required file + * + * @param string $zipFilename + * @param bool $flatExtraction + * @return bool + */ + private function unpackRequiredFile(string $zipFilename, bool $flatExtraction = false): bool + { + if ($flatExtraction) { + return $this->unpackRequiredFileFlat($zipFilename); + } + + return $this->unpackRequiredFileNonFlat($zipFilename); + } + + /** + * Unpack single required file (Non-Flat) + * + * @param string $zipFilename + * @return bool + */ + private function unpackRequiredFileNonFlat(string $zipFilename): bool + { + $zipArchive = new ZipArchive(); + + if ($zipArchive->open($zipFilename) !== true) { + $this->addToMessageBag(sprintf("Failed to open ZIP archive %s", $zipFilename)); + return false; + } + + $numFilesExists = 0; + + for ($i = 0; $i < $zipArchive->numFiles; $i++) { + $zipStat = $zipArchive->statIndex($i); + $realfilename = PathUtils::combinePathWithFile($this->resolveBaseDirectory(), $zipStat['name']); + if (file_exists($realfilename)) { + $numFilesExists++; + } + } + + if ($numFilesExists == $zipArchive->numFiles) { + return true; + } + + if (!$zipArchive->extractTo($this->resolveBaseDirectory())) { + $zipArchive->close(); + $this->addToMessageBag(sprintf("Failed to extract ZIP archive %s", $zipFilename)); + return false; + } + + $zipArchive->close(); + + return true; + } + + /** + * Unpack single required file (Flat) + * + * @param string $zipFilename + * @return bool + */ + private function unpackRequiredFileFlat(string $zipFilename): bool + { + $zipArchive = new ZipArchive(); + + if ($zipArchive->open($zipFilename) !== true) { + $this->addToMessageBag(sprintf("Failed to open ZIP archive %s", $zipFilename)); + return false; + } + + for ($i = 0; $i < $zipArchive->numFiles; $i++) { + $filenameInZip = $zipArchive->getNameIndex($i); + + if (substr($filenameInZip, -1) === '/') { + continue; + } + + $realfilename = $this->resolveBaseDirectory() . DIRECTORY_SEPARATOR . basename($filenameInZip); + + if (file_exists($realfilename)) { + continue; + } + + if (!copy('zip://' . realpath($zipFilename) . '#' . $filenameInZip, $realfilename)) { + $zipArchive->close(); + $this->addToMessageBag(sprintf("Failed to extract %s", $filenameInZip)); + return false; + } + } + + $zipArchive->close(); + + return true; + } + + /** + * Install the validator + * + * @return bool + * @throws DirectoryNotFoundException + * @throws LogicException + */ + private function installValidator(): bool + { + if (file_exists($this->resolveValidatorExecutable())) { + return true; + } + + $installerJarFinder = new Finder(); + $installerJarFinder->files()->name('verapdf-izpack-installer*.jar')->in($this->resolveBaseDirectory()); + + if ($installerJarFinder->hasResults() === false) { + $this->addToMessageBag("There was no installer in the form of a JAR-File found"); + return false; + } + + $installerScriptFilename = PathUtils::combinePathWithFile($this->resolveBaseDirectory(), 'install.xml'); + + if (file_put_contents( + $installerScriptFilename, + sprintf( + ' + + + + %s + + + + + + + + + + ', + $this->resolveBaseDirectory() + ) + ) === false + ) { + $this->addToMessageBag("Failed to create install script"); + return false; + } + + $installerJarIterator = $installerJarFinder->getIterator(); + $installerJarIterator->rewind(); + + $installerJarFilename = $installerJarIterator->current()->getPathname(); + + $installerJarOptions = [ + 'java', + '-jar', + $installerJarFilename, + $installerScriptFilename, + ]; + + if ($this->runProcess($installerJarOptions, $this->resolveBaseDirectory()) === false) { + $this->addToMessageBag("Failed to run installer"); + return false; + } + + return true; + } + + /** + * Runs the validator java application + * + * @return boolean + */ + private function performValidation(): bool + { + if (!file_exists($this->resolveValidatorExecutable())) { + $this->addToMessageBag("Validation application not found"); + return false; + } + + $this->resetFileToValidateFilename(); + + if (file_put_contents($this->resolveFileToValidateFilename(), $this->getPdfContent()) === false) { + $this->addToMessageBag("Cannot create temporary file which contains the PDF to validate"); + return false; + } + + $validatorExecutableOptions = [ + $this->resolveValidatorExecutable(), + '--format', + 'json', + '--flavour', + $this->validatorRuleset, + $this->resolveFileToValidateFilename(), + ]; + + if ($this->runProcessAndGetOutput($validatorExecutableOptions, $this->resolveBaseDirectory(), $validatorExecutableOutput) === false) { + $this->checkValidatorExecutableOutput($validatorExecutableOutput); + return false; + } + + return $this->checkValidatorExecutableOutput($validatorExecutableOutput); + } + + /** + * Read and parse the JSON response + * + * @param string $validatorExecutableOutput + * @return bool + */ + private function checkValidatorExecutableOutput(string $validatorExecutableOutput): bool + { + $validatorExecutableOutputObject = json_decode($validatorExecutableOutput); + + if ($validatorExecutableOutputObject === null && json_last_error() !== JSON_ERROR_NONE) { + $this->addToMessageBag(sprintf("Cannot decode JSON result. Error %s", json_last_error_msg()), static::MSG_TYPE_VALIDATIONERROR); + return false; + } + + if (!isset($validatorExecutableOutputObject->report)) { + $this->addToMessageBag("Invalid report response - no report property found", static::MSG_TYPE_VALIDATIONERROR); + return false; + } + + if (!isset($validatorExecutableOutputObject->report->jobs)) { + $this->addToMessageBag("Invalid report response - no jobs property found", static::MSG_TYPE_VALIDATIONERROR); + return false; + } + + if (!is_array($validatorExecutableOutputObject->report->jobs)) { + $this->addToMessageBag("Invalid report response - jobs property is not an array", static::MSG_TYPE_VALIDATIONERROR); + return false; + } + + if (count($validatorExecutableOutputObject->report->jobs) != 1) { + $this->addToMessageBag("Invalid report response - jobs property should be an array with one element", static::MSG_TYPE_VALIDATIONERROR); + return false; + } + + $validatorExecutableOutputJobObject = $validatorExecutableOutputObject->report->jobs[0]; + + if (!isset($validatorExecutableOutputJobObject->validationResult)) { + $this->addToMessageBag("Invalid report response - job has not a validationResult property", static::MSG_TYPE_VALIDATIONERROR); + return false; + } + + if (!isset($validatorExecutableOutputJobObject->validationResult->details)) { + $this->addToMessageBag("Invalid report response - job has not a details property", static::MSG_TYPE_VALIDATIONERROR); + return false; + } + + if (!isset($validatorExecutableOutputJobObject->validationResult->details->failedRules)) { + $this->addToMessageBag("Invalid report response - job has not a failedRules property", static::MSG_TYPE_VALIDATIONERROR); + return false; + } + + if (!isset($validatorExecutableOutputJobObject->validationResult->details->failedChecks)) { + $this->addToMessageBag("Invalid report response - job has not a failedChecks property", static::MSG_TYPE_VALIDATIONERROR); + return false; + } + + if ($validatorExecutableOutputJobObject->validationResult->details->failedRules == 0 && $validatorExecutableOutputJobObject->validationResult->details->failedChecks == 0) { + return true; + } + + $this->addToMessageBag( + sprintf( + "Validation failed. Failed rules: %s, Failed Checks: %s", + $validatorExecutableOutputJobObject->validationResult->details->failedRules, + $validatorExecutableOutputJobObject->validationResult->details->failedChecks + ), + static::MSG_TYPE_VALIDATIONERROR + ); + + foreach ($validatorExecutableOutputJobObject->validationResult->details->ruleSummaries ?? [] as $ruleSummary) { + $this->addToMessageBag( + sprintf( + "%s, %s, %s --> %s", + $ruleSummary->specification, + $ruleSummary->clause, + $ruleSummary->object, + $ruleSummary->description + ), + static::MSG_TYPE_VALIDATIONERROR + ); + } + + return false; + } + + /** + * Cleanup downloads and created files + * + * @return void + */ + private function cleanupBaseDirectory(): void + { + if ($this->cleanupBaseDirectoryIsDisabled === true) { + return; + } + + if (!is_dir($this->resolveBaseDirectory())) { + return; + } + + $this->cleanupBaseDirectoryInternal($this->resolveBaseDirectory()); + } + + /** + * Helper method for removeBaseDirectory + * + * @param string $directoryToRemove + * @return void + */ + private function cleanupBaseDirectoryInternal(string $directoryToRemove): void + { + if (!is_dir($directoryToRemove)) { + return; + } + + $objects = scandir($directoryToRemove); + + foreach ($objects as $object) { + if ($object !== "." && $object !== "..") { + $fullFilename = PathUtils::combinePathWithFile($directoryToRemove, $object); + if (is_dir($fullFilename) && !is_link($fullFilename)) { + $this->cleanupBaseDirectoryInternal($fullFilename); + } else { + unlink($fullFilename); + } + } + } + + rmdir($directoryToRemove); + } + + /** + * Runs a process. If the process runned successfully this method + * returns true, otherwise false + * + * @param array $command + * @param string $workingdirectory + * @return bool + */ + private function runProcess(array $command, string $workingdirectory): bool + { + return $this->runProcessAndGetOutput($command, $workingdirectory, $_); + } + + /** + * Runs a process. If the process runned successfully this method + * returns true, otherwise false. The output of the process wil be + * returned in $processOutput + * + * @param array $command + * @param string $workingdirectory + * @param null|string &$processOutput + * @return bool + */ + private function runProcessAndGetOutput(array $command, string $workingdirectory, ?string &$processOutput): bool + { + try { + $process = new Process($command); + $process->setTimeout(0.0); + $process->setWorkingDirectory($workingdirectory); + $process->run(); + + $processOutput = $process->getOutput(); + + foreach (preg_split("/\r\n|\n|\r/", $processOutput) as $outputLine) { + $this->addToMessageBag($outputLine, static::MSG_TYPE_PROCESSOUTPUT); + } + + if (!$process->isSuccessful()) { + return false; + } + } catch (Throwable $throwable) { + $this->addToMessageBag($throwable, static::MSG_TYPE_VALIDATIONERROR); + return false; + } + + return true; + } + + /** + * Run a file download. + * + * @param string $url + * @param string $toFilePath + * @param bool $forceOverwrite + * @return bool + */ + private function runFileDownload(string $url, string $toFilePath, bool $forceOverwrite = false): bool + { + try { + if (file_exists($toFilePath) && !$forceOverwrite) { + return true; + } + + file_put_contents($toFilePath, file_get_contents($url)); + } catch (Throwable $throwable) { + $this->addToMessageBag($throwable); + return false; + } + + return true; + } +}