diff --git a/CHANGELOG.md b/CHANGELOG.md index ad39f552eb24b6338489e1694ad0d0c3d0ef070d..03a07f95fb202b3a3be5bec57dd9de48e9608b38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,10 +36,106 @@ Features Bug Fixes ^^^^^^^^^ + +Version 0.25.9 +-------------- + +Date: 17.12.2017 + +Notes +^^^^^ + +Features +^^^^^^^^ + +* #5133 / sendmail: subject and body html entity decode: Introduce options for 'subject' and 'body' to switch on/off HTML encoding / decoding +* Manual.rst: Add notes to QFQ installation, wkhtml problems, paragraph on 'sendEmail' Html2Pdf.php: Add error codes and a hint on wkhtml fails. +* Reformat table qfq-letter.css.less: redefined h1, letter-receiver + +Bug Fixes +^^^^^^^^^ + +* Bug in sendeEmail: invalid SSL_version specified at /usr/share/perl5/IO/Socket/SSL.pm line 575. Patch for sendEmail (see https://unix.stackexchange.com/a/68952). + + +Version 0.25.8 +-------------- + +Date: 11.12.2017 + +Features +^^^^^^^^ + +* #5080 / Dynamic PDF Letter +* #5083 / Bodytext / Report: join lines without spaces. + +Bug Fixes +^^^^^^^^^ + +* Fix problem with commit from 8.12.17 / Store.php: appendToStore.php stopped working - 'report' failed to replace '{{<column>:R}}' +* Store.php: fix problem with empty 'appendToStore()' call. + +Version 0.25.7 +-------------- + +Date: 07.12.2017 + +Notes +^^^^^ + +* Report: parameter in '... AS _sendmail' needs token now - position dependent is removed now. +* Report: parameter 'a:' in '... AS _sendmail' replaced by 'F:' to be compatible with downloads. Do not separate files by comma. +* Manual: most occurences of 'U:' replaced by 'p:' - same meaning. + +Features +^^^^^^^^ + +* #4255 / Attachments for emails implemented. + +Bug Fixes +^^^^^^^^^ + +* Bug - PHP Warning: Declaration of qfq\BuildFormTable::head() should be compatible with qfq\AbstractBuildForm::head($mode = qfq\FORM_LOAD) - fixed + + +Version 0.25.6 +-------------- + +Date: 03.12.2017 + +Notes +^^^^^ + +Bigger changes in update form after save/dynamic update. + +Bug Fixes +^^^^^^^^^ + +* #4865: Pill Dynamic Updates Show / Hide +* #5031 / Missing details in DbException: New definition of SYSTEM_SHOW_DEBUG_INFO: even after config.qfq.ini is parsed + and SIP Infos has been read - if there is no BE User logged in, the value stays on 'auto' (earlier it has been replaced + to 'no'). Staying on 'auto' keeps the information that replacing is still open and not replaced means 'no'-BE User logged in. +* #5016 Loose checkbox value on save - Dirty workaround - better solution necessary. +* #5017 - STORE_RECORD used in FormElement and via '#!report' - save & restore STORE_RECORD. +* #5004 FormElement with state 'ReadOnly' will be saved with empty value - existing values will be overwritten - fixed. +* 'element-update' for type 'UPLOAD seems to make trouble. Exclude it like 'SELECT' + + +Version 0.25.5 +-------------- + +Date: 23.11.17 + +Bug Fixes +^^^^^^^^^ + +* #4771: Workaround which switches off updates to SELECT lists, if they are part of a Multi-FE-Row. + + Version 0.25.4 -------------- -Date: 22.11.17 +Date: 22.11.2017 Notes ^^^^^ diff --git a/Gruntfile.js b/Gruntfile.js index 5d48678a53c54289fe4b4da69031f77d979c2400..316721e2adc76a32fb574c336dcd1bf5b9c89f6a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -492,6 +492,7 @@ module.exports = function (grunt) { production: { files: { "extension/Resources/Public/Css/qfq-bs.css": "less/qfq-bs.css.less", + "extension/Resources/Public/Css/qfq-letter.css": "less/qfq-letter.css.less", "extension/Resources/Public/Css/qfq-plain.css": "less/qfq-plain.css.less", "extension/Resources/Public/Css/bs-tablesorter.css": "less/bs-tablesorter.less" }, @@ -502,6 +503,7 @@ module.exports = function (grunt) { devel: { files: { "css/qfq-bs.css": "less/qfq-bs.css.less", + "css/qfq-letter.css": "less/qfq-letter.css.less", "css/qfq-plain.css": "less/qfq-plain.css.less", "css/bs-tablesorter.css": "less/bs-tablesorter.less" }, diff --git a/doc/NewVersion.md b/doc/NewVersion.md index b8ebaae2bdb1dac5cc2486e38b578389da6af951..8cc67f27ec9f4e273f70d6c60772248a6ec3b0bf 100644 --- a/doc/NewVersion.md +++ b/doc/NewVersion.md @@ -4,7 +4,6 @@ Neuer Build * release: Wird ein *Tag* vergeben (egal welcher Branch) der mit 'v' beginnt, erzeugt das automatisch einen Build - https://w3.math.uzh.ch/qfq/release. * snapshot: Jeder Commit (egal welcher Branch) erzeugt einen Snapshot - https://w3.math.uzh.ch/qfq/snapshot. * nightly: Nach einem Commit auf Branch 'master' tagsueber, wird um 23:55 ein 'nightly' Build erstellt - https://w3.math.uzh.ch/qfq/nightly. - * CR commmittet immer in den Master Branch. Neue Versionsnummer =================== @@ -40,7 +39,7 @@ Neue Versionsnummer make t3sphinx (dadurch fallen Fehler in der RESTdoc Syntax auf) -5) **Commit** +5) **Update Version & Commit** * Update the version number in this document (topic 6) * Commit & Push new version changes to master branch: @@ -48,8 +47,8 @@ Neue Versionsnummer 6) **New Tag**: - git tag v0.25.4 - git push -u origin v0.25.4 + git tag v0.25.9 + git push -u origin v0.25.9 7) PhpStorm: **Sync** all files to VM qfq. diff --git a/doc/PROTOCOL.md b/doc/PROTOCOL.md index 349170e4d22cdeae6f0e956b64adbc526d7a00cc..f6e796da3c7bd98cf9deb2504bf34dd5a3cd19e9 100644 --- a/doc/PROTOCOL.md +++ b/doc/PROTOCOL.md @@ -42,7 +42,7 @@ Stream { "status": "error", ... - "field-name": "<field name>", + "field-name": "<field name>", "field-message": "<message>", ... } diff --git a/extension/Classes/Controller/QfqController.php b/extension/Classes/Controller/QfqController.php index 82ac36f2436f9a32e5db0c3f9d931f309a682a0a..2a92be7ac2d33ac6943b212262d93c7426e109c1 100644 --- a/extension/Classes/Controller/QfqController.php +++ b/extension/Classes/Controller/QfqController.php @@ -32,12 +32,22 @@ class QfqController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController { } catch (qfq\UserFormException $e) { $html = $e->formatMessage(); + } catch (qfq\UserReportException $e) { $html = $e->formatMessage(); + } catch (qfq\CodeException $e) { $html = $e->formatMessage(); + } catch (qfq\DbException $e) { $html = $e->formatMessage(); + + } catch (qfq\ShellException $e) { + $html = $e->formatMessage(); + + } catch (qfq\DownloadException $e) { + $html = $e->formatMessage(); + } catch (\Exception $e) { $html = "Generic Exception: " . $e->getMessage(); } diff --git a/extension/Documentation/Manual.rst b/extension/Documentation/Manual.rst index c0427c179af1ab94eacbba9e79900045e0d57385..c4256ec67bda8e9355bc4df4b12f6bf22b51f93d 100644 --- a/extension/Documentation/Manual.rst +++ b/extension/Documentation/Manual.rst @@ -23,7 +23,7 @@ General ======= * Project homepage: https://qfq.io -* Latest relases: https://w3.math.uzh.ch/qfq/ +* Latest releases: https://w3.math.uzh.ch/qfq/ * Development: https://git.math.uzh.ch/typo3/qfq @@ -32,12 +32,14 @@ General Installation ============ -The following features are only tested on linux hosts: +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 `pdftk`. * Mime type detection for uploads - command `file`. + .. _`preparation`: Preparation @@ -108,6 +110,10 @@ Typosript) from specific IPs **or** if a FE-User is logged in. If there are problems with converting/downloading FE_GROUP protected pages, check `SHOW_DEBUG_INFO = download` to debug. +**Important**: Converting HTML to PDF gives no error message but RC=-1? Check carefully all includes of CSS, JS, images +and so on! Typically some of them fails to load and wkhtml stops running! + + HTML to PDF conversion '''''''''''''''''''''' @@ -133,6 +139,20 @@ Typoscript code to implement a print link on every page:: data = page:uid } +Send Email +^^^^^^^^^^ + +QFQ sends mail via `sendEmail` http://caspian.dotconf.net/menu/Software/SendEmail/ - a small perl script without a central +configuration. + +By default, `sendEmail` uses the local installed MTA, writes a logfile to `typo3conf/mail.log` and handles attachments +via commandline options. A basic HTML email support is implemented. + +The latest version is v1.56, which has at least one bug. That one is patched in the QFQ internal version v1.56p1 (see +QFQ GIT sources in directory 'patches/sendEmail.patch'). + +The Typo3 sendmail eco-system is not used at all. + Setup ----- @@ -382,7 +402,7 @@ Example: *typo3conf/config.qfq.ini* DB_1_NAME = qfq_db DB_INIT = set names utf8 ; DB_INDEX_DATA = 1 - ; DB_INDEX_QFQ = 1 + ; DB_INDEX_QFQ = 1 ; SQL_LOG = sql.log ; SQL_LOG_MODE = modify ; SHOW_DEBUG_INFO = auto @@ -450,8 +470,8 @@ Example: *typo3conf/config.qfq.ini* ;DOCUMENTATION_QFQ = https://docs.typo3.org/typo3cms/drafts/github/T3DocumentationStarter/Public-Info-053/Manual.html ;FILL_STORE_SYSTEM_BY_SQL_1 = 'SELECT s.id AS periodId FROM Period AS s WHERE s.start<=NOW() ORDER BY s.start DESC LIMIT 1' - ; Important: only define an error message, if QFQ should stop running in case of an error or not exact 1 record. - ;FILL_STORE_SYSTEM_BY_SQL_ERROR_MSG_1 = No current period found + ; Important: only define an error message, if QFQ should stop running in case of an error or not exact 1 record. + ;FILL_STORE_SYSTEM_BY_SQL_ERROR_MSG_1 = No current period found ;FORM_LANGUAGE_A_ID = 1 ;FORM_LANGUAGE_A_LABEL = english @@ -616,7 +636,8 @@ Typo3 QFQ content element Insert one or more QFQ content elements on a Typo3 page. Specify column and language per content record as wished. -The title of the QFQ content element will not be rendered. It's only visible in the backend for orientation. +The title of the QFQ content element will not be rendered on the frontend. It's only visible to the webmaster in the +backend for orientation. QFQ Keywords (Bodytext) ^^^^^^^^^^^^^^^^^^^^^^^ @@ -2150,15 +2171,33 @@ Type: fieldset * *name*: technical name, used as HTML identifier. * *label*: Shown title of the fieldset. -Type: pill -^^^^^^^^^^ +Type: pill (tab) +^^^^^^^^^^^^^^^^ + +* Pill is synonymous for a tab and looks like a tab. +* If there is at least one pill defined: + + * every native *FormElement* needs to be assigned to a pill or to a fieldset. + * every *fieldset* needs to be assigned to a pill. + +* Mode: + + * `show`: all child elements will be shown. + * `required`: same as 'show'. This mode has no other meaning than 'show'. + * `readonly`: technical it's like HTML/CSS `disabled`. + + * The pill title is shown, but not clickable. + * The `FormElements` on the pill still exist, but are not reachable for the user via UI. + + * `hidden`: + + * The pill is invisible. + * The `FormElements` on the pill still exist, but are not reachable for the user via UI. + + + * Note: Independent of the *mode*, all child elements are always rendered and processed by the client/server. -* Pill is synonymous for a tab. A pill looks like a tab. -* Pills are only available with mode render='bootstrap'. -* If there is at least one pill defined, every native *FormElement* needs to be assigned to a pill or to a fieldset. -* If there is at least one pill defined, every *fieldset* needs to be assigned to a pill. -* Pills are not 'dynamicUpdate' aware (at the moment). At least during form load, *modeSql* can be dynamically computed to - switch the pill in show / readonly (disabled) / hidden state. +* Pills are 'dynamicUpdate' aware. `title` and `mode` are optional recalculated during 'dynamicUpdate'. * FormElement settings: @@ -2272,8 +2311,8 @@ Fields: +---------------------+-----------------------------+-----------------------------------------------------------------------------------------------------+ |Mode | enum('show', 'readonly', | *Show*: regular user input field. This is the default. | | | 'required', | *Required*: User has to specify a value. Typically, an <empty string> represents 'no value'. | -| | 'disabled' ) | *Readonly*: user can't change any data. Data not saved. | -| | | *Disabled*: *FormElement* is not visible. | +| | 'hidden' ) | *Readonly*: user can't change any data. Data not saved. | +| | | *Hidden*: *FormElement* is not visible. | +---------------------+-----------------------------+-----------------------------------------------------------------------------------------------------+ |Mode sql | `SELECT` statement with | A value given here overwrites the setting from `mode`. Most useful with :ref:`dynamic-update`. | | | a value like in `mode` | E.g.: {{SELECT IF( '{{otherFunding:FR:alnumx}}'='yes' ,'show', 'hidden' }} | @@ -2325,7 +2364,7 @@ Fields: +---------------------+-----------------------------+-----------------------------------------------------------------------------------------------------+ |feGroup | string | Comma-separated list of Typo3 FE Group ID. NOT SURE IF THIS WILL BE IMPLEMENTED. Native | | | | *FormElements*, fieldsets and pills can be assigned to feGroups. Group status: show, hidden, | -| | | disabled. Group Access: FE-Groups. User will be assigned to FE-Groups and the form definition | +| | | hidden. Group Access: FE-Groups. User will be assigned to FE-Groups and the form definition | | | | reference such FE-groups. Easy way of granting permission. | +---------------------+-----------------------------+-----------------------------------------------------------------------------------------------------+ |Deleted | string | 'yes'|'no'. | @@ -2943,7 +2982,7 @@ will be rendered inside the form as a HTML table. * *show / required*: the regular mode to show the subrecords * *readonly*: New / Edit / Delete Buttons are disabled - * *hidden*: The FormElement is rendered, but disabled with `display='none'`. + * *hidden*: The FormElement is rendered, but hidden with `display='none'`. * *dynamicUpdate*: not supported at the moment. @@ -2957,7 +2996,7 @@ will be rendered inside the form as a HTML table. * Columnname: *[title=]<title>[|[maxLength=]<number>][|width=<number>][|nostrip][|icon][|link][|url][|mailto][|_rowClass][|_rowTooltip]* * If the keyword is used, all parameter are position independent. - * Parameter are seperated by '|'. + * Parameter are separated by '|'. * *[title=]<text>*: Title of the column. The keyword 'title=' is optional. Columns with a title starting with '_' won't be rendered. * *[maxLength=]<number>*: Max. number of characters displayed per cell. The keyword 'maxLength=' is optional. Default maxLength '20'. A value of '0' means no limit. This setting also affects the title of the column. @@ -3063,7 +3102,7 @@ and will be processed after saving the primary record and before any action Form * List of mime types (also known as 'media types'): http://www.iana.org/assignments/media-types/media-types.xhtml * If none is specified, 'application/pdf' is set. This forces that always (!) one type is specified. - * One or more media types might be specified, seperated by ','. + * One or more media types might be specified, separated by ','. * Different browser respect the given definitions in different ways. Typically the 'file choose' dialog offer: * the specified mime type (some browers only show 'custom', if more than one mime type is given), @@ -3117,8 +3156,7 @@ If the user deletes a record (e.g. pressing the delete button on a form) which c are deleted too. Slave records, which might be also deleted through a 'delete'-form, are *not* checked for file references and therefore such files are not deleted on the filesystem. -Only columns where the columname contains `pathFileName` are checked for file references. Therefore, always choose a -columnanme which contains `pathFileName`. +Only column(name)s which contains `pathFileName` as part of their name, are checked for file references. If there are other records, which references the same file, such files are not deleted. It's a very basic check: just the current column of the current table is compared. In general it's not a good idea to @@ -3186,7 +3224,6 @@ A typical name for such an 'upload'-FormElement, to show that the name does not sqlAfter={{UPDATE Person SET noteIdPicture = {{slaveId}} WHERE id={{id:R0}} LIMIT 1 }} - .. _class-action: Class: Action @@ -3370,7 +3407,7 @@ Type: sendmail * *sendMailFrom* - Sender of the email. Optional: 'realname <john@doe.com>'. **Mandatory**. * *sendMailSubject* - Subject of the email. * *sendMailReplyTo* - Reply this email address. Optional: 'realname <john@doe.com>'. - * *sendMailAttachment* - List of files to attach to the mail. Multiple files separated by comma. + * *sendMailAttachment* - List of 'sources' to attach to the mail as files. Check `attachment`_ for options. * *sendMailHeader* - Specify custom header. * *sendMailFlagAutoSubmit* - **on|off** - If 'on' (default), the mail contains the header 'Auto-Submitted: auto-send' - this suppress a) OoO replies, b) forwarding of emails. @@ -3378,12 +3415,22 @@ Type: sendmail * *sendMailXId* - Will be copied to the mailLog record. Helps to setup specific logfile queries. * *sendMailXId2* - Will be copied to the mailLog record. Helps to setup specific logfile queries. * *sendMailXId3* - Will be copied to the mailLog record. Helps to setup specific logfile queries. + * *sendMailSubjectHtmlEntity* - **encode|decode|none** - the mail subject will htmlspecialchar() encoded / decoded (default) or none (untouched). + * *sendMailBodyHtmlEntity* - **encode|decode|none** - the mail body will htmlspecialchar() encoded, decoded (default) or none (untouched). * To use values of the submitted form, use the STORE_FORM. E.g. `{{name:F:allbut}}` -* To use the `id` of a new created or already existing one, use the STORE_RECORD. E.g. `{{id:R}}` +* To use the `id` of a new created or already existing primary record, use the STORE_RECORD. E.g. `{{id:R}}`. +* By default, QFQ stores values 'htmlspecialchars()' encoded. If such values have to send by email, the html entities are + unwanted. Therefore the default setting for 'subject' und 'body' is to decode the values via 'htmlspecialchars_decode()'. + If this is not wished, it can be turned off by `sendMailSubjectHtmlEntity=none` and/or `sendMailBodyHtmlEntity=none`. * For debugging, please check `REDIRECT_ALL_MAIL_TO`_. +Example to attach one file1.pdf (with the attachment filename 'readme.pdf') and concatenate two PDF, created on the fly +from the www.example.com and ?export (with the attachment filename 'personal.pdf'): :: + + sendMailAttachmemt = F:fileadmin/file1.pdf|d:readme.pdf|C|u:http://www.example.com|p:?id=export&r=123&_sip=1|d:personal.pdf + Type: paste ''''''''''' @@ -4301,21 +4348,21 @@ Assume that the database has a table person with columns firstName and lastName. 10.sql = SELECT id AS pId, CONCAT(firstName, " ", lastName, " ") AS name FROM person -10 Stands for a *root level* of the report (see section `Structure`_). 10.sql defines a SQL query for this specific level. When the query is executed it will return a result having one single column name containing first and last name +The '10' indicates a *root level* of the report (see section `Structure`_). The expresssion '10.sql' defines a SQL query +for the specific level. When the query is executed, it will return a result having one single column name containing first and last name separated by a space character. -The HTML output displayed on the page resulting from only this definition could look as follows: +The HTML output, displayed on the page, resulting from only this definition, could look as follows: :: John DoeJane MillerFrank Star .. - - I.e., QFQ will simply output the content of the SQL result row after row for each single level. -However, we can modify (wrap) the output by setting the values of various keys for each level: 10.rsep=<br/> for example tells QFQ to seperate the rows of the result by a HTML-line break. The final result in this case is: +However, we can modify (wrap) the output by setting the values of various keys for each level: 10.rsep=<br/> for example +tells QFQ to separate the rows of the result by a HTML-line break. The final result in this case is: :: @@ -4422,7 +4469,7 @@ This would result in Text across several lines ^^^^^^^^^^^^^^^^^^^^^^^^^ -To make SQL queries, or QFQ records in general, more readable, it's possible to split a line across several lines. Lines +To get better human readable SQL queries, it's possible to split a line across several lines. Lines with keywords are on their own (`QFQ Keywords (Bodytext)`_ start a new line). If a line is not a 'keyword' line, it will be appended to the last keyword line. 'Keyword' lines are detected on: @@ -4433,7 +4480,7 @@ be appended to the last keyword line. 'Keyword' lines are detected on: Example:: 10.sql = SELECT 'hello world' - FROM mastertable + FROM mastertable 10.tail = End 20.sql = SELECT 'a warm welcome' @@ -4444,6 +4491,38 @@ Example:: 20.head = <h3> 20.tail = </h3> +Join mode: SQL +'''''''''''''' + +This is the default. All lines are joined with a space in between. E.g.: :: + + 10.sql = SELECT 'hello world' + FROM mastertable + +Results to: `10.sql = SELECT 'hello world' FROM mastertable` + +Notice the space between "...world'" and "FROM ...". + +Join mode: strip whitespace +''''''''''''''''''''''''''' + +Ending a line with a '\' forces the removing off all leading and trailing whitespaces in that line and do not insert an +extra space in between. E.g.: :: + + 10.sql = SELECT 'hello world', 'd:final.pdf \ + |p:id=export \ + |t:Download' AS _pdf \ + +Results to: `10.sql = SELECT 'hello world', 'd:final.pdf|p:id=export|t:Download' AS _pdf` + +Note: the '\' does not force the joining, it only removes the whitespaces. + +To get the same result, the following is also possible: :: + + 10.sql = SELECT 'hello world', CONCAT('d:final.pdf' + '|p:id=export', + '|t:Download') AS _pdf + Nesting of levels ^^^^^^^^^^^^^^^^^ @@ -4660,7 +4739,7 @@ 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 | |Download |d:[<exportFilename>] |d:complete.pdf |Link points to `api/download.php`. Additonal parameter are encoded into a SIP. 'Download' needs an enabled SIP. See `download`_. | +|x | |Download |d:[<exportFilename>] |d:complete.pdf |Link points to `api/download.php`. Additional parameter are encoded into a SIP. 'Download' needs an enabled SIP. See `download`_. | +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ | | |Text |t:<text> |t:Firstname Lastname |- | +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ @@ -4688,7 +4767,7 @@ Column: _link +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ | |x |Check |C:[<color>] |C:green |Show checked with '<color>'. Colors: blue, gray, green, pink, red, yellow. Default Color: green. | +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| | |URL Params |U:<key1>=<value1>[&<keyN>=<valueN>]|U:a=value1&b=value2&c=... |Any number of additional Params. Links to forms: U:form=Person&r=1234 | +| | |URL Params |U:<key1>=<value1>[&<keyN>=<valueN>]|U:a=value1&b=value2&c=... |Any number of additional Params. Links to forms: U:form=Person&r=1234. Used to create 'record delete'-links. | +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ | | |Tooltip |o:<text> |o:More information here |Tooltip text | +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ @@ -4708,7 +4787,7 @@ Column: _link +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ | | |Mode |M:file|pdf|zip |M:file, M:pdf, M:zip |Mode. Used to specify type of download. One or more element sources needs to be configured. See `download`_. | +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| | |File |f:<filename> |f:fileadmin/file.pdf |Element source for download mode file|pdf|zip. See `download`_. | +| | |File |F:<filename> |F:fileadmin/file.pdf |Element source for download mode file|pdf|zip. See `download`_. | +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ | | |Delete record | x[:a|r|c] |x, x:r, x:c |a: ajax (only QFQ internal used), r: report (default), c: close (current page, open last page) | +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ @@ -4777,7 +4856,7 @@ 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 "s:1|d:full.pdf|M:pdf|U:id=det1&r=12|U:id=det2|f:cv.pdf| | <a href="typo3conf/ext/qfq/qfq/api/download.php?s=badcaffee1234">Download</a> | +| SELECT "s:1|d:full.pdf|M:pdf|p:id=det1&r=12|p:id=det2|F:cv.pdf| | <a href="typo3conf/ext/qfq/qfq/api/download.php?s=badcaffee1234">Download</a> | | t:Download|a:Create complete PDF - please wait" as _link | | +-----------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ @@ -4796,7 +4875,7 @@ Question * If a user clicks on a link, an alert is shown. If the user answers the alert by clicking on the 'positive button', the browser opens the specified link. If the user click on the negative answer (or waits for timout), the alert is closed and the browser does nothing. * All parameter are optional. -* Parameter are seperated by ':' +* Parameter are separated by ':' * To use ':' inside the text, the colon has to be escaped by '\\'. E.g. 'ok\\: I understand'. +----------------------+--------------------------------------------------------------------------------------------------------------------------+ @@ -4886,7 +4965,7 @@ Parameter and (element) sources is allowed per download link (no concatenation). * In case of multiple element sources, only `pdf` or `zip` is supported. - * If `m:zip` is used together with `U:...` oder `u:..`, those HTML pages will be converted to PDF. Those files + * If `m:zip` is used together with `p:...`, `U:...` or `u:..`, those HTML pages will be converted to PDF. Those files get generic filenames inside the archive. * If not specified, the **default** 'Mode' depends on the number of specified element sources (=file or web page): @@ -4895,14 +4974,14 @@ Parameter and (element) sources * *element sources* - for `m:pdf` or `m:zip`, all of the following three element sources might be specified multiple times. Any combination and order of the three options are allowed. - * *file*: `f:<pathFilename>` - relative or absolute pathFilename offered for a) download (single), or to be concatenated + * *file*: `F:<pathFilename>` - relative or absolute pathFilename offered for a) download (single), or to be concatenated in a PDF or ZIP. - * *urlParam*: `U:id=<t3 page>&<key 1>=<value 1>&<key 2>=<value 2>&...&<key n>=<value n>`. + * *page*: `p:id=<t3 page>&<key 1>=<value 1>&<key 2>=<value 2>&...&<key n>=<value n>`. * By default, the options given to wkhtml will *not* be encoded by a SIP! * To encode the parameter via SIP: Add '_sip=1' to the URL GET parameter. - E.g. `U:id=form&_sip=1&form=Person&r=1`. + E.g. `p:id=form&_sip=1&form=Person&r=1`. In that way, specific sources for the `download` might be SIP encrypted. @@ -4913,60 +4992,153 @@ Parameter and (element) sources * *url*: `u:<url>` - any URL, pointing to an internal or external destination. - * *WKHTML Options* for `urlParam` or `url`: + * *WKHTML Options* for `page`, `urlParam` or `url`: * The 'HTML to PDF' will be done via `wkhtmltopdf`. - * All possible options, suitable for `wkhtmltopdf`, can be submitted in the `u:...` or `U:...` element source. + * All possible options, suitable for `wkhtmltopdf`, can be submitted in the `p:...`, `u:...` or `U:...` element source. Check `wkhtmltopdf.txt <https://wkhtmltopdf.org/usage/wkhtmltopdf.txt>`_ for possible options. Be aware that key/value tuple in the documentation is separated by a space, but to respect the QFQ key/value notation of URLs, - the key/value tuple in `u:...` or `U:...` has to be separated by '='. Please see last example below. + the key/value tuple in `p:...`, `u:...` or `U:...` has to be separated by '='. Please see last example below. Most of the other Link-Class attributes can be used to customize the link as well. Example `_link`: :: # single `file`. Specifying a popup message window text is not necessary, cause a file directly accessed is fast. - SELECT "d:file.pdf|s|t:Download|f:fileadmin/pdf/test.pdf" AS _link + SELECT "d:file.pdf|s|t:Download|F:fileadmin/pdf/test.pdf" AS _link # single `file`, with mode - SELECT "d:file.pdf|m:pdf|s|t:Download|f:fileadmin/pdf/test.pdf" AS _link + SELECT "d:file.pdf|m:pdf|s|t:Download|F:fileadmin/pdf/test.pdf" AS _link # three sources: two pages and one file - SELECT "d:complete.pdf|s|t:Complete PDF|U:id=detail&r=1|U:id=detail2&r=1|f:fileadmin/pdf/test.pdf" AS _link + SELECT "d:complete.pdf|s|t:Complete PDF|p:id=detail&r=1|p:id=detail2&r=1|F:fileadmin/pdf/test.pdf" AS _link # three sources: two pages and one file - SELECT "d:complete.pdf|s|t:Complete PDF|U:id=detail&r=1|U:id=detail2&r=1|f:fileadmin/pdf/test.pdf" AS _link + SELECT "d:complete.pdf|s|t:Complete PDF|p:id=detail&r=1|p:id=detail2&r=1|F:fileadmin/pdf/test.pdf" AS _link # three sources: two pages and one file, parameter to wkhtml will be SIP encoded - SELECT "d:complete.pdf|s|t:Complete PDF|U:id=detail&r=1&_sip=1|U:id=detail2&r=1&_sip=1|f:fileadmin/pdf/test.pdf" AS _link + SELECT "d:complete.pdf|s|t:Complete PDF|p:id=detail&r=1&_sip=1|p:id=detail2&r=1&_sip=1|F:fileadmin/pdf/test.pdf" AS _link # three sources: two pages and one file, the second page will be in landscape and pagesize A3 - SELECT "d:complete.pdf|s|t:Complete PDF|U:id=detail&r=1|U:id=detail2&r=1&--orientation=Landscape&--page-size=A3|f:fileadmin/pdf/test.pdf" AS _link + SELECT "d:complete.pdf|s|t:Complete PDF|p:id=detail&r=1|p:id=detail2&r=1&--orientation=Landscape&--page-size=A3|F:fileadmin/pdf/test.pdf" AS _link .. Example `_pdf`, `_zip`: :: - # File 1: U:id=1&--orientation=Landscape&--page-size=A3 - # File 2: U:id=form - # File 3: f:fileadmin/file.pdf - SELECT 't:PDF|a:Creating a new PDF|U:id=1&--orientation=Landscape&--page-size=A3|U:id=form|f:fileadmin/file.pdf' AS _pdf + # File 1: p:id=1&--orientation=Landscape&--page-size=A3 + # File 2: p:id=form + # File 3: F:fileadmin/file.pdf + SELECT 't:PDF|a:Creating a new PDF|p:id=1&--orientation=Landscape&--page-size=A3|p:id=form|F:fileadmin/file.pdf' AS _pdf - # File 1: U:id=1 + # File 1: p:id=1 # File 2: u:http://www.example.com - # File 3: f:fileadmin/file.pdf - SELECT 't:PDF - 3 Files|a:Please be patient|U:id=1|u:http://www.example.com|f:fileadmin/file.pdf' AS _pdf + # File 3: F:fileadmin/file.pdf + SELECT 't:PDF - 3 Files|a:Please be patient|p:id=1|u:http://www.example.com|F:fileadmin/file.pdf' AS _pdf - # File 1: U:id=1 - # File 2: U:id=form - # File 3: f:fileadmin/file.pdf - SELECT CONCAT('t:ZIP - 3 Pages|a:Please be patient|U:id=1|U:id=form|f:', p.pathFilename) AS _zip + # File 1: p:id=1 + # File 2: p:id=form + # File 3: F:fileadmin/file.pdf + SELECT CONCAT('t:ZIP - 3 Pages|a:Please be patient|p:id=1|p:id=form|F:', p.pathFilename) AS _zip .. -Use the `--print-media-type` as wkthml option to access the page with media type 'printer'. Depending on the website +Use the `--print-media-type` as wkhtml option to access the page with media type 'printer'. Depending on the website configuration this switches off navigation and background images. +Rendering 'official' look-alike PDF letters +''''''''''''''''''''''''''''''''''''''''''' + +`wkhtmltopdf`, with the header and footer options, can be used to render multi page PDF letters (repeating header, +pagination) in combination with dynamic content. Such PDFs might look-alike official letters, together with logo and signature. + +Best practice: + +#. Create a clean (=no menu, no website layout) letter layout in a separated T3 branch: :: + + page = PAGE + page.typeNum = 0 + page.includeCSS { + 10 = typo3conf/ext/qfq/Resources/Public/Css/qfq-letter.css + } + + // Grant access to any logged in user or specific development IPs + [usergroup = *] || [IP = 127.0.0.1,192.168.1.* ] + page.10 < styles.content.get + [else] + page.10 = TEXT + page.10.value = access forbidden + [global] + +#. Create a T3 `body` page (e.g. page alias: 'letterbody') with some content. Example static HTML content: :: + + <div class="letter-receiver"> + <p>Address</p> + </div> + <div class="letter-sender"> + <p><b>firstName name</b><br> + Phone +00 00 000 00 00<br> + Fax +00 00 000 00 00<br> + </p> + </div> + + <div class="letter-date"> + Zurich, 01.12.2017 + </div> + + <div class="letter-body"> + <h1>Subject</h1> + + <p>Dear Mrs...</p> + <p>Lucas ipsum dolor sit amet organa solo skywalker darth c-3p0 anakin jabba mara greedo skywalker.</p> + + <div class="letter-no-break"> + <p>Regards</p> + <p>Company</p> + <img class="letter-signature" src=""> + <p>Firstname Name<br>Function</p> + </div> + </div> + +#. Create a T3 letter-`header` page (e.g. page alias: 'letterheader') , with only the header information: :: + + <header> + <img src="fileadmin/logo.png" class="letter-logo"> + + <div class="letter-unit"> + <p class="letter-title">Department</p> + <p> + Company name<br> + Company department<br> + Street<br> + City + </p> + </div> + </header> + +#. Create a) a link (Report) to the PDF letter or b) attach the PDF (on the fly rendered) to a mail. Both will call the + `wkhtml` via the `download` mode and forwards the necessary parameter. + +Use in `report`: :: + + sql = SELECT CONCAT('d:Letter.pdf|t:',p.firstName, ' ', p.name, + '|p:id=letterbody&pId=', p.id, '&_sip=1&--margin-top=50mm&--margin-bottom=20mm&', + '--header-html={{BASE_URL_PRINT:Y}}?id=letterheader&', + '--footer-right="Seite: [page]/[toPage]"&', + '--footer-font-size=8&--footer-spacing=10') AS _pdf + FROM Person AS p ORDER BY p.id + + +Sendmail. Parameter: :: + + sendMailAttachment={{SELECT 'd:Letter.pdf|t:', p.firstName, ' ', p.name, '|p:id=letterbody&pId=', p.id, '&_sip=1&--margin-top=50mm&--margin-bottom=20mm&--header-html={{BASE_URL_PRINT:Y}}?id=letterheader&--footer-right="Seite: [page]/[toPage]"&--footer-font-size=8&--footer-spacing=10' FROM Person AS p WHERE p.id={{id:S}} }} + +Replace the static content elements from 2. and 3. by QFQ Content elements as needed: :: + + 10.sql = SELECT '<div class="letter-receiver"><p>', p.name AS '_+br', p.street AS '_+br', p.city AS '_+br', '</p>' + FROM Person AS p WHERE p.id={{pId:S}} + + Export area ''''''''''' @@ -4997,7 +5169,7 @@ The colum name is composed of the string *page* and a trailing character to spec SELECT "[options]" AS _page[<link type>] - with: [options] = [p:<page & param>]|[t:<text>]|[o:<tooltip>]|[q:<question parameter>]|[c:<class>]|[g:<target>]|[r:<render mode>] + with: [options] = [p:<page & param>][|t:<text>][|o:<tooltip>][|q:<question parameter>][|c:<class>][|g:<target>][|r:<render mode>] <link type> = c,d,e,h,i,n,s @@ -5229,58 +5401,79 @@ Easily create Email links. Column: _sendmail ^^^^^^^^^^^^^^^^^ -t:<TO:email[,email]>|f:<FROM:email>|s:<subject>|b:<body>|[F:<REPLY-TO:email>]|[a:<flag autosubmit: on /off>]|[g:<grid>]|[x:xId]|[c:<CC:email[,email]]>|[B:<BCC:email[,email]]|[y:xId2]|[z:xId3]> +:: + t:<TO:email[,email]>|F:<FROM:email>|s:<subject>|b:<body> + [|c:<CC:email[,email]]>[|B:<BCC:email[,email]]>[|F:<REPLY-TO:email>] + [|a:<flag autosubmit: on /off>][|g:<grid>][|x:<xId>][|y:<xId2>][|z:<xId3>] + [|C][d:<filename of the attachment>][|F:<file to attach>][|u:<url>][|p:<T3 uri>] -Send text emails. Every mail will be logged in the table `mailLog`. +Send text emails. Every mail will be logged in the table `mailLog`. Attachments are supported. **Syntax** :: - SELECT "t:john@doe.com|f:jane@doe.com|s:Reminder tomorrow|b:Please dont miss the meeting tomorrow" AS _sendmail - SELECT "t:john@doe.com|f:jane@doe.com|s:Reminder tomorrow|b:Please dont miss the meeting tomorrow|A:off|g:1|x:2|y:3|z:4" AS _sendmail + SELECT "t:john@doe.com|F:jane@doe.com|s:Reminder tomorrow|b:Please dont miss the meeting tomorrow" AS _sendmail + SELECT "t:john@doe.com|F:jane@doe.com|s:Reminder tomorrow|b:Please dont miss the meeting tomorrow|A:off|g:1|x:2|y:3|z:4" AS _sendmail .. -+---+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ -|***Token** | **Parameter** |**Description** |**Required**| -+===+========================================+==================================================================================================+============+ -| f | FROM:email |**FROM**: Sender of the email. Optional: 'realname <john@doe.com>' | yes | -+---+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ -| t | email[,email] |**TO**: Comma separated list of receiver email addresses. Optional: `realname <john@doe.com>` | yes | -+---+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ -| c | email[,email] |**CC**: Comma separated list of receiver email addresses. Optional: 'realname <john@doe.com>' | | -+---+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ -| B | email[,email] |**BCC**: Comma separated list of receiver email addresses. Optional: 'realname <john@doe.com>' | | -+---+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ -| r | REPLY-TO:email |**Reply-to**: Email address to reply to (if different from sender) | | -+---+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ -| s | Subject |**Subject**: Subject of the email | yes | -+---+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ -| b | Body |**Body**: Message | yes | -+---+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ -| h | Header |**Custom Header**: Separate multiple header with \r\n | | -+---+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ -| a | Attachment |**Attachment**: Comma separated list of filenames to attach to the mail | | -+---+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ -| A | flagAutoSubmit 'on' / 'off' |If 'on' (default), add mail header 'Auto-Submitted: auto-send' - suppress OoO replies | | -+---+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ -| g | grId |Will be copied to the mailLog record. Helps to setup specific logfile queries | | -+---+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ -| x | xId |Will be copied to the mailLog record. Helps to setup specific logfile queries | | -+---+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ -| y | xId2 |Will be copied to the mailLog record. Helps to setup specific logfile queries | | -+---+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ -| z | xId3 |Will be copied to the mailLog record. Helps to setup specific logfile queries | | -+---+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +|**Token** | **Parameter** |**Description** |**Required**| ++==========+========================================+==================================================================================================+============+ +| f | email |**FROM**: Sender of the email. Optional: 'realname <john@doe.com>' | yes | ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +| t | email[,email] |**TO**: Comma separated list of receiver email addresses. Optional: `realname <john@doe.com>` | yes | ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +| c | email[,email] |**CC**: Comma separated list of receiver email addresses. Optional: 'realname <john@doe.com>' | | ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +| B | email[,email] |**BCC**: Comma separated list of receiver email addresses. Optional: 'realname <john@doe.com>' | | ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +| r | REPLY-TO:email |**Reply-to**: Email address to reply to (if different from sender) | | ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +| s | Subject |**Subject**: Subject of the email | yes | ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +| b | Body |**Body**: Message | yes | ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +| h | Mail header |**Custom mail header**: Separate multiple header with \r\n | | ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +| F | Attach file |**Attachment**: File to attach to the mail. Repeatable. | | ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +| u | Attach created PDF of a given URL |**Attachment**: Convert the given URL to a PDF and attach it the mail. Repeatable. | | ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +| p | Attach created PDF of a given T3 URL |**Attachment**: Convert the given URL to a PDF and attach it the mail. Repeatable. | | ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +| d | Filename of the attachment |**Attachment**: Useful for URL to PDF converted attachments. Repeatable. | | ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +| C | Concat multiple F|p|u| together |**Attachment**: All following (until the next 'C') 'F|p|u' concatenated to one attachment. | | +| | | Repeatable. | | ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +| A | flagAutoSubmit 'on' / 'off' |If 'on' (default), add mail header 'Auto-Submitted: auto-send' - suppress OoO replies | | ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +| g | grId |Will be copied to the mailLog record. Helps to setup specific logfile queries | | ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +| x | xId |Will be copied to the mailLog record. Helps to setup specific logfile queries | | ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +| y | xId2 |Will be copied to the mailLog record. Helps to setup specific logfile queries | | ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +| z | xId3 |Will be copied to the mailLog record. Helps to setup specific logfile queries | | ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +| e | encode|decode|none |**Subject**: will be htmlspecialchar() encoded, decoded (default) or none (untouched) | | ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +| E | encode|decode|none |**Body**: will be htmlspecialchar() encoded, decoded (default) or none (untouched). | | ++----------+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ + +* **e|E**: By default, QFQ stores values 'htmlspecialchars()' encoded. If such values have to send by email, the html entities are + unwanted. Therefore the default setting for 'subject' und 'body' is to decode the values via 'htmlspecialchars_decode()'. + If this is not wished, it can be turned off by `e=none` and/or `E=none`. **Minimal Example** :: - 10.sql = SELECT "t:john.doe@example.com|f:company@example.com|s:Latest News|b:The new version is now available." AS _sendmail + 10.sql = SELECT "t:john.doe@example.com|F:company@example.com|s:Latest News|b:The new version is now available." AS _sendmail .. @@ -5291,7 +5484,7 @@ This will send an email with subject *Latest News* from company@example.com to j :: 10.sql = SELECT "t:customer1@example.com,Firstname Lastname <customer2@example.com>, Firstname Lastname <customer3@example.com>| - f:company@example.com|s:Latest News|b:The new version is now available.|r:sales@example.com|A:on|g:101|x:222|c:ceo@example.com|B:backup@example.com" AS _sendmail + F:company@example.com|s:Latest News|b:The new version is now available.|r:sales@example.com|A:on|g:101|x:222|c:ceo@example.com|B:backup@example.com" AS _sendmail .. @@ -5301,6 +5494,54 @@ Additional the CEO as well as backup will receive the mail via CC and BCC. For debugging, please check `REDIRECT_ALL_MAIL_TO`_. +.. _attachment: + +Attachment +'''''''''' + +The following options are provided to attach files to an email: + ++-------+------------------------------------------------------+--------------------------------------------------------+ +| Token | Example | Comment | ++=======+======================================================+========================================================+ +| F | F:fileadmin/file3.pdf | Single file to attach | ++-------+------------------------------------------------------+--------------------------------------------------------+ +| u | u:www.example.com/index.html?key=value&... | A URL, will be converted to a PDF and than attached. | ++-------+------------------------------------------------------+--------------------------------------------------------+ +| p | p:?id=export&r=123&_sip=1 | A SIP protected local T3 page. | +| | | Will be converted to a PDF and than attached. | ++-------+------------------------------------------------------+--------------------------------------------------------+ +| d | d:myfile.pdf | Name of the attachment in the email. | ++-------+------------------------------------------------------+--------------------------------------------------------+ +| C | C|u:http://www.example.com|F:file1.pdf|C|F:file2.pdf | Concatenate all named sources to one PDF file. The | +| | | souces has to be PDF files or a web page, which will be| +| | | converted to a PDF first. | ++-------+------------------------------------------------------+--------------------------------------------------------+ + +Any combination (incl. repeating them) are possible. Any source will be added as a single attachment. + +Optional any number of sources can be concatenated to a single PDF file: 'C|F:<file1>|F:<file2>|p:export&a=123'. + +Examples in Report:: + + # One file attached. + 10.sql = SELECT "t:john.doe@example.com|F:company@example.com|s:Latest News|b:The new version is now available.|F:fileadmin/summary.pdf" AS _sendmail + + # Two files attached. + 10.sql = SELECT "t:john.doe@example.com|F:company@example.com|s:Latest News|b:The new version is now available.|F:fileadmin/summary.pdf|F:fileadmin/detail.pdf" AS _sendmail + + # Two files and a webpage (converted to PDF) are attached. + 10.sql = SELECT "t:john.doe@example.com|F:company@example.com|s:Latest News|b:The new version is now available.|F:fileadmin/summary.pdf|F:fileadmin/detail.pdf|p:?id=export&r=123|d:person.pdf" AS _sendmail + + # Two webpages (converted to PDF) are attached. + 10.sql = SELECT "t:john.doe@example.com|F:company@example.com|s:Latest News|b:The new version is now available.|p:?id=export&r=123|d:person123.pdf|p:?id=export&r=234|d:person234.pdf" AS _sendmail + + # One file and two webpages (converted to PDF) are *concatenated* to one PDF and attached. + 10.sql = SELECT "t:john.doe@example.com|F:company@example.com|s:Latest News|b:The new version is now available.|C|F:fileadmin/summary.pdf|p:?id=export&r=123|p:?id=export&r=234|d:complete.pdf" AS _sendmail + + # One T3 webpage, protected by a SIP, are attached. + 10.sql = SELECT "t:john.doe@example.com|F:company@example.com|s:Latest News|b:The new version is now available.|p:?id=export&r=123&_sip=1|d:person123.pdf" AS _sendmail + .. _column_img: Column: _img @@ -5405,23 +5646,23 @@ Most of the other Link-Class attributes can be used to customize the link. SELECT "[options]" AS _pdf, "[options]" AS _file, "[options]" AS _zip - with: [options] = [d:<exportFilename]|[U:<params>]|[u:<url>]|[f:file]|[t:<text>]|[a:<message>]|[o:<tooltip>]|[c:<class>]|[r:<render mode>] + with: [options] = [d:<exportFilename][|p:<params>][|U:<params>][|u:<url>][|F:file][|t:<text>][|a:<message>][|o:<tooltip>][|c:<class>][|r:<render mode>] * Parameter are position independent. * *<params>*: see `download-parameter-files`_ -* For column `_pdf` and `_zip`, the element sources `U:...`, `u:...`, `f:...` might repeated multiple times. +* For column `_pdf` and `_zip`, the element sources `p:...`, `U:...`, `u:...`, `F:...` might repeated multiple times. * Example: :: - SELECT "f:fileadmin/test.pdf" as _pdf, "f:fileadmin/test.pdf" as _file, "f:fileadmin/test.pdf" as _zip - SELECT "U:id=export&r=1" as _pdf, "U:id=export&r=1" as _file, "U:id=export&r=1" as _zip + SELECT "F:fileadmin/test.pdf" as _pdf, "F:fileadmin/test.pdf" as _file, "F:fileadmin/test.pdf" as _zip + SELECT "p:id=export&r=1" as _pdf, "p:id=export&r=1" as _file, "p:id=export&r=1" as _zip - SELECT "t:Download PDF|f:fileadmin/test.pdf" as _pdf, "t:Download PDF|f:fileadmin/test.pdf" as _file, "t:Download ZIP|f:fileadmin/test.pdf" as _zip - SELECT "t:Download PDF|U:id=export&r=1" as _pdf, "t:Download PDF|U:id=export&r=1" as _file, "t:Download ZIP|U:id=export&r=1" as _zip + SELECT "t:Download PDF|F:fileadmin/test.pdf" as _pdf, "t:Download PDF|F:fileadmin/test.pdf" as _file, "t:Download ZIP|F:fileadmin/test.pdf" as _zip + SELECT "t:Download PDF|p:id=export&r=1" as _pdf, "t:Download PDF|p:id=export&r=1" as _file, "t:Download ZIP|p:id=export&r=1" as _zip - SELECT "d:complete.pdf|t:Download PDF|f:fileadmin/test1.pdf|f:fileadmin/test2.pdf" as _pdf, "d:complete.zip|t:Download ZIP|f:fileadmin/test1.pdf|f:fileadmin/test2.pdf" as _zip + SELECT "d:complete.pdf|t:Download PDF|F:fileadmin/test1.pdf|F:fileadmin/test2.pdf" as _pdf, "d:complete.zip|t:Download ZIP|F:fileadmin/test1.pdf|F:fileadmin/test2.pdf" as _zip - SELECT "d:complete.pdf|t:Download PDF|f:fileadmin/test.pdf|U:id=export&r=1|u:www.w3c.org" AS _pdf + SELECT "d:complete.pdf|t:Download PDF|F:fileadmin/test.pdf|p:id=export&r=1|u:www.example.com" AS _pdf .. _column_ppdf: diff --git a/extension/Documentation/Release.rst b/extension/Documentation/Release.rst index ad39f552eb24b6338489e1694ad0d0c3d0ef070d..03a07f95fb202b3a3be5bec57dd9de48e9608b38 100644 --- a/extension/Documentation/Release.rst +++ b/extension/Documentation/Release.rst @@ -36,10 +36,106 @@ Features Bug Fixes ^^^^^^^^^ + +Version 0.25.9 +-------------- + +Date: 17.12.2017 + +Notes +^^^^^ + +Features +^^^^^^^^ + +* #5133 / sendmail: subject and body html entity decode: Introduce options for 'subject' and 'body' to switch on/off HTML encoding / decoding +* Manual.rst: Add notes to QFQ installation, wkhtml problems, paragraph on 'sendEmail' Html2Pdf.php: Add error codes and a hint on wkhtml fails. +* Reformat table qfq-letter.css.less: redefined h1, letter-receiver + +Bug Fixes +^^^^^^^^^ + +* Bug in sendeEmail: invalid SSL_version specified at /usr/share/perl5/IO/Socket/SSL.pm line 575. Patch for sendEmail (see https://unix.stackexchange.com/a/68952). + + +Version 0.25.8 +-------------- + +Date: 11.12.2017 + +Features +^^^^^^^^ + +* #5080 / Dynamic PDF Letter +* #5083 / Bodytext / Report: join lines without spaces. + +Bug Fixes +^^^^^^^^^ + +* Fix problem with commit from 8.12.17 / Store.php: appendToStore.php stopped working - 'report' failed to replace '{{<column>:R}}' +* Store.php: fix problem with empty 'appendToStore()' call. + +Version 0.25.7 +-------------- + +Date: 07.12.2017 + +Notes +^^^^^ + +* Report: parameter in '... AS _sendmail' needs token now - position dependent is removed now. +* Report: parameter 'a:' in '... AS _sendmail' replaced by 'F:' to be compatible with downloads. Do not separate files by comma. +* Manual: most occurences of 'U:' replaced by 'p:' - same meaning. + +Features +^^^^^^^^ + +* #4255 / Attachments for emails implemented. + +Bug Fixes +^^^^^^^^^ + +* Bug - PHP Warning: Declaration of qfq\BuildFormTable::head() should be compatible with qfq\AbstractBuildForm::head($mode = qfq\FORM_LOAD) - fixed + + +Version 0.25.6 +-------------- + +Date: 03.12.2017 + +Notes +^^^^^ + +Bigger changes in update form after save/dynamic update. + +Bug Fixes +^^^^^^^^^ + +* #4865: Pill Dynamic Updates Show / Hide +* #5031 / Missing details in DbException: New definition of SYSTEM_SHOW_DEBUG_INFO: even after config.qfq.ini is parsed + and SIP Infos has been read - if there is no BE User logged in, the value stays on 'auto' (earlier it has been replaced + to 'no'). Staying on 'auto' keeps the information that replacing is still open and not replaced means 'no'-BE User logged in. +* #5016 Loose checkbox value on save - Dirty workaround - better solution necessary. +* #5017 - STORE_RECORD used in FormElement and via '#!report' - save & restore STORE_RECORD. +* #5004 FormElement with state 'ReadOnly' will be saved with empty value - existing values will be overwritten - fixed. +* 'element-update' for type 'UPLOAD seems to make trouble. Exclude it like 'SELECT' + + +Version 0.25.5 +-------------- + +Date: 23.11.17 + +Bug Fixes +^^^^^^^^^ + +* #4771: Workaround which switches off updates to SELECT lists, if they are part of a Multi-FE-Row. + + Version 0.25.4 -------------- -Date: 22.11.17 +Date: 22.11.2017 Notes ^^^^^ diff --git a/extension/Documentation/Settings.cfg b/extension/Documentation/Settings.cfg index d68e42d958b18dc3e12ffb07f635a4eef9663d50..894d5f4bc406fc0465da78e0a961037229547756 100644 --- a/extension/Documentation/Settings.cfg +++ b/extension/Documentation/Settings.cfg @@ -3,7 +3,7 @@ project = QFQ - Quick Form Query version = 0.25 -release = 0.25.4 +release = 0.25.9 t3author = Carsten Rose copyright = since 2017 by the author diff --git a/extension/Documentation/_make/conf.py b/extension/Documentation/_make/conf.py index 9c190b27927769178dc9900413d288e8e4d7233f..588ea0e2acaebf754386f2363404b7b095a86242 100644 --- a/extension/Documentation/_make/conf.py +++ b/extension/Documentation/_make/conf.py @@ -59,7 +59,7 @@ copyright = u'2017, Carsten Rose' # The short X.Y version. version = '0.25' # The full version, including alpha/beta/rc tags. -release = '0.25.4' +release = '0.25.9' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/extension/RELEASE.txt b/extension/RELEASE.txt index ad39f552eb24b6338489e1694ad0d0c3d0ef070d..03a07f95fb202b3a3be5bec57dd9de48e9608b38 100644 --- a/extension/RELEASE.txt +++ b/extension/RELEASE.txt @@ -36,10 +36,106 @@ Features Bug Fixes ^^^^^^^^^ + +Version 0.25.9 +-------------- + +Date: 17.12.2017 + +Notes +^^^^^ + +Features +^^^^^^^^ + +* #5133 / sendmail: subject and body html entity decode: Introduce options for 'subject' and 'body' to switch on/off HTML encoding / decoding +* Manual.rst: Add notes to QFQ installation, wkhtml problems, paragraph on 'sendEmail' Html2Pdf.php: Add error codes and a hint on wkhtml fails. +* Reformat table qfq-letter.css.less: redefined h1, letter-receiver + +Bug Fixes +^^^^^^^^^ + +* Bug in sendeEmail: invalid SSL_version specified at /usr/share/perl5/IO/Socket/SSL.pm line 575. Patch for sendEmail (see https://unix.stackexchange.com/a/68952). + + +Version 0.25.8 +-------------- + +Date: 11.12.2017 + +Features +^^^^^^^^ + +* #5080 / Dynamic PDF Letter +* #5083 / Bodytext / Report: join lines without spaces. + +Bug Fixes +^^^^^^^^^ + +* Fix problem with commit from 8.12.17 / Store.php: appendToStore.php stopped working - 'report' failed to replace '{{<column>:R}}' +* Store.php: fix problem with empty 'appendToStore()' call. + +Version 0.25.7 +-------------- + +Date: 07.12.2017 + +Notes +^^^^^ + +* Report: parameter in '... AS _sendmail' needs token now - position dependent is removed now. +* Report: parameter 'a:' in '... AS _sendmail' replaced by 'F:' to be compatible with downloads. Do not separate files by comma. +* Manual: most occurences of 'U:' replaced by 'p:' - same meaning. + +Features +^^^^^^^^ + +* #4255 / Attachments for emails implemented. + +Bug Fixes +^^^^^^^^^ + +* Bug - PHP Warning: Declaration of qfq\BuildFormTable::head() should be compatible with qfq\AbstractBuildForm::head($mode = qfq\FORM_LOAD) - fixed + + +Version 0.25.6 +-------------- + +Date: 03.12.2017 + +Notes +^^^^^ + +Bigger changes in update form after save/dynamic update. + +Bug Fixes +^^^^^^^^^ + +* #4865: Pill Dynamic Updates Show / Hide +* #5031 / Missing details in DbException: New definition of SYSTEM_SHOW_DEBUG_INFO: even after config.qfq.ini is parsed + and SIP Infos has been read - if there is no BE User logged in, the value stays on 'auto' (earlier it has been replaced + to 'no'). Staying on 'auto' keeps the information that replacing is still open and not replaced means 'no'-BE User logged in. +* #5016 Loose checkbox value on save - Dirty workaround - better solution necessary. +* #5017 - STORE_RECORD used in FormElement and via '#!report' - save & restore STORE_RECORD. +* #5004 FormElement with state 'ReadOnly' will be saved with empty value - existing values will be overwritten - fixed. +* 'element-update' for type 'UPLOAD seems to make trouble. Exclude it like 'SELECT' + + +Version 0.25.5 +-------------- + +Date: 23.11.17 + +Bug Fixes +^^^^^^^^^ + +* #4771: Workaround which switches off updates to SELECT lists, if they are part of a Multi-FE-Row. + + Version 0.25.4 -------------- -Date: 22.11.17 +Date: 22.11.2017 Notes ^^^^^ diff --git a/extension/ext_emconf.php b/extension/ext_emconf.php index 7995fad2ff5719b6eed95fdf28796b3f29627090..fc841dee0e364fdfb009ff614a4f1c913c42ba9b 100644 --- a/extension/ext_emconf.php +++ b/extension/ext_emconf.php @@ -10,6 +10,6 @@ $EM_CONF[$_EXTKEY] = array( 'dependencies' => 'fluid,extbase', 'clearcacheonload' => true, 'state' => 'alpha', - 'version' => '0.25.4' + 'version' => '0.25.9' ); diff --git a/extension/qfq/api/download.php b/extension/qfq/api/download.php index 07de5dd3ceb3b930ab1b5c2b058fd0cc5f987813..05c1873e7e63c23eea54554aca8130fa6eb51eab 100644 --- a/extension/qfq/api/download.php +++ b/extension/qfq/api/download.php @@ -28,7 +28,7 @@ try { $download = new \qfq\Download(); // If all is fine - 'process()' never returns! The output file is delivered and PHP is stopped after that. - $data = $download->process(); + $data = $download->process(STORE_SIP, OUTPUT_MODE_DIRECT); } catch (qfq\CodeException $e) { $data = $e->formatMessage(); diff --git a/extension/qfq/api/load.php b/extension/qfq/api/load.php index aa49f0760bddab08984352ee909a02ac0b4fdb42..0f6a73d7447b187114b8970c825d5a5c6b6aeb36 100644 --- a/extension/qfq/api/load.php +++ b/extension/qfq/api/load.php @@ -30,10 +30,10 @@ require_once(__DIR__ . '/../qfq/QuickFormQuery.php'); * * Description: * - * Save successfull. Button 'close', 'new'. Form.forward: 'auto'. Client logic decide to redirect or not. Show message + * Save successful. Button 'close', 'new'. Form.forward: 'auto'. Client logic decide to redirect or not. Show message * if no redirect. status = 'success' message = <message> redirect = 'client' * - * Save successfull. Button 'close': Form.forward: 'page'. Client redirect to url. + * Save successful. Button 'close': Form.forward: 'page'. Client redirect to url. * status = 'success' * message = <message> * redirect = 'url' diff --git a/extension/qfq/api/save.php b/extension/qfq/api/save.php index d891d85bb7fe8790e25217b9fa126458e8bb9e10..9e9dc739e7c1faa072222d0a9830021b5821b844 100644 --- a/extension/qfq/api/save.php +++ b/extension/qfq/api/save.php @@ -33,10 +33,10 @@ require_once(__DIR__ . '/../qfq/exceptions/DbException.php'); * * Description: * - * Save successfull. Button 'close', 'new'. Form.forward: 'auto'. Client logic decide to redirect or not. Show message + * Save successful. Button 'close', 'new'. Form.forward: 'auto'. Client logic decide to redirect or not. Show message * if no redirect. status = 'success' message = <message> redirect = 'client' * - * Save successfull. Button 'close': Form.forward: 'page'. Client redirect to url. + * Save successful. Button 'close': Form.forward: 'page'. Client redirect to url. * status = 'success' * message = <message> * redirect = 'url' diff --git a/extension/qfq/external/AutoCron.php b/extension/qfq/external/AutoCron.php index 730f9b361181b59796f8b1d6cd9aae30cdeaec8a..ba45453b88cde47a869751ffc3e9640a3483bb86 100644 --- a/extension/qfq/external/AutoCron.php +++ b/extension/qfq/external/AutoCron.php @@ -15,7 +15,7 @@ require_once(__DIR__ . '/../qfq/Constants.php'); require_once(__DIR__ . '/../qfq/database/Database.php'); require_once(__DIR__ . '/../qfq/exceptions/ShellException.php'); require_once(__DIR__ . '/../qfq/Evaluate.php'); -require_once(__DIR__ . '/../qfq/report/Sendmail.php'); +require_once(__DIR__ . '/../qfq/report/SendMail.php'); class AutoCron { @@ -164,7 +164,9 @@ class AutoCron { $mailArr = $this->evaluate->parse($job[AUTOCRON_SQL1]); - $mailsSent = 0; + $sendMail = new SendMail(); + + $mailCount = 0; foreach ($mailArr as $mailEntry) { $this->store->setStore($mailEntry, STORE_PARENT_RECORD, true); @@ -175,27 +177,27 @@ class AutoCron { if ($content == '' OR ($mailEntry[FE_SENDMAIL_TO] == '' AND $mailEntry[FE_SENDMAIL_CC] == '' AND $mailEntry[FE_SENDMAIL_BCC] == '')) { continue; // no receiver: skip } - $mail[SENDMAIL_IDX_RECEIVER] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_TO]); - $mail[SENDMAIL_IDX_SENDER] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_FROM]); - $mail[SENDMAIL_IDX_SUBJECT] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_SUBJECT]); - $mail[SENDMAIL_IDX_BODY] = $content; - $mail[SENDMAIL_IDX_REPLY_TO] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_REPLY_TO]); - $mail[SENDMAIL_IDX_FLAG_AUTO_SUBMIT] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_FLAG_AUTO_SUBMIT]) === 'off' ? 'off' : 'on'; - $mail[SENDMAIL_IDX_GR_ID] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_GR_ID]); - $mail[SENDMAIL_IDX_X_ID] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_X_ID]); - $mail[SENDMAIL_IDX_RECEIVER_CC] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_CC]); - $mail[SENDMAIL_IDX_RECEIVER_BCC] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_BCC]); - $mail[SENDMAIL_IDX_X_ID2] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_X_ID2]); - $mail[SENDMAIL_IDX_X_ID3] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_X_ID3]); - - $mail[SENDMAIL_IDX_SRC] = "AutoCron: Cron.id=" . $job[COLUMN_ID]; + $mail[SENDMAIL_TOKEN_RECEIVER] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_TO]); + $mail[SENDMAIL_TOKEN_SENDER] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_FROM]); + $mail[SENDMAIL_TOKEN_SUBJECT] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_SUBJECT]); + $mail[SENDMAIL_TOKEN_BODY] = $content; + $mail[SENDMAIL_TOKEN_REPLY_TO] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_REPLY_TO]); + $mail[SENDMAIL_TOKEN_FLAG_AUTO_SUBMIT] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_FLAG_AUTO_SUBMIT]) === 'off' ? 'off' : 'on'; + $mail[SENDMAIL_TOKEN_GR_ID] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_GR_ID]); + $mail[SENDMAIL_TOKEN_X_ID] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_X_ID]); + $mail[SENDMAIL_TOKEN_RECEIVER_CC] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_CC]); + $mail[SENDMAIL_TOKEN_RECEIVER_BCC] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_BCC]); + $mail[SENDMAIL_TOKEN_X_ID2] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_X_ID2]); + $mail[SENDMAIL_TOKEN_X_ID3] = $this->evaluate->parse($mailEntry[FE_SENDMAIL_X_ID3]); + + $mail[SENDMAIL_TOKEN_SRC] = "AutoCron: Cron.id=" . $job[COLUMN_ID]; // Mail: send - new Sendmail($mail); - $mailsSent++; + $sendMail->process($mail); + $mailCount++; } - $job[AUTOCRON_LAST_STATUS] = "OK: $mailsSent mails sent"; + $job[AUTOCRON_LAST_STATUS] = "OK: $mailCount mails sent"; return $job; } diff --git a/extension/qfq/external/sendEmail b/extension/qfq/external/sendEmail index 9f9392e6e4bd9d2ef27825b808f9217e7bcfb9b5..5666b646529e5cfb08cdc027f8cf8c8c99ce7583 100755 --- a/extension/qfq/external/sendEmail +++ b/extension/qfq/external/sendEmail @@ -46,7 +46,7 @@ use IO::Socket; my %conf = ( ## General "programName" => $0, ## The name of this program - "version" => '1.56', ## The version of this program + "version" => '1.56p1', ## The version of this program "authorName" => 'Brandon Zehm', ## Author's Name "authorEmail" => 'caspian@dotconf.net', ## Author's Email Address "timezone" => '+0000', ## We always use +0000 for the time zone @@ -1903,7 +1903,7 @@ else { if ($conf{'tls_server'} == 1 and $conf{'tls_client'} == 1 and $opt{'tls'} =~ /^(yes|auto)$/) { printmsg("DEBUG => Starting TLS", 2); if (SMTPchat('STARTTLS')) { quit($conf{'error'}, 1); } - if (! IO::Socket::SSL->start_SSL($SERVER, SSL_version => 'SSLv3 TLSv1')) { + if (! IO::Socket::SSL->start_SSL($SERVER)) { quit("ERROR => TLS setup failed: " . IO::Socket::SSL::errstr(), 1); } printmsg("DEBUG => TLS: Using cipher: ". $SERVER->get_cipher(), 3); diff --git a/extension/qfq/qfq/AbstractBuildForm.php b/extension/qfq/qfq/AbstractBuildForm.php index 1463d0aa9d3126a186f38936c6279c8b3c35d89b..2c4bf197c2bc892dd1d61d146a39ea0b7ec36c5e 100644 --- a/extension/qfq/qfq/AbstractBuildForm.php +++ b/extension/qfq/qfq/AbstractBuildForm.php @@ -71,7 +71,7 @@ abstract class AbstractBuildForm { private $bodytextParser = null; /** - * @var Array of Database instantiated class + * @var Database[] Array of Database instantiated class */ protected $dbArray = array(); @@ -245,7 +245,7 @@ abstract class AbstractBuildForm { * * @return string */ - public function head() { + public function head($mode = FORM_LOAD) { $html = ''; $html .= '<div ' . Support::doAttribute('class', $this->formSpec[F_CLASS], true) . '>'; // main <div class=...> around everything @@ -485,7 +485,9 @@ abstract class AbstractBuildForm { $this->bodytextParser = new BodytextParser(); } - $value = $this->report->process($this->bodytextParser->process($value)); + $storeRecord = $this->store->getStore(STORE_RECORD); + $value = $this->report->process($this->bodytextParser->process($value), false); + $this->store->setStore($storeRecord, STORE_RECORD, true); } @@ -562,8 +564,8 @@ abstract class AbstractBuildForm { $fe[FE_NOTE] = $this->processReportSyntax($fe[FE_NOTE]); if (isset($fe[FE_FILL_STORE_VAR])) { - $fe[FE_FILL_STORE_VAR]=$this->evaluate->parse($fe[FE_FILL_STORE_VAR]); - $this->store->appendToStore(STORE_VAR, $fe[FE_FILL_STORE_VAR]); + $fe[FE_FILL_STORE_VAR] = $this->evaluate->parse($fe[FE_FILL_STORE_VAR]); + $this->store->appendToStore($fe[FE_FILL_STORE_VAR], STORE_VAR); } @@ -607,7 +609,8 @@ abstract class AbstractBuildForm { } if ($formElement[FE_ENCODE] === FE_ENCODE_SPECIALCHAR) { - $value = htmlspecialchars_decode($value, ENT_QUOTES); +// $value = htmlspecialchars_decode($value, ENT_QUOTES); + $value = Support::htmlEntityEncodeDecode(MODE_DECODE, $value); } // Typically: $htmlElementNameIdZero = true @@ -632,7 +635,7 @@ abstract class AbstractBuildForm { } } else { // for non-container elements: just add the current json status - if ($modeCollectFe === FLAG_ALL || ($modeCollectFe == FLAG_DYNAMIC_UPDATE && $fe[FE_DYNAMIC_UPDATE] == 'yes')) { + if ($modeCollectFe === FLAG_ALL || ($modeCollectFe == FLAG_DYNAMIC_UPDATE && $fe[FE_DYNAMIC_UPDATE] === 'yes')) { if (isset($jsonElement[0]) && is_array($jsonElement[0])) { // Checkboxes are delivered as array of arrays: unnest them and append them to the existing json array. $json = array_merge($json, $jsonElement); @@ -901,7 +904,11 @@ abstract class AbstractBuildForm { } // #3647 - if (!$flagRowUpdate) { +// if (!$flagRowUpdate) { + + // #4771 - temporary workaround: SELECT in 'multi FE row' won't updated after 'save' or with dynamic update. + //TODO #5016 - exception for FE_TYPE_CHECKBOX should be removed ASAP + if ($formElement[FE_TYPE] != FE_TYPE_SELECT && $formElement[FE_TYPE] != FE_TYPE_UPLOAD && $formElement[FE_TYPE] != FE_TYPE_CHECKBOX) { $json[API_ELEMENT_UPDATE][$formElement[FE_HTML_ID]][API_ELEMENT_ATTRIBUTE]['value'] = $value; } } @@ -1793,7 +1800,8 @@ abstract class AbstractBuildForm { if ($formElement[FE_CHECKBOX_CHECKED] === $value) { $attribute .= Support::doAttribute('checked', 'checked'); - $valueJson = true; +// $valueJson = true; + $valueJson = $value; } $attribute .= $this->getAttributeList($formElement, ['autofocus']); diff --git a/extension/qfq/qfq/BodytextParser.php b/extension/qfq/qfq/BodytextParser.php index fb9f5274bd629c6ed20b11360ec7221330b1d410..0cc5dbd293a1d64f3ca1ed9637d8a7e5a767cb42 100644 --- a/extension/qfq/qfq/BodytextParser.php +++ b/extension/qfq/qfq/BodytextParser.php @@ -128,7 +128,7 @@ class BodytextParser { } /** - * Join lines. Preservers Nesting. + * Join lines. Nesting isn't changed. * * Iterates over all lines. * Is a line a 'new line'? @@ -150,13 +150,14 @@ class BodytextParser { * c,d,e,f: ^\d+(\.\d+)*(\s*{)?$ * g,h: ^(\d+\.)*(sql|head)\s*= * - * @param array $bodytextArray - * + * @param $bodyText + * @param $nestingOpen + * @param $nestingClose * @return string */ - private function joinLine($bodytext, $nestingOpen, $nestingClose) { + private function joinLine($bodyText, $nestingOpen, $nestingClose) { $data = array(); - $bodytextArray = explode(PHP_EOL, $bodytext); + $bodytextArray = explode(PHP_EOL, $bodyText); $nestingOpenRegexp = $nestingOpen; if ($nestingOpen === '(' || $nestingOpen === '[') { @@ -164,35 +165,42 @@ class BodytextParser { } $full = ''; + $joinDelimiter = ' '; foreach ($bodytextArray as $row) { - -// if ((1 === preg_match('/^\s*(\d*(\.)?)*\s*(' . TOKEN_VALID_LIST . ') *=/', $row)) -// || (1 === preg_match('/^\s*(\d*(\.)?)*\s*(' . $nestingOpen . '|' . $nestingClose . ')/', $row)) -// || (1 === preg_match('/^\s*(\d+(\.)?)+/', $row)) + // Line end with '\'? + if (substr($row, -1) == '\\') { + $row = trim(substr($row, 0, -1)); // remove last char and trim + $joinDelimiterNext = ''; + } else { + $joinDelimiterNext = ' '; + } if (($row == $nestingOpen || $row == $nestingClose) || (1 === preg_match('/^\d+(\.\d+)*(\s*' . $nestingOpenRegexp . ')?$/', $row)) || (1 === preg_match('/^(\d+\.)*(' . TOKEN_VALID_LIST . ')\s*=/', $row)) ) { - // if there is already something: save this - if ($full !== '') + // if there is already something: save this. + if ($full !== '') { $data[] = $full; + } // start new line $full = $row; } else { - - // continue row: concat - $full .= ' ' . $row; + // continue row: concat - the space is necessary to join SQL statements correctly: 'SELECT ... FROM ... WHERE ... AND\np.id=...' - here a 'AND' and 'p.id' need a space. + $full .= $joinDelimiter . $row; } + + $joinDelimiter = $joinDelimiterNext; } // Save last line - if ($full !== '') + if ($full !== '') { $data[] = $full; + } return implode(PHP_EOL, $data); } diff --git a/extension/qfq/qfq/BuildFormBootstrap.php b/extension/qfq/qfq/BuildFormBootstrap.php index 8e7081bd97548b15247f1d643134dd7a91975c03..9a11f9917879f7e86ed064e1914c0c1688e59de0 100644 --- a/extension/qfq/qfq/BuildFormBootstrap.php +++ b/extension/qfq/qfq/BuildFormBootstrap.php @@ -107,18 +107,18 @@ class BuildFormBootstrap extends AbstractBuildForm { /** * @return string */ - public function head() { + public function head($mode = FORM_LOAD) { $html = ''; $html .= '<div ' . Support::doAttribute('class', $this->formSpec[F_CLASS], true) . '>'; // main <div class=...> around everything, Whole FORM; class="container" or class="container-fluid" //TODO: nicer error reporting - make test with 'unknown index' here - unset($this->formSpec['title']) - See #3424 - $title = Support::wrapTag('<div class="hidden-xs col-sm-6 col-md-8">', Support::wrapTag('<h3>', $this->formSpec['title'])); + $title = Support::wrapTag('<div class="hidden-xs col-sm-6 col-md-8">', Support::wrapTag('<h3>', $this->formSpec[F_TITLE])); $button = Support::wrapTag('<div class="col-xs-12 col-sm-6 col-md-4">', $this->buildButtons()); $html .= Support::wrapTag('<div class="row">', $title . $button); - - $pill = $this->buildPillNavigation(OnArray::filter($this->feSpecNative, 'type', 'pill')); + $dummy = array(); + $pill = $this->buildPillNavigation($mode, OnArray::filter($this->feSpecNative, FE_TYPE, FE_TYPE_PILL), $dummy); $html .= Support::wrapTag('<div class="row">', $pill); $html .= $this->getFormTag(); @@ -356,18 +356,24 @@ class BuildFormBootstrap extends AbstractBuildForm { } /** - * @param $pillArray + * Builds the BS-pills on top of a form. + * + * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE + * @param array $pillArray * + * @param array $json * @return string + * @throws CodeException * @throws UserFormException */ - private function buildPillNavigation($pillArray) { + private function buildPillNavigation($mode = FORM_LOAD, array $pillArray, array &$json) { $pillButton = ''; $pillDropdown = ''; $htmlDropdown = ''; - if ($pillArray == null) + if ($pillArray == null) { return ''; + } $maxVisiblePill = (isset($this->formSpec['maxVisiblePill']) && $this->formSpec['maxVisiblePill'] !== '') ? $this->formSpec['maxVisiblePill'] : 1000; @@ -376,8 +382,16 @@ class BuildFormBootstrap extends AbstractBuildForm { // Iterate over all 'pill' $ii = 0; $active = 'class="active"'; + $recordId = $this->store->getVar(COLUMN_ID, STORE_RECORD . STORE_ZERO); foreach ($pillArray as $formElement) { + if ($mode != FORM_LOAD && $formElement[FE_DYNAMIC_UPDATE] !== 'yes') { + continue; // During save/update: Process only FE dynamic_update=yes + } + + $htmlIdLi = $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_PILL_LI; + $htmlIdLiA = $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_PILL_LI_A; + $formElement = $this->evaluate->parseArray($formElement); HelperFormElement::explodeParameter($formElement, F_PARAMETER); $formElement = HelperFormElement::setLanguage($formElement, $parameterLanguageFieldName); @@ -396,33 +410,50 @@ class BuildFormBootstrap extends AbstractBuildForm { // Anker for pill navigation // $a = '<a ' . Support::doAttribute('href', '#' . $this->createAnker($formElement[FE_ID])) . ' data-toggle="tab">' . $formElement[FE_LABEL] . '</a>'; - $attributeA = 'data-toggle="tab" '; + $attributeLiA = 'data-toggle="tab" '; $hrefTarget = '#' . $this->createAnker($formElement[FE_ID]); + $htmlFormElementName = HelperFormElement::buildFormElementName($formElement, $recordId); switch ($formElement[FE_MODE]) { case FE_MODE_SHOW: case FE_MODE_REQUIRED: $attributeLi = ''; + $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLi][API_ELEMENT_ATTRIBUTE][HTML_ATTR_CLASS] = ''; + $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLiA][API_ELEMENT_ATTRIBUTE][HTML_ATTR_CLASS] = ''; break; + case FE_MODE_READONLY: - $attributeLi = Support::doAttribute('class', 'disabled'); $hrefTarget = '#'; - $attributeA .= Support::doAttribute('class', 'noclick'); + + $attributeLi = Support::doAttribute('class', 'disabled'); + $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLi][API_ELEMENT_ATTRIBUTE][HTML_ATTR_CLASS] = 'disabled'; + + $attributeLiA .= Support::doAttribute('class', 'noclick'); + $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLiA][API_ELEMENT_ATTRIBUTE][HTML_ATTR_CLASS] = 'noclick'; break; + case FE_MODE_HIDDEN: - $attributeLi = Support::doAttribute('style', 'display: none'); +// $attributeLi = Support::doAttribute('style', 'display: none'); + $attributeLi = Support::doAttribute('class', 'hidden'); + $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLi][API_ELEMENT_ATTRIBUTE][HTML_ATTR_CLASS] = 'hidden'; $a = ''; break; + default: throw new UserFormException("Unknown Mode: " . $formElement[FE_MODE], ERROR_UNKNOWN_MODE); } - $a = Support::wrapTag("<a $attributeA" . Support::doAttribute('href', $hrefTarget) . ">", $formElement[FE_LABEL]); + + $attributeLi .= Support::doAttribute(HTML_ATTR_ID, $htmlIdLi); + $attributeLiA .= Support::doAttribute(HTML_ATTR_ID, $htmlIdLiA); + $a = Support::wrapTag("<a $attributeLiA" . Support::doAttribute('href', $hrefTarget) . ">", $formElement[FE_LABEL]); + $json[$htmlFormElementName][API_ELEMENT_UPDATE][$htmlIdLiA][API_ELEMENT_CONTENT] = $formElement[FE_LABEL]; if ($ii <= $maxVisiblePill) { $pillButton .= '<li role="presentation"' . $attributeLi . $active . ">" . $a . "</li>"; } else { $pillDropdown .= '<li ' . $attributeLi . '>' . $a . "</li>"; } + $active = ''; } @@ -735,4 +766,35 @@ EOF; return Support::wrapTag("<span name='qfq-subrecord' $attribute>", $html); } + + + /** + * Builds complete 'form'. + * + * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE + * + * @return string|array $mode=LOAD_FORM: The whole form as HTML, $mode=FORM_UPDATE /FORM_SAVE: array of all + * formElement.dynamicUpdate-yes values/states + * @throws CodeException + * @throws DbException + * @throws \qfq\UserFormException + */ + public function process($mode, $htmlElementNameIdZero = false, $latestFeSpecNative = array()) { + + $json = array(); + $data = parent::process($mode, $htmlElementNameIdZero, $latestFeSpecNative); + + switch ($mode) { + case FORM_SAVE: + case FORM_UPDATE: + $pillArray = OnArray::filter($this->feSpecNative, FE_TYPE, FE_TYPE_PILL); + $this->buildPillNavigation($mode, $pillArray, $json); + $data = array_merge($data, $json); + break; + default: + break; + } + + return $data; + } } \ No newline at end of file diff --git a/extension/qfq/qfq/BuildFormTable.php b/extension/qfq/qfq/BuildFormTable.php index 7f30c6965d60929dfb352f7aeef712ac33041fda..6e27b1877ed1bc57c39ba495799593649877b81a 100644 --- a/extension/qfq/qfq/BuildFormTable.php +++ b/extension/qfq/qfq/BuildFormTable.php @@ -72,7 +72,7 @@ class BuildFormTable extends AbstractBuildForm { /** * @return string */ - public function head() { + public function head($mode = FORM_LOAD) { $html = ''; $html .= '<div ' . Support::doAttribute('class', $this->formSpec[F_CLASS], true) . '>'; // main <div class=...> around everything diff --git a/extension/qfq/qfq/Constants.php b/extension/qfq/qfq/Constants.php index 44075b1906ec9b7f738e48ddcfff1fdf60739ee7..55435d73ea282e583718291b9b8f09b09c9c23d7 100644 --- a/extension/qfq/qfq/Constants.php +++ b/extension/qfq/qfq/Constants.php @@ -52,12 +52,14 @@ const RETURN_URL = 'return_url'; const RETURN_SIP = 'return_sip'; const RETURN_ARRAY = 'return_array'; +const SQL_FORM_ELEMENT_RAW = "SELECT * FROM FormElement AS fe WHERE fe.formId = ? AND fe.deleted = 'no' AND fe.enabled='yes' ORDER BY fe.ord, fe.id"; const SQL_FORM_ELEMENT_SPECIFIC_CONTAINER = "SELECT *, ? AS 'nestedInFieldSet' FROM FormElement AS fe WHERE fe.formId = ? AND fe.deleted = 'no' AND FIND_IN_SET(fe.class, ? ) AND fe.feIdContainer = ? AND fe.enabled='yes' ORDER BY fe.ord, fe.id"; const SQL_FORM_ELEMENT_ALL_CONTAINER = "SELECT *, ? AS 'nestedInFieldSet' FROM FormElement AS fe WHERE fe.formId = ? AND fe.deleted = 'no' AND FIND_IN_SET(fe.class, ? ) AND fe.enabled='yes' ORDER BY fe.ord, fe.id"; const SQL_FORM_ELEMENT_SIMPLE_ALL_CONTAINER = "SELECT fe.id, fe.feIdContainer, fe.name, fe.label, fe.type, fe.encode, fe.checkType, fe.checkPattern, fe.mode, fe.modeSql, fe.parameter, fe.dynamicUpdate FROM FormElement AS fe, Form AS f WHERE f.name = ? AND f.id = fe.formId AND fe.deleted = 'no' AND fe.class = 'native' AND fe.enabled='yes' ORDER BY fe.ord, fe.id"; const SQL_FORM_ELEMENT_CONTAINER_TEMPLATE_GROUP = "SELECT fe.id, fe.name, fe.label, fe.maxLength, fe.parameter FROM FormElement AS fe, Form AS f WHERE f.name = ? AND f.id = fe.formId AND fe.deleted = 'no' AND fe.class = 'container' AND fe.type='templateGroup' AND fe.enabled='yes' ORDER BY fe.ord, fe.id"; const SQL_FORM_ELEMENT_TEMPLATE_GROUP_FE_ID = "SELECT * FROM FormElement AS fe WHERE fe.id = ? AND fe.deleted = 'no' AND fe.class = 'container' AND fe.type='templateGroup' AND fe.enabled='yes' "; -const SQL_FORM_ELEMENT_NATIVE_TG_COUNT = "SELECT fe.*, IFNULL(feTg.maxLength,0) AS _tgCopies FROM FormElement AS fe LEFT JOIN FormElement AS feTg ON fe.feIdContainer=feTg.id AND feTg.deleted = 'no' AND feTg.class = 'container' AND feTg.type='templateGroup' AND feTg.enabled='yes' WHERE fe.formId = ? AND fe.deleted = 'no' AND fe.class = 'native' AND fe.enabled='yes'"; +//const SQL_FORM_ELEMENT_NATIVE_TG_COUNT = "SELECT fe.*, IFNULL(feTg.maxLength,0) AS _tgCopies FROM FormElement AS fe LEFT JOIN FormElement AS feTg ON fe.feIdContainer=feTg.id AND feTg.deleted = 'no' AND feTg.class = 'container' AND feTg.type='templateGroup' AND feTg.enabled='yes' WHERE fe.formId = ? AND fe.deleted = 'no' AND fe.class = 'native' AND fe.enabled='yes'"; +const SQL_FORM_ELEMENT_NATIVE_TG_COUNT = "SELECT fe.*, IFNULL(feTg.maxLength,0) AS _tgCopies FROM FormElement AS fe LEFT JOIN FormElement AS feTg ON fe.feIdContainer=feTg.id AND feTg.deleted = 'no' AND feTg.class = 'container' AND feTg.type='templateGroup' AND feTg.enabled='yes' WHERE fe.formId = ? AND fe.deleted = 'no' AND (fe.class = 'native' OR (fe.class = 'container' AND fe.type='pill')) AND fe.enabled='yes'"; const NAME_TG_COPIES = '_tgCopies'; // Number of templatesGroup copies to create on the fly. Also used in SQL_FORM_ELEMENT_NATIVE_TG_COUNT. const FE_TG_INDEX = '_tgIndex'; // Index of the current copy of a templateGroup FE. @@ -112,7 +114,7 @@ const ERROR_SIP_INVALID = 1006; const ERROR_MISSING_RECORD_ID = 1007; const ERROR_IN_SQL_STATEMENT = 1008; const ERROR_MISSING_REQUIRED_PARAMETER = 1009; - +const ERROR_FE_NESTED_TOO_MUCH = 1010; const ERROR_BROKEN_PARAMETER = 1011; const ERROR_FE_USER_UID_CHANGED = 1012; const ERROR_SIP_NOT_FOUND = 1013; @@ -278,6 +280,9 @@ const ERROR_MISSING_SESSIONNAME = 2400; const ERROR_QFQ_SESSION_MISSING = 2401; const ERROR_SESSION_BROKEN_SCRIPT_PATH = 2402; +const ERROR_HTML2PDF_MISSING_CONFIG = 2500; +const ERROR_HTML2PDF_WKHTML_NOT_EXECUTABLE = 2501; +const ERROR_HTML2PDF_WKHTML_FAILED = 2502; // // Store Names: Identifier // @@ -384,7 +389,7 @@ const SYSTEM_MAIL_LOG_FILE = '../../mail.log'; const SYSTEM_SHOW_DEBUG_INFO = 'SHOW_DEBUG_INFO'; const SYSTEM_SHOW_DEBUG_INFO_YES = 'yes'; const SYSTEM_SHOW_DEBUG_INFO_NO = 'no'; -const SYSTEM_SHOW_DEBUG_INFO_AUTO = 'auto'; +const SYSTEM_SHOW_DEBUG_INFO_AUTO = 'auto'; // Remains on value 'auto' as long as there is no BE User logged in. In other words: 'auto'='no'. #5031 const SYSTEM_SHOW_DEBUG_INFO_DOWNLOAD = 'download'; const SYSTEM_CSS_LINK_CLASS_INTERNAL = 'CSS_LINK_CLASS_INTERNAL'; @@ -469,7 +474,7 @@ const SYSTEM_RECORD_LOCK_TIMEOUT_SECONDS = 'RECORD_LOCK_TIMEOUT_SECONDS'; const SYSTEM_RECORD_LOCK_TIMEOUT_SECONDS_DEFAULT = 900; // 15 mins // Deprecated, replaced by SYSTEM_FILL_STORE_SYSTEM_BY_SQ -const SYSTEM_VAR_ADD_BY_SQL = 'VAR_ADD_BY_SQL'; +const SYSTEM_VAR_ADD_BY_SQL = 'VAR_ADD_BY_SQL'; // since 1.12.17 //const SYSTEM_VAR_ADD_BY_SQL_DEFAULT = 'SELECT id AS periodId FROM Period WHERE start<=NOW() ORDER BY start DESC LIMIT 1'; const SYSTEM_FILL_STORE_SYSTEM_BY_SQL = 'FILL_STORE_SYSTEM_BY_SQL'; @@ -626,6 +631,7 @@ const API_FORM_UPDATE_REQUIRED = 'required'; const API_ELEMENT_UPDATE = 'element-update'; const API_ELEMENT_ATTRIBUTE = 'attr'; const API_ELEMENT_CONTENT = 'content'; + const API_SUBMIT_REASON = 'submit_reason'; const API_SUBMIT_REASON_SAVE = 'save'; const API_SUBMIT_REASON_SAVE_CLOSE = 'save,close'; @@ -884,6 +890,9 @@ const FE_SENDMAIL_GR_ID = 'sendMailGrId'; // gr_id: used to classify mail log en const FE_SENDMAIL_X_ID = 'sendMailXId'; // x_id: used to classify mail log entries ind table mailLog const FE_SENDMAIL_X_ID2 = 'sendMailXId2'; // x_id: used to classify mail log entries ind table mailLog const FE_SENDMAIL_X_ID3 = 'sendMailXId3'; // x_id: used to classify mail log entries ind table mailLog +const FE_SENDMAIL_SUBJECT_HTML_ENTITY = 'sendMailSubjectHtmlEntity'; // encode|decode >> DECODE, ENCODE +const FE_SENDMAIL_BODY_HTML_ENTITY = 'sendMailBodyHtmlEntity'; // encode | decode >> DECODE, ENCODE +const FE_SENDMAIL_ATTACHMENT = 'sendMailAttachment'; // x_id: used to classify mail log entries in table mailLog const FE_AUTOFOCUS = 'autofocus'; // value: <none>|0|1 , <none>==1, this element becomes the focus during form load. const FE_RETYPE = 'retype'; // value: <none>|0|1 , <none>==1, this element becomes the focus during form load. const FE_RETYPE_LABEL = 'retypeLabel'; // value: label text for retype FormElement @@ -948,8 +957,6 @@ const RETYPE_FE_NAME_EXTENSION = 'RETYPE'; const TYPEAHEAD_PLACEHOLDER = '?'; -const FE_HTML_ID = 'htmlId'; // Will be dynamically computed during runtime. - // Values const FE_ENCODE_SPECIALCHAR = 'specialchar'; const FE_ENCODE_NONE = 'none'; @@ -985,18 +992,13 @@ const FE_TYPE_SENDMAIL = 'sendMail'; const FE_TYPE_PASTE = 'paste'; const FE_TYPE_TEMPLATE_GROUP = 'templateGroup'; +const FE_TYPE_PILL = 'pill'; -const SHEBANG_REPORT = '#!report'; - -// SUPPORT -const PARAM_T3_ALL = 't3 all'; -const PARAM_T3_NO_ID = "t3 no id"; -const ESCAPE_WITH_BACKSLASH = 'backslash'; -const ESCAPE_WITH_HTML_QUOTE = 'htmlquote'; +const FE_HTML_ID = 'htmlId'; // Will be dynamically computed during runtime. -// AbstractBuildForm -const FLAG_ALL = 'flagAll'; -const FLAG_DYNAMIC_UPDATE = 'flagDynamicUpdate'; +const MODE_ENCODE = 'encode'; +const MODE_DECODE = 'decode'; +const MODE_NONE = 'none'; const HTML_DELIMITER_NAME = '-'; const HTML_DELIMITER_ID = HTML_DELIMITER_NAME; @@ -1007,6 +1009,24 @@ const HTML_ID_EXTENSION_NOTE = '-n'; const HTML_ID_EXTENSION_TOOLTIP = '-t'; const HTML_ID_EXTENSION_ROW = '-r'; const HTML_ID_EXTENSION_CHARACTER_COUNT = '-cc'; +const HTML_ID_EXTENSION_PILL_LI = '-pl'; +const HTML_ID_EXTENSION_PILL_LI_A = '-pla'; + +const HTML_ATTR_ID = 'id'; +const HTML_ATTR_NAME = 'name'; +const HTML_ATTR_CLASS = 'class'; + +const SHEBANG_REPORT = '#!report'; + +// SUPPORT +const PARAM_T3_ALL = 't3 all'; +const PARAM_T3_NO_ID = "t3 no id"; +const ESCAPE_WITH_BACKSLASH = 'backslash'; +const ESCAPE_WITH_HTML_QUOTE = 'htmlquote'; + +// AbstractBuildForm +const FLAG_ALL = 'flagAll'; +const FLAG_DYNAMIC_UPDATE = 'flagDynamicUpdate'; const QUERY_TYPE_SELECT = 'type: select,show,describe,explain'; const QUERY_TYPE_INSERT = 'type: insert'; @@ -1065,22 +1085,6 @@ const COLUMN_PATH_FILE_NAME = 'pathFileName'; const EXISTING_PATH_FILE_NAME = '_existingPathFileName'; //SENDMAIL -const SENDMAIL_IDX_RECEIVER = 0; -const SENDMAIL_IDX_SENDER = 1; -const SENDMAIL_IDX_SUBJECT = 2; -const SENDMAIL_IDX_BODY = 3; -const SENDMAIL_IDX_REPLY_TO = 4; -const SENDMAIL_IDX_FLAG_AUTO_SUBMIT = 5; -const SENDMAIL_IDX_GR_ID = 6; -const SENDMAIL_IDX_X_ID = 7; -const SENDMAIL_IDX_RECEIVER_CC = 8; -const SENDMAIL_IDX_RECEIVER_BCC = 9; -const SENDMAIL_IDX_ATTACHMENT = 10; -const SENDMAIL_IDX_HEADER = 11; -const SENDMAIL_IDX_X_ID2 = 12; -const SENDMAIL_IDX_X_ID3 = 13; -const SENDMAIL_IDX_SRC = 14; - const SENDMAIL_TOKEN_RECEIVER = 't'; const SENDMAIL_TOKEN_SENDER = 'f'; const SENDMAIL_TOKEN_SUBJECT = 's'; @@ -1091,12 +1095,23 @@ const SENDMAIL_TOKEN_GR_ID = 'g'; const SENDMAIL_TOKEN_X_ID = 'x'; const SENDMAIL_TOKEN_RECEIVER_CC = 'c'; const SENDMAIL_TOKEN_RECEIVER_BCC = 'B'; -const SENDMAIL_TOKEN_ATTACHMENT = 'a'; +const SENDMAIL_TOKEN_ATTACHMENT = 'attachment'; +const SENDMAIL_TOKEN_ATTACHMENT_FILE = 'F'; +const SENDMAIL_TOKEN_ATTACHMENT_FILE_DEPRECATED = 'a'; // since 5.12.17 const SENDMAIL_TOKEN_HEADER = 'h'; const SENDMAIL_TOKEN_X_ID2 = 'y'; const SENDMAIL_TOKEN_X_ID3 = 'z'; +const SENDMAIL_TOKEN_SUBJECT_HTML_ENTITY = 'e'; +const SENDMAIL_TOKEN_BODY_HTML_ENTITY = 'E'; const SENDMAIL_TOKEN_SRC = 'S'; +const SENDMAIL_TOKEN_CONCAT = 'C'; +const SENDMAIL_TOKEN_DOWNLOAD_FILENAME = 'd'; +const SENDMAIL_TOKEN_DOWNLOAD_MODE = 'M'; +const SENDMAIL_TOKEN_ATTACHMENT_URL = 'u'; +const SENDMAIL_TOKEN_ATTACHMENT_URL_PARAM = 'U'; +const SENDMAIL_TOKEN_ATTACHMENT_PAGE = 'p'; + // Report, BodyText const TOKEN_SQL = 'sql'; const TOKEN_HEAD = 'head'; @@ -1163,10 +1178,13 @@ const DOWNLOAD_MODE_PDF = 'pdf'; const DOWNLOAD_MODE_EXCEL = 'excel'; const DOWNLOAD_MODE_ZIP = 'zip'; const DOWNLOAD_EXPORT_FILENAME = '_exportFilename'; -const DOWNLOAD_FILE_PREFIX = 'qfq.temp'; // temporary filename on server of single export file -const DOWNLOAD_OUTPUT_PDF = 'output'; +const TMP_FILE_PREFIX = 'qfq.temp.'; // temporary filename on server of single export file +const DOWNLOAD_OUTPUT_FILENAME = 'output'; const DOWNLOAD_SIP_ENCODE_PARAMETER = '_sip'; +const OUTPUT_MODE_DIRECT = 'direct'; +const OUTPUT_MODE_FILE = 'file'; + // HTML2PDF const HTML2PDF_PAGEID = 'id'; const HTML2PDF_PARAM_GET = 'paramGet'; @@ -1176,6 +1194,7 @@ const SESSION_COOKIE_PREFEIX = 'qfq.cookie.'; // temporary 'cookie file' to forw // Class: LINK const PARAM_DELIMITER = '|'; +const PARAM_TOKEN_DELIMITER = ':'; const TOKEN_URL = 'u'; const TOKEN_MAIL = 'm'; @@ -1204,7 +1223,8 @@ const TOKEN_ENCRYPTION = 'e'; const TOKEN_SIP = 's'; const TOKEN_URL_PARAM = 'U'; const TOKEN_RIGHT = 'R'; -const TOKEN_FILE = 'f'; +const TOKEN_FILE = 'F'; +const TOKEN_FILE_DEPRECATED = 'f'; // since 5.12.17 const TOKEN_DOWNLOAD_MODE = 'M'; const TOKEN_ACTION_DELETE = 'x'; diff --git a/extension/qfq/qfq/QuickFormQuery.php b/extension/qfq/qfq/QuickFormQuery.php index e73ce75d888eb7f8ea1958a22087347571c4187b..79a666ea3cddfb0871eba0f6b34c76a9a8b3b77b 100644 --- a/extension/qfq/qfq/QuickFormQuery.php +++ b/extension/qfq/qfq/QuickFormQuery.php @@ -69,7 +69,7 @@ class QuickFormQuery { protected $store = null; /** - * @var Array of Database instantiated class + * @var \qfq\Database[] - Array of Database instantiated class */ protected $dbArray = array(); @@ -80,6 +80,7 @@ class QuickFormQuery { protected $formSpec = array(); protected $feSpecAction = array(); // Form Definition: copy of the loaded form protected $feSpecNative = array(); // FormEelement Definition: all formElement.class='action' of the loaded form + protected $feSpecNativeRaw = array(); // FormEelement Definition: all formElement.class='action' of the loaded form /** * @var array @@ -383,7 +384,7 @@ class QuickFormQuery { $this->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $recordId, STORE_RECORD); // SAVE - $save = new Save($this->formSpec, $this->feSpecAction, $this->feSpecNative); + $save = new Save($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->feSpecNativeRaw); $rc = $save->process(); // Reload fresh saved record and fill STORE_RECORD with it. @@ -424,7 +425,10 @@ class QuickFormQuery { // Values of FormElements might be changed during 'afterSave': rebuild the form to load the new values. Especially for non primary template groups. // $this->loadFormSpecification($formMode, $recordId, $foundInStore); - $this->feSpecNative = $this->getNativeFormElements(SQL_FORM_ELEMENT_NATIVE_TG_COUNT, [$this->formSpec["id"]], $this->formSpec); + $feSpecNative = $this->getNativeFormElements(SQL_FORM_ELEMENT_NATIVE_TG_COUNT, [$this->formSpec[F_ID]], $this->formSpec); + $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM); + $feSpecNative = HelperFormElement::setLanguage($feSpecNative, $parameterLanguageFieldName); + $this->feSpecNative = HelperFormElement::setFeContainerFormElementId($feSpecNative, $this->formSpec[F_ID], $recordId); // Retrieve FE Values as JSON // $data['form-update']=... @@ -679,7 +683,7 @@ class QuickFormQuery { if (!empty($fillStoreVar)) { $rows = $this->eval->parse($fillStoreVar); if (is_array($rows)) { - $this->store->appendToStore(STORE_VAR, $rows[0]); + $this->store->appendToStore($rows[0], STORE_VAR); } else { if (!empty($rows)) { throw new UserFormException("Invalid statement for '" . FE_FILL_STORE_VAR . "': " . $formSpec[FE_FILL_STORE_VAR], ERROR_INVALID_OR_MISSING_PARAMETER); @@ -692,6 +696,9 @@ class QuickFormQuery { // Clear $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM); + // Read all 'active' FE + $this->feSpecNativeRaw = $this->dbArray[$this->dbIndexQfq]->sql(SQL_FORM_ELEMENT_RAW, ROW_REGULAR, [$this->formSpec["id"]]); + // FE: Action $this->feSpecAction = $this->dbArray[$this->dbIndexQfq]->sql(SQL_FORM_ELEMENT_ALL_CONTAINER, ROW_REGULAR, ['no', $this->formSpec["id"], 'action']); HelperFormElement::explodeParameterInArrayElements($this->feSpecAction, FE_PARAMETER); @@ -707,7 +714,7 @@ class QuickFormQuery { case FORM_SAVE: case FORM_UPDATE: - $feSpecNative = $this->getNativeFormElements(SQL_FORM_ELEMENT_NATIVE_TG_COUNT, [$this->formSpec["id"]], $this->formSpec); + $feSpecNative = $this->getNativeFormElements(SQL_FORM_ELEMENT_NATIVE_TG_COUNT, [$this->formSpec[F_ID]], $this->formSpec); break; case FORM_DELETE: @@ -719,6 +726,7 @@ class QuickFormQuery { } $this->feSpecNative = HelperFormElement::setLanguage($feSpecNative, $parameterLanguageFieldName); + $this->feSpecNative = HelperFormElement::setFeContainerFormElementId($this->feSpecNative, $this->formSpec[F_ID], $recordId); return $formName; } diff --git a/extension/qfq/qfq/Save.php b/extension/qfq/qfq/Save.php index 283d55f58d07d1e4b113fb502cd910c216448928..d5cf1904b649fe98205e153480f892e8d1f70d0e 100644 --- a/extension/qfq/qfq/Save.php +++ b/extension/qfq/qfq/Save.php @@ -23,6 +23,7 @@ class Save { private $formSpec = array(); // copy of the loaded form private $feSpecAction = array(); // copy of all formElement.class='action' of the loaded form private $feSpecNative = array(); // copy of all formElement.class='native' of the loaded form + private $feSpecNativeRaw = array(); // copy of all formElement.class='native' of the loaded form /** * @var null|Store */ @@ -35,11 +36,13 @@ class Save { * @param array $formSpec * @param array $feSpecAction * @param array $feSpecNative + * @param array $feSpecNativeRaw */ - public function __construct(array $formSpec, array $feSpecAction, array $feSpecNative) { + public function __construct(array $formSpec, array $feSpecAction, array $feSpecNative, array $feSpecNativeRaw) { $this->formSpec = $formSpec; $this->feSpecAction = $feSpecAction; $this->feSpecNative = $feSpecNative; + $this->feSpecNativeRaw = $feSpecNativeRaw; $this->store = Store::getInstance(); $this->db = new Database(); $this->evaluate = new Evaluate($this->store, $this->db); @@ -91,8 +94,7 @@ class Save { break; } $feName = $formElement[FE_NAME]; - if (!isset($formValues[$feName])) { - + if (!isset($formValues[$feName]) && $this->isMemberOfTemplateGroup($formElement)) { $formValues[$feName] = $formElement[FE_VALUE]; } } @@ -101,6 +103,39 @@ class Save { } /** + * Check if the current $formElement is member of a templateGroup. + * + * @param array $formElement + * @param int $depth + * @return bool + * @throws UserFormException + */ + private function isMemberOfTemplateGroup(array $formElement, $depth = 0) { + $depth++; + + if ($depth > 15) { + throw new UserFormException('FormElement nested too much (in each other - endless?): stop recursion', ERROR_FE_NESTED_TOO_MUCH); + } + + if ($formElement[FE_TYPE] == FE_TYPE_TEMPLATE_GROUP) { + return true; + } + + if ($formElement[FE_ID_CONTAINER] == 0) { + return false; + } + + // Get the parent element + $formElementArr = OnArray::filter($this->feSpecNativeRaw, FE_ID, $formElement[FE_ID_CONTAINER]); + if (isset($formElementArr[0])) { + return $this->isMemberOfTemplateGroup($formElementArr[0], $depth); + } + + return false; // This should not be reached, + } + + /** + * * @param $feName * * @return bool @@ -282,19 +317,19 @@ class Save { foreach ($this->feSpecNative AS $formElement) { // skip non upload formElements - if ($formElement[FE_TYPE] != 'upload') { + if ($formElement[FE_TYPE] != FE_TYPE_UPLOAD) { continue; } $formElement = HelperFormElement::initUploadFormElement($formElement); if (isset($formElement[FE_FILL_STORE_VAR])) { - $this->store->appendToStore(STORE_VAR, $formElement[FE_FILL_STORE_VAR]); + $this->store->appendToStore($formElement[FE_FILL_STORE_VAR], STORE_VAR); } // Preparation for Log, Debug $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM); - $column = $formElement['name']; + $column = $formElement[FE_NAME]; $pathFileName = $this->doUpload($formElement, $formValues[$column], $sip, $modeUpload); // Upload Type: Simple or Advanced @@ -606,21 +641,4 @@ class Save { return $slaveId; } - - /** - * Get the complete FormElement for $name - * - * @param $name - * - * @return bool|array if found the FormElement, else false. - */ - private function getFormElementByName($name) { - - foreach ($this->feSpecNative as $formElement) { - if ($formElement['name'] === $name) - return $formElement; - } - - return false; - } } \ No newline at end of file diff --git a/extension/qfq/qfq/database/DatabaseUpdateData.php b/extension/qfq/qfq/database/DatabaseUpdateData.php index e00226bd11123617145abb16460c0aa075439ef2..e553fc48908f4d78f9981bee9539384c9ddfecc5 100644 --- a/extension/qfq/qfq/database/DatabaseUpdateData.php +++ b/extension/qfq/qfq/database/DatabaseUpdateData.php @@ -90,6 +90,11 @@ $UPDATE_ARRAY = array( "ALTER TABLE `MailLog` ADD `xId2` INT NOT NULL AFTER `xId`, ADD `xId3` INT NOT NULL AFTER `xId2`", ], + '0.25.7' => [ + "ALTER TABLE `MailLog` CHANGE `attach` `attach` VARCHAR(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT ''", + ], + + ); diff --git a/extension/qfq/qfq/exceptions/AbstractException.php b/extension/qfq/qfq/exceptions/AbstractException.php index 447d6fb0f06e90a8b45a1423b5cf2ebede340356..7bc3c72897fa7d80d64a1358430b5106c6603ff1 100644 --- a/extension/qfq/qfq/exceptions/AbstractException.php +++ b/extension/qfq/qfq/exceptions/AbstractException.php @@ -89,12 +89,14 @@ class AbstractException extends \Exception { // Layout $debug = '<tr bgcolor="#dddddd"><td colspan="2">Exception</td></tr>'; foreach ($this->messageArray as $key => $value) { + // Array to string if (is_array($value)) { $value = var_export($value, true); } - if ($value !== '' && $value !== false) + if (!empty($value)) { $debug .= "<tr>" . "<td>$key</td>" . "<td>" . htmlspecialchars($value) . "</td>" . "</tr>"; + } } $debug = "<table border=1>" . $debug . "</table>"; } diff --git a/extension/qfq/qfq/exceptions/ErrorHandler.php b/extension/qfq/qfq/exceptions/ErrorHandler.php index e19acfa10f517636500a45fb0bf5e1375fe4c03d..702ba275ba33e6a550fcc9efb695c0210f9e5490 100644 --- a/extension/qfq/qfq/exceptions/ErrorHandler.php +++ b/extension/qfq/qfq/exceptions/ErrorHandler.php @@ -18,9 +18,11 @@ class ErrorHandler { // This error code is not included in error_reporting return false; } -// throw new CodeException(": Catchable Error in '$file' on line $line: " . $message . " - CWD: " . getcwd(), $severity, NULL); + // Do not show too much to the user. E.g. 'ldap_bind()' might have problems, but the user should not see the file and linenumber. - throw new CodeException($message, $severity, null); +// throw new CodeException($message, $severity, null); + + return ''; } } \ No newline at end of file diff --git a/extension/qfq/qfq/form/FormAction.php b/extension/qfq/qfq/form/FormAction.php index e08d7b6258c55f8a669570696a7ad7f5cb6d74f1..7cb9f3b9a5d79bad4046352e8e1c319a61d5840e 100644 --- a/extension/qfq/qfq/form/FormAction.php +++ b/extension/qfq/qfq/form/FormAction.php @@ -12,7 +12,7 @@ require_once(__DIR__ . '/../Constants.php'); require_once(__DIR__ . '/../database/Database.php'); require_once(__DIR__ . '/../store/Store.php'); require_once(__DIR__ . '/../Evaluate.php'); -require_once(__DIR__ . '/../report/Sendmail.php'); +require_once(__DIR__ . '/../report/SendMail.php'); require_once(__DIR__ . '/../helper/HelperFormElement.php'); require_once(__DIR__ . '/../exceptions/UserFormException.php'); @@ -91,7 +91,7 @@ class FormAction { if (isset($fe[FE_FILL_STORE_VAR])) { $rows = $this->evaluate->parse($fe[FE_FILL_STORE_VAR]); if (is_array($rows)) { - $this->store->appendToStore(STORE_VAR, $rows[0]); + $this->store->appendToStore($rows[0], STORE_VAR); } else { if (!empty($rows)) { throw new UserFormException("Invalid statement for 'fillStoreVar': " . $fe[FE_FILL_STORE_VAR], ERROR_INVALID_OR_MISSING_PARAMETER); @@ -164,7 +164,7 @@ class FormAction { } if ($fe[FE_TYPE] === FE_TYPE_SENDMAIL) { - $this->sendMail($fe); + $this->doSendMail($fe); //no further processing of current element necessary. continue; } @@ -239,24 +239,30 @@ class FormAction { /** * @param array $feSpecAction */ - private function sendMail(array $feSpecAction) { - - $mail[SENDMAIL_IDX_RECEIVER] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_TO]); - $mail[SENDMAIL_IDX_SENDER] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_FROM]); - $mail[SENDMAIL_IDX_SUBJECT] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_SUBJECT]); - $mail[SENDMAIL_IDX_BODY] = $this->evaluate->parse($feSpecAction[FE_VALUE]); - $mail[SENDMAIL_IDX_REPLY_TO] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_REPLY_TO]); - $mail[SENDMAIL_IDX_FLAG_AUTO_SUBMIT] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_FLAG_AUTO_SUBMIT]) === 'off' ? 'off' : 'on'; - $mail[SENDMAIL_IDX_GR_ID] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_GR_ID]); - $mail[SENDMAIL_IDX_X_ID] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_X_ID]); - $mail[SENDMAIL_IDX_RECEIVER_CC] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_CC]); - $mail[SENDMAIL_IDX_RECEIVER_BCC] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_BCC]); - $mail[SENDMAIL_IDX_SRC] = "FormId: " . $feSpecAction[FE_FORM_ID] . ", FormElementId: " . $feSpecAction['id']; - $mail[SENDMAIL_IDX_X_ID2] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_X_ID2]); - $mail[SENDMAIL_IDX_X_ID3] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_X_ID3]); + private function doSendMail(array $feSpecAction) { + + $args = array(); + + $args[] = SENDMAIL_TOKEN_RECEIVER . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_TO]); + $args[] = SENDMAIL_TOKEN_SENDER . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_FROM]); + $args[] = SENDMAIL_TOKEN_SUBJECT . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_SUBJECT]); + $args[] = SENDMAIL_TOKEN_BODY . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_VALUE]); + $args[] = SENDMAIL_TOKEN_REPLY_TO . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_REPLY_TO]); + $autoSubmit = ($this->evaluate->parse($feSpecAction[FE_SENDMAIL_FLAG_AUTO_SUBMIT]) === 'off') ? 'off' : 'on'; + $args[] = SENDMAIL_TOKEN_FLAG_AUTO_SUBMIT . PARAM_TOKEN_DELIMITER . $autoSubmit; + $args[] = SENDMAIL_TOKEN_GR_ID . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_GR_ID]); + $args[] = SENDMAIL_TOKEN_X_ID . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_X_ID]); + $args[] = SENDMAIL_TOKEN_RECEIVER_CC . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_CC]); + $args[] = SENDMAIL_TOKEN_RECEIVER_BCC . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_BCC]); + $args[] = SENDMAIL_TOKEN_SRC . PARAM_TOKEN_DELIMITER . "FormId: " . $feSpecAction[FE_FORM_ID] . ", FormElementId: " . $feSpecAction['id']; + $args[] = SENDMAIL_TOKEN_X_ID2 . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_X_ID2]); + $args[] = SENDMAIL_TOKEN_X_ID3 . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_X_ID3]); + $args[] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_ATTACHMENT]); // Mail: send - new Sendmail($mail); + $sendMail = new SendMail(); + $mailConfig = $sendMail->parseStringToArray(implode(PARAM_DELIMITER, $args)); + $sendMail->process($mailConfig); } /** @@ -298,7 +304,7 @@ class FormAction { $msg = $this->evaluate->parse($fe[FE_MESSAGE_FAIL]); // Replace possible dynamic parts - // Throw user defineable error message + // Throw user error message throw new UserFormException($msg, ERROR_REPORT_FAILED_ACTION); } diff --git a/extension/qfq/qfq/helper/HelperFile.php b/extension/qfq/qfq/helper/HelperFile.php new file mode 100644 index 0000000000000000000000000000000000000000..9e08a28f0eb57301cafe5b57b844ecf535bc6b45 --- /dev/null +++ b/extension/qfq/qfq/helper/HelperFile.php @@ -0,0 +1,59 @@ +<?php +/** + * Created by PhpStorm. + * User: crose + * Date: 1/28/16 + * Time: 8:05 AM + */ + +namespace qfq; + +require_once(__DIR__ . '/../Constants.php'); + +class HelperFile { + + /** + * Iterate over array $files. Delete only named files which are stored in '/tmp/' . DOWNLOAD_FILE_PREFIX. + * + * @param array $files + */ + public static function cleanTempFiles(array $files) { + + foreach ($files as $file) { + if (self::isQfqTemp($file)) { + unlink($file); + } + + $dir = dirname($file); + if (self::isQfqTemp($dir)) { + rmdir($dir); + } + } + } + + + /** + * Check against standard QFQ Temp location. If it is a Qfq Temp location, return true, else false. + * + * @param string $name Absolute filename. + * + * @return bool + */ + public static function isQfqTemp($name) { + $prefix = sys_get_temp_dir() . '/' . TMP_FILE_PREFIX; + $len = strlen($prefix); + + return (substr($name, 0, $len) == $prefix); + } + + /** + * Creates a temporary directory. + */ + public static function mktempdir() { + $name = tempnam(sys_get_temp_dir(), TMP_FILE_PREFIX); + unlink($name); + mkdir($name); + + return $name; + } +} \ No newline at end of file diff --git a/extension/qfq/qfq/helper/HelperFormElement.php b/extension/qfq/qfq/helper/HelperFormElement.php index 5211d5d2a9c95067a5a711990b5512cf3a8c1e90..88cf9178fda634c2fd0ef201cabd99740c385776 100644 --- a/extension/qfq/qfq/helper/HelperFormElement.php +++ b/extension/qfq/qfq/helper/HelperFormElement.php @@ -125,6 +125,32 @@ class HelperFormElement { return "$formId" . HTML_DELIMITER_ID . "$formElementId" . HTML_DELIMITER_ID . "$recordId" . HTML_DELIMITER_ID . "$formElementCopy"; } + /** + * In an array for $feSpecNative, set FE_HTML_ID for all fe.class=FE_CONTAINER Elements. + * + * @param array $feSpecNative + * @param $formId + * @param $recordId + * @param int $formElementCopy + * @return array + */ + public static function setFeContainerFormElementId(array $feSpecNative, $formId, $recordId, $formElementCopy = 0) { + + foreach ($feSpecNative as $key => $fe) { + + switch ($fe[FE_CLASS]) { + case FE_CLASS_CONTAINER: + $feSpecNative[$key][FE_HTML_ID] = self::buildFormElementId($formId, $fe[FE_ID], $recordId, $formElementCopy); + break; + default: + break; + } + + } + + return $feSpecNative; + } + /** * Checkboxen, belonging to one element, grouped together by name: <fe>_<field>_<index> * @@ -217,7 +243,7 @@ class HelperFormElement { $list = [FE_TYPE, FE_SLAVE_ID, FE_SQL_VALIDATE, FE_SQL_BEFORE, FE_SQL_INSERT, FE_SQL_UPDATE, FE_SQL_DELETE, FE_SQL_AFTER, FE_EXPECT_RECORDS, FE_REQUIRED_LIST, FE_MESSAGE_FAIL, FE_SENDMAIL_TO, FE_SENDMAIL_CC, FE_SENDMAIL_BCC, FE_SENDMAIL_FROM, FE_SENDMAIL_SUBJECT, FE_SENDMAIL_REPLY_TO, FE_SENDMAIL_FLAG_AUTO_SUBMIT, - FE_SENDMAIL_GR_ID, FE_SENDMAIL_X_ID]; + FE_SENDMAIL_GR_ID, FE_SENDMAIL_X_ID, FE_SENDMAIL_X_ID2, FE_SENDMAIL_X_ID3]; foreach ($list as $key) { Support::setIfNotSet($fe, $key); diff --git a/extension/qfq/qfq/helper/Sanitize.php b/extension/qfq/qfq/helper/Sanitize.php index 220e7283aa5b15cf8d304f04b9957a46c1c5ada7..9a74e72b54a8c9443255466cbfa0892fd6137408 100644 --- a/extension/qfq/qfq/helper/Sanitize.php +++ b/extension/qfq/qfq/helper/Sanitize.php @@ -158,7 +158,7 @@ class Sanitize { * * @return mixed */ - public static function safeFilename($filename) { + public static function safeFilename($filename, $flagBaseName = false) { $search = array( // Definition of German Umlauts START '/ß/', @@ -177,6 +177,10 @@ class Sanitize { '_', ); + if ($flagBaseName) { + $filename = basename($filename); + } + return preg_replace($search, $replace, $filename); } // safeFilename() diff --git a/extension/qfq/qfq/helper/Support.php b/extension/qfq/qfq/helper/Support.php index 2852e41a32f59575694b4f30475b332c39c05832..28c35670bb451f38ea76439a1241010b1e792282 100644 --- a/extension/qfq/qfq/helper/Support.php +++ b/extension/qfq/qfq/helper/Support.php @@ -1041,4 +1041,28 @@ class Support { return $data; } + + /** + * @param $mode + * @param $data + * @return string + * @throws UserFormException + */ + public static function htmlEntityEncodeDecode($mode, $data) { + + switch ($mode) { + case MODE_ENCODE: + $data = htmlspecialchars($data, ENT_QUOTES); + break; + case MODE_DECODE: + $data = htmlspecialchars_decode($data, ENT_QUOTES); + break; + case MODE_NONE: + break; + default: + throw new UserFormException('Unknown mode=' . $mode, ERROR_UNKNOWN_MODE); + } + + return $data; + } } \ No newline at end of file diff --git a/extension/qfq/qfq/report/Download.php b/extension/qfq/qfq/report/Download.php index b84c97f5661f7d72f663f980521672b229d7bf90..006e0d652ae5e9606e733ac82ef03714dcf9674b 100644 --- a/extension/qfq/qfq/report/Download.php +++ b/extension/qfq/qfq/report/Download.php @@ -18,6 +18,7 @@ require_once(__DIR__ . '/../store/Store.php'); require_once(__DIR__ . '/../helper/OnArray.php'); require_once(__DIR__ . '/../helper/Logger.php'); require_once(__DIR__ . '/../helper/Sanitize.php'); +require_once(__DIR__ . '/../helper/HelperFile.php'); require_once(__DIR__ . '/../report/Html2Pdf.php'); //require_once(__DIR__ . '/Link.php'); //require_once(__DIR__ . '/Sendmail.php'); @@ -97,7 +98,8 @@ class Download { break; } - $concatFile = tempnam(sys_get_temp_dir(), DOWNLOAD_FILE_PREFIX); + + $concatFile = tempnam(sys_get_temp_dir(), TMP_FILE_PREFIX); if (false === $concatFile) { throw new DownloadException('Error creating output file.', ERROR_DOWNLOAD_CREATE_NEW_FILE); } @@ -172,34 +174,13 @@ class Download { header("Content-Length: $length"); // If defined as 'attachment': PDFs are not shown inside the browser (if user configured that). Instead, always a 'save as'-dialog appears (Chrome, FF) // header("Content-Disposition: attachment; filename=$outputFilename"); - header("Content-Disposition: inline; filename=\"$outputFilename\""); + header("Content-Disposition: inline; filename=\"$outputFilename\"; name=\"$outputFilename\""); header("Pragma: no-cache"); header("Expires: 0"); print file_get_contents($file); } - /** - * Iterate over array $files. Delete all named files which exist in DOWNLOAD_TMP_DIR. - * - * @param array $files - */ - private function cleanTempFiles(array $files) { - - if ($this->downloadDebugLog != '') { - return; - } - - $prefix = sys_get_temp_dir() . '/' . DOWNLOAD_FILE_PREFIX; - $len = strlen($prefix); - - foreach ($files as $file) { - if (substr($file, 0, $len) == $prefix) { - unlink($file); - } - } - } - /** * Interprets $element and fetches corresponding content as file. * @@ -222,10 +203,12 @@ class Download { switch ($token) { case TOKEN_URL: case TOKEN_URL_PARAM: + case TOKEN_PAGE: $filename = $this->html2pdf->page2pdf($token, $value); break; case TOKEN_FILE: + case TOKEN_FILE_DEPRECATED: $filename = $value; break; default: @@ -247,7 +230,7 @@ class Download { */ private function zipFiles(array $files) { - $zipFile = tempnam(sys_get_temp_dir(), DOWNLOAD_FILE_PREFIX); + $zipFile = tempnam(sys_get_temp_dir(), TMP_FILE_PREFIX); if (false === $zipFile) { throw new DownloadException("Error creating output file.", ERROR_DOWNLOAD_CREATE_NEW_FILE); } @@ -258,12 +241,12 @@ class Download { throw new DownloadException("Error creating/opening new empty zip file: $zipFile", ERROR_IO_OPEN); } - $len = strlen(DOWNLOAD_FILE_PREFIX); + $len = strlen(TMP_FILE_PREFIX); $ii = 1; foreach ($files AS $filename) { $localname = substr($filename, strrpos($filename, '/') + 1); - if (substr($localname, 0, $len) == DOWNLOAD_FILE_PREFIX) { + if (substr($localname, 0, $len) == TMP_FILE_PREFIX) { $localname = 'file-' . $ii; $ii++; } @@ -283,13 +266,16 @@ class Download { * <i>_id=<Typo3 pageId> * <i>_<key>=<value i> * Direct - * <i>_file=<filename> + * <i>_file=<filename> * * @param array $vars [ DOWNLOAD_EXPORT_FILENAME, DOWNLOAD_MODE, SIP_DOWNLOAD_PARAMETER ] * + * @param string $outputMode OUTPUT_MODE_DIRECT | OUTPUT_MODE_FILE + * @return string Filename of the generated file. The filename only points to a real existing filename with $outputMode=OUTPUT_MODE_FILE + * @throws CodeException * @throws DownloadException */ - private function doElements(array $vars) { + private function doElements(array $vars, $outputMode) { $tmpFiles = array(); @@ -310,6 +296,9 @@ class Download { switch ($downloadMode) { case DOWNLOAD_MODE_ZIP: $filename = $this->zipFiles($tmpFiles); + if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) { + $vars[DOWNLOAD_EXPORT_FILENAME] = basename($filename); + } break; case DOWNLOAD_MODE_EXCEL: @@ -319,12 +308,24 @@ class Download { case DOWNLOAD_MODE_FILE: $filename = $tmpFiles[0]; if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) { - $vars[DOWNLOAD_EXPORT_FILENAME] = substr($filename, strpos($filename, '/')); + $vars[DOWNLOAD_EXPORT_FILENAME] = basename($filename); } break; case DOWNLOAD_MODE_PDF: $filename = $this->concatPdfFiles($tmpFiles); + // try to find a meaningful filename + if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) { + if (count($tmpFiles) > 1) { + $vars[DOWNLOAD_EXPORT_FILENAME] = DOWNLOAD_OUTPUT_FILENAME . ".pdf"; + } else { + if (HelperFile::isQfqTemp($filename)) { + $vars[DOWNLOAD_EXPORT_FILENAME] = DOWNLOAD_OUTPUT_FILENAME . ".pdf"; + } else { + $vars[DOWNLOAD_EXPORT_FILENAME] = basename($filename); + } + } + } break; default: @@ -332,24 +333,42 @@ class Download { break; } - $exportFilename = empty($vars[DOWNLOAD_EXPORT_FILENAME]) ? DOWNLOAD_OUTPUT_PDF : $vars[DOWNLOAD_EXPORT_FILENAME]; + switch ($outputMode) { + + case OUTPUT_MODE_FILE: + break; - $this->outputFile($filename, $exportFilename); + case OUTPUT_MODE_DIRECT: + $this->outputFile($filename, $vars[DOWNLOAD_EXPORT_FILENAME]); + HelperFile::cleanTempFiles([$filename]); + break; - $tmpFiles[] = $filename; - $this->cleanTempFiles($tmpFiles); + default: + throw new CodeException('Unkown mode: ' . $outputMode, ERROR_UNKNOWN_MODE); + } + + return $filename; } + /** + * Process download as requested in $vars. Output is either directly send to the browser, or a file which has to be deleted later. + * + * @param string|array $vars If $config is not an array, get values from STORE_SIP. If $config is an array, take it. + * @param string $outputMode OUTPUT_MODE_DIRECT | OUTPUT_MODE_FILE + * * @return string * @throws CodeException + * @throws DownloadException * @throws UserFormException */ - public function process() { + public function process($vars, $outputMode = OUTPUT_MODE_DIRECT) { - $vars = $this->store->getStore(STORE_SIP); + if (!is_array($vars)) { + $vars = $this->store->getStore(STORE_SIP); + } - $this->doElements($vars); + return $this->doElements($vars, $outputMode); } } diff --git a/extension/qfq/qfq/report/Html2Pdf.php b/extension/qfq/qfq/report/Html2Pdf.php index 3ca8692a6e1ec62847803cbcbd77f79d211a0516..df5fafc40bddb6e79fd1f79864713d945e12f5c7 100644 --- a/extension/qfq/qfq/report/Html2Pdf.php +++ b/extension/qfq/qfq/report/Html2Pdf.php @@ -67,15 +67,15 @@ class Html2Pdf { $this->config = $config; if (!isset($config[SYSTEM_BASE_URL_PRINT]) || $config[SYSTEM_BASE_URL_PRINT] == '') { - throw new \exception(CONFIG_INI . ' - Missing ' . SYSTEM_BASE_URL_PRINT); + throw new \exception(CONFIG_INI . ' - Missing ' . SYSTEM_BASE_URL_PRINT, ERROR_HTML2PDF_MISSING_CONFIG); } if (!isset($config[SYSTEM_WKHTMLTOPDF]) || $config[SYSTEM_WKHTMLTOPDF] == '') { - throw new \exception(CONFIG_INI . ' - Missing ' . SYSTEM_WKHTMLTOPDF); + throw new \exception(CONFIG_INI . ' - Missing ' . SYSTEM_WKHTMLTOPDF, ERROR_HTML2PDF_MISSING_CONFIG); } if (!is_executable($config[SYSTEM_WKHTMLTOPDF])) { - throw new \exception(CONFIG_INI . ' - ' . SYSTEM_WKHTMLTOPDF . '=' . $config[SYSTEM_WKHTMLTOPDF] . ' - not found or not executable.'); + throw new \exception(CONFIG_INI . ' - ' . SYSTEM_WKHTMLTOPDF . '=' . $config[SYSTEM_WKHTMLTOPDF] . ' - not found or not executable.', ERROR_HTML2PDF_WKHTML_NOT_EXECUTABLE); } $this->session = Session::getInstance($phpUnit); @@ -177,6 +177,7 @@ class Html2Pdf { $urlParamString = empty($arr[1]) ? '' : $arr[1]; break; case TOKEN_URL_PARAM: + case TOKEN_PAGE: $host = $this->config[SYSTEM_BASE_URL_PRINT]; $urlParamString = $url; break; @@ -206,14 +207,14 @@ class Html2Pdf { $urlPrint = escapeshellarg($url); $wkhtmlToPdf = $this->config[SYSTEM_WKHTMLTOPDF]; - $filename = tempnam(sys_get_temp_dir(), DOWNLOAD_FILE_PREFIX); + $filename = tempnam(sys_get_temp_dir(), TMP_FILE_PREFIX); $filenameEscape = escapeshellarg($filename); $cookieOptions = '--cookie-jar ' . escapeshellarg($this->sessionCookie->getFile()); $customHeader = '--custom-header User-Agent ' . escapeshellarg($_SERVER['HTTP_USER_AGENT']) . ' --custom-header-propagation'; // By default 'Typo3' expects the same User-Agent for the FE-Session // Very important: The current lock on session SESSION_NAME has to be freed, cause wkhtmltopdf will use the same - // session in a few moments and this script remains active all the time. + // session in a few moments and this script remains active during that the time and that would cause a deadlock else. $this->session->close(); $cmd = "$wkhtmlToPdf $customHeader $cookieOptions $options $urlPrint $filenameEscape"; @@ -225,7 +226,8 @@ class Html2Pdf { $line = system($cmd, $rc); if ($rc != 0) { - throw new \exception("Error [RC=$rc] $line: $cmd"); + throw new \exception("Error [RC=$rc] $line: $cmd - in case of trouble: check carefully that *all* CSS, JS, " . + "images are accessible. 'wkhtml' does not report problems but fails.", ERROR_HTML2PDF_WKHTML_FAILED); } return $filename; diff --git a/extension/qfq/qfq/report/Link.php b/extension/qfq/qfq/report/Link.php index ef7c81aae479876f6ff40ad0d5ea64a79698e918..a0ba557d4379dc053431ed56d05ed1c54b175955 100644 --- a/extension/qfq/qfq/report/Link.php +++ b/extension/qfq/qfq/report/Link.php @@ -161,6 +161,7 @@ class Link { TOKEN_NEW => 'buildNew', TOKEN_SHOW => 'buildShow', TOKEN_FILE => 'buildFile', + TOKEN_FILE_DEPRECATED => 'buildFile', TOKEN_GLYPH => 'buildGlyph', TOKEN_BOOTSTRAP_BUTTON => 'buildBootstrapButton', ]; @@ -195,6 +196,7 @@ class Link { TOKEN_RIGHT => NAME_RIGHT, TOKEN_ACTION_DELETE => NAME_ACTION_DELETE, TOKEN_FILE => NAME_FILE, + TOKEN_FILE_DEPRECATED => NAME_FILE, ]; // Used to find double definitions. @@ -471,7 +473,7 @@ class Link { $value = $this->checkValue($key, $value); // Store value - if (isset($tokenGiven[TOKEN_DOWNLOAD]) && ($key == TOKEN_URL || $key == TOKEN_URL_PARAM || $key == TOKEN_FILE)) { + if (isset($tokenGiven[TOKEN_DOWNLOAD]) && ($key == TOKEN_PAGE || $key == TOKEN_URL || $key == TOKEN_URL_PARAM || $key == TOKEN_FILE || $key == TOKEN_FILE_DEPRECATED)) { $vars[NAME_DOWNLOAD_ELEMENTS][] = $key . ':' . $value; @@ -729,7 +731,7 @@ class Link { // Determine default. if ($mode == '') { if ($cnt == 1) { - $mode = (substr($vars[NAME_DOWNLOAD_ELEMENTS][0], 0, 1) == TOKEN_FILE) ? DOWNLOAD_MODE_FILE : DOWNLOAD_MODE_PDF; + $mode = (substr($vars[NAME_DOWNLOAD_ELEMENTS][0], 0, 1) == TOKEN_FILE_DEPRECATED) ? DOWNLOAD_MODE_FILE : DOWNLOAD_MODE_PDF; } else { $mode = DOWNLOAD_MODE_PDF; } diff --git a/extension/qfq/qfq/report/Report.php b/extension/qfq/qfq/report/Report.php index 142524660ba54b23551e46108af7f420c51e743c..cfbb6afc4d2e12a64c024dea2a0407408ce565b4 100644 --- a/extension/qfq/qfq/report/Report.php +++ b/extension/qfq/qfq/report/Report.php @@ -16,7 +16,7 @@ require_once(__DIR__ . '/Variables.php'); require_once(__DIR__ . '/Error.php'); require_once(__DIR__ . '/../database/Database.php'); require_once(__DIR__ . '/Link.php'); -require_once(__DIR__ . '/Sendmail.php'); +require_once(__DIR__ . '/SendMail.php'); require_once(__DIR__ . '/../exceptions/UserReportExtension.php'); require_once(__DIR__ . '/../Evaluate.php'); require_once(__DIR__ . '/../helper/KeyValueStringParser.php'); @@ -83,6 +83,8 @@ class Report { private $showDebugInfoFlag = false; + private $flagFillStoreRecord = true; + /** * Report constructor. * @@ -167,12 +169,13 @@ class Report { * * @return string */ - public function process($bodyText) { + public function process($bodyText, $flagFillStoreRecord = true) { //phpUnit Test: clean environment $this->frArray = array(); $this->indexArray = array(); $this->levelCount = 0; + $this->flagFillStoreRecord = $flagFillStoreRecord; // Iteration over Bodytext $ttLineArray = explode("\n", $bodyText); @@ -601,7 +604,9 @@ class Report { } } - $this->store->appendToStore(STORE_RECORD, $assoc); + if ($this->flagFillStoreRecord) { + $this->store->appendToStore($assoc, STORE_RECORD); + } return ($content); } @@ -762,54 +767,18 @@ class Report { break; case "sendmail": - // '<receiver1>,<receiver2>,...|<sender>|<subject>|<body>|<reply-to>|<flag autosubmit: on /off>' -// $mailarr = explode("|", $columnValue); - $mailConfig = $this->sendmailConvertToken($columnValue); + $sendMail = new SendMail(); + $mailConfig = $sendMail->parseStringToArray($columnValue); if (count($mailConfig) < 4) { throw new SyntaxReportException ("Too few parameter for sendmail: $columnValue", ERROR_TOO_FEW_PARAMETER_FOR_SENDMAIL, null, __FILE__, __LINE__, $this->fr_error); } -// if (!isset($mailarr[SENDMAIL_IDX_REPLY_TO])) { -// $mailarr[SENDMAIL_IDX_REPLY_TO] = ''; -// } -// -// if (!isset($mailarr[SENDMAIL_IDX_FLAG_AUTO_SUBMIT])) { -// $mailarr[SENDMAIL_IDX_FLAG_AUTO_SUBMIT] = 'on'; -// } -// -// if (!isset($mailarr[SENDMAIL_IDX_GR_ID])) { -// $mailarr[SENDMAIL_IDX_GR_ID] = '0'; -// } -// -// if (!isset($mailarr[SENDMAIL_IDX_X_ID])) { -// $mailarr[SENDMAIL_IDX_X_ID] = '0'; -// } -// -// if (!isset($mailarr[SENDMAIL_IDX_RECEIVER_CC])) { -// $mailarr[SENDMAIL_IDX_RECEIVER_CC] = ''; -// } -// -// if (!isset($mailarr[SENDMAIL_IDX_RECEIVER_BCC])) { -// $mailarr[SENDMAIL_IDX_RECEIVER_BCC] = ''; -// } -// -// if (!isset($mailarr[SENDMAIL_IDX_X_ID])) { -// $mailarr[SENDMAIL_IDX_X_ID] = '0'; -// } -// -// if (!isset($mailarr[SENDMAIL_IDX_X_ID2])) { -// $mailarr[SENDMAIL_IDX_X_ID] = '0'; -// } -// -// if (!isset($mailarr[SENDMAIL_IDX_X_ID3])) { -// $mailarr[SENDMAIL_IDX_X_ID] = '0'; -// } - - $mailConfig[SENDMAIL_IDX_SRC] = "Report: T3 pageId=" . $this->store->getVar('pageId', STORE_TYPO3) . + $mailConfig[SENDMAIL_TOKEN_SRC] = "Report: T3 pageId=" . $this->store->getVar('pageId', STORE_TYPO3) . ", T3 ttcontentId=" . $this->store->getVar('ttcontentUid', STORE_TYPO3) . ", Level=" . $full_level; - new Sendmail($mailConfig); + $sendMail->process($mailConfig); + break; case "vertical": @@ -1261,48 +1230,4 @@ class Report { return false; } - private function sendmailConvertToken($data) { - - $sendmailToken = [SENDMAIL_TOKEN_RECEIVER => SENDMAIL_IDX_RECEIVER, - SENDMAIL_TOKEN_SENDER => SENDMAIL_IDX_SENDER, - SENDMAIL_TOKEN_SUBJECT => SENDMAIL_IDX_SUBJECT, - SENDMAIL_TOKEN_BODY => SENDMAIL_IDX_BODY, - SENDMAIL_TOKEN_REPLY_TO => SENDMAIL_IDX_REPLY_TO, - SENDMAIL_TOKEN_RECEIVER_CC => SENDMAIL_IDX_RECEIVER_CC, - SENDMAIL_TOKEN_RECEIVER_BCC => SENDMAIL_IDX_RECEIVER_BCC, - SENDMAIL_TOKEN_HEADER => SENDMAIL_IDX_HEADER, - SENDMAIL_TOKEN_ATTACHMENT => SENDMAIL_IDX_ATTACHMENT, - SENDMAIL_TOKEN_FLAG_AUTO_SUBMIT => SENDMAIL_IDX_FLAG_AUTO_SUBMIT, - SENDMAIL_TOKEN_GR_ID => SENDMAIL_IDX_GR_ID, - SENDMAIL_TOKEN_X_ID => SENDMAIL_IDX_X_ID, - SENDMAIL_TOKEN_SRC => SENDMAIL_IDX_SRC, - SENDMAIL_TOKEN_X_ID2 => SENDMAIL_IDX_X_ID2, - SENDMAIL_TOKEN_X_ID3 => SENDMAIL_IDX_X_ID3 - ]; - - - if (!isset($data[1]) || $data[1] != ':') { - return explode('|', $data); - } - - $segments = explode('|', $data); - $arr = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']; - foreach ($segments AS $line) { - $piece = explode(':', $line, 2); - - if (empty($piece[0])) { - throw new UserReportException ("Missing token in sendmail", ERROR_UNKNOWN_TOKEN); - } - - if (!isset($sendmailToken[$piece[0]])) { - throw new UserReportException ("Unknown token in sendmail: $piece[0]", ERROR_UNKNOWN_TOKEN); - } - - $idx = $sendmailToken[$piece[0]]; - $arr[$idx] = empty($piece[1]) ? '' : $piece[1]; - } - - return ($arr); - } - } diff --git a/extension/qfq/qfq/report/SendMail.php b/extension/qfq/qfq/report/SendMail.php new file mode 100644 index 0000000000000000000000000000000000000000..cf846c4e445583dc33ce1c18b8313325f62e8a21 --- /dev/null +++ b/extension/qfq/qfq/report/SendMail.php @@ -0,0 +1,423 @@ +<?php + +namespace qfq; + +//use qfq; + +require_once(__DIR__ . '/../Constants.php'); +require_once(__DIR__ . '/../database/Database.php'); +require_once(__DIR__ . '/../store/Store.php'); +require_once(__DIR__ . '/../report/Download.php'); +require_once(__DIR__ . '/../helper/Sanitize.php'); +require_once(__DIR__ . '/../helper/HelperFile.php'); +require_once(__DIR__ . '/../helper/Support.php'); + + +class SendMail { + + /** + * @var Store + */ + private $store = null; + + /** + * Sends a mail as specified in $mailarr. + * If there is no receiver specified as 'TO': no mail is sent. This is ok and no error. + * Logs every send mail as a record in table `MailLog`. Additionally a `grId`, `xId` , `xId2` and `xId3` can be specified + * to assign the log entry to a specific action. + * The log record also contains some information who/where generates the mail (form/formelement or QFQ query). + * + * @throws UserFormException + */ + public function __construct() { + + $this->store = Store::getInstance(''); + } + + + /** + * @param array $mailConfig + * @throws UserFormException + */ + public function process(array $mailConfig) { + + // If there is no 'Receiver': do not send a mail. + if (!isset($mailConfig[SENDMAIL_TOKEN_RECEIVER]) || $mailConfig[SENDMAIL_TOKEN_RECEIVER] === '') { + return; + } + + if (count($mailConfig) < 4 || $mailConfig[SENDMAIL_TOKEN_SENDER] === '' || $mailConfig[SENDMAIL_TOKEN_SUBJECT] === '' || $mailConfig[SENDMAIL_TOKEN_BODY] === '') { + throw new UserFormException("Error sendmail missing one of: receiver, sender, subject or body", ERROR_SENDMAIL_MISSING_VALUE); + } + + $redirectAllMail = $this->store->getVar(SYSTEM_REDIRECT_ALL_MAIL_TO, STORE_SYSTEM); + + if ($redirectAllMail !== false) { + $addBody = "All QFQ outgoing mails are catched and redirected to you." . PHP_EOL . "Original receiver:" . PHP_EOL; + $addBody .= 'TO: ' . $mailConfig[SENDMAIL_TOKEN_RECEIVER] . PHP_EOL; + $addBody .= 'CC: ' . $mailConfig[SENDMAIL_TOKEN_RECEIVER_CC] . PHP_EOL; + $addBody .= 'BCC: ' . $mailConfig[SENDMAIL_TOKEN_RECEIVER_BCC] . PHP_EOL; + $addBody .= PHP_EOL . "==========================================" . PHP_EOL . PHP_EOL; + + $mailConfig[SENDMAIL_TOKEN_BODY] = $addBody . $mailConfig[SENDMAIL_TOKEN_BODY]; + + $mailConfig[SENDMAIL_TOKEN_RECEIVER] = $redirectAllMail; + $mailConfig[SENDMAIL_TOKEN_RECEIVER_CC] = ''; + $mailConfig[SENDMAIL_TOKEN_RECEIVER_BCC] = ''; + } + + $mailConfig = $this->setDefault($mailConfig); + + $logAttachments = $this->sendEmail($mailConfig); + $this->mailLog($mailConfig, $logAttachments); + } + + /** + * @param array $mailConfig + * @return array + */ + private function setDefault(array $mailConfig) { + + if (empty($mailConfig[SENDMAIL_TOKEN_FLAG_AUTO_SUBMIT]) || $mailConfig[SENDMAIL_TOKEN_FLAG_AUTO_SUBMIT] === '') { + $mailConfig[SENDMAIL_TOKEN_FLAG_AUTO_SUBMIT] = 'on'; + } + + if (empty($mailConfig[SENDMAIL_TOKEN_SUBJECT_HTML_ENTITY]) || $mailConfig[SENDMAIL_TOKEN_SUBJECT_HTML_ENTITY] !== MODE_ENCODE) { + $mailConfig[SENDMAIL_TOKEN_SUBJECT_HTML_ENTITY] = MODE_DECODE; + } + + if (empty($mailConfig[SENDMAIL_TOKEN_BODY_HTML_ENTITY]) || $mailConfig[SENDMAIL_TOKEN_BODY_HTML_ENTITY] !== MODE_ENCODE) { + $mailConfig[SENDMAIL_TOKEN_BODY_HTML_ENTITY] = MODE_DECODE; + } + + return $mailConfig; + } + + /** + * Use the programm 'sendEmail' - http://caspian.dotconf.net/menu/Software/SendEmail + * Body and Subject is UTF8 encoded. Append attachments with '-a <file>'. + * + * @param array $mailConfig + * @return string + * @throws CodeException + * @throws UserFormException + * @throws UserReportException + */ + private function sendEmail(array $mailConfig) { + $args = array(); + $attachments = array(); + $cmdAttachments = ''; + + $mailConfig[SENDMAIL_TOKEN_SUBJECT] = Support::htmlEntityEncodeDecode($mailConfig[SENDMAIL_TOKEN_SUBJECT_HTML_ENTITY], $mailConfig[SENDMAIL_TOKEN_SUBJECT]); + $mailConfig[SENDMAIL_TOKEN_BODY] = Support::htmlEntityEncodeDecode($mailConfig[SENDMAIL_TOKEN_BODY_HTML_ENTITY], $mailConfig[SENDMAIL_TOKEN_BODY]); + + foreach ($mailConfig as $key => $value) { + if (is_array($value)) { + continue; + } + $mailConfig[$key] = Support::escapeDoubleTick($value); +// $mailConfig[$key] = addslashes($value); + } + + $args[] = '-f "' . $mailConfig[SENDMAIL_TOKEN_SENDER] . '"'; + $args[] = '-t "' . $mailConfig[SENDMAIL_TOKEN_RECEIVER] . '"'; + $args[] = '-o message-charset="utf-8"'; + + $logFile = $this->store->getVar(SYSTEM_MAIL_LOG, STORE_SYSTEM); + if ($logFile != '' && $logFile !== false) { + $args[] = '-l "' . $logFile . '"';; + } + + if (!empty($mailConfig[SENDMAIL_TOKEN_RECEIVER_CC])) { + $args[] = '-cc "' . $mailConfig[SENDMAIL_TOKEN_RECEIVER_CC] . '"';; + } + + if (!empty($mailConfig[SENDMAIL_TOKEN_RECEIVER_BCC])) { + $args[] = '-bcc "' . $mailConfig[SENDMAIL_TOKEN_RECEIVER_BCC]; + } + + if (!empty($mailConfig[SENDMAIL_TOKEN_SUBJECT])) { + + // The subject needs to be encoded to UTF-8 separately - https://stackoverflow.com/questions/4389676/email-from-php-has-broken-subject-header-encoding/27648245#27648245 + $preferences = ["scheme" => "Q", "input-charset" => "UTF-8", "output-charset" => "UTF-8"]; + $encodedSubject = iconv_mime_encode("Subject", $mailConfig[SENDMAIL_TOKEN_SUBJECT], $preferences); + $encodedSubject = substr($encodedSubject, 9); // remove 'Subject: ' + + $args[] = '-u "' . $encodedSubject . '"';; + } + + if (!empty($mailConfig[SENDMAIL_TOKEN_BODY])) { + + $args[] = '-m "' . $mailConfig[SENDMAIL_TOKEN_BODY] . '"';; + } + + if (!empty($mailConfig[SENDMAIL_TOKEN_REPLY_TO])) { + $args[] = '-o reply-to="' . $mailConfig[SENDMAIL_TOKEN_REPLY_TO] . '"';; + } + + if ($mailConfig[SENDMAIL_TOKEN_FLAG_AUTO_SUBMIT] === 'on') { + $args[] = '-o message-header="Auto-Submitted: auto-send"'; + } + + if (!empty($mailConfig[SENDMAIL_TOKEN_ATTACHMENT])) { + $attachments = $this->attachmentsBuild($mailConfig[SENDMAIL_TOKEN_ATTACHMENT]); + if (!empty($attachments)) { + $cmdAttachments = '-a ' . implode(' -a ', $attachments); + $args[] = $cmdAttachments; + } + } + + if (!empty($mailConfig[SENDMAIL_TOKEN_HEADER])) { + $args[] = '-o message-header="' . $mailConfig[SENDMAIL_TOKEN_HEADER] . '"'; + } + + $sendEmail = $this->store->getVar(SYSTEM_SEND_E_MAIL, STORE_SYSTEM); + if (empty($sendEmail)) { + throw new UserFormException("Missing 'sendEmail'", ERROR_SENDMAIL); + } + + $sendEmailOptions = $this->store->getVar(SYSTEM_SEND_E_MAIL_OPTIONS, STORE_SYSTEM); + if (!empty($sendEmailOptions)) { + $args[] = $sendEmailOptions; + } + + OnArray::arrayEscapeshellarg($args); + $cmd = $sendEmail . ' ' . implode(' ', $args); + + exec($cmd, $arr, $rc); + + if ($rc != 0) { + // After first installation of QFQ extension, the PERL script is not executable: is this the problem here? + $perms = fileperms($sendEmail); + if (!($perms & 0x0040)) { + chmod($sendEmail, 0755); + exec($cmd, $arr, $rc); // Give it a second try. + } + + if ($rc != 0) { + $output = $rc . " - " . implode('<br>', $arr) . " - " . $cmd; + throw new UserFormException("Error sendmail failed: " . $output, ERROR_SENDMAIL); + } + } + + HelperFile::cleanTempFiles($attachments); + + return $cmdAttachments; + } + + + /** + * Creates a new MailLog Record based on $mailArr / $header. + * + * @param array $mailConfig + * + * @throws CodeException + * @throws DbException + */ + private function mailLog(array $mailConfig, $attachmentsLine = '') { + + $log = array(); + + $header = 'OoO:' . $mailConfig[SENDMAIL_TOKEN_FLAG_AUTO_SUBMIT]; + if (!empty($mailConfig[SENDMAIL_TOKEN_HEADER])) { + $header .= PHP_EOL . 'Custom: ' . $mailConfig[SENDMAIL_TOKEN_HEADER]; + } + if (!empty($mailConfig[SENDMAIL_TOKEN_ATTACHMENT])) { + $header .= PHP_EOL . 'Attachment: ' . $mailConfig[SENDMAIL_TOKEN_ATTACHMENT]; + } + + // Log + $log[] = $mailConfig[SENDMAIL_TOKEN_RECEIVER]; + $log[] = $mailConfig[SENDMAIL_TOKEN_SENDER]; + $log[] = $mailConfig[SENDMAIL_TOKEN_SUBJECT]; + $log[] = $mailConfig[SENDMAIL_TOKEN_BODY]; + $log[] = $header; + $log[] = $attachmentsLine; + $log[] = empty($mailConfig[SENDMAIL_TOKEN_GR_ID]) ? 0 : $mailConfig[SENDMAIL_TOKEN_GR_ID]; + $log[] = empty($mailConfig[SENDMAIL_TOKEN_X_ID]) ? 0 : $mailConfig[SENDMAIL_TOKEN_X_ID]; + $log[] = empty($mailConfig[SENDMAIL_TOKEN_X_ID2]) ? 0 : $mailConfig[SENDMAIL_TOKEN_X_ID2]; + $log[] = empty($mailConfig[SENDMAIL_TOKEN_X_ID3]) ? 0 : $mailConfig[SENDMAIL_TOKEN_X_ID3]; + $log[] = empty($mailConfig[SENDMAIL_TOKEN_SRC]) ? 0 : $mailConfig[SENDMAIL_TOKEN_SRC]; + + $db = new Database(); + $db->sql('INSERT INTO MailLog (`receiver`, `sender`, `subject`, `body`, `header`, `attach`, `grId`, `xId`, `xId2`, `xId3`, `src`, `modified`, `created`) VALUES ( ?, ? , ? , ? ,?, ?, ? ,?, ?, ?, ?, NOW(), NOW() )', ROW_REGULAR, $log); + + } + + /** + * + * @param array $attachments Array of attachments. Per attachment, different & multiple sources are possible. + * [ [ 0 -> 'F:/etc/hostname' ], + * [ 0 -> 'u:http://nzz.ch', 1 -> 'd:nzz.pdf' ], + * [ 0 -> 'p:id=detailPerson&form=Person&r=1&_sip=1', 1 -> 'F:/etc/hostname' ] + * [ 0 -> 'd:all.pdf', 1 -> 'U:?id=detailPerson&form=Person&r=1&_sip=1', 2 -> 'F:/etc/hostname' ] ] + * @return array Array of filenames. Those files has to be deleted later, if they are temporary files. + * @throws UserReportException + */ + private function attachmentsBuild(array $attachments) { + $files = array(); + $download = new Download(); + $tmpDir = sys_get_temp_dir(); + + // Several attachments are possible. Process one by one. + foreach ($attachments as $attach) { + $vars = array(); + + // Extract Filename + $exportFilename = ''; + $downloadMode = ''; + + // Per attachment: extract 'mode','filename'. Leave the 'sources' untouched. + foreach ($attach as $key => $element) { + $token = $element[0]; + switch ($token) { + + case SENDMAIL_TOKEN_DOWNLOAD_FILENAME: + $exportFilename = substr($element, 2); + unset($attach[$key]); + break; + + case SENDMAIL_TOKEN_DOWNLOAD_MODE: + $downloadMode = substr($element, 2); + unset($attach[$key]); + break; + + case SENDMAIL_TOKEN_ATTACHMENT_FILE: + if ($downloadMode == '') { // Set only if not explicit given. + $downloadMode = DOWNLOAD_MODE_FILE; + } + break; + + case SENDMAIL_TOKEN_ATTACHMENT_URL: + case SENDMAIL_TOKEN_ATTACHMENT_URL_PARAM: + case SENDMAIL_TOKEN_ATTACHMENT_PAGE: + if ($downloadMode == '') { // Set only if not explicit given. + $downloadMode = DOWNLOAD_MODE_PDF; + } + break; + + default: + throw new UserReportException('Unknown token in _sendmail: ' . $token, ERROR_UNKNOWN_TOKEN); + break; + } + } + + $vars[SIP_DOWNLOAD_PARAMETER] = implode(PARAM_DELIMITER, $attach); + $vars[DOWNLOAD_MODE] = (count($attach) > 1) ? DOWNLOAD_MODE_PDF : $downloadMode; + + $file = $download->process($vars, OUTPUT_MODE_FILE); + + if (empty($exportFilename) && HelperFile::isQfqTemp($file)) { + + $exportFilename = DOWNLOAD_OUTPUT_FILENAME; + if ($downloadMode == DOWNLOAD_MODE_PDF) { + $exportFilename .= '.pdf'; + } + } + + // In case an exportFilename is given: move/rename it, if it is necessary. + if (!empty($exportFilename)) { + + $exportFilename = Sanitize::safeFilename($exportFilename, true); + + $dir = HelperFile::mktempdir(); // Attachments might have the same filename - create one directory per attachment, to ensure same filenames do not conflict. + $exportFilename = $dir . '/' . $exportFilename; + + if (HelperFile::isQfqTemp($file)) { + rename($file, $exportFilename); + } else { + copy($file, $exportFilename); + } + $file = $exportFilename; + } + $files[] = $file; + } + + return $files; + } + + + /** + * Convert a token based sendMail string into an array. + * - Each attachment (single file or mulitple concatenated files) is an array in the array. + * + * @param string $data E.g.: 't:john@doe.com|f:jane@miller.com|s:Latest|b:Dear John ...' + * @return array + * @throws UserFormException + * @throws UserReportException + */ + public function parseStringToArray($data) { + + $args = array(); + $attachment = array(); + + $flagConcat = false; + $flagSource = false; + $flagFilename = false; + + $param = explode(PARAM_DELIMITER, $data); + + // Iterate over all parameter: use token as key. Collect corresponding attachments arguments in separate array elements + foreach ($param AS $line) { + + if (empty($line)) { + continue; + } + + $token = $line[0]; + + if (strlen($line) > 2 && $line[1] != PARAM_TOKEN_DELIMITER) { + throw new UserFormException('Missing token delimiter "' . PARAM_TOKEN_DELIMITER . '" in: ' . $line, ERROR_UNKNOWN_TOKEN); + } + + // Check for deprecated token. + if ($token == SENDMAIL_TOKEN_ATTACHMENT_FILE_DEPRECATED) { + throw new UserFormException('Sendmail: Option "a:" is deprecated, please use "' . SENDMAIL_TOKEN_ATTACHMENT_FILE . '" instead', ERROR_UNKNOWN_TOKEN); + } + + switch ($token) { + case SENDMAIL_TOKEN_CONCAT: + $flagConcat = true; + if (!empty($attachment)) { + $args[SENDMAIL_TOKEN_ATTACHMENT][] = $attachment; + $attachment = array(); + } + break; + + case SENDMAIL_TOKEN_ATTACHMENT_FILE: + case SENDMAIL_TOKEN_ATTACHMENT_URL: + case SENDMAIL_TOKEN_ATTACHMENT_URL_PARAM: + case SENDMAIL_TOKEN_ATTACHMENT_PAGE: + if ($flagSource && !$flagConcat) { + $args[SENDMAIL_TOKEN_ATTACHMENT][] = $attachment; + $attachment = array(); + $flagFilename = false; + } + $flagSource = true; + $attachment[] = $line; + break; + + case SENDMAIL_TOKEN_DOWNLOAD_FILENAME: + if ($flagFilename && !$flagConcat) { + $args[SENDMAIL_TOKEN_ATTACHMENT][] = $attachment; + $attachment = array(); + $flagSource = false; + } + $flagFilename = true; + $attachment[] = $line; + break; + + default: + $args[$token] = substr($line, 2); + break; + } + } + + if (!empty($attachment)) { + $args[SENDMAIL_TOKEN_ATTACHMENT][] = $attachment; + } + + return ($args); + } + +} diff --git a/extension/qfq/qfq/report/Sendmail.php b/extension/qfq/qfq/report/Sendmail.php deleted file mode 100644 index 384e026d8e48a1697c7dc17ea3420750465355b0..0000000000000000000000000000000000000000 --- a/extension/qfq/qfq/report/Sendmail.php +++ /dev/null @@ -1,240 +0,0 @@ -<?php - -namespace qfq; - -//use qfq; - -require_once(__DIR__ . '/../Constants.php'); -require_once(__DIR__ . '/../database/Database.php'); -require_once(__DIR__ . '/../store/Store.php'); - - -class Sendmail { - - /** - * @var Store - */ - private $store = null; - - /** - * Sends a mail as specified in $mailarr. - * If there is no receiver specified as 'TO': no mail is sent. This is ok and no error. - * Logs every send mail as a record in table `mailLog`. Additionally a `grId` and a `xId` can be specified - * to assign the log entry to a specific action. - * The log record also contains some information which generates the mail (form/formelement or QFQ query). - * - * Structure mailarr: - * SENDMAIL_IDX_RECEIVER email address(es) - * SENDMAIL_IDX_SENDER email address - * SENDMAIL_IDX_SUBJECT string - * SENDMAIL_IDX_BODY string - * SENDMAIL_IDX_REPLY_TO optional: email address - * SENDMAIL_IDX_FLAG_AUTO_SUBMIT optional: 'on'|'off', default: 'on' - * SENDMAIL_IDX_GR_ID optional: integer - * SENDMAIL_IDX_X_ID optional: integer - * - * @param $mailConfig - * - * @throws UserFormException - */ - public function __construct(array $mailConfig) { - - // If there is no 'Receiver': do not send a mail. - if (!isset($mailConfig[SENDMAIL_IDX_RECEIVER]) || $mailConfig[SENDMAIL_IDX_RECEIVER] === '') { - return; - } - - if (count($mailConfig) < 4 || $mailConfig[SENDMAIL_IDX_SENDER] === '' || $mailConfig[SENDMAIL_IDX_SUBJECT] === '' || $mailConfig[SENDMAIL_IDX_BODY] === '') { - throw new UserFormException("Error sendmail missing one of: receiver, sender, subject or body", ERROR_SENDMAIL_MISSING_VALUE); - } - - $this->store = Store::getInstance(''); - - $redirectAllMail = $this->store->getVar(SYSTEM_REDIRECT_ALL_MAIL_TO, STORE_SYSTEM); - - if ($redirectAllMail !== false) { - $addBody = "All QFQ outgoing mails are catched and redirected to you." . PHP_EOL . "Original receiver:" . PHP_EOL; - $addBody .= 'TO: ' . $mailConfig[SENDMAIL_IDX_RECEIVER] . PHP_EOL; - $addBody .= 'CC: ' . $mailConfig[SENDMAIL_IDX_RECEIVER_CC] . PHP_EOL; - $addBody .= 'BCC: ' . $mailConfig[SENDMAIL_IDX_RECEIVER_BCC] . PHP_EOL; - $addBody .= PHP_EOL . "==========================================" . PHP_EOL . PHP_EOL; - - $mailConfig[SENDMAIL_IDX_BODY] = $addBody . $mailConfig[SENDMAIL_IDX_BODY]; - - $mailConfig[SENDMAIL_IDX_RECEIVER] = $redirectAllMail; - $mailConfig[SENDMAIL_IDX_RECEIVER_CC] = ''; - $mailConfig[SENDMAIL_IDX_RECEIVER_BCC] = ''; - } - - if (empty($mailConfig[SENDMAIL_IDX_FLAG_AUTO_SUBMIT]) || $mailConfig[SENDMAIL_IDX_FLAG_AUTO_SUBMIT] === '') { - $mailConfig[SENDMAIL_IDX_FLAG_AUTO_SUBMIT] = 'on'; - } - -// $header = $this->buildHeader($mailarr); -// if (!(mb_send_mail($mailarr[SENDMAIL_IDX_RECEIVER], $mailarr[SENDMAIL_IDX_SUBJECT], $addBody . $mailarr[SENDMAIL_IDX_BODY], $header, "-f " . $mailarr[SENDMAIL_IDX_SENDER]))) { -// throw new UserFormException("Error sendmail failed.", ERROR_SENDMAIL); -// } - - $this->sendEmail($mailConfig); - $this->mailLog($mailConfig); - } - - /** - * Use the programm 'sendEmail' - http://caspian.dotconf.net/menu/Software/SendEmail - * - * @param array $mailConfig - * @throws CodeException - * @throws UserFormException - */ - private function sendEmail(array $mailConfig) { - $args = array(); - - foreach ($mailConfig as $key => $value) { - $mailConfig[$key] = Support::escapeDoubleTick($value); - } - - $args[] = '-f "' . $mailConfig[SENDMAIL_IDX_SENDER] . '"'; - $args[] = '-t "' . $mailConfig[SENDMAIL_IDX_RECEIVER] . '"'; - $args[] = '-o message-charset="utf-8"'; - $args[] = '-q '; - - $logFile = $this->store->getVar(SYSTEM_MAIL_LOG, STORE_SYSTEM); - if ($logFile != '' && $logFile !== false) { - $args[] = '-l "' . $logFile . '"';; - } - - if (!empty($mailConfig[SENDMAIL_IDX_RECEIVER_CC])) { - $args[] = '-cc "' . $mailConfig[SENDMAIL_IDX_RECEIVER_CC] . '"';; - } - - if (!empty($mailConfig[SENDMAIL_IDX_RECEIVER_BCC])) { - $args[] = '-bcc "' . $mailConfig[SENDMAIL_IDX_RECEIVER_BCC]; - } - - if (!empty($mailConfig[SENDMAIL_IDX_SUBJECT])) { - // The subject needs to be encoded to UTF-8 separately - https://stackoverflow.com/questions/4389676/email-from-php-has-broken-subject-header-encoding/27648245#27648245 - $preferences = ["scheme" => "Q", "input-charset" => "UTF-8", "output-charset" => "UTF-8"]; - $encodedSubject = iconv_mime_encode("Subject", $mailConfig[SENDMAIL_IDX_SUBJECT], $preferences); - $encodedSubject = substr($encodedSubject, 9); // remove 'Subject: ' - - $args[] = '-u "' . $encodedSubject . '"';; - } - - if (!empty($mailConfig[SENDMAIL_IDX_BODY])) { - $args[] = '-m "' . $mailConfig[SENDMAIL_IDX_BODY] . '"';; - } - - if (!empty($mailConfig[SENDMAIL_IDX_REPLY_TO])) { - $args[] = '-o reply-to="' . $mailConfig[SENDMAIL_IDX_REPLY_TO] . '"';; - } - - if ($mailConfig[SENDMAIL_IDX_FLAG_AUTO_SUBMIT] === 'on') { - $args[] = '-o message-header="Auto-Submitted: auto-send"'; - } - - if (!empty($mailConfig[SENDMAIL_IDX_ATTACHMENT])) { - $pieces = explode(',', $mailConfig[SENDMAIL_IDX_ATTACHMENT]); - foreach ($pieces as $piece) { - $args[] = '-a ' . $piece; - } - } - - if (!empty($mailConfig[SENDMAIL_IDX_HEADER])) { - $args[] = '-o message-header="' . $mailConfig[SENDMAIL_IDX_HEADER] . '"'; - } - - $sendEmail = $this->store->getVar(SYSTEM_SEND_E_MAIL, STORE_SYSTEM); - if (empty($sendEmail)) { - throw new UserFormException("Missing 'sendEmail'", ERROR_SENDMAIL); - } - - $sendEmailOptions = $this->store->getVar(SYSTEM_SEND_E_MAIL_OPTIONS, STORE_SYSTEM); - if (!empty($sendEmailOptions)) { - $args[] = $sendEmailOptions; - } - - $cmd = $sendEmail . ' ' . implode(' ', $args); - - exec($cmd, $arr, $rc); - if ($rc != 0) { - // After first installation of QFQ extension, the PERL script is not executable: is this the case? - $perms = fileperms($sendEmail); - if (!($perms & 0x0040)) { - chmod($sendEmail, 0755); - exec($cmd, $arr, $rc); // Give it a second try. - } - - if ($rc != 0) { - $output = $rc . " - " . implode('<br>', $arr); - throw new UserFormException("Error sendmail failed: " . $output, ERROR_SENDMAIL); - } - } - } - - /** - * @param array $mailConfig - * - * @return string - */ - private function buildHeader(array $mailConfig) { - - // "\r\n" needs to be enclosed in double ticks to correctly converted to 0x0d 0x0a, - $header = "From: " . $mailConfig[SENDMAIL_IDX_SENDER] . "\r\n"; - - if (isset($mailConfig[SENDMAIL_IDX_REPLY_TO]) && $mailConfig[SENDMAIL_IDX_REPLY_TO] != '') { - $header .= "Reply-To: " . $mailConfig[SENDMAIL_IDX_REPLY_TO] . "\r\n"; - } - - // By default 'on' - if (!isset($mailConfig[SENDMAIL_IDX_FLAG_AUTO_SUBMIT]) || $mailConfig[SENDMAIL_IDX_FLAG_AUTO_SUBMIT] === 'on') { - $header .= "Auto-Submitted: auto-send\r\n"; - } - - if (isset($mailConfig[SENDMAIL_IDX_RECEIVER_CC]) && $mailConfig[SENDMAIL_IDX_RECEIVER_CC] != '') { - $header .= "Cc: " . $mailConfig[SENDMAIL_IDX_RECEIVER_CC] . "\r\n"; - } - - if (isset($mailConfig[SENDMAIL_IDX_RECEIVER_BCC]) && $mailConfig[SENDMAIL_IDX_RECEIVER_BCC] != '') { - $header .= "Bcc: " . $mailConfig[SENDMAIL_IDX_RECEIVER_BCC] . "\r\n"; - } - - return $header; - } - - /** - * Creates a new MailLog Record based on $mailArr / $header. - * - * @param array $mailConfig - * - * @throws CodeException - * @throws DbException - */ - private function mailLog(array $mailConfig) { - - $log = array(); - - $header = 'OoO:' . $mailConfig[SENDMAIL_IDX_FLAG_AUTO_SUBMIT]; - if (!empty($mailConfig[SENDMAIL_IDX_HEADER])) { - $header .= PHP_EOL . 'Custom: ' . $mailConfig[SENDMAIL_IDX_HEADER]; - } - if (!empty($mailConfig[SENDMAIL_IDX_ATTACHMENT])) { - $header .= PHP_EOL . 'Attachment: ' . $mailConfig[SENDMAIL_IDX_ATTACHMENT]; - } - - // Log - $log[] = $mailConfig[SENDMAIL_IDX_RECEIVER]; - $log[] = $mailConfig[SENDMAIL_IDX_SENDER]; - $log[] = $mailConfig[SENDMAIL_IDX_SUBJECT]; - $log[] = $mailConfig[SENDMAIL_IDX_BODY]; - $log[] = $header; - $log[] = empty($mailConfig[SENDMAIL_IDX_GR_ID]) ? 0 : $mailConfig[SENDMAIL_IDX_GR_ID]; - $log[] = empty($mailConfig[SENDMAIL_IDX_X_ID]) ? 0 : $mailConfig[SENDMAIL_IDX_X_ID]; - $log[] = empty($mailConfig[SENDMAIL_IDX_X_ID2]) ? 0 : $mailConfig[SENDMAIL_IDX_X_ID2]; - $log[] = empty($mailConfig[SENDMAIL_IDX_X_ID3]) ? 0 : $mailConfig[SENDMAIL_IDX_X_ID3]; - $log[] = empty($mailConfig[SENDMAIL_IDX_SRC]) ? 0 : $mailConfig[SENDMAIL_IDX_SRC]; - - $db = new Database(); - $db->sql('INSERT INTO MailLog (`receiver`, `sender`, `subject`, `body`, `header`, `grId`, `xId`, `xId2`, `xId3`, `src`, `modified`, `created`) VALUES ( ?, ? , ? , ? ,?, ?, ? ,?, ?, ?, NOW(), NOW() )', ROW_REGULAR, $log); - - } -} diff --git a/extension/qfq/qfq/store/FillStoreForm.php b/extension/qfq/qfq/store/FillStoreForm.php index 7ed403ecf04cb5321fb81735429f5ffadd1294c6..cf5f770b8f936d7059a1ba3a8e1d0b67f95f9ca4 100644 --- a/extension/qfq/qfq/store/FillStoreForm.php +++ b/extension/qfq/qfq/store/FillStoreForm.php @@ -241,7 +241,8 @@ class FillStoreForm { if ($val !== '') { $val = Sanitize::sanitize($val, $formElement[FE_CHECK_TYPE], $formElement[FE_CHECK_PATTERN], SANITIZE_EXCEPTION); if ($formElement[FE_ENCODE] === FE_ENCODE_SPECIALCHAR) { - $val = htmlspecialchars($val, ENT_QUOTES); +// $val = htmlspecialchars($val, ENT_QUOTES); + $val = Support::htmlEntityEncodeDecode(MODE_ENCODE, $val); } } $newValues[$formElement[FE_NAME]] = $val; diff --git a/extension/qfq/qfq/store/Store.php b/extension/qfq/qfq/store/Store.php index 4d8f7931dc9dc22d21bc2922a8af37a3e5d1c408..6d992bc066444a795bafc50fe34f3b06b4f5e4d4 100644 --- a/extension/qfq/qfq/store/Store.php +++ b/extension/qfq/qfq/store/Store.php @@ -170,10 +170,9 @@ class Store { self::fillStoreTypo3($bodytext); // should be filled before fillStoreSystem() to offer T3 variables self::fillStoreClient(); // should be filled before fillStoreSystem() to offer Client variables + self::fillStoreExtra(); // should be filled before fillStoreSystem() to restore variables like SYSTEM_SHOW_DEBUG_INFO self::fillStoreSystem($fileConfigIni); self::fillStoreSip(); - self::fillStoreExtra(); - } /** @@ -273,10 +272,8 @@ class Store { private static function adjustConfigShowDebugInfo($value, $flag) { // Check if SHOW_DEBUG_INFO contains 'auto'. Replace with appropriate. - if (Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_AUTO, $value)) { - $replace = $flag ? SYSTEM_SHOW_DEBUG_INFO_YES : SYSTEM_SHOW_DEBUG_INFO_NO; - - $value = str_replace(SYSTEM_SHOW_DEBUG_INFO_AUTO, $replace, $value); + if (Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_AUTO, $value) && $flag) { + $value = str_replace(SYSTEM_SHOW_DEBUG_INFO_AUTO, SYSTEM_SHOW_DEBUG_INFO_YES, $value); } return $value; @@ -514,8 +511,10 @@ class Store { } else { self::setStore($_SESSION[SESSION_NAME][STORE_EXTRA], STORE_EXTRA, true); } + } + /** * Returns a pointer to this Class. * @@ -869,17 +868,21 @@ class Store { * Append an array (in case of 'array of array': the first row of array) to store $storeName. * Existing values will be overwritten. * + * @param array $dataArray - in special cases it might be an empty string -therefore no type definition to 'array'. * @param $storeName - * @param array $values * @throws CodeException * @throws UserFormException */ - public static function appendToStore($storeName, array $values) { + public static function appendToStore($dataArray, $storeName) { + + if (empty($dataArray) || !is_array($dataArray)) { + return; + } - if (isset($values[0]) && is_array($values[0])) { - $append = $values[0]; + if (isset($dataArray[0]) && is_array($dataArray[0])) { + $append = $dataArray[0]; } else { - $append = $values; + $append = $dataArray; } if (empty($append)) { diff --git a/extension/qfq/sql/formEditor.sql b/extension/qfq/sql/formEditor.sql index c8ed7679bc903ab0de7caa406af0b199565b6862..0fa2345e0755c10c81ca7ae50f7a64c2217d00de 100644 --- a/extension/qfq/sql/formEditor.sql +++ b/extension/qfq/sql/formEditor.sql @@ -357,20 +357,20 @@ VALUES #DROP TABLE IF EXISTS `MailLog`; CREATE TABLE IF NOT EXISTS `MailLog` ( - `id` INT(11) NOT NULL AUTO_INCREMENT, - `grId` INT(11) NOT NULL DEFAULT '0', - `xId` INT(11) NOT NULL DEFAULT '0', - `xId2` INT(11) NOT NULL DEFAULT '0', - `xId3` INT(11) NOT NULL DEFAULT '0', - `receiver` TEXT NOT NULL, - `sender` VARCHAR(255) NOT NULL DEFAULT '', - `subject` VARCHAR(255) NOT NULL DEFAULT '', - `body` TEXT NOT NULL, - `header` VARCHAR(255) NOT NULL DEFAULT '', - `attach` VARCHAR(255) NOT NULL DEFAULT '', - `src` VARCHAR(255) NOT NULL DEFAULT '', - `modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - `created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', + `id` INT(11) NOT NULL AUTO_INCREMENT, + `grId` INT(11) NOT NULL DEFAULT '0', + `xId` INT(11) NOT NULL DEFAULT '0', + `xId2` INT(11) NOT NULL DEFAULT '0', + `xId3` INT(11) NOT NULL DEFAULT '0', + `receiver` TEXT NOT NULL, + `sender` VARCHAR(255) NOT NULL DEFAULT '', + `subject` VARCHAR(255) NOT NULL DEFAULT '', + `body` TEXT NOT NULL, + `header` VARCHAR(255) NOT NULL DEFAULT '', + `attach` VARCHAR(1024) NOT NULL DEFAULT '', + `src` VARCHAR(255) NOT NULL DEFAULT '', + `modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`id`) ) diff --git a/extension/qfq/tests/phpunit/BodytextParserTest.php b/extension/qfq/tests/phpunit/BodytextParserTest.php index 42ac80c5773aec1f45e8274141717d59fcf474b9..665f08029ad4eeb4b354400656055207211f822b 100644 --- a/extension/qfq/tests/phpunit/BodytextParserTest.php +++ b/extension/qfq/tests/phpunit/BodytextParserTest.php @@ -344,6 +344,25 @@ EOF; } + public function testProcessPlainEscape() { + $btp = new BodytextParser(); + + // Simple row, nothing to remove + $given = "10.sql = SELECT 'Hello World', 'p:id=1&\\\ngrId=2"; + $expected = "10.sql = SELECT 'Hello World', 'p:id=1&grId=2"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + $given = "10.sql = SELECT 'Hello World', 'p:id=1& \\\n grId=2 "; + $expected = "10.sql = SELECT 'Hello World', 'p:id=1&grId=2"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + $given = "10.sql = SELECT 'Hello World', 'p:id=1& \\\n grId=2 \\\n 20.sql = SELECT ''"; + $expected = "10.sql = SELECT 'Hello World', 'p:id=1&grId=2\n20.sql = SELECT ''"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + } /** * @expectedException \qfq\UserFormException @@ -352,7 +371,7 @@ EOF; public function testProcessExceptionClose() { $btp = new BodytextParser(); - // Nested: unclosed close bracket + // Nested: unmatched close bracket $btp->process("10.sql = SELECT 'Hello World'\n } \n30.sql = SELECT 'Hello World'\n"); } @@ -364,7 +383,7 @@ EOF; public function testProcessExceptionOpen() { $btp = new BodytextParser(); - // Nested: unclosed open bracket + // Nested: unmatched open bracket $btp->process("10.sql = SELECT 'Hello World'\n20 { \n30.sql = SELECT 'Hello World'\n"); } diff --git a/extension/qfq/tests/phpunit/BuildFormPlainTest.php b/extension/qfq/tests/phpunit/BuildFormPlainTest.php index 9bc31841c34a9cf172b7cf99c02a13d7fd2ec0ff..613234614ad86abf8213d66934093133b81517ec 100644 --- a/extension/qfq/tests/phpunit/BuildFormPlainTest.php +++ b/extension/qfq/tests/phpunit/BuildFormPlainTest.php @@ -77,6 +77,7 @@ class BuildFormPlainTest extends AbstractDatabaseTest { $build = new \qfq\BuildFormPlain($form, array(), [$formElement], $this->dbArray); $label['123-l'][API_ELEMENT_CONTENT] = '<label for="name:1" class="control-label" >Name</label>'; + $label['123'][API_ELEMENT_ATTRIBUTE] = ['value' => '']; $label['123-r'][API_ELEMENT_ATTRIBUTE] = ['class' => '']; $result = $build->buildInput($formElement, 'name:1', '', $json); @@ -155,6 +156,7 @@ class BuildFormPlainTest extends AbstractDatabaseTest { $formElement['placeholder'] = 'Please type a name'; $result = $build->buildInput($formElement, 'name:1', 'Hello World', $json); $this->assertEquals('<input id="123" name="name:1" class="form-control" maxlength="255" type="input" size="40" value="Hello World" placeholder="Please type a name" title="Nice Tooltip" data-hidden="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); + $label['123'][API_ELEMENT_ATTRIBUTE] = ['value' => 'Hello World']; $this->assertEquals(['disabled' => false, FE_MODE_REQUIRED => '', 'form-element' => 'name:1', 'value' => 'Hello World', 'disabled' => false, API_ELEMENT_UPDATE => $label], $json); // textarea @@ -463,7 +465,7 @@ class BuildFormPlainTest extends AbstractDatabaseTest { $formElement = [ 'id' => 123, - FE_ID => 1, + FE_FORM_ID => 1, FE_ID_CONTAINER => 0, FE_DYNAMIC_UPDATE => 'no', 'enabled' => 'yes', diff --git a/extension/qfq/tests/phpunit/HelperFormElementTest.php b/extension/qfq/tests/phpunit/HelperFormElementTest.php index 3b84b666b6b462ba5134d605dad16317734e7ab4..39fe1a28457a4f92cf87b8d9a64d63fd412950c5 100644 --- a/extension/qfq/tests/phpunit/HelperFormElementTest.php +++ b/extension/qfq/tests/phpunit/HelperFormElementTest.php @@ -101,7 +101,7 @@ class HelperFormElementTest extends \PHPUnit_Framework_TestCase { $list = [FE_TYPE, FE_SLAVE_ID, FE_SQL_VALIDATE, FE_SQL_BEFORE, FE_SQL_INSERT, FE_SQL_UPDATE, FE_SQL_DELETE, FE_SQL_AFTER, FE_EXPECT_RECORDS, FE_REQUIRED_LIST, FE_MESSAGE_FAIL, FE_SENDMAIL_TO, FE_SENDMAIL_CC, FE_SENDMAIL_BCC, FE_SENDMAIL_FROM, FE_SENDMAIL_SUBJECT, FE_SENDMAIL_REPLY_TO, FE_SENDMAIL_FLAG_AUTO_SUBMIT, - FE_SENDMAIL_GR_ID, FE_SENDMAIL_X_ID]; + FE_SENDMAIL_GR_ID, FE_SENDMAIL_X_ID, FE_SENDMAIL_X_ID2, FE_SENDMAIL_X_ID3]; $expect = array(); foreach ($list as $key) { diff --git a/extension/qfq/tests/phpunit/ReportTest.php b/extension/qfq/tests/phpunit/ReportTest.php index fb2d0badd2a5936f3dce5b658144ad70c500892a..33043d26d7787b26fe7247b259719756caf34bdb 100644 --- a/extension/qfq/tests/phpunit/ReportTest.php +++ b/extension/qfq/tests/phpunit/ReportTest.php @@ -1073,6 +1073,7 @@ EOF; } + /** * */ @@ -1087,7 +1088,6 @@ EOF; // TODO: implement } - /** * @throws Exception */ diff --git a/extension/qfq/tests/phpunit/SaveTest.php b/extension/qfq/tests/phpunit/SaveTest.php index 82dd3ebd0dc7fb7b1b5a359237f9c0ecaefcb578..63d5721da065db29df7bca9243423564943d0fc6 100644 --- a/extension/qfq/tests/phpunit/SaveTest.php +++ b/extension/qfq/tests/phpunit/SaveTest.php @@ -19,7 +19,7 @@ class SaveTest extends AbstractDatabaseTest { public function testUpdateRecord() { - $save = new qfq\Save(array(), array(), array()); + $save = new qfq\Save(array(), array(), array(), array()); // $db = new qfq\Database(); $values = ['name' => 'Doe', 'firstName' => 'John']; @@ -31,13 +31,13 @@ class SaveTest extends AbstractDatabaseTest { $sql = "SELECT name, firstName FROM Person WHERE id = ? "; $result = $this->dbArray[DB_INDEX_DATA_DEFAULT]->sql($sql, ROW_REGULAR, [$id]); - $this->assertEquals($values, $result[0]); + $this->assertEquals($values, $result[0], array()); } public function testInsertRecord() { - $save = new qfq\Save(array(), array(), array()); + $save = new qfq\Save(array(), array(), array(), array()); // $db = new qfq\Database(); $values = ['name' => 'Doe', 'firstName' => 'John']; diff --git a/extension/qfq/tests/phpunit/SendMailTest.php b/extension/qfq/tests/phpunit/SendMailTest.php new file mode 100644 index 0000000000000000000000000000000000000000..aa49feb6d8428af1530e37c95c532053fae8f90c --- /dev/null +++ b/extension/qfq/tests/phpunit/SendMailTest.php @@ -0,0 +1,92 @@ +<?php + + +namespace qfq; + +use qfq; + +require_once(__DIR__ . '/../../qfq/report/SendMail.php'); + + +/** + * Created by PhpStorm. + * User: crose + * Date: 2/2/16 + * Time: 10:07 PM + */ +class SendMailTest extends \PHPUnit_Framework_TestCase { + + /** + * @var qfq\SendMail + */ + private $sendMail = null; + + public function testParseStringToArray() { + + $this->sendMail = new SendMail(); + + // Minimal setup + $result = $this->sendMail->parseStringToArray(''); + $expect = []; + $this->assertEquals($expect, $result); + + // Simple 'fixed position' DEPRECATED - not sure if it's ok that the array is not fillled up to the maximum. +// $result = $this->sendMail->parseStringToArray('john@doe.com|jane@miller.com|Latest|Dear John'); +// $expect = ['john@doe.com', 'jane@miller.com', 'Latest', 'Dear John']; +// $this->assertEquals($expect, $result); + + // Simple 'token based' + $result = $this->sendMail->parseStringToArray('t:john@doe.com|f:jane@miller.com|s:Latest|b:Dear John'); + $expect = [SENDMAIL_TOKEN_RECEIVER => 'john@doe.com', SENDMAIL_TOKEN_SENDER => 'jane@miller.com', + SENDMAIL_TOKEN_SUBJECT => 'Latest', SENDMAIL_TOKEN_BODY => 'Dear John']; + $this->assertEquals($expect, $result); + + // All (but attachment) 'token based' + $result = $this->sendMail->parseStringToArray('t:john@doe.com|f:jane@miller.com|s:Latest|b:Dear John|r:reply@doe.com|A:on|g:123|x:234|c:july@doe.com,steve@doe.com|B:ceo@doe.com|h:Auto-Submit: fake|y:345|z:456|S:test.php'); + $expect = [SENDMAIL_TOKEN_RECEIVER => 'john@doe.com', SENDMAIL_TOKEN_SENDER => 'jane@miller.com', + SENDMAIL_TOKEN_SUBJECT => 'Latest', SENDMAIL_TOKEN_BODY => 'Dear John', SENDMAIL_TOKEN_REPLY_TO => 'reply@doe.com', + SENDMAIL_TOKEN_FLAG_AUTO_SUBMIT => 'on', SENDMAIL_TOKEN_GR_ID => '123', SENDMAIL_TOKEN_X_ID => '234', + SENDMAIL_TOKEN_RECEIVER_CC => 'july@doe.com,steve@doe.com', SENDMAIL_TOKEN_RECEIVER_BCC => 'ceo@doe.com', + SENDMAIL_TOKEN_HEADER => 'Auto-Submit: fake', SENDMAIL_TOKEN_X_ID2 => '345', SENDMAIL_TOKEN_X_ID3 => '456', SENDMAIL_TOKEN_SRC => 'test.php']; + $this->assertEquals($expect, $result); + + // Single attachment 'token based' + $attach = [['F:fileadmin/test1.pdf']]; + $result = $this->sendMail->parseStringToArray('t:john@doe.com|f:jane@miller.com|s:Latest|b:Dear John|F:fileadmin/test1.pdf'); + $expect = [SENDMAIL_TOKEN_RECEIVER => 'john@doe.com', SENDMAIL_TOKEN_SENDER => 'jane@miller.com', + SENDMAIL_TOKEN_SUBJECT => 'Latest', SENDMAIL_TOKEN_BODY => 'Dear John', SENDMAIL_TOKEN_ATTACHMENT => $attach]; + + $this->assertEquals($expect, $result); + + // Three individual attachment 'token based' +// $attach = [ [ [ 'F' => 'fileadmin/test1.pdf' ], [ 'F' => 'fileadmin/test2.pdf' ], [ 'F' => 'fileadmin/test3.pdf' ] ] ] ; + $attach = [['F:fileadmin/test1.pdf'], ['F:fileadmin/test2.pdf'], ['F:fileadmin/test3.pdf']]; + $result = $this->sendMail->parseStringToArray('t:john@doe.com|f:jane@miller.com|s:Latest|b:Dear John|F:fileadmin/test1.pdf|F:fileadmin/test2.pdf|F:fileadmin/test3.pdf'); + $expect = [SENDMAIL_TOKEN_RECEIVER => 'john@doe.com', SENDMAIL_TOKEN_SENDER => 'jane@miller.com', + SENDMAIL_TOKEN_SUBJECT => 'Latest', SENDMAIL_TOKEN_BODY => 'Dear John', SENDMAIL_TOKEN_ATTACHMENT => $attach]; + $this->assertEquals($expect, $result); + + // One individual attachment, one dual combined attachment + $attach = [['F:fileadmin/test1.pdf'], ['F:fileadmin/test2.pdf', 'F:fileadmin/test3.pdf']]; + $result = $this->sendMail->parseStringToArray('t:john@doe.com|f:jane@miller.com|s:Latest|b:Dear John|F:fileadmin/test1.pdf|C|F:fileadmin/test2.pdf|F:fileadmin/test3.pdf'); + $expect = [SENDMAIL_TOKEN_RECEIVER => 'john@doe.com', SENDMAIL_TOKEN_SENDER => 'jane@miller.com', + SENDMAIL_TOKEN_SUBJECT => 'Latest', SENDMAIL_TOKEN_BODY => 'Dear John', SENDMAIL_TOKEN_ATTACHMENT => $attach]; + $this->assertEquals($expect, $result); + + // One individual attachment, one quad combined attachment + $attach = [['F:fileadmin/test1.pdf'], ['F:fileadmin/test2.pdf', 'F:fileadmin/test3.pdf', 'u:http://nzz.ch', 'U:export&a=100']]; + $result = $this->sendMail->parseStringToArray('t:john@doe.com|f:jane@miller.com|s:Latest|b:Dear John|F:fileadmin/test1.pdf|C|F:fileadmin/test2.pdf|F:fileadmin/test3.pdf|u:http://nzz.ch|U:export&a=100'); + $expect = [SENDMAIL_TOKEN_RECEIVER => 'john@doe.com', SENDMAIL_TOKEN_SENDER => 'jane@miller.com', + SENDMAIL_TOKEN_SUBJECT => 'Latest', SENDMAIL_TOKEN_BODY => 'Dear John', SENDMAIL_TOKEN_ATTACHMENT => $attach]; + $this->assertEquals($expect, $result); + + // Two quad combined attachments + $attach = [['d:output1.pdf', 'F:fileadmin/test11.pdf', 'F:fileadmin/test12.pdf', 'u:http://nzz.ch.1', 'U:export1&a=100'], ['d:output2.pdf', 'F:fileadmin/test21.pdf', 'F:fileadmin/test22.pdf', 'u:http://nzz.ch.2', 'U:export2&a=100']]; + $result = $this->sendMail->parseStringToArray('t:john@doe.com|f:jane@miller.com|s:Latest|b:Dear John|C|d:output1.pdf|F:fileadmin/test11.pdf|F:fileadmin/test12.pdf|u:http://nzz.ch.1|U:export1&a=100|C|d:output2.pdf|F:fileadmin/test21.pdf|F:fileadmin/test22.pdf|u:http://nzz.ch.2|U:export2&a=100'); + $expect = [SENDMAIL_TOKEN_RECEIVER => 'john@doe.com', SENDMAIL_TOKEN_SENDER => 'jane@miller.com', + SENDMAIL_TOKEN_SUBJECT => 'Latest', SENDMAIL_TOKEN_BODY => 'Dear John', SENDMAIL_TOKEN_ATTACHMENT => $attach]; + $this->assertEquals($expect, $result); + + } +} + diff --git a/extension/qfq/tests/phpunit/StoreTest.php b/extension/qfq/tests/phpunit/StoreTest.php index 061dd4c9e61b06a2fa0e9fee5b84b23039b25557..ef931d54a2e577699847f6f6a31fdf3ba135fe0e 100644 --- a/extension/qfq/tests/phpunit/StoreTest.php +++ b/extension/qfq/tests/phpunit/StoreTest.php @@ -212,6 +212,41 @@ class StoreTest extends \PHPUnit_Framework_TestCase { $this->assertEquals(false, $this->store->getStore(STORE_SYSTEM), "Retrieve system store."); } + /** + * + */ + public function testAppendToStore() { + $this->store->unsetStore(STORE_RECORD); + + $this->store->appendToStore(array(), STORE_RECORD); + $this->assertEquals(array(), $this->store->getStore(STORE_RECORD)); + + $param = ['a' => 'hello', 'b' => 'world']; + $this->store->appendToStore($param, STORE_RECORD); + $this->assertEquals($param, $this->store->getStore(STORE_RECORD)); + + $this->store->appendToStore(null, STORE_RECORD); + $this->assertEquals($param, $this->store->getStore(STORE_RECORD)); + + $this->store->appendToStore('', STORE_RECORD); + $this->assertEquals($param, $this->store->getStore(STORE_RECORD)); + + $this->store->appendToStore(array(), STORE_RECORD); + $this->assertEquals($param, $this->store->getStore(STORE_RECORD)); + + $param2 = ['c' => 'nice', 'd' => 'job']; + $this->store->appendToStore($param2, STORE_RECORD); + $this->assertEquals(array_merge($param, $param2), $this->store->getStore(STORE_RECORD)); + + $param3 = ['a' => 'well', 'b' => 'done']; + $this->store->appendToStore($param3, STORE_RECORD); + $this->assertEquals(array_merge($param3, $param2), $this->store->getStore(STORE_RECORD)); + + $this->store->setStore($param, STORE_RECORD, true); + $this->store->appendToStore([$param2, $param3, $param3], STORE_RECORD); + $this->assertEquals(array_merge($param, $param2), $this->store->getStore(STORE_RECORD)); + } + /** * @param $body * @@ -256,7 +291,7 @@ EOT; SYSTEM_DB_INDEX_QFQ => '1', SYSTEM_DATE_FORMAT => 'yyyy-mm-dd', - SYSTEM_SHOW_DEBUG_INFO => SYSTEM_SHOW_DEBUG_INFO_NO, + SYSTEM_SHOW_DEBUG_INFO => SYSTEM_SHOW_DEBUG_INFO_AUTO, F_BS_COLUMNS => '12', F_BS_LABEL_COLUMNS => '3', diff --git a/extension/qfq/tests/phpunit/SupportTest.php b/extension/qfq/tests/phpunit/SupportTest.php index 03b184d6e8da4f979613e42beeaf20f9dd99d5d0..19a3df83a41004887fd5913f4944e2f836a79d4d 100644 --- a/extension/qfq/tests/phpunit/SupportTest.php +++ b/extension/qfq/tests/phpunit/SupportTest.php @@ -566,6 +566,43 @@ class SupportTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('Hello World', Support::unWrapTag('<p>', '<p>Hello World</p>')); } + public function testHtmlEntityEncodeDecode() { + $this->assertEquals('', Support::htmlEntityEncodeDecode(MODE_NONE, '')); + $this->assertEquals('test', Support::htmlEntityEncodeDecode(MODE_NONE, 'test')); + $this->assertEquals('te"st', Support::htmlEntityEncodeDecode(MODE_NONE, 'te"st')); + $this->assertEquals("te'st", Support::htmlEntityEncodeDecode(MODE_NONE, "te'st")); + $this->assertEquals("te's\\'t", Support::htmlEntityEncodeDecode(MODE_NONE, "te's\\'t")); + $this->assertEquals("te & st", Support::htmlEntityEncodeDecode(MODE_NONE, "te & st")); + $this->assertEquals("te & & st", Support::htmlEntityEncodeDecode(MODE_NONE, "te & & st")); + $this->assertEquals("te < st", Support::htmlEntityEncodeDecode(MODE_NONE, "te < st")); + $this->assertEquals("te > st", Support::htmlEntityEncodeDecode(MODE_NONE, "te > st")); + $this->assertEquals("te < st", Support::htmlEntityEncodeDecode(MODE_NONE, "te < st")); + $this->assertEquals("te > st", Support::htmlEntityEncodeDecode(MODE_NONE, "te > st")); + + $this->assertEquals('', Support::htmlEntityEncodeDecode(MODE_ENCODE, '')); + $this->assertEquals('test', Support::htmlEntityEncodeDecode(MODE_ENCODE, 'test')); + $this->assertEquals('te"st', Support::htmlEntityEncodeDecode(MODE_ENCODE, 'te"st')); + $this->assertEquals('te'st', Support::htmlEntityEncodeDecode(MODE_ENCODE, "te'st")); + $this->assertEquals('te's\'t', Support::htmlEntityEncodeDecode(MODE_ENCODE, "te's\\'t")); + $this->assertEquals('te & st', Support::htmlEntityEncodeDecode(MODE_ENCODE, "te & st")); + $this->assertEquals('te & &amp; st', Support::htmlEntityEncodeDecode(MODE_ENCODE, "te & & st")); + $this->assertEquals("te < st", Support::htmlEntityEncodeDecode(MODE_ENCODE, "te < st")); + $this->assertEquals("te > st", Support::htmlEntityEncodeDecode(MODE_ENCODE, "te > st")); + $this->assertEquals("te &lt; st", Support::htmlEntityEncodeDecode(MODE_ENCODE, "te < st")); + $this->assertEquals("te &gt; st", Support::htmlEntityEncodeDecode(MODE_ENCODE, "te > st")); + + $this->assertEquals('', Support::htmlEntityEncodeDecode(MODE_DECODE, '')); + $this->assertEquals('test', Support::htmlEntityEncodeDecode(MODE_DECODE, 'test')); + $this->assertEquals('te"st', Support::htmlEntityEncodeDecode(MODE_DECODE, 'te"st')); + $this->assertEquals("te'st", Support::htmlEntityEncodeDecode(MODE_DECODE, "te'st")); + $this->assertEquals("te's\\'t", Support::htmlEntityEncodeDecode(MODE_DECODE, "te's\\'t")); + $this->assertEquals('te & st', Support::htmlEntityEncodeDecode(MODE_DECODE, "te & st")); + $this->assertEquals('te & & st', Support::htmlEntityEncodeDecode(MODE_DECODE, "te & & st")); + $this->assertEquals("te < st", Support::htmlEntityEncodeDecode(MODE_DECODE, "te < st")); + $this->assertEquals("te > st", Support::htmlEntityEncodeDecode(MODE_DECODE, "te > st")); + } + + protected function setUp() { parent::setUp(); diff --git a/less/qfq-bs.css.less b/less/qfq-bs.css.less index a55986c79d664f5194060b60a160749c296c2746..509b3d4f38a648f4546bf02b18eb743d237cdb51 100644 --- a/less/qfq-bs.css.less +++ b/less/qfq-bs.css.less @@ -38,7 +38,7 @@ i.@{spinner_class} { margin-top: 10px; } -/*inline elements in horizontal mode are too much left*/ +/* inline elements in horizontal mode are too much left */ .form-horizontal { .form-inline { .form-group { @@ -337,4 +337,3 @@ i.@{spinner_class} { a.noclick { pointer-events: none; } - diff --git a/less/qfq-letter.css.less b/less/qfq-letter.css.less new file mode 100644 index 0000000000000000000000000000000000000000..0f0f6e5ab038f634141b2288a18181cae0fae39f --- /dev/null +++ b/less/qfq-letter.css.less @@ -0,0 +1,77 @@ +// PDF Letter classes + +body { + font-family: Arial, sans-serif; +} + +header { + height: 205px; +} + +h1 { + font-size: 1em; + font-weight: bold; +} + +.letter-unit { + position: absolute; + /* background-color: yellow; */ + display: inline-block; + font-size: 0.9em; + left: 585px; + top: 0; + width: 285px; + height: 205px; +} + +.letter-logo { + position: absolute; + top: 0; + left: 28px; + width: 235px; +} + +.letter-title { + font-size: 1.1em; + font-weight: bold; +} + +.letter-sender { + position: relative; + display: inline-block; + /* background-color: lime; */ + font-size: 0.9em; + left: 118px; + top: 0; + width: 285px; + height: 250px; +} + +.letter-receiver { + position: relative; + display: inline-block; + top: 31px; + left: 111px; + width: 455px; + height: 250px; + /* background-color: red; */ +} + +.letter-date { + position: relative; + display: block; + /* background-color: blue; */ + top: -43px; + left: 111px; + width: 750px; +} + +.letter-no-break { + page-break-inside: avoid; +} + +.letter-body { + margin-left: 111px; + width: 750px; + /* background-color: orange; */ +} diff --git a/patches/sendEmail.patch b/patches/sendEmail.patch new file mode 100644 index 0000000000000000000000000000000000000000..220b7d62a33f90a8429451fb5c9073e354916c4a --- /dev/null +++ b/patches/sendEmail.patch @@ -0,0 +1,20 @@ +--- sendEmail 2017-11-21 21:02:47.202887534 +0100 ++++ sendEmail.new 2017-12-13 20:10:20.638913440 +0100 +@@ -46,7 +46,7 @@ + my %conf = ( + ## General + "programName" => $0, ## The name of this program +- "version" => '1.56', ## The version of this program ++ "version" => '1.56p1', ## The version of this program + "authorName" => 'Brandon Zehm', ## Author's Name + "authorEmail" => 'caspian@dotconf.net', ## Author's Email Address + "timezone" => '+0000', ## We always use +0000 for the time zone +@@ -1903,7 +1903,7 @@ + if ($conf{'tls_server'} == 1 and $conf{'tls_client'} == 1 and $opt{'tls'} =~ /^(yes|auto)$/) { + printmsg("DEBUG => Starting TLS", 2); + if (SMTPchat('STARTTLS')) { quit($conf{'error'}, 1); } +- if (! IO::Socket::SSL->start_SSL($SERVER, SSL_version => 'SSLv3 TLSv1')) { ++ if (! IO::Socket::SSL->start_SSL($SERVER)) { + quit("ERROR => TLS setup failed: " . IO::Socket::SSL::errstr(), 1); + } + printmsg("DEBUG => TLS: Using cipher: ". $SERVER->get_cipher(), 3); diff --git a/t3/qfq.php b/t3/qfq.php deleted file mode 100644 index a2d0acfcd13c54a28101b2171a4b4b35f3d67899..0000000000000000000000000000000000000000 --- a/t3/qfq.php +++ /dev/null @@ -1,50 +0,0 @@ -<?php -/** - * Created by PhpStorm. - * User: ep - * Date: 12/23/15 - * Time: 6:16 PM - */ - -namespace qfq; - -use qfq; - -//use qfq\UserException; -//use qfq\CodeException; -//use qfq\DbException; - -require_once(__DIR__ . '/../qfq/QuickFormQuery.php'); -require_once(__DIR__ . '/../qfq/exceptions/UserException.php'); -require_once(__DIR__ . '/../qfq/exceptions/CodeException.php'); -require_once(__DIR__ . '/../qfq/exceptions/DbException.php'); - - -//TODO: unit tests fuer alle abgefangenen Exceptions - -/* - * - * @param string $bodytext keyvalue pairs. - * - * @return $string LOAD: the form as HTML, SAVE: success or failure message, or JS redirect to destination page. - */ -function renderForm($bodytext = "") { - - try { - $qfq = new QuickFormQuery($bodytext); - echo $qfq->process(); - - echo "test"; - - - } catch (UserFormException $e) { - echo $e->formatMessage(); - } catch (CodeException $e) { - echo $e->formatMessage(); - } catch (DbException $e) { - echo $e->formatMessage(); - } catch (\Exception $e) { - echo "Generic Exception: " . $e->getMessage(); - } - -} \ No newline at end of file diff --git a/test.html b/test.html deleted file mode 100644 index d09bd02730b68966379aa297cc4c8de21d474f15..0000000000000000000000000000000000000000 --- a/test.html +++ /dev/null @@ -1,17 +0,0 @@ -<div class="col-md-6 col-sm-9"> - - <!-- CONTENT ELEMENT, uid:4/html [begin] --> - <div id="c4"> - <!-- Raw HTML content: [begin] --> - <a href="?u10" class="button-big blue play">U10</a> - <a href="?u12" class="button-big blue crayon">U12</a> - <a href="?u14" class="button-big blue library">U14</a> - <a href="?fs" class="button-big blue promo font-big">Frühstudium</a> - <a href="?u18" class="button-big blue blackboard">U18</a> - <a href="?olympics" class="button-big blue track font-big">Olympics</a> - <!-- Raw HTML content: [end] --> - </div> - <!-- CONTENT ELEMENT, uid:4/html [end] --> - - -</div> \ No newline at end of file diff --git a/version b/version index 35aa2f3c0799c4bc021a1ebe62d38c5c58582d63..85a7643a48cef00f0b261bfbdbfaefb0bedd7757 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.25.4 +0.25.9