Commit 4e01a68b authored by Carsten  Rose's avatar Carsten Rose
Browse files

#3218 / download.php / export

Implemented download.php to offer SIP protected downloads for single files (any filetype) as well as concatenated PDF files and converted HTML pages.
download.php: API Interface
DownloadException.php: New exception class for downloads - might be extended for better error handling.
OnArray.php: new function getArrayItemKeyNameStartWith() to filter for specific elements in an array. New function arrayEscapeshellarg() to escape args
Download.php: Main class.
Link.php, Report.php: implemented new link type 'd' (=download)
parent 178334d7
...@@ -3244,6 +3244,8 @@ Special column names ...@@ -3244,6 +3244,8 @@ Special column names
+------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|_pageX or _PageX |Shortcut version of the link interface for fast creation of internal links. The column name is composed of the string *page*/*Page* and a optional character to specify the type of the link.| |_pageX or _PageX |Shortcut version of the link interface for fast creation of internal links. The column name is composed of the string *page*/*Page* and a optional character to specify the type of the link.|
+------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|_download or _Download |Shortcut version of the link interface for fast creation of download links. Used to offer single file download or to concatenate severral PDFs and printout of websites to one PDF file. |
+------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|_sendmail |Send emails. | |_sendmail |Send emails. |
+------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|_exec |Run batch files or executables on the webserver. | |_exec |Run batch files or executables on the webserver. |
...@@ -3279,6 +3281,8 @@ Column: _link ...@@ -3279,6 +3281,8 @@ Column: _link
+---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+
|x | |Page |p:<pageId> |p:impressum |Prepend '?' or '?id=', no hostname qualifier (automatically set by browser), default link class: internal, default value: {{pageId}} | |x | |Page |p:<pageId> |p:impressum |Prepend '?' or '?id=', no hostname qualifier (automatically set by browser), default link class: internal, default value: {{pageId}} |
+---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+
|x | |Download |d |d |If an image is specified, it will be rendered inside the link, default link class: internal. Link points to `api/download.php` |
+---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+
| | |Text |t:<text> |t:Firstname Lastname |- | | | |Text |t:<text> |t:Firstname Lastname |- |
+---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+
| | |Render |r:<mode> |r:[0-5] |See: `render-mode`_, Default: 0 | | | |Render |r:<mode> |r:[0-5] |See: `render-mode`_, Default: 0 |
...@@ -3386,6 +3390,8 @@ Link Examples ...@@ -3386,6 +3390,8 @@ Link Examples
+-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+
|SELECT "U:form=Person&r=123|x|t:Delete" as _link |<a href="typo3conf/ext/qfq/qfq/api/delete.php?s=badcaffee1234">Delete</a> | |SELECT "U:form=Person&r=123|x|t:Delete" as _link |<a href="typo3conf/ext/qfq/qfq/api/delete.php?s=badcaffee1234">Delete</a> |
+-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+
|SELECT "d:U:1_pageId=req&1_id=12&1_mode=html2pdf" |<a href="typo3conf/ext/qfq/qfq/api/download.php?s=badcaffee1234">Download</a> |
+-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+
.. _question: .. _question:
...@@ -3435,6 +3441,78 @@ Examples: ...@@ -3435,6 +3441,78 @@ Examples:
| SELECT "p:form_person|q:Edit Person:::10:0" AS _link | The Alert will be shown 10 seconds and is not modal. | | SELECT "p:form_person|q:Edit Person:::10:0" AS _link | The Alert will be shown 10 seconds and is not modal. |
+------------------------------------------------------------+---------------------------------------------------------------------------+ +------------------------------------------------------------+---------------------------------------------------------------------------+
Download
^^^^^^^^
Download links can be used to offer:
* to download a single file, or
* to concatenate several files and/or web pages (=HTML to PDF) into one output file, or
* to create an `Excel` export.
The downloads are SIP protected. Only the current user can use the link to download files.
By using the `_link` columnname:
* the option `d` enables the download mode
* setting `s` (or `s=1`) is mandatory for download mode
By using `_download` or `_Download` as columnname the options `d` and `s` will be set by automatically.
All files will be read by PHP - therefore the directory might be protected against direct web access. This way is the
preferred way to offer secure downloads via QFQ.
In case the download needs a persistant URL (no SIP, no user session), a regular
link, pointing directly to a file, have to be used - the download described here won't help.
.. _download-parameter-files:
Specify file(s)/web page(s)
'''''''''''''''''''''''''''
The following parameter have to be specified as 'U' parameter. The SIP encoded parameter remains on the server and
are not transferred to the client. This prohibits parameter manipulation.
* *Link parameter 'U:.....'* :
* *exportFilename* = <filename for save as> - Name, offered in the 'File save as' browser dialog. Default: 'output.pdf'.
The user typically expect meaningfull and distinct filenames for different downloads.
* *mode* = <file | pdf | excel> - This parameter is optional and can be skipped in most situations.
* If `mode=file`, the mimetype is derived dynamically from the specified file. All others are fix (PDF or Excel).
* In case of multiple files and/or web pages, only `pdf` is supported.
* `excel` is not implemented now.
* The **default** depends on the number of specified `elements` (=file or web page). If only one `file` is specifed,
the default is `file`. If there is a) a page defined or b) multiple elements, the default is `pdf`.
* For `web page` or `excel` export, all pages have to be specified with the necessary parameters (might
vary per page). The preceeding number `<i>` aggregates all parameter to one element (=`file` or `page`). The ordering has to start with `1`
and has to be consecutive.
* *<i>_id* = <Typo3 pageId> - `id` is fix and mandatory for `web page` and `excel`.
* *<i>_<keyname>* = <value key i> - <keyname> is free of choice except `id` and `file`.
* For `file`
* *<i>_file* = <pathfilename> - `file` is fix and mandatory for direct `file` access.
Most of the other Link-Class attributes can be used to customize the link.
Example: ::
# single `file`
SELECT "d|s|t:PDF|U:exportFilename=final.pdf&1_file=fileadmin/pdf/test.pdf" AS _link
# single `file`, with mode
SELECT "d|s|t:PDF|U:exportFilename=final.pdf&mode=file&1_file=fileadmin/pdf/test.pdf" AS _link
# two pages (1_id, 2_id) and one file (3_file)
SELECT "d|s|t:PDF|U:exportFilename=final.pdf&1_id=exportP1&2_id=123&3_file=fileadmin/pdf/test.pdf" AS _link
# two pages (1_id, 2_id) and one file (3_file) with mode
SELECT "d|s|t:PDF|U:exportFilename=final.pdf&mode=pdf&1_id=exportP1&2_id=123&3_file=fileadmin/pdf/test.pdf" AS _link
..
Columns: _page[X] Columns: _page[X]
^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^
...@@ -3831,6 +3909,43 @@ Runs batch files or executables on the webserver. In case of an error, returncod ...@@ -3831,6 +3909,43 @@ Runs batch files or executables on the webserver. In case of an error, returncod
.. ..
Column: _download
^^^^^^^^^^^^^^^^^
Most of the other Link-Class attributes can be used to customize the link. Most usefull:
::
SELECT "[options]" AS _download
with: [options] = U:<params>[t:<text>]|[o:<tooltip>]|[c:<class>]|[g:<target>]|[r:<render mode>]
* Parameter are position independent.
* *<params>*: see ref:`download-parameter-files`
* Example: ::
SELECT "U:exportFilename=file.pdf&mode=pdf&1_id=xRequest&2_id=123&2_r=456&3_file=fileadmin/test.pdf|t:PDF download|o:Click to view the PDF" AS _download
..
Column: _Download
^^^^^^^^^^^^^^^^^
A limited set of attributes is supported: ::
SELECT "[options]" AS _Download
with: [options] =<params>|[<text>]|[<tooltip>]|[<class>]|[<target>]|[<render mode>]
* Parameter are position dependent and therefore without a qualifier!
* *<params>*: see ref:`download-parameter-files`
* Example: ::
SELECT "exportFilename=file.pdf&mode=pdf&1_id=xRequest&2_id=123&2_r=456&3_file=fileadmin/test.pdf|PDF download|Click to view the PDF" AS _download
..
Column: _F Column: _F
^^^^^^^^^^ ^^^^^^^^^^
......
<?php
/**
* Created by PhpStorm.
* User: crose
* Date: 4/17/17
* Time: 5:51 PM
*/
namespace qfq;
use qfq;
require_once(__DIR__ . '/../qfq/report/Download.php');
//require_once(__DIR__ . '/../qfq/store/Store.php');
require_once(__DIR__ . '/../qfq/Constants.php');
try {
$download = new \qfq\Download();
// If all is fine - this function never returns! The output file is delivered and PHP is stopped after that.
$data = $download->process();
} catch (\Exception $e) {
$data = "Exception: " . $e->getMessage();
}
echo $data;
...@@ -201,11 +201,18 @@ const ERROR_UPLOAD = 1500; ...@@ -201,11 +201,18 @@ const ERROR_UPLOAD = 1500;
const ERROR_UNKNOWN_ACTION = 1502; const ERROR_UNKNOWN_ACTION = 1502;
const ERROR_NO_TARGET_PATH_FILE_NAME = 1503; const ERROR_NO_TARGET_PATH_FILE_NAME = 1503;
// LDAP
const ERROR_LDAP_CONNECT = 1600; const ERROR_LDAP_CONNECT = 1600;
const ERROR_MISSING_TYPE_AHEAD_LDAP_SEARCH = 1601; const ERROR_MISSING_TYPE_AHEAD_LDAP_SEARCH = 1601;
const ERROR_MISSING_TYPE_AHEAD_LDAP_SEARCH_PREFETCH = 1602; const ERROR_MISSING_TYPE_AHEAD_LDAP_SEARCH_PREFETCH = 1602;
const ERROR_LDAP_BIND = 1603; const ERROR_LDAP_BIND = 1603;
// Download
const ERROR_DOWNLOAD_CREATE_NEW_FILE = 1700;
const ERROR_DOWNLOAD_NO_FILES = 1701;
const ERROR_DOWNLOAD_NOTHING_TO_DO = 1702;
const ERROR_DOWNLOAD_UNEXPECTED_MIMETYPE = 1703;
// KeyValueParser // KeyValueParser
const ERROR_KVP_VALUE_HAS_NO_KEY = 1900; const ERROR_KVP_VALUE_HAS_NO_KEY = 1900;
...@@ -374,7 +381,7 @@ const MSG_ERROR_CODE = 'errorCode'; ...@@ -374,7 +381,7 @@ const MSG_ERROR_CODE = 'errorCode';
const SIP_TOKEN_LENGTH = 13; // length of string returned by `uniqid()` const SIP_TOKEN_LENGTH = 13; // length of string returned by `uniqid()`
const SIP_SIP = CLIENT_SIP; // s const SIP_SIP = CLIENT_SIP; // s
const SIP_RECORD_ID = CLIENT_RECORD_ID; // r const SIP_RECORD_ID = CLIENT_RECORD_ID; // r
const SIP_TARGET_URL= '_targetUrl'; // URL where to jump after delete() const SIP_TARGET_URL = '_targetUrl'; // URL where to jump after delete()
const SIP_MODE_ANSWER = '_modeAnswer'; // Mode how delete() will answer to client: MODE_HTML, MODE_JSON const SIP_MODE_ANSWER = '_modeAnswer'; // Mode how delete() will answer to client: MODE_HTML, MODE_JSON
const SIP_FORM = CLIENT_FORM; const SIP_FORM = CLIENT_FORM;
const SIP_TABLE = 'table'; // delete a record from 'table' const SIP_TABLE = 'table'; // delete a record from 'table'
...@@ -459,6 +466,7 @@ const MODE_LDAP_MULTI = 'ldapMulti'; ...@@ -459,6 +466,7 @@ const MODE_LDAP_MULTI = 'ldapMulti';
// api/save.php, api/delete.php, api/load.php // api/save.php, api/delete.php, api/load.php
const API_DELETE_PHP = 'delete.php'; const API_DELETE_PHP = 'delete.php';
const API_DOWNLOAD_PHP = 'download.php';
const API_STATUS = 'status'; const API_STATUS = 'status';
const API_MESSAGE = 'message'; const API_MESSAGE = 'message';
...@@ -822,6 +830,7 @@ const COLUMN_PPAGEH = "Pageh"; ...@@ -822,6 +830,7 @@ const COLUMN_PPAGEH = "Pageh";
const COLUMN_PPAGEI = "Pagei"; const COLUMN_PPAGEI = "Pagei";
const COLUMN_PPAGEN = "Pagen"; const COLUMN_PPAGEN = "Pagen";
const COLUMN_PPAGES = "Pages"; const COLUMN_PPAGES = "Pages";
const COLUMN_DDOWNLOAD = "Download";
const COLUMN_PAGE = "page"; const COLUMN_PAGE = "page";
const COLUMN_PAGEC = "pagec"; const COLUMN_PAGEC = "pagec";
...@@ -831,8 +840,27 @@ const COLUMN_PAGEH = "pageh"; ...@@ -831,8 +840,27 @@ const COLUMN_PAGEH = "pageh";
const COLUMN_PAGEI = "pagei"; const COLUMN_PAGEI = "pagei";
const COLUMN_PAGEN = "pagen"; const COLUMN_PAGEN = "pagen";
const COLUMN_PAGES = "pages"; const COLUMN_PAGES = "pages";
const COLUMN_DOWNLOAD = "download";
const FORM_NAME_FORM = 'form'; const FORM_NAME_FORM = 'form';
const FORM_NAME_FORM_ELEMENT = 'formElement'; const FORM_NAME_FORM_ELEMENT = 'formElement';
const PENALTY_TIME_BROKEN_SIP = 5; const PENALTY_TIME_BROKEN_SIP = 5;
\ No newline at end of file
// DOWNLOAD
const DOWNLOAD_MODE = 'mode';
const DOWNLOAD_MODE_FILE = 'file';
const DOWNLOAD_MODE_PDF = 'pdf';
const DOWNLOAD_MODE_EXCEL = 'excel';
const DOWNLOAD_EXPORT_FILENAME = 'exportFilename';
const DOWNLOAD_PAGE_ID = 'id';
const DOWNLOAD_FILE = 'file';
const DOWNLOAD_FILE_PREFIX = 'qfq.temp';
const DOWNLOAD_OUTPUT_PDF = 'output.pdf';
const DOWNLOAD_PARAMETER_DELIMITER = '_';
// HTML2PDF
const HTML2PDF_PAGEID = 'id';
const HTML2PDF_PARAM_GET = 'paramGet';
const HTML2PDF_URL_PRINT = 'urlPrint';
<?php
/**
* Created by PhpStorm.
* User: crose
* Date: 4/17/17
* Time: 9:20 PM
*/
namespace qfq;
require_once(__DIR__ . '/AbstractException.php');
/**
* Class DownloadException
*
* Thrown by Download
*
* @package qfq\exceptions
*/
class DownloadException extends AbstractException {
/*
* @return string HTML formatted error string
*/
public function formatMessage() {
$this->messageArray['Type'] = 'Download Exception';
return parent::formatException();
}
}
\ No newline at end of file
...@@ -228,6 +228,33 @@ class OnArray { ...@@ -228,6 +228,33 @@ class OnArray {
return $new; return $new;
} }
/**
* Copies all items whose keyNames starts with $keyName.
* Remove Prefix '$keyName' from all keys.
* Return $new
*
* E.g. [ 'mode' => 'pdf', '1_pageId' => 123, '2_file' => 'example.pdf' ], with $keyName='1_' >> [ 'pageId' => 123 ]
*
* @param array $src
* @param string $keyName
* @return array
*/
public static function getArrayItemKeyNameStartWith(array $src, $keyName) {
$new = array();
$length = strlen($keyName);
// Extract necessary elements
foreach ($src as $key => $value) {
$keyTmp = substr($key, 0, $length);
if ($keyTmp == $keyName) {
$newKey = substr($key, $length);
$new[$newKey] = $value;
}
}
return $new;
}
/** /**
* Iterates over an array and replaces all occurences of $search with $replace. Returns the new array. * Iterates over an array and replaces all occurences of $search with $replace. Returns the new array.
* *
...@@ -245,4 +272,24 @@ class OnArray { ...@@ -245,4 +272,24 @@ class OnArray {
return $new; return $new;
} }
/**
* Performs escapeshellarg() on all elements of an array.
*
* @param array $src
* @return array
*/
public static function arrayEscapeshellarg(array $src) {
$new = array();
foreach ($src as $key => $value) {
if (is_array($value)) {
$new[$key] = self::arrayEscapeshellarg($value);
} else {
$new[$key] = escapeshellarg($value);
}
}
return $new;
}
} }
\ No newline at end of file
<?php
/**
* Created by PhpStorm.
* User: crose
* Date: 4/17/17
* Time: 11:32 AM
*/
namespace qfq;
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
require_once(__DIR__ . '/../Constants.php');
require_once(__DIR__ . '/../store/Session.php');
require_once(__DIR__ . '/../store/Store.php');
require_once(__DIR__ . '/../helper/OnArray.php');
require_once(__DIR__ . '/../report/Html2Pdf.php');
//require_once(__DIR__ . '/Link.php');
//require_once(__DIR__ . '/Sendmail.php');
require_once(__DIR__ . '/../exceptions/DownloadException.php');
//require_once(__DIR__ . '/../Evaluate.php');
//require_once(__DIR__ . '/../helper/KeyValueStringParser.php');
//
/**
* Class Download
*
* Param: i=1..n
* <i>_mode=direct | html2pdf
* <i>_id=<pageId>
* <i>_<key i>=<value i>
*
* @package qfq
*/
class Download {
/**
* @var Store
*/
private $store = null;
/**
* @var Session
*/
private $session = null;
/**
* @var Database
*/
private $db = null;
/**
* @var Html2Pdf
*/
private $html2pdf = null;
/**
* @param bool|false $phpUnit
*/
public function __construct($phpUnit = false) {
$this->session = Session::getInstance($phpUnit);
$this->store = Store::getInstance('', $phpUnit);
$this->db = new Database();
$this->html2pdf = new Html2Pdf($this->store->getStore(STORE_SYSTEM));
}
/**
* Collect all elements, separated per element.
* Skip parameter which do not start with '<i>_' (i=integer).
* Create a numbered and sorted (by key) array, with all parameter belonging to one element in one subarray.
*
* E.g.: exportFilename=final.pdf&mode=pdf&1_id=exportP1&2_id=123&3_file=fileadmin/pdf/test.pdf&1_grId=78
* result [
* [1] => [ 'id' => 'exportP1', 'grId' = '78' ],
* [2] => [ 'id' => '123' ],
* [3] => [ 'file' => 'fileadmin/pdf/test.pdf' ],
* [mode] => 'pdf|file|excel',
* [exportFilename] => '....'
* ]
*
* @param array $vars
* @return array
*/
private function collectElement(array $vars) {
$final = array();
foreach ($vars as $key => $value) {
$splitArr = explode(DOWNLOAD_PARAMETER_DELIMITER, $key, 2);
$idx = $splitArr[0];
if ((count($splitArr) == 2) && is_numeric($idx) && !isset($final[$idx])) {
$arr = OnArray::getArrayItemKeyNameStartWith($vars, $idx . DOWNLOAD_PARAMETER_DELIMITER);
if (count($arr) > 0) {
$final[$idx] = $arr;
}
}
}
// sort array
ksort($final, SORT_NATURAL);
return $final;
}
/**
* Concatenate all named files to one PDF file. Return name of new full PDF.
*
* @param array $files
* @return string - fileName of concatenated file
* @throws DownloadException
*/
private function concatPdfFiles(array $files) {
$concatFile = tempnam(sys_get_temp_dir(), DOWNLOAD_FILE_PREFIX);
if (false === $concatFile) {
throw new DownloadException('Error creating output file.', ERROR_DOWNLOAD_CREATE_NEW_FILE);
}
// Check that all files are of type 'application/pdf'
foreach ($files AS $filename) {
$mimetype = mime_content_type($filename);
if ($mimetype != 'application/pdf') {
throw new downloadException("Error concat file $filename. Mimetype 'application/pdf' expected, got: $mimetype", ERROR_DOWNLOAD_UNEXPECTED_MIMETYPE);
}
}
$files = OnArray::arrayEscapeshellarg($files);
$inputFiles = implode(' ', $files);
if (trim($inputFiles) == '') {
throw new DownloadException('No files to concatenate.', ERROR_DOWNLOAD_NO_FILES);
}
$cmd = "pdftk $inputFiles cat output $concatFile";
exec($cmd, $output, $rc);
if ($rc != 0) {
throw new DownloadException ("<p>Failed: RC=$rc $cmd</p>" . implode("<br>", $output));
}
return $concatFile;
}
/**
* Set header type and output $filename. Be careful not to send any additional characters.
*
* @param $filename
* @param $outputFilename
*/
private function outputFile($filename, $outputFilename) {
$mimetype = mime_content_type($filename);
$length = filesize($filename);
header("Content-type: $mimetype");
header("Content-Length: $length");
header("Content-Disposition: inline; filename=$outputFilename");
print file_get_contents($filename);
}
/**
* Iterate over array $files. Delete all named files which exist in DOWNLOAD_TMP_DIR.
*
* @param array $files
*/
private function cleanTempFiles(array $files) {
$prefix = sys_get_temp_dir() . '/' . DOWNLOAD_FILE_PREFIX;
$len = strlen($prefix);
foreach ($files as $file) {
if (substr($file, 0, $len) == $prefix) {
unlink($file);
}
}
}
/**
* @param $element
* @return mixed
* @throws DownloadException
* @throws \exception
*/
private function getFile($element) {
// $first = each($element);
// $key = $first['key'];
// $arr = explode(DOWNLOAD_PARAMETER_DELIMITER, $key, 2);
// $idx = $arr[0];
if (isset($element[DOWNLOAD_FILE])) {
$filename = $element[DOWNLOAD_FILE];
} elseif (isset($element[DOWNLOAD_PAGE_ID])) {
$filename = $this->html2pdf->page2pdf($element);
} else {
throw new DownloadException('Neither found: ' . DOWNLOAD_FILE . ', ' . DOWNLOAD_PAGE_ID, ERROR_MISSING_REQUIRED_PARAMETER);
}
return $filename;
}
/**
* exportFilename=<new filename>
* mode=file | pdf | excel - default is 'file' in case of only one or 'pdf' in case of multiple sources.
* HTML to PDF | Excel
* <i>_id=<Typo3 pageId>
* <i>_<key>=<value i>
* Direct
* <i>_file=<filename>
*
* @param array $elements
* @throws DownloadException
*/
private function doElements(array $elements, array $vars) {
$tmpFiles = array();
$workDir = $this->store->getVar(SYSTEM_SITE_PATH, STORE_SYSTEM);
if (!chdir($workDir)) {
throw new DownloadException ("Error chdir($workDir)", ERROR_IO_CHDIR);
}