diff --git a/Documentation/Manual.rst b/Documentation/Manual.rst index e876eedba767cb0060d9b4a3febd4a77c16b25ae..1897edcb3e784e9929ff02e6c839e0c6bd34ff0a 100644 --- a/Documentation/Manual.rst +++ b/Documentation/Manual.rst @@ -51,6 +51,7 @@ The following features are only tested / supported on linux hosts: * General: QFQ is coded to run on Linux hosts, preferable on Debian derivates like Ubuntu. * HTML to PDF conversion - command `wkhtmltopdf`. * Concatenation of PDF files - command `pdfunite`. +* PDF decrypt (used for merge with pdfunite) - command `qpdf`. * Mime type detection for uploads - command `file`. @@ -66,12 +67,12 @@ To normalize UTF8 input, *php-intl* package is needed by * normalizer::normalize() -For the `download`_ function, the programs `pdfunite` and `file` are necessary to concatenate PDF files. +For the `download`_ function, the programs `pdfunite`, `qpdf` and `file` are necessary to concatenate PDF files. Preparation for Ubuntu:: sudo apt install php-intl - sudo apt install poppler-utils libxrender1 file pdf2svg # for file upload, PDF and 'HTML to PDF' (wkhtmltopdf), PDF split + sudo apt install poppler-utils libxrender1 file pdf2svg pdfunite qpdf # for file upload, PDF and 'HTML to PDF' (wkhtmltopdf), PDF split sudo apt install inkscape imagemagick # to render thumbnails .. _wkhtml: diff --git a/extension/Classes/Core/Helper/HelperFile.php b/extension/Classes/Core/Helper/HelperFile.php index 78c7103c734db4fb0cc37912cedee69dd085182a..bc0cb0cce3f479de5463f06162e27829a3ebc3d1 100644 --- a/extension/Classes/Core/Helper/HelperFile.php +++ b/extension/Classes/Core/Helper/HelperFile.php @@ -247,9 +247,9 @@ class HelperFile { public static function chmod($pathFileName, $mode = false) { if ($mode !== false) { - if (false === chmod($pathFileName, $mode)) { + if (false === @chmod($pathFileName, $mode)) { throw new \UserFormException( - json_encode([ERROR_MESSAGE_TO_USER => 'Failed: chmod', ERROR_MESSAGE_TO_DEVELOPER => "Failed: chmod $mode '$pathFileName'"]), + json_encode([ERROR_MESSAGE_TO_USER => 'Failed: chmod', ERROR_MESSAGE_TO_DEVELOPER => self::errorGetLastAsString()]), ERROR_IO_CHMOD); } } @@ -277,7 +277,7 @@ class HelperFile { */ public static function chdir($cwd) { - if (false === chdir($cwd)) { + if (false === @chdir($cwd)) { $msg = self::errorGetLastAsString() . " - chdir($cwd)"; throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'chdir failed', ERROR_MESSAGE_TO_DEVELOPER => $msg]), ERROR_IO_CHDIR); } @@ -300,7 +300,7 @@ class HelperFile { Logger::logMessageWithPrefix("Unlink: $filename", $logFilename); } - if (false === unlink($filename)) { + if (false === @unlink($filename)) { $msg = self::errorGetLastAsString() . " - unlink($filename)"; throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'unlink failed', ERROR_MESSAGE_TO_DEVELOPER => $msg]), ERROR_IO_UNLINK); } @@ -317,7 +317,7 @@ class HelperFile { */ public static function rmdir($tempDir) { - if (false === rmdir($tempDir)) { + if (false === @rmdir($tempDir)) { $msg = self::errorGetLastAsString() . " - rmdir($tempDir)"; throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'rmdir failed', ERROR_MESSAGE_TO_DEVELOPER => $msg]), ERROR_IO_RMDIR); } @@ -335,7 +335,7 @@ class HelperFile { */ public static function rename($oldname, $newname) { - if (false === rename($oldname, $newname)) { + if (false === @rename($oldname, $newname)) { $msg = self::errorGetLastAsString() . " - rename($oldname ,$newname)"; throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'unlink failed', ERROR_MESSAGE_TO_DEVELOPER => $msg]), ERROR_IO_RENAME); } @@ -358,7 +358,7 @@ class HelperFile { touch($dest); } - if (false === copy($source, $dest)) { + if (false === @copy($source, $dest)) { if (!is_readable($source)) { throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'copy failed', ERROR_MESSAGE_TO_DEVELOPER => "Can't read file '$source'"]), ERROR_IO_READ_FILE); diff --git a/extension/Classes/Core/Report/Download.php b/extension/Classes/Core/Report/Download.php index a55c0b86c6af9352d29a946bcdf2cbbd164d64d4..f0458ca86ba8256bf95f3dc9df340979a352b56c 100644 --- a/extension/Classes/Core/Report/Download.php +++ b/extension/Classes/Core/Report/Download.php @@ -10,20 +10,19 @@ namespace IMATHUZH\Qfq\Core\Report; +use IMATHUZH\Qfq\Core\Database\Database; +use IMATHUZH\Qfq\Core\Helper\DownloadPage; +use IMATHUZH\Qfq\Core\Helper\HelperFile; use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser; -use IMATHUZH\Qfq\Core\Store\Session; -use IMATHUZH\Qfq\Core\Store\Store; -use IMATHUZH\Qfq\Core\Store\Sip; +use IMATHUZH\Qfq\Core\Helper\Logger; use IMATHUZH\Qfq\Core\Helper\OnArray; use IMATHUZH\Qfq\Core\Helper\OnString; -use IMATHUZH\Qfq\Core\Helper\Logger; use IMATHUZH\Qfq\Core\Helper\Sanitize; -use IMATHUZH\Qfq\Core\Helper\HelperFile; -use IMATHUZH\Qfq\Core\Helper\DownloadPage; -use IMATHUZH\Qfq\Core\QuickFormQuery; - -use IMATHUZH\Qfq\Core\Database\Database; use IMATHUZH\Qfq\Core\Helper\Support; +use IMATHUZH\Qfq\Core\QuickFormQuery; +use IMATHUZH\Qfq\Core\Store\Session; +use IMATHUZH\Qfq\Core\Store\Sip; +use IMATHUZH\Qfq\Core\Store\Store; /** * Class Download @@ -147,7 +146,7 @@ class Download { Logger::logMessage("Download: $cmd", $this->downloadDebugLog); } - exec($cmd, $output, $rc); + $rc = $this->concatPdfFilesPdfUnite($cmd, $output); if ($rc != 0) { throw new \DownloadException (json_encode([ERROR_MESSAGE_TO_USER => "Failed to merge PDF file", @@ -158,6 +157,69 @@ class Download { return $concatFile; } + /** + * Fires the merge command. + * If for any reason the command fails: check if the reason is 'unencrypted files'. + * If 'yes': try to decrypt them with qpdf. + * After one decrypt, try merge again. + * Try to merge and decrypt as long as there are encrypted files. + * + * @param $cmd + * @param $rcOutput + * @return mixed + * @throws \DownloadException + * @throws \UserFormException + */ + private function concatPdfFilesPdfUnite($cmd, &$rcOutput) { + $last = ''; + $rcOutput = '-'; + + // Try to merge the PDFs as long as a problematic PDF has been repaired. + while ($last != $rcOutput) { + + $last = $rcOutput; // Remember last + + // Merge + exec($cmd, $rcOutput, $rc); + + if ($rc == 0) { + break; // skip rest if everything is fine + } + + // Possible output: "Unimplemented Feature: Could not merge encrypted files ('ct.18.06.092-097.pdf')" + $line = implode(',', $rcOutput); + if (false !== strstr($line, "Unimplemented Feature: Could not merge encrypted files (")) { + + $arr = explode("'", $line, 3); + if (!empty($arr[1]) && file_exists($arr[1])) { + $file = $arr[1]; // problematic file + + // Create a backup file: only one per day! + $backup = $file . date('.Y-m-d'); + if (!file_exists($backup)) { + HelperFile::copy($file, $backup); + } + + $cmdQpdf = "qpdf --decrypt '$backup' '$file' 2>&1"; // Try to decrypt file + exec($cmdQpdf, $outputQpdf, $rcQpdf); + + if ($rcQpdf != 0) { + // qpdf failed: restore origfile in case the $file has been destroyed. + HelperFile::copy($backup, $file); + throw new \DownloadException (json_encode([ERROR_MESSAGE_TO_USER => "Failed to decrypt PDF", + ERROR_MESSAGE_TO_DEVELOPER => "CMD: " . $cmdQpdf . "<br>RC: $rc<br>Output: " . implode("<br>", $outputQpdf)]) + , ERROR_DOWNLOAD_MERGE_FAILED); + } + } + } else { + throw new \DownloadException (json_encode([ERROR_MESSAGE_TO_USER => "Merge PDF file failed.", + ERROR_MESSAGE_TO_DEVELOPER => "CMD: " . $cmd . "<br>RC: $rc<br>Output: " . implode("<br>", $rcOutput)]) + , ERROR_DOWNLOAD_MERGE_FAILED); + } + } + return $rc; + } + /** * Get the mimetype of $filename and store them in $rcMimetype. * Checks if the extension of $outputFilename fit's to the mimetype. If not, append the mimetype extension. @@ -168,7 +230,8 @@ class Download { * * @return string possible updated $outputFilename, according the mimetype. */ - private function targetFilenameExtension($filename, $outputFilename, &$rcMimetype) { + private + function targetFilenameExtension($filename, $outputFilename, &$rcMimetype) { $rcMimetype = mime_content_type($filename); @@ -182,7 +245,8 @@ class Download { * @param $outputFilename * @throws \DownloadException */ - private function outputFile($file, $outputFilename) { + private + function outputFile($file, $outputFilename) { $json = ''; $flagJson = ($this->getOutputFormat() === DOWNLOAD_OUTPUT_FORMAT_JSON); @@ -237,7 +301,8 @@ class Download { * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception */ - private function getElement($element, $downloadMode, &$rcData) { + private + function getElement($element, $downloadMode, &$rcData) { $filename = ''; $rcArgs = array(); @@ -323,7 +388,8 @@ class Download { * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception */ - private function getEvaluatedBodyText($uid, $urlParam) { + private + function getEvaluatedBodyText($uid, $urlParam) { foreach ($urlParam as $key => $paramValue) { $this->store->setVar($key, $paramValue, STORE_SIP); } @@ -346,7 +412,8 @@ class Download { * @return string ZIP filename - has to be deleted later. * @throws \DownloadException */ - private function zipFiles(array $files) { + private + function zipFiles(array $files) { $zipFile = HelperFile::tempnam(); if (false === $zipFile) { @@ -398,7 +465,8 @@ class Download { * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception */ - private function doElements(array $vars, $outputMode) { + private + function doElements(array $vars, $outputMode) { $tmpFiles = array(); @@ -519,7 +587,8 @@ class Download { * @throws \UserFormException * @throws \UserReportException */ - private function doThumbnail($urlParam) { + private + function doThumbnail($urlParam) { $thumbnail = new Thumbnail(); $pathFilenameThumbnail = $thumbnail->process($urlParam, THUMBNAIL_VIA_DOWNLOAD); @@ -543,7 +612,8 @@ class Download { * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception */ - public function process($vars, $outputMode = OUTPUT_MODE_DIRECT) { + public + function process($vars, $outputMode = OUTPUT_MODE_DIRECT) { if (!is_array($vars)) { $vars = $this->store->getStore(STORE_SIP); @@ -557,14 +627,16 @@ class Download { /** * @param $outputFormat */ - private function setOutputFormat($outputFormat) { + private + function setOutputFormat($outputFormat) { $this->outputFormat = $outputFormat; } /** * @return string - DOWNLOAD_OUTPUT_FORMAT_RAW | DOWNLOAD_OUTPUT_FORMAT_JSON */ - public function getOutputFormat() { + public + function getOutputFormat() { return $this->outputFormat; } }