diff --git a/CHANGELOG.md b/CHANGELOG.md index d938f8b16f4ca7d086fcbbdc98e8921b2985e6bf..ad39f552eb24b6338489e1694ad0d0c3d0ef070d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,10 +36,72 @@ Features Bug Fixes ^^^^^^^^^ +Version 0.25.4 +-------------- + +Date: 22.11.17 + +Notes +^^^^^ + +* New keywords / features in report: + + * `altsql`: Fire the query if there is no record selected in `sql`. Shown after `althead` + * `shead`: Static head - will always be shown (before `head`), independent of sql selects records or not. + * `stail`: Static tail - will always be shown (after `tail`), independent of sql selects records or not. + +Features +^^^^^^^^ + +* #2948 /altsql, shead, stail - new directives in Report. +* #4255 / Attachments fuer 'Email'. Static files can be attached to mails. + +Bug Fixes +^^^^^^^^^ + +* #4980 / Variables in Report: a) nested not replaced, b) 'rbeg' not replaced, c) missing unit tests. + + +Version 0.25.3 +-------------- + +Date: 19.11.2017 + +Notes +^^^^^ + +* Report: + + * Special column name 'sendmail': the old way of position dependent parameter are deprecated. Instead use the new + defined token. See https://docs.typo3.org/typo3cms/drafts/github/T3DocumentationStarter/Public-Info-053/Manual.html#column_sendmail + + * Every row is now merged in STORE_RECORD. Inner SQL statement can now retrieve outer values via STORE_RECORD. + E.g. `{{column:R}}`. No more level keys! + +* The config.qfq.ini directive `VAR_ADD_BY_SQL` is replaced by `FILL_STORE_SYSTEM_BY_SQL_?`. Up to 3 statements are possible. + +Features +^^^^^^^^ + +* Report / sendmail: control via token. +* #4967 / config.qfq.ini: Rename 'VAR_ADD_BY_SQL' to 'FILL_STORE_SYSTEM_BY_SQL_1'. Handle up to 3 FILL_STORE_SYSTEM_SQL_x. + Implement an optional error message together with a full stop. +* #4766: Set STORE_RECORD in Report per row. + +Bug Fixes +^^^^^^^^^ + +* #4966 / Variable {{feUser:T}} is not available in config.qfq.ini `FILL_STORE_SYSTEM_?` - changed ordering of store + initialization. Now: TCY... +* #4944 / Delete: broken when using 'tableName' (instead of form). +* #4904 / Undefined Index: DIRTY_FE_USER - PHP problem that constants cant be replaced inside of single ticks. Fixed. +* #4965: insert path to QFQ cookie/session, to make usage of multiple QFQ installation on one host possible. + + Version 0.25.2 -------------- -Date: 8.11.17 +Date: 8.11.2017 Notes ^^^^^ @@ -66,7 +128,7 @@ Bug Fixes Version 0.25.1 -------------- -Date: 3.11.17 +Date: 3.11.2017 Bug Fixes ^^^^^^^^^ diff --git a/Makefile b/Makefile index a75d02b7b1d0e1ec05226b143337c07dc35812e5..25607bb6a2b4c3b6b262655787f371e5641ac5c8 100644 --- a/Makefile +++ b/Makefile @@ -140,7 +140,7 @@ sonar: .sonar_scanner .PHONY: nightly maintainer-clean snapshot release git-revision t3sphinx build-dist make-dist-dir dist-move-doc dist-copy-extension pip-temp-directory plantuml sonar -copyReleasNotes: +copyReleaseNotes: cp extension/Documentation/Release.rst extension/RELEASE.txt cp extension/Documentation/Release.rst CHANGELOG.md diff --git a/doc/NewVersion.md b/doc/NewVersion.md index 77ddc67bbdcbe4580a503dccfc5f4e8426a43d62..b8ebaae2bdb1dac5cc2486e38b578389da6af951 100644 --- a/doc/NewVersion.md +++ b/doc/NewVersion.md @@ -20,7 +20,7 @@ Neue Versionsnummer * **Anpassen**: qfq/extension/Documentation/Release.rst - * Release.rst **verteilen**: make copyReleasNotes + * Release.rst **verteilen**: make copyReleaseNotes * Manuell: @@ -48,8 +48,8 @@ Neue Versionsnummer 6) **New Tag**: - git tag v0.25.2 - git push -u origin v0.25.2 + git tag v0.25.4 + git push -u origin v0.25.4 7) PhpStorm: **Sync** all files to VM qfq. diff --git a/extension/Documentation/Index.rst b/extension/Documentation/Index.rst index d4315f6ffc324dabd3738ebda4a2f58e3ac0dafa..bc68233068be5138f8a26f9eb7c485d53e7a6efe 100644 --- a/extension/Documentation/Index.rst +++ b/extension/Documentation/Index.rst @@ -61,3 +61,4 @@ QFQ Extension Release Links README + License diff --git a/extension/Documentation/License.rst b/extension/Documentation/License.rst new file mode 100644 index 0000000000000000000000000000000000000000..ac4f527828e89d48f7eef5c926e407ca63127a86 --- /dev/null +++ b/extension/Documentation/License.rst @@ -0,0 +1,35 @@ +.. ================================================== +.. Header hierachy +.. == +.. -- +.. ^^ +.. '' +.. ;; +.. ,, +.. +.. -------------------------------------------------- +.. Best Practice T3 reST https://docs.typo3.org/typo3cms/drafts/github/xperseguers/RstPrimer/ +.. External Links: `Bootstrap <http://getbootstrap.com/>`_: +.. Add Images: https://wiki.typo3.org/ReST_Syntax#Images +.. +.. -*- coding: utf-8 -*- with BOM. + + +.. include:: Includes.txt + +.. _license: + +License +======= + +* QFQ is licensed under GNU GENERAL PUBLIC LICENSE, Version 3, 29 June 2007 + +Software distributed together with QFQ +====================================== + +* jQuery - http://jquery.com +* jQWidgets - https://www.jqwidgets.com +* Bootstrap - http://getbootstrap.com +* Fabric.js - http://fabricjs.com +* Chart.js - https://github.com/nnnick/Chart.js.git +* sendEmail - https://github.com/mogaal/sendemail diff --git a/extension/Documentation/Manual.rst b/extension/Documentation/Manual.rst index 58f8a2fd638fff6b7ab0cb26407ea404236a4931..c0427c179af1ab94eacbba9e79900045e0d57385 100644 --- a/extension/Documentation/Manual.rst +++ b/extension/Documentation/Manual.rst @@ -61,14 +61,14 @@ For the `download`_ function, the programs `pdftk` and `file` are necessary to c Preparation for Ubuntu 14.04:: sudo apt-get install php5-mysqlnd php5-intl - sudo apt-get install pdftk file pdf2svg # for file upload and PDF, PDF split + sudo apt-get install pdftk file # for file upload and PDF sudo php5enmod mysqlnd sudo service apache2 restart Preparation for Ubuntu 16.04:: sudo apt install php7.0-intl - sudo apt install pdftk libxrender1 file pdf2svg # for file upload, PDF, PDF split and 'HTML to PDF' (wkhtmltopdf) + sudo apt install pdftk libxrender1 file pdf2svg # for file upload, PDF and 'HTML to PDF' (wkhtmltopdf), PDF split .. _wkhtml: @@ -238,11 +238,15 @@ config.qfq.ini +-----------------------------+-------------------------------------------------+----------------------------------------------------------------------------+ | DB_INDEX_QFQ | DB_INDEX_QFQ = 1 | Optional. Default: 1. | +-----------------------------+-------------------------------------------------+----------------------------------------------------------------------------+ -| SQL_LOG | SQL_LOG=sql.log | Filename to log SQL commands: relative to <ext_dir> or absolute. | +| SQL_LOG | SQL_LOG=../../sql.log | Filename to log SQL commands: relative to <ext_dir> or absolute. | +-----------------------------+-------------------------------------------------+----------------------------------------------------------------------------+ | SQL_LOG_MODE | SQL_LOG_MODE=modify | *all*: every statement will be logged - this might a lot. | | | | *modify*: log only statements who change data. | +-----------------------------+-------------------------------------------------+----------------------------------------------------------------------------+ +| MAIL_LOG | SQL_LOG=../../mail.log | Filename to log `sendEmail` commands: relative to <ext_dir> or absolute. | ++-----------------------------+-------------------------------------------------+----------------------------------------------------------------------------+ +| SEND_E_MAIL_OPTIONS | SEND_E_MAIL_OPTIONS="-o tls=yes" | General options. Check: http://caspian.dotconf.net/menu/Software/SendEmail | ++-----------------------------+-------------------------------------------------+----------------------------------------------------------------------------+ | SHOW_DEBUG_INFO | SHOW_DEBUG_INFO=auto | FE - Possible values: yes|no|auto|download. For 'auto': If a BE User is | | | | logged in, a debug information will be shown on the FE. | +-----------------------------+-------------------------------------------------+----------------------------------------------------------------------------+ @@ -353,8 +357,8 @@ config.qfq.ini | DOCUMENTATION_QFQ | DOCUMENTATION_QFQ=http://docs.typo3.org... | Link to the online documentation of QFQ. Every QFQ installation also | | | | contains a local copy: typo3conf/ext/qfq/Documentation/html/Manual.html | +-----------------------------+-------------------------------------------------+----------------------------------------------------------------------------+ -| VAR_ADD_BY_SQL | VAR_ADD_BY_SQL = {{!SELECT s.id AS ... | Specific values read from the database to fill the system store during QFQ | -| | | load. See `VariablesAddBySql`_ for a usecase. | +| FILL_STORE_SYSTEM_BY_SQL | FILL_STORE_SYSTEM_BY_SQL = {{!SELECT s.id AS ...| Specific values read from the database to fill the system store during QFQ | +| | | load. See `fillStoreSystemBySql`_ for a usecase. | +-----------------------------+-------------------------------------------------+----------------------------------------------------------------------------+ | FORM_LANGUAGE_A_ID | FORM_LANGUAGE_A__ID = 1 | In Typo3 configured pageLanguage id. The number after the 'L' parameter. | | FORM_LANGUAGE_B_ID | | | @@ -445,7 +449,9 @@ Example: *typo3conf/config.qfq.ini* ; Local Documentation (doc fits to installed version): typo3conf/ext/qfq/Documentation/html/Manual.html ;DOCUMENTATION_QFQ = https://docs.typo3.org/typo3cms/drafts/github/T3DocumentationStarter/Public-Info-053/Manual.html - ;VAR_ADD_BY_SQL = 'SELECT s.id AS _periodId FROM Period AS s WHERE s.start<=NOW() ORDER BY s.start DESC LIMIT 1' + ;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 ;FORM_LANGUAGE_A_ID = 1 ;FORM_LANGUAGE_A_LABEL = english @@ -474,17 +480,23 @@ E.g. to setup a contact address and reuse the information inside your installati {{ADMINISTRATIVE_CONTACT:Y}}, {{ADMINISTRATIVE_ADDRESS:Y}}, {{ADMINISTRATIVE_NAME}} -.. _`VariablesAddBySql`: +.. _`fillStoreSystemBySql`: -Variables add by SQL -^^^^^^^^^^^^^^^^^^^^ +Fill STORE_SYSTEM by SQL +^^^^^^^^^^^^^^^^^^^^^^^^ -A specified SELECT statement in `config.qfq.ini`_ in variable `VAR_ADD_BY_SQL` fill be fired after filling the SYSTEM STORE. -The query should have 0 (nothing happens) or 1 row. The column names and column values will be added as variables to the SYSTEM_STORE. -Existing variables will be overwritten. Be carefull not to overwrite needed values. +A specified SELECT statement in `config.qfq.ini`_ in variable `FILL_STORE_SYSTEM_BY_SQL_1` (or `FILL_STORE_SYSTEM_BY_SQL_2`, +or `FILL_STORE_SYSTEM_BY_SQL_3`) will be fired. The query should have 0 (nothing happens) or 1 row. All columns will be +**added** to the existing STORE_SYSTEM. Existing variables will be overwritten. Be careful not to overwrite system values. -This option is usefull to make generic custom values, saved in the database, accessible to all QFQ Report and Forms. -Access such variables as usual via `{{<varname>:Y}}`. +This option is useful to make generic custom values, saved in the database, accessible to all QFQ Report and Forms. +Access such variables via `{{<varname>:Y}}`. + +In case QFQ should stop working if a given query does not select exact one record (e.g. a missing period), define an +error message: :: + + FILL_STORE_SYSTEM_BY_SQL_1 = "SELECT name FROM Person WHERE name='Doe'" + FILL_STORE_SYSTEM_BY_SQL_ERROR_MSG_1 = Too many or to few "Doe's" in our database .. _`periodId`: @@ -493,23 +505,21 @@ periodId This is -* a usecase, implemented via `VariablesAddBySql`_, +* a usecase, implemented via `fillStoreSystemBySql`_, * a way to access `Period.id` with respect to the current period (the period itself is custom defined). After a full QFQ installation, three things are prepared: * a table `Period` (extend / change it to your needs, fill them with your periods), * one sample record in table `Period`, -* in `config.qfq.ini`_ the default definition of `VAR_ADD_BY_SQL` will set the variable `periodId` during QFQ load. +* in `config.qfq.ini`_ the default definition of `FILL_STORE_SYSTEM_BY_SQL_1` will set the variable `periodId` during QFQ load. Websites, delivering semester data, schoolyears schedules, or any other type or periods, often need an index to the -*current* period. One way is a) to mark the current period and b) to change the marker every time when the next period -becomes current. -The QFQ approach works without a marker and without manual intervention: the whished index will be computed during QFQ load. +*current* period. In `config.qfq.ini`: :: - VAR_ADD_BY_SQL = 'SELECT id AS periodId FROM Period WHERE start<=NOW() ORDER BY start DESC LIMIT 1' + FILL_STORE_SYSTEM_BY_SQL_1 = 'SELECT id AS periodId FROM Period WHERE start<=NOW() ORDER BY start DESC LIMIT 1' a variable 'periodId' will automatically computed and filled in STORE SYSTEM. Access it via `{{periodId:Y0}}`. To get the name and current period: :: @@ -629,13 +639,19 @@ QFQ Keywords (Bodytext) +-------------------+---------------------------------------------------------------------------------+ | <level>.fend | End token for every field (=column) | +-------------------+---------------------------------------------------------------------------------+ - | <level>.head | Start token for whole <level> | + | <level>.shead | Static start token for whole <level>, independent if records are selected | + | | Shown before `head`. | + +-------------------+---------------------------------------------------------------------------------+ + | <level>.stail | Static end token for whole <level>, independent if records are selected. | + | | Shown after `tail`. | +-------------------+---------------------------------------------------------------------------------+ - | <level>.tail | End token for whole <level> | + | <level>.head | Dynamic start token for whole <level>. Only if at least one record is select. | + +-------------------+---------------------------------------------------------------------------------+ + | <level>.tail | Dynamic end token for whole <level>. Only if at least one record is select. | +-------------------+---------------------------------------------------------------------------------+ | <level>.rbeg | Start token for row. | +-------------------+---------------------------------------------------------------------------------+ - | <level>.rbgd | Alternating (per row) token | + | <level>.rbgd | Alternating (per row) token. | +-------------------+---------------------------------------------------------------------------------+ | <level>.rend | End token for row. Will be rendered **before** subsequent levels are processed | +-------------------+---------------------------------------------------------------------------------+ @@ -647,7 +663,10 @@ QFQ Keywords (Bodytext) +-------------------+---------------------------------------------------------------------------------+ | <level>.sql | SQL Query | +-------------------+---------------------------------------------------------------------------------+ - | <level>.althead | If <level>.sql is empty, these token will be rendered | + | <level>.althead | If <level>.sql is empty, these token will be rendered. | + +-------------------+---------------------------------------------------------------------------------+ + | <level>.altsql | If <level>.sql is empty, these query will be fired. No sub queries. | + | | Shown after `althead` | +-------------------+---------------------------------------------------------------------------------+ | debugShowBodyText | If='1' and config.qfq.ini:*SHOW_DEBUG_INFO = yes*, shows a tooltip with bodytext| +-------------------+---------------------------------------------------------------------------------+ @@ -656,10 +675,64 @@ QFQ Keywords (Bodytext) | sqlLogMode | Overwrites config.qfq.ini: `SQL_LOG_MODE`_ . Only affects `Report`, not `Form`. | +-------------------+---------------------------------------------------------------------------------+ +Databases +--------- + +A Typo3 / QFQ Installation needs at least two databases. One for the Typo3 installation and one for QFQ. + +QFQ itself can be separated in 'QFQ system' and 'QFQ data' databases, if necessary (than at least three databases are needed). +Furthermore a `Form` can operate on any additional database, specified per `Form`.parameter.dbIndex and configured via `config.qfq.ini`_. + +* Option 'A' is the most simple and commonly used. +* Option 'B' separate the T3 and QFQ databases on two database hosts. +* Option 'C' is like 'B' but with a shared 'QFQ data'-database between three 'Typo3 / QFQ' instances. +* Further variants are possible. + ++---+----------------+--------------+-------------------------------+------------------------------+----------------------------------+ +| | Domain | Website Host | T3 | QFQ system | QFQ data | ++===+================+==============+===============================+==============================+==================================+ +| A | standalone.edu | 'w' | <dbHost>, <dbname>_t3, <dbnameSingle>_db | ++---+----------------+--------------+-------------------------------+------------------------------+----------------------------------+ +| B | appB1.edu | 'wApp' | <dbHostApp>, <dbnameB1>_t3 | <dbHostB1>, <dbnameApp>_db | ++---+----------------+--------------+-------------------------------+------------------------------+----------------------------------+ +| B | appB2.edu | 'wApp' | <dbHostApp>, <dbnameB2>_t3 | <dbHostB2>, <dbnameApp>_db | ++---+----------------+--------------+-------------------------------+------------------------------+----------------------------------+ +| C | appC1.edu | 'wAppC' | <dbHostAppC>, <dbnameC1>_t3 | <dbHostC>, <dbnameSysC1>_db | <dbHostData>_db, <dbNameData>_db | ++---+----------------+--------------+-------------------------------+------------------------------+----------------------------------+ +| C | appC2.edu | 'wAppC' | <dbHostAppC>, <dbnameC2>_t3 | <dbHostC>, <dbnameSysC2>_db | <dbHostData>_db, <dbNameData>_db | ++---+----------------+--------------+-------------------------------+------------------------------+----------------------------------+ +| C | appC3.edu | 'wAppC3' | <dbHostAppC3>, <dbnameC3>_t3 | <dbHostC3>, <dbnameSysC3>_db | <dbHostData>_db, <dbNameData>_db | ++---+----------------+--------------+-------------------------------+------------------------------+----------------------------------+ + +In `config.qfq.ini`_ mutliple database credentials can be prepared. Mandatory is at least one credential setup like +`DB_1_USER`, `DB_1_SERVER`, `DB_1_PASSWORD`, `DB_1_NAME`. The number '1' indicates the `dbIndex`. Increment the number +to specify further database credential setups. + +Often the `DB_1_xxx` is identically to the used Typo3 database credentials. + +If not explicit specified, 'QFQ system' and 'QFQ database' will use the same database with the same credentials (setup 'A'). + +To define separate 'QFQ data' and 'QFQ system', in `config.qfq.ini`_ define `DB_1_USER`, ... (e.g. 'QFQ data') and `DB_2_USER`, +... (e.g. 'QFQ system') and specify the assignment:: + + DB_INDEX_DATA = 1 + DB_INDEX_QFQ = 2 + +To let a form operate (show, load and save) on a different database, use `Form.parameter.dbIndexData` (see `form-parameter`_). + +Different QFQ versions, shared database +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When using different QFQ versions and a shared 'QFQ data'-database, there is some risk of conflicting +'QFQ system' tables. Best is to always use the same QFQ version on all instances. + .. _debug: Debug -^^^^^ +===== + +SQL Logging +----------- File: `config.qfq.ini`_ @@ -713,6 +786,11 @@ File: `config.qfq.ini`_ .. _REDIRECT_ALL_MAIL_TO: +Redirect all mail to (catch all) +-------------------------------- + +File: `config.qfq.ini`_ + * *REDIRECT_ALL_MAIL_TO=john@doe.com* * During the development, it might be helpful to configure a 'catch all' email address, which QFQ uses as the final receiver @@ -726,57 +804,6 @@ File: `config.qfq.ini`_ .. _variables: -Databases ---------- - -A Typo3 / QFQ Installation needs at least two databases. One for the Typo3 installation and one for QFQ. - -QFQ itself can be separated in 'QFQ system' and 'QFQ data' databases, if necessary (than at least three databases are needed). -Furthermore a `Form` can operate on any additional database, specified per `Form`.parameter.dbIndex and configured via `config.qfq.ini`_. - -* Option 'A' is the most simple and commonly used. -* Option 'B' separate the T3 and QFQ databases on two database hosts. -* Option 'C' is like 'B' but with a shared 'QFQ data'-database between three 'Typo3 / QFQ' instances. -* Further variants are possible. - -+---+----------------+--------------+-------------------------------+------------------------------+----------------------------------+ -| | Domain | Website Host | T3 | QFQ system | QFQ data | -+===+================+==============+===============================+==============================+==================================+ -| A | standalone.edu | 'w' | <dbHost>, <dbname>_t3, <dbnameSingle>_db | -+---+----------------+--------------+-------------------------------+------------------------------+----------------------------------+ -| B | appB1.edu | 'wApp' | <dbHostApp>, <dbnameB1>_t3 | <dbHostB1>, <dbnameApp>_db | -+---+----------------+--------------+-------------------------------+------------------------------+----------------------------------+ -| B | appB2.edu | 'wApp' | <dbHostApp>, <dbnameB2>_t3 | <dbHostB2>, <dbnameApp>_db | -+---+----------------+--------------+-------------------------------+------------------------------+----------------------------------+ -| C | appC1.edu | 'wAppC' | <dbHostAppC>, <dbnameC1>_t3 | <dbHostC>, <dbnameSysC1>_db | <dbHostData>_db, <dbNameData>_db | -+---+----------------+--------------+-------------------------------+------------------------------+----------------------------------+ -| C | appC2.edu | 'wAppC' | <dbHostAppC>, <dbnameC2>_t3 | <dbHostC>, <dbnameSysC2>_db | <dbHostData>_db, <dbNameData>_db | -+---+----------------+--------------+-------------------------------+------------------------------+----------------------------------+ -| C | appC3.edu | 'wAppC3' | <dbHostAppC3>, <dbnameC3>_t3 | <dbHostC3>, <dbnameSysC3>_db | <dbHostData>_db, <dbNameData>_db | -+---+----------------+--------------+-------------------------------+------------------------------+----------------------------------+ - -In `config.qfq.ini`_ mutliple database credentials can be prepared. Mandatory is at least one credential setup like -`DB_1_USER`, `DB_1_SERVER`, `DB_1_PASSWORD`, `DB_1_NAME`. The number '1' indicates the `dbIndex`. Increment the number -to specify further database credential setups. - -Often the `DB_1_xxx` is identically to the used Typo3 database credentials. - -If not explicit specified, 'QFQ system' and 'QFQ database' will use the same database with the same credentials (setup 'A'). - -To define separate 'QFQ data' and 'QFQ system', in `config.qfq.ini`_ define `DB_1_USER`, ... (e.g. 'QFQ data') and `DB_2_USER`, -... (e.g. 'QFQ system') and specify the assignment:: - - DB_INDEX_DATA = 1 - DB_INDEX_QFQ = 2 - -To let a form operate (show, load and save) on a different database, use `Form.parameter.dbIndexData` (see `form-parameter`_). - -Different QFQ versions, shared database -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When using different QFQ versions and a shared 'QFQ data'-database, there is some risk of conflicting -'QFQ system' tables. Best is to always use the same QFQ version on all instances. - Variable ======== @@ -793,7 +820,7 @@ Some examples, including nesting:: #--------------------------------------------- {{r}} {{index:FS}} - {{name:FS:alnumx:s}} + {{name:FS:alnumx:s:my default}} # SQL #--------------------------------------------- @@ -1305,13 +1332,9 @@ Store: *VARS* - V +-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ | slaveId | see *FormElement* `action` | +-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ - | fileDestination | Destination (path & filename) for an uploaded file. Defined in an 'upload'-FormElement.parameter. Valid: same as 'filename'. | - +-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ | filename | Original filename of an uploaded file via an 'upload'-FormElement. Valid only during processing of the current 'upload'-formElement. | +-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ - | filenameBase | Like 'filename', but without the extension (if there is one) | - +-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ - | filenameExt | Only the extension of 'filename' (if there is one) | + | fileDestination | Destination (path & filename) for an uploaded file. Defined in an 'upload'-FormElement.parameter. Valid: same as 'filename'. | +-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ The directive `fillStoreVar` will fill the this store with custom values. Existing values will remain or are overwritten, @@ -1320,7 +1343,7 @@ depending of if they exist in the given statement. E.g.: :: fillStoreVar = {{!SELECT p.name, p.email FROM Person AS p WHERE p.id={{pId:S}} }} * After filling the store, the values can be retrieved via `{{name:v}}` and `{{email:V}}`. -* Be carefull by specifying general purpose variables like `id`, `r`, `pageId` and so on. This might conflict with existing variables. +* Be careful by specifying general purpose variables like `id`, `r`, `pageId` and so on. This might conflict with existing variables. * `fillStoreVar` can be used in `form-parameter`_ and `fe-parameter-attributes`_ @@ -1417,7 +1440,7 @@ Server configurations. To decide which Parameter should be placed on *Form.parameter* and which on *FormElement.parameter*: If LDAP access is ... -* only necessary in one *FormElement*, most usefull setup is to specify all values in that specific *FormElement*, +* only necessary in one *FormElement*, most useful setup is to specify all values in that specific *FormElement*, * needed on multiple *FormElement*s (of the same *Form*, e.g. one *input* with *typeAhead*, one *note* and one *action*), it's more efficient to specify the base parameter *ldapServer*, *ldapBaseDn* in *Form.parameter* and the rest on the current *FormElement*. @@ -1545,7 +1568,7 @@ E.g.:: Result: (& (|(a=*X*)(b=*X*)) (|(a=*Y*)(b=*Y*)) -Attention: this option is only usefull in specific environments. Only use it, if it is really needed. The query becomes +Attention: this option is only useful in specific environments. Only use it, if it is really needed. The query becomes much more cpu / IO intensive. @@ -2252,7 +2275,7 @@ Fields: | | 'disabled' ) | *Readonly*: user can't change any data. Data not saved. | | | | *Disabled*: *FormElement* is not visible. | +---------------------+-----------------------------+-----------------------------------------------------------------------------------------------------+ -|Mode sql | `SELECT` statement with | A value given here overwrites the setting from `mode`. Most usefull with :ref:`dynamic-update`. | +|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' }} | +---------------------+-----------------------------+-----------------------------------------------------------------------------------------------------+ |Class | enum('native', 'action', | Details below. | @@ -3251,7 +3274,7 @@ Parameter: sqlBefore / sqlInsert / sqlUpdate / sqlDelete / sqlAfter * *sqlBefore*: always fired (before any *sqlInsert*, *sqlUpdate*, ..) * *sqlInsert*: fired if *slaveId* == `0` or *slaveId* == `''`. * *sqlUpdate*: fired if *slaveId* > `0`. - * *sqlDelete*: fired if *slaveId* > `0`, after *sqlInsert* or *sqlUpdate*. Be carefull not to delete filled records! + * *sqlDelete*: fired if *slaveId* > `0`, after *sqlInsert* or *sqlUpdate*. Be careful not to delete filled records! Always add a check, if values given, not to delete the record! *sqlHonorFormElements* helps to skip such checks. * *sqlAfter*: always fired (after *sqlInsert*, *sqlUpdate* or *sqlDelete*). * *sqlHonorFormElements*: list of *FormElement* names (this parameter is optional). @@ -3347,10 +3370,14 @@ 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. + * *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. * *sendMailGrId* - Will be copied to the mailLog record. Helps to setup specific logfile queries. * *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. * 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}}` @@ -4256,19 +4283,13 @@ FAQ Report ====== -The QFQ extension is activated through tt-content records. One or more tt-content records per page are necessary to render -*forms* and *reports*. - QFQ content element ------------------- -QFQ is used by configuring Typo3 content elements. 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 QFQ extension is activated through tt-content records. One or more tt-content records per page are necessary to render +*forms* and *reports*. Specify column and language per content record as wished. -General -------- +The title of the QFQ content element will not be rendered. It's only visible in the backend for orientation of the webmaster. To display a report on any given TYPO3 page, create a content element of type 'QFQ Element' (plugin) on that page. @@ -4317,15 +4338,18 @@ Syntax of `report` For **each** row of a query (this means *all* queries), all subqueries will be fired once. - * E.g. if the outer query selects 5 rows, and a nested query always select 3 rows, than the total number of rows are 5 x 3 = 15 rows. + * E.g. if the outer query selects 5 rows, and a nested query select 3 rows, than the total number of rows are 5 x 3 = 15 rows. There is a set of **variables** that will get replaced before the SQL-Query gets executed: - Column values of the recent rows: {{<level>.<columnname>}} + Variables from specific stores: {{<name>[:<store/s>[:...]]}} - Global variables: {{global.<name>}} + STORE_RECORD is automatically merged with the existing STORE_RECORD content and the current row. Use the STORE_RECORD + to access outer level values. - Variables from specific stores: {{<name>[:<store/s>[:<sanitize class>]]}} + Column values of a given row: {{<level>.<columnname>}} + + Global variables: {{global.<name>}} Current row index: {{<level>.line.count}} @@ -4338,22 +4362,21 @@ Syntax of `report` Be aware that line.count / line.total have to be known before the query is fired. E.g. `10.sql = SELECT {{10.line.count}}, ... WHERE {{10.line.count}} = ...` won't work as expected. `{{10.line.count}}` can't be replaced before the query is fired, but will be replaced during processing the result! - Different types of SQL queries are possible: SELECT, INSERT, UPDATE, DELETE, SHOW Only SELECT and SHOW queries will fire subqueries. - * Processing of the resulting rows and columns: + Processing of the resulting rows and columns: - * In general, all columns of all rows will be printed out sequentially. + * In general, all columns of all rows will be printed out sequentially. - On a per column base, printing of columns can be suppressed. This might be useful to select values which will be - accessed later on in another query via the {{level.columnname}} variable. To suppress printing of a column, use a - underscore as column name prefix. + * On a per column base, printing of columns can be suppressed by starting the columnname with an underscore '_'. E.g. `SELECT id AS _id`. + This might be useful to store values, which will be used later on in another query via the `{{id:R}}` or `{{level.columnname}}` variable. To suppress printing of a column, use a + underscore as column name prefix. E.g. `SELECT id AS _id` - Reserved column names have a special meaning and will be processed in a special way. See `Processing of columns in the SQL result`_ for details. + Reserved column names have a special meaning and will be processed in a special way. See `Processing of columns in the SQL result`_ for details. - There are extensive ways to wrap columns and rows automatically. See :ref:`wrapping-rows-and-columns` + There are extensive ways to wrap columns and rows automatically. See :ref:`wrapping-rows-and-columns` Debug the bodytext ------------------ @@ -4496,7 +4519,33 @@ Be careful to: Access column values ^^^^^^^^^^^^^^^^^^^^ -Columns of the upper / outer level result can be accessed via variables, eg. {{10.pId}} will be replaced by the value in the pId column. +Columns of the upper / outer level result can be accessed via variables in two ways + + * STORE_RECORD: `{{pId:R}}` + * Level Key: `{{10.pId}}` + +The STORE_RECORD will always be merged with previous content. The Level Keys are unique. + +Example STORE_RECORD: :: + + 10.sql= SELECT p.id AS _pId, p.name FROM Person AS p + 10.5.sql = SELECT adr.city, 'dummy' AS _pId FROM Address AS adr WHERE adr.pId={{pId:R}} + 10.5.20.sql = SELECT '{{pId:R}}' + 10.10.sql = SELECT '{{pId:R}}' + +The line '10.10' will output 'dummy' in cases where there is at least one corresponding address. +If there are no addresses (all persons) it reports the person id. +If there is at least one address, it reports 'dummy', cause that's the last stored content. This behaviour might change in the future. + +Example 'Level Key': :: + + 10.sql= SELECT p.id AS _pId, p.name FROM Person AS p + 10.5.sql = SELECT adr.city, 'dummy' AS _pId FROM Address AS adr WHERE adr.pId={{10.pId}} + 10.5.20.sql = SELECT '{{10.pId}}' + 10.10.sql = SELECT '{{10.pId}}' + + +Notes to the level level: +-------------+------------------------------------------------------------------------------------------------------------------------+ | Levels |A report is divided into levels. The Example has levels *10*, *20*, *30*, *30.5*, *30.5.1*, *50* | @@ -4934,7 +4983,7 @@ is allowed to access: :: page.10.value = Please access from localhost or log in as 'admin' user. [global] -.. +.. _column_pageX: Columns: _page[X] ^^^^^^^^^^^^^^^^^ @@ -5002,6 +5051,8 @@ The colum name is composed of the string *page* and a trailing character to spec |<create sip> |s | |'s': create a SIP | +-------------+-------------------------------------------------------------------------------------------------+----------------------------------------------------------+---------------------------------------------------------------+ +.. _column_paged: + Column: _paged ^^^^^^^^^^^^^^ @@ -5047,6 +5098,8 @@ Examples: SELECT 'U:table=Person&r=123|q:Do you want delete John Doe?' AS _paged SELECT 'U:form=person-main&r=123|q:Do you want delete John Doe?' AS _paged +.. _column_ppageX: + Columns: _Page[X] ^^^^^^^^^^^^^^^^^ @@ -5057,7 +5110,7 @@ Columns: _Page[X] "[<page id|alias>[¶m=value&...]] | [text] | [tooltip] | [question parameter] | [class] | [target] | [render mode]" as _Pagee. -.. +.. _column_ppaged: Column: _Paged ^^^^^^^^^^^^^^ @@ -5070,7 +5123,7 @@ Column: _Paged "[table=<table name>&r-<record id>[¶m=value&...] | [text] | [tooltip] | [question parameter] | [class] | [render mode]" as _Paged. "[form=<form name>&r-<record id>[¶m=value&...] | [text] | [tooltip] | [question parameter] | [class] | [render mode]" as _Paged. -.. +.. _column_vertical: Column: _vertical ^^^^^^^^^^^^^^^^^ @@ -5122,7 +5175,7 @@ angle. .. - +.. _column_mailto: Column: _mailto ^^^^^^^^^^^^^^^ @@ -5171,11 +5224,12 @@ Easily create Email links. .. +.. _column_sendmail: Column: _sendmail ^^^^^^^^^^^^^^^^^ -<TO:email[,email]>|<FROM:email>|<subject>|<body>|[<REPLY-TO:email>]|[<flag autosubmit: on /off>]|[<grid>]|[xId]|<CC:email[,email]>|<BCC:email[,email]> +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]> Send text emails. Every mail will be logged in the table `mailLog`. @@ -5184,54 +5238,60 @@ Send text emails. Every mail will be logged in the table `mailLog`. :: - SELECT "john@doe.com|jane@doe.com|Reminder tomorrow|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" 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 .. -+------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ -|**Parameter** |**Description** |**Required**| -+============================================================+==========================================================================================+============+ -|TO:email[,email] |Comma-separated list of receiver email addresses. Optional: `realname <john@doe.com>` | yes | -+------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ -|FROM:email |Sender of the email. Optional: 'realname <john@doe.com>' | yes | -+------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ -|subject |Subject of the email | yes | -+------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ -|body |Message | yes | -+------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ -|REPLY-TO:email |Email address to reply to (if different from sender) | | -+------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ -|flagAutoSubmit 'on' / 'off' |If 'on' (default), add mail header 'Auto-Submitted: auto-send' - suppress OoO replies | | -+------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ -|grId |Will be copied to the mailLog record. Helps to setup specific logfile queries | | -+------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ -|xId |Will be copied to the mailLog record. Helps to setup specific logfile queries | | -+------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ -|CC:email[,email] |Comma-separated list of receiver email addresses. Optional: 'realname <john@doe.com>' | | -+------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ -|BCC:email[,email] |Comma-separated list of receiver email addresses. Optional: 'realname <john@doe.com>' | | -+------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ ++---+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ +|***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 | | ++---+----------------------------------------+--------------------------------------------------------------------------------------------------+------------+ **Minimal Example** :: - - 10.sql = SELECT "john.doe@example.com|company@example.com|Latest News|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 .. - - This will send an email with subject *Latest News* from company@example.com to john.doe@example.com. **Advanced Examples** :: - - 10.sql = SELECT "customer1@example.com,Firstname Lastname <customer2@example.com>, Firstname Lastname <customer3@example.com>|company@example.com|Latest News|The new version is now available.|sales@example.com|on|101|222|ceo@example.com|backup@example.com" AS _sendmail + 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 .. @@ -5241,6 +5301,7 @@ Additional the CEO as well as backup will receive the mail via CC and BCC. For debugging, please check `REDIRECT_ALL_MAIL_TO`_. +.. _column_img: Column: _img ^^^^^^^^^^^^ @@ -5299,7 +5360,7 @@ Renders images. Allows to define an alternative text and a title attribute for t .. - +.. _column_exec: Column: _exec ^^^^^^^^^^^^^ @@ -5334,6 +5395,7 @@ Runs batch files or executables on the webserver. In case of an error, returncod .. +.. _column_pdf: Column: _pdf | _file | _zip ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -5361,7 +5423,7 @@ Most of the other Link-Class attributes can be used to customize the link. SELECT "d:complete.pdf|t:Download PDF|f:fileadmin/test.pdf|U:id=export&r=1|u:www.w3c.org" AS _pdf -.. +.. _column_ppdf: Column: _Pdf | _File | _Zip ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -5384,6 +5446,7 @@ A limited set of attributes is supported: :: .. +.. _column_F: Column: _F ^^^^^^^^^^ @@ -5819,6 +5882,8 @@ Same as above, but written in the nested notation :: * Columns starting with a '_' won't be printed but can be accessed as regular columns. +.. _help: + Help ==== @@ -5832,6 +5897,39 @@ Tips: * Always check the Javascript console of your browser, see `javascriptProblem`_. * Always check the Webserver logfiles, see `webserverErrorLog`_. +QFQ specific +------------ + +Variable empty: {{...}} +^^^^^^^^^^^^^^^^^^^^^^^ + +Specify the required sanitize class. Remember: for STORE_FORM and STORE_CLIENT the default is `digit`. This means if +the variable content is a string, this violates the sanitize class and the replaced content will be an empty string! + +Form: put the problematic variable or SQL statement in the 'title' or note 'field' of a `FormElement`. This should show +the content. For SQL statements insert a character before the SQL Keyword to avoid SQL triggering. + +Error read file config.qfq.ini: syntax error on line xx +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Check the given line number. If it's a SQL statement, enclose it in single or double ticks. + +Logging +------- + +General webserver error log +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For apache: /var/log/apache2/error_log + +Especially if you got a blank page (no rendering at all), this is typically an uncaught PHP error. Check the error message +and report the bug. + +Call to undefined function qfq\\mb_internal_encoding() +'''''''''''''''''''''''''''''''''''''''''''''''''''''' + +Check that all required php modules are installed. See `preparation`_. + Error Messages -------------- @@ -5856,22 +5954,4 @@ Open the 'Webdeveloper Tools' (FF: F12, Chrome/Opera: Right mouse click > Inspec .. _`webserverErrorLog`: -Webserver error log -------------------- - -For apache: /var/log/apache2/error_log - -Call to undefined function qfq\\mb_internal_encoding() -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Check that all required php modules are installed. See `preparation`_. - -QFQ specific ------------- - -Variable empty: {{...}} -^^^^^^^^^^^^^^^^^^^^^^^ - -Specify the required sanitize class. Remember: for STORE_FORM and STORE_CLIENT the default is `digit`. This means if -the variable content is a string, this violates the sanitize class and the replaced content will be an empty string! \ No newline at end of file diff --git a/extension/Documentation/Release.rst b/extension/Documentation/Release.rst index d938f8b16f4ca7d086fcbbdc98e8921b2985e6bf..ad39f552eb24b6338489e1694ad0d0c3d0ef070d 100644 --- a/extension/Documentation/Release.rst +++ b/extension/Documentation/Release.rst @@ -36,10 +36,72 @@ Features Bug Fixes ^^^^^^^^^ +Version 0.25.4 +-------------- + +Date: 22.11.17 + +Notes +^^^^^ + +* New keywords / features in report: + + * `altsql`: Fire the query if there is no record selected in `sql`. Shown after `althead` + * `shead`: Static head - will always be shown (before `head`), independent of sql selects records or not. + * `stail`: Static tail - will always be shown (after `tail`), independent of sql selects records or not. + +Features +^^^^^^^^ + +* #2948 /altsql, shead, stail - new directives in Report. +* #4255 / Attachments fuer 'Email'. Static files can be attached to mails. + +Bug Fixes +^^^^^^^^^ + +* #4980 / Variables in Report: a) nested not replaced, b) 'rbeg' not replaced, c) missing unit tests. + + +Version 0.25.3 +-------------- + +Date: 19.11.2017 + +Notes +^^^^^ + +* Report: + + * Special column name 'sendmail': the old way of position dependent parameter are deprecated. Instead use the new + defined token. See https://docs.typo3.org/typo3cms/drafts/github/T3DocumentationStarter/Public-Info-053/Manual.html#column_sendmail + + * Every row is now merged in STORE_RECORD. Inner SQL statement can now retrieve outer values via STORE_RECORD. + E.g. `{{column:R}}`. No more level keys! + +* The config.qfq.ini directive `VAR_ADD_BY_SQL` is replaced by `FILL_STORE_SYSTEM_BY_SQL_?`. Up to 3 statements are possible. + +Features +^^^^^^^^ + +* Report / sendmail: control via token. +* #4967 / config.qfq.ini: Rename 'VAR_ADD_BY_SQL' to 'FILL_STORE_SYSTEM_BY_SQL_1'. Handle up to 3 FILL_STORE_SYSTEM_SQL_x. + Implement an optional error message together with a full stop. +* #4766: Set STORE_RECORD in Report per row. + +Bug Fixes +^^^^^^^^^ + +* #4966 / Variable {{feUser:T}} is not available in config.qfq.ini `FILL_STORE_SYSTEM_?` - changed ordering of store + initialization. Now: TCY... +* #4944 / Delete: broken when using 'tableName' (instead of form). +* #4904 / Undefined Index: DIRTY_FE_USER - PHP problem that constants cant be replaced inside of single ticks. Fixed. +* #4965: insert path to QFQ cookie/session, to make usage of multiple QFQ installation on one host possible. + + Version 0.25.2 -------------- -Date: 8.11.17 +Date: 8.11.2017 Notes ^^^^^ @@ -66,7 +128,7 @@ Bug Fixes Version 0.25.1 -------------- -Date: 3.11.17 +Date: 3.11.2017 Bug Fixes ^^^^^^^^^ diff --git a/extension/Documentation/Settings.cfg b/extension/Documentation/Settings.cfg index 049c4ba5eaf2c7831aed485dbe937243d2f44402..d68e42d958b18dc3e12ffb07f635a4eef9663d50 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.2 +release = 0.25.4 t3author = Carsten Rose copyright = since 2017 by the author diff --git a/extension/Documentation/_make/conf.py b/extension/Documentation/_make/conf.py index 00defb2ddadb07b49d7f874c1bb3367644b19686..9c190b27927769178dc9900413d288e8e4d7233f 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.2' +release = '0.25.4' # 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 d938f8b16f4ca7d086fcbbdc98e8921b2985e6bf..ad39f552eb24b6338489e1694ad0d0c3d0ef070d 100644 --- a/extension/RELEASE.txt +++ b/extension/RELEASE.txt @@ -36,10 +36,72 @@ Features Bug Fixes ^^^^^^^^^ +Version 0.25.4 +-------------- + +Date: 22.11.17 + +Notes +^^^^^ + +* New keywords / features in report: + + * `altsql`: Fire the query if there is no record selected in `sql`. Shown after `althead` + * `shead`: Static head - will always be shown (before `head`), independent of sql selects records or not. + * `stail`: Static tail - will always be shown (after `tail`), independent of sql selects records or not. + +Features +^^^^^^^^ + +* #2948 /altsql, shead, stail - new directives in Report. +* #4255 / Attachments fuer 'Email'. Static files can be attached to mails. + +Bug Fixes +^^^^^^^^^ + +* #4980 / Variables in Report: a) nested not replaced, b) 'rbeg' not replaced, c) missing unit tests. + + +Version 0.25.3 +-------------- + +Date: 19.11.2017 + +Notes +^^^^^ + +* Report: + + * Special column name 'sendmail': the old way of position dependent parameter are deprecated. Instead use the new + defined token. See https://docs.typo3.org/typo3cms/drafts/github/T3DocumentationStarter/Public-Info-053/Manual.html#column_sendmail + + * Every row is now merged in STORE_RECORD. Inner SQL statement can now retrieve outer values via STORE_RECORD. + E.g. `{{column:R}}`. No more level keys! + +* The config.qfq.ini directive `VAR_ADD_BY_SQL` is replaced by `FILL_STORE_SYSTEM_BY_SQL_?`. Up to 3 statements are possible. + +Features +^^^^^^^^ + +* Report / sendmail: control via token. +* #4967 / config.qfq.ini: Rename 'VAR_ADD_BY_SQL' to 'FILL_STORE_SYSTEM_BY_SQL_1'. Handle up to 3 FILL_STORE_SYSTEM_SQL_x. + Implement an optional error message together with a full stop. +* #4766: Set STORE_RECORD in Report per row. + +Bug Fixes +^^^^^^^^^ + +* #4966 / Variable {{feUser:T}} is not available in config.qfq.ini `FILL_STORE_SYSTEM_?` - changed ordering of store + initialization. Now: TCY... +* #4944 / Delete: broken when using 'tableName' (instead of form). +* #4904 / Undefined Index: DIRTY_FE_USER - PHP problem that constants cant be replaced inside of single ticks. Fixed. +* #4965: insert path to QFQ cookie/session, to make usage of multiple QFQ installation on one host possible. + + Version 0.25.2 -------------- -Date: 8.11.17 +Date: 8.11.2017 Notes ^^^^^ @@ -66,7 +128,7 @@ Bug Fixes Version 0.25.1 -------------- -Date: 3.11.17 +Date: 3.11.2017 Bug Fixes ^^^^^^^^^ diff --git a/extension/config.qfq.example.ini b/extension/config.qfq.example.ini index 812edd509edb866eebf45302bd3e8957ce820ecb..08b14d462ce934a3712f1309f74f768da4e11a2f 100644 --- a/extension/config.qfq.example.ini +++ b/extension/config.qfq.example.ini @@ -23,6 +23,9 @@ SQL_LOG = ../../sql.log ; all|modify|error|none SQL_LOG_MODE = modify +;MAIL_LOG = ../../mail.log +;SEND_E_MAIL_OPTIONS = "-o ... " - check http://caspian.dotconf.net/menu/Software/SendEmail + ; [auto|yes|no][,download]. 'auto': if BE User is logged in the value will be replaced by 'yes', else 'no'. Additional choose 'download'. SHOW_DEBUG_INFO = auto @@ -108,7 +111,9 @@ WKHTMLTOPDF = /opt/wkhtmltox/bin/wkhtmltopdf ; Local Documentation (doc fits to installed version): typo3conf/ext/qfq/Documentation/html/Manual.html ;DOCUMENTATION_QFQ = https://docs.typo3.org/typo3cms/drafts/github/T3DocumentationStarter/Public-Info-053/Manual.html -; VAR_ADD_BY_SQL = SELECT id AS _periodId FROM Period WHERE start<=NOW() ORDER BY start DESC LIMIT 1 +; FILL_STORE_SYSTEM_BY_SQL_1 = "SELECT id AS _periodId FROM Period WHERE start<=NOW() ORDER BY start DESC LIMIT 1" +; Important: only define an error message, if QFQ should stop running in case of an SQL error or not exact 1 record. +; FILL_STORE_SYSTEM_BY_SQL_ERROR_MSG_1 = No current period found ; FORM_LANGUAGE_A_ID = E.g. FORM_LANGUAGE_A_ID = 1 ; FORM_LANGUAGE_A_LABEL = E.g. FORM_LANGUAGE_A_ID = English diff --git a/extension/ext_emconf.php b/extension/ext_emconf.php index 4d144aaffe8b9327bccca0d943b5a36d1850c1bf..7995fad2ff5719b6eed95fdf28796b3f29627090 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.2' + 'version' => '0.25.4' ); diff --git a/extension/qfq/external/AutoCron.php b/extension/qfq/external/AutoCron.php index e35b0e8e091743d79488280d7053313a8a4094d0..730f9b361181b59796f8b1d6cd9aae30cdeaec8a 100644 --- a/extension/qfq/external/AutoCron.php +++ b/extension/qfq/external/AutoCron.php @@ -142,7 +142,8 @@ class AutoCron { */ private function mailEntryFill(array $mailEntry) { foreach ([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] as $key) { + FE_SENDMAIL_REPLY_TO, FE_SENDMAIL_FLAG_AUTO_SUBMIT, FE_SENDMAIL_GR_ID, FE_SENDMAIL_X_ID, + FE_SENDMAIL_X_ID2, FE_SENDMAIL_X_ID3] as $key) { if (!isset($mailEntry[$key])) { $mailEntry[$key] = ''; } @@ -184,6 +185,9 @@ class AutoCron { $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: send diff --git a/extension/qfq/external/sendEmail b/extension/qfq/external/sendEmail new file mode 100755 index 0000000000000000000000000000000000000000..9f9392e6e4bd9d2ef27825b808f9217e7bcfb9b5 --- /dev/null +++ b/extension/qfq/external/sendEmail @@ -0,0 +1,2235 @@ +#!/usr/bin/perl -w +############################################################################## +## sendEmail +## Written by: Brandon Zehm <caspian@dotconf.net> +## +## License: +## sendEmail (hereafter referred to as "program") is free software; +## you can redistribute it and/or modify it under the terms of the GNU General +## Public License as published by the Free Software Foundation; either version +## 2 of the License, or (at your option) any later version. +## When redistributing modified versions of this source code it is recommended +## that that this disclaimer and the above coder's names are included in the +## modified code. +## +## Disclaimer: +## This program is provided with no warranty of any kind, either expressed or +## implied. It is the responsibility of the user (you) to fully research and +## comprehend the usage of this program. As with any tool, it can be misused, +## either intentionally (you're a vandal) or unintentionally (you're a moron). +## THE AUTHOR(S) IS(ARE) NOT RESPONSIBLE FOR ANYTHING YOU DO WITH THIS PROGRAM +## or anything that happens because of your use (or misuse) of this program, +## including but not limited to anything you, your lawyers, or anyone else +## can dream up. And now, a relevant quote directly from the GPL: +## +## NO WARRANTY +## +## 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +## FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +## OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +## PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +## OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +## MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +## TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +## PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +## REPAIR OR CORRECTION. +## +############################################################################## +use strict; +use IO::Socket; + + +######################## +## Global Variables ## +######################## + +my %conf = ( + ## General + "programName" => $0, ## The name of this program + "version" => '1.56', ## 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 + "hostname" => 'changeme', ## Used in printmsg() for all output (is updated later in the script). + "debug" => 0, ## Default debug level + "error" => '', ## Error messages will often be stored here + + ## Logging + "stdout" => 1, + "logging" => 0, ## If this is true the printmsg function prints to the log file + "logFile" => '', ## If this is specified (form the command line via -l) this file will be used for logging. + + ## Network + "server" => 'localhost', ## Default SMTP server + "port" => 25, ## Default port + "bindaddr" => '', ## Default local bind address + "alarm" => '', ## Default timeout for connects and reads, this gets set from $opt{'timeout'} + "tls_client" => 0, ## If TLS is supported by the client (us) + "tls_server" => 0, ## If TLS is supported by the remote SMTP server + + ## Email + "delimiter" => "----MIME delimiter for sendEmail-" ## MIME Delimiter + . rand(1000000), ## Add some randomness to the delimiter + "Message-ID" => rand(1000000) . "-sendEmail", ## Message-ID for email header + +); + + +## This hash stores the options passed on the command line via the -o option. +my %opt = ( + ## Addressing + "reply-to" => '', ## Reply-To field + + ## Message + "message-file" => '', ## File to read message body from + "message-header" => '', ## Additional email header line(s) + "message-format" => 'normal', ## If "raw" is specified the message is sent unmodified + "message-charset" => 'iso-8859-1', ## Message character-set + "message-content-type" => 'auto', ## auto, text, html or an actual string to put into the content-type header. + + ## Network + "timeout" => 60, ## Default timeout for connects and reads, this is copied to $conf{'alarm'} later. + "fqdn" => 'changeme', ## FQDN of this machine, used during SMTP communication (is updated later in the script). + + ## eSMTP + "username" => '', ## Username used in SMTP Auth + "password" => '', ## Password used in SMTP Auth + "tls" => 'auto', ## Enable or disable TLS support. Options: auto, yes, no + +); + +## More variables used later in the program +my $SERVER; +my $CRLF = "\015\012"; +my $subject = ''; +my $header = ''; +my $message = ''; +my $from = ''; +my @to = (); +my @cc = (); +my @bcc = (); +my @attachments = (); +my @attachments_names = (); + +## For printing colors to the console +my ${colorRed} = "\033[31;1m"; +my ${colorGreen} = "\033[32;1m"; +my ${colorCyan} = "\033[36;1m"; +my ${colorWhite} = "\033[37;1m"; +my ${colorNormal} = "\033[m"; +my ${colorBold} = "\033[1m"; +my ${colorNoBold} = "\033[0m"; + +## Don't use shell escape codes on Windows systems +if ($^O =~ /win/i) { + ${colorRed} = ${colorGreen} = ${colorCyan} = ${colorWhite} = ${colorNormal} = ${colorBold} = ${colorNoBold} = ""; +} + +## Load IO::Socket::SSL if it's available +eval { require IO::Socket::SSL; }; +if ($@) { $conf{'tls_client'} = 0; } +else { $conf{'tls_client'} = 1; } + + + + + + +############################# +## ## +## FUNCTIONS ## +## ## +############################# + + + + + +############################################################################################### +## Function: initialize () +## +## Does all the script startup jibberish. +## +############################################################################################### +sub initialize { + + ## Set STDOUT to flush immediatly after each print + $| = 1; + + ## Intercept signals + $SIG{'QUIT'} = sub { quit("EXITING: Received SIG$_[0]", 1); }; + $SIG{'INT'} = sub { quit("EXITING: Received SIG$_[0]", 1); }; + $SIG{'KILL'} = sub { quit("EXITING: Received SIG$_[0]", 1); }; + $SIG{'TERM'} = sub { quit("EXITING: Received SIG$_[0]", 1); }; + + ## ALARM and HUP signals are not supported in Win32 + unless ($^O =~ /win/i) { + $SIG{'HUP'} = sub { quit("EXITING: Received SIG$_[0]", 1); }; + $SIG{'ALRM'} = sub { quit("EXITING: Received SIG$_[0]", 1); }; + } + + ## Fixup $conf{'programName'} + $conf{'programName'} =~ s/(.)*[\/,\\]//; + $0 = $conf{'programName'} . " " . join(" ", @ARGV); + + ## Fixup $conf{'hostname'} and $opt{'fqdn'} + if ($opt{'fqdn'} eq 'changeme') { $opt{'fqdn'} = get_hostname(1); } + if ($conf{'hostname'} eq 'changeme') { $conf{'hostname'} = $opt{'fqdn'}; $conf{'hostname'} =~ s/\..*//; } + + return(1); +} + + + + + + + + + + + + + + + +############################################################################################### +## Function: processCommandLine () +## +## Processes command line storing important data in global vars (usually %conf) +## +############################################################################################### +sub processCommandLine { + + + ############################ + ## Process command line ## + ############################ + + my @ARGS = @ARGV; ## This is so later we can re-parse the command line args later if we need to + my $numargv = @ARGS; + help() unless ($numargv); + my $counter = 0; + + for ($counter = 0; $counter < $numargv; $counter++) { + + if ($ARGS[$counter] =~ /^-h$/i) { ## Help ## + help(); + } + + elsif ($ARGS[$counter] eq "") { ## Ignore null arguments + ## Do nothing + } + + elsif ($ARGS[$counter] =~ /^--help/) { ## Topical Help ## + $counter++; + if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { + helpTopic($ARGS[$counter]); + } + else { + help(); + } + } + + elsif ($ARGS[$counter] =~ /^-o$/i) { ## Options specified with -o ## + $counter++; + ## Loop through each option passed after the -o + while ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { + + if ($ARGS[$counter] !~ /(\S+)=(\S.*)/) { + printmsg("WARNING => Name/Value pair [$ARGS[$counter]] is not properly formatted", 0); + printmsg("WARNING => Arguments proceeding -o should be in the form of \"name=value\"", 0); + } + else { + if (exists($opt{$1})) { + if ($1 eq 'message-header') { + $opt{$1} .= $2 . $CRLF; + } + else { + $opt{$1} = $2; + } + printmsg("DEBUG => Assigned \$opt{} key/value: $1 => $2", 3); + } + else { + printmsg("WARNING => Name/Value pair [$ARGS[$counter]] will be ignored: unknown key [$1]", 0); + printmsg("HINT => Try the --help option to find valid command line arguments", 1); + } + } + $counter++; + } $counter--; + } + + elsif ($ARGS[$counter] =~ /^-f$/) { ## From ## + $counter++; + if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { $from = $ARGS[$counter]; } + else { printmsg("WARNING => The argument after -f was not an email address!", 0); $counter--; } + } + + elsif ($ARGS[$counter] =~ /^-t$/) { ## To ## + $counter++; + while ($ARGS[$counter] && ($ARGS[$counter] !~ /^-/)) { + if ($ARGS[$counter] =~ /[;,]/) { + push (@to, split(/[;,]/, $ARGS[$counter])); + } + else { + push (@to,$ARGS[$counter]); + } + $counter++; + } $counter--; + } + + elsif ($ARGS[$counter] =~ /^-cc$/) { ## Cc ## + $counter++; + while ($ARGS[$counter] && ($ARGS[$counter] !~ /^-/)) { + if ($ARGS[$counter] =~ /[;,]/) { + push (@cc, split(/[;,]/, $ARGS[$counter])); + } + else { + push (@cc,$ARGS[$counter]); + } + $counter++; + } $counter--; + } + + elsif ($ARGS[$counter] =~ /^-bcc$/) { ## Bcc ## + $counter++; + while ($ARGS[$counter] && ($ARGS[$counter] !~ /^-/)) { + if ($ARGS[$counter] =~ /[;,]/) { + push (@bcc, split(/[;,]/, $ARGS[$counter])); + } + else { + push (@bcc,$ARGS[$counter]); + } + $counter++; + } $counter--; + } + + elsif ($ARGS[$counter] =~ /^-m$/) { ## Message ## + $counter++; + $message = ""; + while ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { + if ($message) { $message .= " "; } + $message .= $ARGS[$counter]; + $counter++; + } $counter--; + + ## Replace '\n' with $CRLF. + ## This allows newlines with messages sent on the command line + $message =~ s/\\n/$CRLF/g; + } + + elsif ($ARGS[$counter] =~ /^-u$/) { ## Subject ## + $counter++; + $subject = ""; + while ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { + if ($subject) { $subject .= " "; } + $subject .= $ARGS[$counter]; + $counter++; + } $counter--; + } + + elsif ($ARGS[$counter] =~ /^-s$/) { ## Server ## + $counter++; + if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { + $conf{'server'} = $ARGS[$counter]; + if ($conf{'server'} =~ /:/) { ## Port ## + ($conf{'server'},$conf{'port'}) = split(":",$conf{'server'}); + } + } + else { printmsg("WARNING - The argument after -s was not the server!", 0); $counter--; } + } + + elsif ($ARGS[$counter] =~ /^-b$/) { ## Bind Address ## + $counter++; + if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { + $conf{'bindaddr'} = $ARGS[$counter]; + } + else { printmsg("WARNING - The argument after -b was not the bindaddr!", 0); $counter--; } + } + + elsif ($ARGS[$counter] =~ /^-a$/) { ## Attachments ## + $counter++; + while ($ARGS[$counter] && ($ARGS[$counter] !~ /^-/)) { + push (@attachments,$ARGS[$counter]); + $counter++; + } $counter--; + } + + elsif ($ARGS[$counter] =~ /^-xu$/) { ## AuthSMTP Username ## + $counter++; + if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { + $opt{'username'} = $ARGS[$counter]; + } + else { + printmsg("WARNING => The argument after -xu was not valid username!", 0); + $counter--; + } + } + + elsif ($ARGS[$counter] =~ /^-xp$/) { ## AuthSMTP Password ## + $counter++; + if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { + $opt{'password'} = $ARGS[$counter]; + } + else { + printmsg("WARNING => The argument after -xp was not valid password!", 0); + $counter--; + } + } + + elsif ($ARGS[$counter] =~ /^-l$/) { ## Logging ## + $counter++; + $conf{'logging'} = 1; + if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { $conf{'logFile'} = $ARGS[$counter]; } + else { printmsg("WARNING - The argument after -l was not the log file!", 0); $counter--; } + } + + elsif ($ARGS[$counter] =~ s/^-v+//i) { ## Verbosity ## + my $tmp = (length($&) - 1); + $conf{'debug'} += $tmp; + } + + elsif ($ARGS[$counter] =~ /^-q$/) { ## Quiet ## + $conf{'stdout'} = 0; + } + + else { + printmsg("Error: \"$ARGS[$counter]\" is not a recognized option!", 0); + help(); + } + + } + + + + + + + + + ################################################### + ## Verify required variables are set correctly ## + ################################################### + + ## Make sure we have something in $conf{hostname} and $opt{fqdn} + if ($opt{'fqdn'} =~ /\./) { + $conf{'hostname'} = $opt{'fqdn'}; + $conf{'hostname'} =~ s/\..*//; + } + + if (!$conf{'server'}) { $conf{'server'} = 'localhost'; } + if (!$conf{'port'}) { $conf{'port'} = 25; } + if (!$from) { + quit("ERROR => You must specify a 'from' field! Try --help.", 1); + } + if ( ((scalar(@to)) + (scalar(@cc)) + (scalar(@bcc))) <= 0) { + quit("ERROR => You must specify at least one recipient via -t, -cc, or -bcc", 1); + } + + ## Make sure email addresses look OK. + foreach my $addr (@to, @cc, @bcc, $from, $opt{'reply-to'}) { + if ($addr) { + if (!returnAddressParts($addr)) { + printmsg("ERROR => Can't use improperly formatted email address: $addr", 0); + printmsg("HINT => Try viewing the extended help on addressing with \"--help addressing\"", 1); + quit("", 1); + } + } + } + + ## Make sure all attachments exist. + foreach my $file (@attachments) { + if ( (! -f $file) or (! -r $file) ) { + printmsg("ERROR => The attachment [$file] doesn't exist!", 0); + printmsg("HINT => Try specifying the full path to the file or reading extended help with \"--help message\"", 1); + quit("", 1); + } + } + + if ($conf{'logging'} and (!$conf{'logFile'})) { + quit("ERROR => You used -l to enable logging but didn't specify a log file!", 1); + } + + if ( $opt{'username'} ) { + if (!$opt{'password'}) { + ## Prompt for a password since one wasn't specified with the -xp option. + $SIG{'ALRM'} = sub { quit("ERROR => Timeout waiting for password inpupt", 1); }; + alarm(60) if ($^O !~ /win/i); ## alarm() doesn't work in win32 + print "Password: "; + $opt{'password'} = <STDIN>; chomp $opt{'password'}; + if (!$opt{'password'}) { + quit("ERROR => A username for SMTP authentication was specified, but no password!", 1); + } + } + } + + ## Validate the TLS setting + $opt{'tls'} = lc($opt{'tls'}); + if ($opt{'tls'} !~ /^(auto|yes|no)$/) { + quit("ERROR => Invalid TLS setting ($opt{'tls'}). Must be one of auto, yes, or no.", 1); + } + + ## If TLS is set to "yes", make sure sendEmail loaded the libraries needed. + if ($opt{'tls'} eq 'yes' and $conf{'tls_client'} == 0) { + quit("ERROR => No TLS support! SendEmail can't load required libraries. (try installing Net::SSLeay and IO::Socket::SSL)", 1); + } + + ## Return 0 errors + return(0); +} + + + + + + + + + + + + + + + + +## getline($socketRef) +sub getline { + my ($socketRef) = @_; + local ($/) = "\r\n"; + return $$socketRef->getline; +} + + + + +## Receive a (multiline?) SMTP response from ($socketRef) +sub getResponse { + my ($socketRef) = @_; + my ($tmp, $reply); + local ($/) = "\r\n"; + return undef unless defined($tmp = getline($socketRef)); + return("getResponse() socket is not open") unless ($$socketRef->opened); + ## Keep reading lines if it's a multi-line response + while ($tmp =~ /^\d{3}-/o) { + $reply .= $tmp; + return undef unless defined($tmp = getline($socketRef)); + } + $reply .= $tmp; + $reply =~ s/\r?\n$//o; + return $reply; +} + + + + +############################################################################################### +## Function: SMTPchat ( [string $command] ) +## +## Description: Sends $command to the SMTP server (on SERVER) and awaits a successful +## reply form the server. If the server returns an error, or does not reply +## within $conf{'alarm'} seconds an error is generated. +## NOTE: $command is optional, if no command is specified then nothing will +## be sent to the server, but a valid response is still required from the server. +## +## Input: [$command] A (optional) valid SMTP command (ex. "HELO") +## +## +## Output: Returns zero on success, or non-zero on error. +## Error messages will be stored in $conf{'error'} +## A copy of the last SMTP response is stored in the global variable +## $conf{'SMTPchat_response'} +## +## +## Example: SMTPchat ("HELO mail.isp.net"); +############################################################################################### +sub SMTPchat { + my ($command) = @_; + + printmsg("INFO => Sending: \t$command", 1) if ($command); + + ## Send our command + print $SERVER "$command$CRLF" if ($command); + + ## Read a response from the server + $SIG{'ALRM'} = sub { $conf{'error'} = "alarm"; $SERVER->close(); }; + alarm($conf{'alarm'}) if ($^O !~ /win/i); ## alarm() doesn't work in win32; + my $result = $conf{'SMTPchat_response'} = getResponse(\$SERVER); + alarm(0) if ($^O !~ /win/i); ## alarm() doesn't work in win32; + + ## Generate an alert if we timed out + if ($conf{'error'} eq "alarm") { + $conf{'error'} = "ERROR => Timeout while reading from $conf{'server'}:$conf{'port'} There was no response after $conf{'alarm'} seconds."; + return(1); + } + + ## Make sure the server actually responded + if (!$result) { + $conf{'error'} = "ERROR => $conf{'server'}:$conf{'port'} returned a zero byte response to our query."; + return(2); + } + + ## Validate the response + if (evalSMTPresponse($result)) { + ## conf{'error'} will already be set here + return(2); + } + + ## Print the success messsage + printmsg($conf{'error'}, 1); + + ## Return Success + return(0); +} + + + + + + + + + + + + +############################################################################################### +## Function: evalSMTPresponse (string $message ) +## +## Description: Searches $message for either an SMTP success or error code, and returns +## 0 on success, and the actual error code on error. +## +## +## Input: $message Data received from a SMTP server (ex. "220 +## +## +## Output: Returns zero on success, or non-zero on error. +## Error messages will be stored in $conf{'error'} +## +## +## Example: SMTPchat ("HELO mail.isp.net"); +############################################################################################### +sub evalSMTPresponse { + my ($message) = @_; + + ## Validate input + if (!$message) { + $conf{'error'} = "ERROR => No message was passed to evalSMTPresponse(). What happened?"; + return(1) + } + + printmsg("DEBUG => evalSMTPresponse() - Checking for SMTP success or error status in the message: $message ", 3); + + ## Look for a SMTP success code + if ($message =~ /^([23]\d\d)/) { + printmsg("DEBUG => evalSMTPresponse() - Found SMTP success code: $1", 2); + $conf{'error'} = "SUCCESS => Received: \t$message"; + return(0); + } + + ## Look for a SMTP error code + if ($message =~ /^([45]\d\d)/) { + printmsg("DEBUG => evalSMTPresponse() - Found SMTP error code: $1", 2); + $conf{'error'} = "ERROR => Received: \t$message"; + return($1); + } + + ## If no SMTP codes were found return an error of 1 + $conf{'error'} = "ERROR => Received a message with no success or error code. The message received was: $message"; + return(2); + +} + + + + + + + + + + +######################################################### +# SUB: &return_month(0,1,etc) +# returns the name of the month that corrosponds +# with the number. returns 0 on error. +######################################################### +sub return_month { + my $x = $_[0]; + if ($x == 0) { return 'Jan'; } + if ($x == 1) { return 'Feb'; } + if ($x == 2) { return 'Mar'; } + if ($x == 3) { return 'Apr'; } + if ($x == 4) { return 'May'; } + if ($x == 5) { return 'Jun'; } + if ($x == 6) { return 'Jul'; } + if ($x == 7) { return 'Aug'; } + if ($x == 8) { return 'Sep'; } + if ($x == 9) { return 'Oct'; } + if ($x == 10) { return 'Nov'; } + if ($x == 11) { return 'Dec'; } + return (0); +} + + + + + + + + + + + + + + + + +######################################################### +# SUB: &return_day(0,1,etc) +# returns the name of the day that corrosponds +# with the number. returns 0 on error. +######################################################### +sub return_day { + my $x = $_[0]; + if ($x == 0) { return 'Sun'; } + if ($x == 1) { return 'Mon'; } + if ($x == 2) { return 'Tue'; } + if ($x == 3) { return 'Wed'; } + if ($x == 4) { return 'Thu'; } + if ($x == 5) { return 'Fri'; } + if ($x == 6) { return 'Sat'; } + return (0); +} + + + + + + + + + + + + + + + + +############################################################################################### +## Function: returnAddressParts(string $address) +## +## Description: Returns a two element array containing the "Name" and "Address" parts of +## an email address. +## +## Example: "Brandon Zehm <caspian@dotconf.net>" +## would return: ("Brandon Zehm", "caspian@dotconf.net"); +## +## "caspian@dotconf.net" +## would return: ("caspian@dotconf.net", "caspian@dotconf.net") +############################################################################################### +sub returnAddressParts { + my $input = $_[0]; + my $name = ""; + my $address = ""; + + ## Make sure to fail if it looks totally invalid + if ($input !~ /(\S+\@\S+)/) { + $conf{'error'} = "ERROR => The address [$input] doesn't look like a valid email address, ignoring it"; + return(undef()); + } + + ## Check 1, should find addresses like: "Brandon Zehm <caspian@dotconf.net>" + elsif ($input =~ /^\s*(\S(.*\S)?)\s*<(\S+\@\S+)>/o) { + ($name, $address) = ($1, $3); + } + + ## Otherwise if that failed, just get the address: <caspian@dotconf.net> + elsif ($input =~ /<(\S+\@\S+)>/o) { + $name = $address = $1; + } + + ## Or maybe it was formatted this way: caspian@dotconf.net + elsif ($input =~ /(\S+\@\S+)/o) { + $name = $address = $1; + } + + ## Something stupid happened, just return an error. + unless ($name and $address) { + printmsg("ERROR => Couldn't parse the address: $input", 0); + printmsg("HINT => If you think this should work, consider reporting this as a bug to $conf{'authorEmail'}", 1); + return(undef()); + } + + ## Make sure there aren't invalid characters in the address, and return it. + my $ctrl = '\000-\037'; + my $nonASCII = '\x80-\xff'; + if ($address =~ /[<> ,;:"'\[\]\\$ctrl$nonASCII]/) { + printmsg("WARNING => The address [$address] seems to contain invalid characters: continuing anyway", 0); + } + return($name, $address); +} + + + + + + + + + + + + + + + + +############################################################################################### +## Function: base64_encode(string $data, bool $chunk) +## +## Description: Returns $data as a base64 encoded string. +## If $chunk is true, the encoded data is returned in 76 character long lines +## with the final \CR\LF removed. +## +## Note: This is only used from the smtp auth section of code. +## At some point it would be nice to merge the code that encodes attachments and this. +############################################################################################### +sub base64_encode { + my $data = $_[0]; + my $chunk = $_[1]; + my $tmp = ''; + my $base64 = ''; + my $CRLF = "\r\n"; + + ################################### + ## Convert binary data to base64 ## + ################################### + while ($data =~ s/(.{45})//s) { ## Get 45 bytes from the binary string + $tmp = substr(pack('u', $&), 1); ## Convert the binary to uuencoded text + chop($tmp); + $tmp =~ tr|` -_|AA-Za-z0-9+/|; ## Translate from uuencode to base64 + $base64 .= $tmp; + } + + ########################## + ## Encode the leftovers ## + ########################## + my $padding = ""; + if ( ($data) and (length($data) > 0) ) { + $padding = (3 - length($data) % 3) % 3; ## Set flag if binary data isn't divisible by 3 + $tmp = substr(pack('u', $data), 1); ## Convert the binary to uuencoded text + chop($tmp); + $tmp =~ tr|` -_|AA-Za-z0-9+/|; ## Translate from uuencode to base64 + $base64 .= $tmp; + } + + ############################ + ## Fix padding at the end ## + ############################ + $data = ''; + $base64 =~ s/.{$padding}$/'=' x $padding/e if $padding; ## Fix the end padding if flag (from above) is set + if ($chunk) { + while ($base64 =~ s/(.{1,76})//s) { ## Put $CRLF after each 76 characters + $data .= "$1$CRLF"; + } + } + else { + $data = $base64; + } + + ## Remove any trailing CRLF's + $data =~ s/(\r|\n)*$//s; + return($data); +} + + + + + + + + + +######################################################### +# SUB: send_attachment("/path/filename") +# Sends the mime headers and base64 encoded file +# to the email server. +######################################################### +sub send_attachment { + my ($filename) = @_; ## Get filename passed + my (@fields, $y, $filename_name, $encoding, ## Local variables + @attachlines, $content_type); + my $bin = 1; + + @fields = split(/\/|\\/, $filename); ## Get the actual filename without the path + $filename_name = pop(@fields); + push @attachments_names, $filename_name; ## FIXME: This is only used later for putting in the log file + + ########################## + ## Autodetect Mime Type ## + ########################## + + @fields = split(/\./, $filename_name); + $encoding = $fields[$#fields]; + + if ($encoding =~ /txt|text|log|conf|^c$|cpp|^h$|inc|m3u/i) { $content_type = 'text/plain'; } + elsif ($encoding =~ /html|htm|shtml|shtm|asp|php|cfm/i) { $content_type = 'text/html'; } + elsif ($encoding =~ /sh$/i) { $content_type = 'application/x-sh'; } + elsif ($encoding =~ /tcl/i) { $content_type = 'application/x-tcl'; } + elsif ($encoding =~ /pl$/i) { $content_type = 'application/x-perl'; } + elsif ($encoding =~ /js$/i) { $content_type = 'application/x-javascript'; } + elsif ($encoding =~ /man/i) { $content_type = 'application/x-troff-man'; } + elsif ($encoding =~ /gif/i) { $content_type = 'image/gif'; } + elsif ($encoding =~ /jpg|jpeg|jpe|jfif|pjpeg|pjp/i) { $content_type = 'image/jpeg'; } + elsif ($encoding =~ /tif|tiff/i) { $content_type = 'image/tiff'; } + elsif ($encoding =~ /xpm/i) { $content_type = 'image/x-xpixmap'; } + elsif ($encoding =~ /bmp/i) { $content_type = 'image/x-MS-bmp'; } + elsif ($encoding =~ /pcd/i) { $content_type = 'image/x-photo-cd'; } + elsif ($encoding =~ /png/i) { $content_type = 'image/png'; } + elsif ($encoding =~ /aif|aiff/i) { $content_type = 'audio/x-aiff'; } + elsif ($encoding =~ /wav/i) { $content_type = 'audio/x-wav'; } + elsif ($encoding =~ /mp2|mp3|mpa/i) { $content_type = 'audio/x-mpeg'; } + elsif ($encoding =~ /ra$|ram/i) { $content_type = 'audio/x-pn-realaudio'; } + elsif ($encoding =~ /mpeg|mpg/i) { $content_type = 'video/mpeg'; } + elsif ($encoding =~ /mov|qt$/i) { $content_type = 'video/quicktime'; } + elsif ($encoding =~ /avi/i) { $content_type = 'video/x-msvideo'; } + elsif ($encoding =~ /zip/i) { $content_type = 'application/x-zip-compressed'; } + elsif ($encoding =~ /tar/i) { $content_type = 'application/x-tar'; } + elsif ($encoding =~ /jar/i) { $content_type = 'application/java-archive'; } + elsif ($encoding =~ /exe|bin/i) { $content_type = 'application/octet-stream'; } + elsif ($encoding =~ /ppt|pot|ppa|pps|pwz/i) { $content_type = 'application/vnd.ms-powerpoint'; } + elsif ($encoding =~ /mdb|mda|mde/i) { $content_type = 'application/vnd.ms-access'; } + elsif ($encoding =~ /xls|xlt|xlm|xld|xla|xlc|xlw|xll/i) { $content_type = 'application/vnd.ms-excel'; } + elsif ($encoding =~ /doc|dot/i) { $content_type = 'application/msword'; } + elsif ($encoding =~ /rtf/i) { $content_type = 'application/rtf'; } + elsif ($encoding =~ /pdf/i) { $content_type = 'application/pdf'; } + elsif ($encoding =~ /tex/i) { $content_type = 'application/x-tex'; } + elsif ($encoding =~ /latex/i) { $content_type = 'application/x-latex'; } + elsif ($encoding =~ /vcf/i) { $content_type = 'application/x-vcard'; } + else { $content_type = 'application/octet-stream'; } + + + ############################ + ## Process the attachment ## + ############################ + + ##################################### + ## Generate and print MIME headers ## + ##################################### + + $y = "$CRLF--$conf{'delimiter'}$CRLF"; + $y .= "Content-Type: $content_type;$CRLF"; + $y .= " name=\"$filename_name\"$CRLF"; + $y .= "Content-Transfer-Encoding: base64$CRLF"; + $y .= "Content-Disposition: attachment; filename=\"$filename_name\"$CRLF"; + $y .= "$CRLF"; + print $SERVER $y; + + + ########################################################### + ## Convert the file to base64 and print it to the server ## + ########################################################### + + open (FILETOATTACH, $filename) || do { + printmsg("ERROR => Opening the file [$filename] for attachment failed with the error: $!", 0); + return(1); + }; + binmode(FILETOATTACH); ## Hack to make Win32 work + + my $res = ""; + my $tmp = ""; + my $base64 = ""; + while (<FILETOATTACH>) { ## Read a line from the (binary) file + $res .= $_; + + ################################### + ## Convert binary data to base64 ## + ################################### + while ($res =~ s/(.{45})//s) { ## Get 45 bytes from the binary string + $tmp = substr(pack('u', $&), 1); ## Convert the binary to uuencoded text + chop($tmp); + $tmp =~ tr|` -_|AA-Za-z0-9+/|; ## Translate from uuencode to base64 + $base64 .= $tmp; + } + + ################################ + ## Print chunks to the server ## + ################################ + while ($base64 =~ s/(.{76})//s) { + print $SERVER "$1$CRLF"; + } + + } + + ################################### + ## Encode and send the leftovers ## + ################################### + my $padding = ""; + if ( ($res) and (length($res) >= 1) ) { + $padding = (3 - length($res) % 3) % 3; ## Set flag if binary data isn't divisible by 3 + $res = substr(pack('u', $res), 1); ## Convert the binary to uuencoded text + chop($res); + $res =~ tr|` -_|AA-Za-z0-9+/|; ## Translate from uuencode to base64 + } + + ############################ + ## Fix padding at the end ## + ############################ + $res = $base64 . $res; ## Get left overs from above + $res =~ s/.{$padding}$/'=' x $padding/e if $padding; ## Fix the end padding if flag (from above) is set + if ($res) { + while ($res =~ s/(.{1,76})//s) { ## Send it to the email server. + print $SERVER "$1$CRLF"; + } + } + + close (FILETOATTACH) || do { + printmsg("ERROR - Closing the filehandle for file [$filename] failed with the error: $!", 0); + return(2); + }; + + ## Return 0 errors + return(0); + +} + + + + + + + + + +############################################################################################### +## Function: $string = get_hostname (boot $fqdn) +## +## Description: Tries really hard to returns the short (or FQDN) hostname of the current +## system. Uses techniques and code from the Sys-Hostname module. +## +## Input: $fqdn A true value (1) will cause this function to return a FQDN hostname +## rather than a short hostname. +## +## Output: Returns a string +############################################################################################### +sub get_hostname { + ## Assign incoming parameters to variables + my ( $fqdn ) = @_; + my $hostname = ""; + + ## STEP 1: Get short hostname + + ## Load Sys::Hostname if it's available + eval { require Sys::Hostname; }; + unless ($@) { + $hostname = Sys::Hostname::hostname(); + } + + ## If that didn't get us a hostname, try a few other things + else { + ## Windows systems + if ($^O !~ /win/i) { + if ($ENV{'COMPUTERNAME'}) { $hostname = $ENV{'COMPUTERNAME'}; } + if (!$hostname) { $hostname = gethostbyname('localhost'); } + if (!$hostname) { chomp($hostname = `hostname 2> NUL`) }; + } + + ## Unix systems + else { + local $ENV{PATH} = '/usr/bin:/bin:/usr/sbin:/sbin'; ## Paranoia + + ## Try the environment first (Help! What other variables could/should I be checking here?) + if ($ENV{'HOSTNAME'}) { $hostname = $ENV{'HOSTNAME'}; } + + ## Try the hostname command + eval { local $SIG{__DIE__}; local $SIG{CHLD}; $hostname = `hostname 2>/dev/null`; chomp($hostname); } || + + ## Try POSIX::uname(), which strictly can't be expected to be correct + eval { local $SIG{__DIE__}; require POSIX; $hostname = (POSIX::uname())[1]; } || + + ## Try the uname command + eval { local $SIG{__DIE__}; $hostname = `uname -n 2>/dev/null`; chomp($hostname); }; + + } + + ## If we can't find anything else, return "" + if (!$hostname) { + print "WARNING => No hostname could be determined, please specify one with -o fqdn=FQDN option!\n"; + return("unknown"); + } + } + + ## Return the short hostname + unless ($fqdn) { + $hostname =~ s/\..*//; + return(lc($hostname)); + } + + ## STEP 2: Determine the FQDN + + ## First, if we already have one return it. + if ($hostname =~ /\w\.\w/) { return(lc($hostname)); } + + ## Next try using + eval { $fqdn = (gethostbyname($hostname))[0]; }; + if ($fqdn) { return(lc($fqdn)); } + return(lc($hostname)); +} + + + + + + + + +############################################################################################### +## Function: printmsg (string $message, int $level) +## +## Description: Handles all messages - printing them to the screen only if the messages +## $level is >= the global debug level. If $conf{'logFile'} is defined it +## will also log the message to that file. +## +## Input: $message A message to be printed, logged, etc. +## $level The debug level of the message. If +## not defined 0 will be assumed. 0 is +## considered a normal message, 1 and +## higher is considered a debug message. +## +## Output: Prints to STDOUT +## +## Assumptions: $conf{'hostname'} should be the name of the computer we're running on. +## $conf{'stdout'} should be set to 1 if you want to print to stdout +## $conf{'logFile'} should be a full path to a log file if you want that +## $conf{'debug'} should be an integer between 0 and 10. +## +## Example: printmsg("WARNING: We believe in generic error messages... NOT!", 0); +############################################################################################### +sub printmsg { + ## Assign incoming parameters to variables + my ( $message, $level ) = @_; + + ## Make sure input is sane + $level = 0 if (!defined($level)); + $message =~ s/\s+$//sgo; + $message =~ s/\r?\n/, /sgo; + + ## Continue only if the debug level of the program is >= message debug level. + if ($conf{'debug'} >= $level) { + + ## Get the date in the format: Dec 3 11:14:04 + my ($sec, $min, $hour, $mday, $mon) = localtime(); + $mon = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')[$mon]; + my $date = sprintf("%s %02d %02d:%02d:%02d", $mon, $mday, $hour, $min, $sec); + + ## Print to STDOUT always if debugging is enabled, or if conf{stdout} is true. + if ( ($conf{'debug'} >= 1) or ($conf{'stdout'} == 1) ) { + print "$date $conf{'hostname'} $conf{'programName'}\[$$\]: $message\n"; + } + + ## Print to the log file if $conf{'logging'} is true + if ($conf{'logFile'}) { + if (openLogFile($conf{'logFile'})) { $conf{'logFile'} = ""; printmsg("ERROR => Opening the file [$conf{'logFile'}] for appending returned the error: $!", 1); } + print LOGFILE "$date $conf{'hostname'} $conf{'programName'}\[$$\]: $message\n"; + } + + } + + ## Return 0 errors + return(0); +} + + + + + + + + + + + + +############################################################################################### +## FUNCTION: +## openLogFile ( $filename ) +## +## +## DESCRIPTION: +## Opens the file $filename and attaches it to the filehandle "LOGFILE". Returns 0 on success +## and non-zero on failure. Error codes are listed below, and the error message gets set in +## global variable $!. +## +## +## Example: +## openFile ("/var/log/sendEmail.log"); +## +############################################################################################### +sub openLogFile { + ## Get the incoming filename + my $filename = $_[0]; + + ## Make sure our file exists, and if the file doesn't exist then create it + if ( ! -f $filename ) { + print STDERR "NOTICE: The log file [$filename] does not exist. Creating it now with mode [0600].\n" if ($conf{'stdout'}); + open (LOGFILE, ">>$filename"); + close LOGFILE; + chmod (0600, $filename); + } + + ## Now open the file and attach it to a filehandle + open (LOGFILE,">>$filename") or return (1); + + ## Put the file into non-buffering mode + select LOGFILE; + $| = 1; + select STDOUT; + + ## Return success + return(0); +} + + + + + + + + +############################################################################################### +## Function: read_file (string $filename) +## +## Description: Reads the contents of a file and returns a two part array: +## ($status, $file-contents) +## $status is 0 on success, non-zero on error. +## +## Example: ($status, $file) = read_file("/etc/passwd"); +############################################################################################### +sub read_file { + my ( $filename ) = @_; + + ## If the value specified is a file, load the file's contents + if ( (-e $filename and -r $filename) ) { + my $FILE; + if(!open($FILE, ' ' . $filename)) { + return((1, "")); + } + my $file = ''; + while (<$FILE>) { + $file .= $_; + } + ## Strip an ending \r\n + $file =~ s/\r?\n$//os; + } + return((1, "")); +} + + + + + + + + + +############################################################################################### +## Function: quit (string $message, int $errorLevel) +## +## Description: Exits the program, optionally printing $message. It +## returns an exit error level of $errorLevel to the +## system (0 means no errors, and is assumed if empty.) +## +## Example: quit("Exiting program normally", 0); +############################################################################################### +sub quit { + my ( $message, $errorLevel ) = @_; + $errorLevel = 0 if (!defined($errorLevel)); + + ## Print exit message + if ($message) { + printmsg($message, 0); + } + + ## Exit + exit($errorLevel); +} + + + + + + + + + + + + +############################################################################################### +## Function: help () +## +## Description: For all those newbies ;) +## Prints a help message and exits the program. +## +############################################################################################### +sub help { +exit(1) if (!$conf{'stdout'}); +print <<EOM; + +${colorBold}$conf{'programName'}-$conf{'version'} by $conf{'authorName'} <$conf{'authorEmail'}>${colorNoBold} + +Synopsis: $conf{'programName'} -f ADDRESS [options] + + ${colorRed}Required:${colorNormal} + -f ADDRESS from (sender) email address + * At least one recipient required via -t, -cc, or -bcc + * Message body required via -m, STDIN, or -o message-file=FILE + + ${colorGreen}Common:${colorNormal} + -t ADDRESS [ADDR ...] to email address(es) + -u SUBJECT message subject + -m MESSAGE message body + -s SERVER[:PORT] smtp mail relay, default is $conf{'server'}:$conf{'port'} + + ${colorGreen}Optional:${colorNormal} + -a FILE [FILE ...] file attachment(s) + -cc ADDRESS [ADDR ...] cc email address(es) + -bcc ADDRESS [ADDR ...] bcc email address(es) + -xu USERNAME username for SMTP authentication + -xp PASSWORD password for SMTP authentication + + ${colorGreen}Paranormal:${colorNormal} + -b BINDADDR[:PORT] local host bind address + -l LOGFILE log to the specified file + -v verbosity, use multiple times for greater effect + -q be quiet (i.e. no STDOUT output) + -o NAME=VALUE advanced options, for details try: --help misc + -o message-content-type=<auto|text|html> + -o message-file=FILE -o message-format=raw + -o message-header=HEADER -o message-charset=CHARSET + -o reply-to=ADDRESS -o timeout=SECONDS + -o username=USERNAME -o password=PASSWORD + -o tls=<auto|yes|no> -o fqdn=FQDN + + + ${colorGreen}Help:${colorNormal} + --help the helpful overview you're reading now + --help addressing explain addressing and related options + --help message explain message body input and related options + --help networking explain -s, -b, etc + --help output explain logging and other output options + --help misc explain -o options, TLS, SMTP auth, and more + +EOM +exit(1); +} + + + + + + + + + +############################################################################################### +## Function: helpTopic ($topic) +## +## Description: For all those newbies ;) +## Prints a help message and exits the program. +## +############################################################################################### +sub helpTopic { + exit(1) if (!$conf{'stdout'}); + my ($topic) = @_; + + CASE: { + + + + +## ADDRESSING + ($topic eq 'addressing') && do { + print <<EOM; + +${colorBold}ADDRESSING DOCUMENTATION${colorNormal} + +${colorGreen}Addressing Options${colorNormal} +Options related to addressing: + -f ADDRESS + -t ADDRESS [ADDRESS ...] + -cc ADDRESS [ADDRESS ...] + -bcc ADDRESS [ADDRESS ...] + -o reply-to=ADDRESS + +-f ADDRESS + This required option specifies who the email is from, I.E. the sender's + email address. + +-t ADDRESS [ADDRESS ...] + This option specifies the primary recipient(s). At least one recipient + address must be specified via the -t, -cc. or -bcc options. + +-cc ADDRESS [ADDRESS ...] + This option specifies the "carbon copy" recipient(s). At least one + recipient address must be specified via the -t, -cc. or -bcc options. + +-bcc ADDRESS [ADDRESS ...] + This option specifies the "blind carbon copy" recipient(s). At least + one recipient address must be specified via the -t, -cc. or -bcc options. + +-o reply-to=ADDRESS + This option specifies that an optional "Reply-To" address should be + written in the email's headers. + + +${colorGreen}Email Address Syntax${colorNormal} +Email addresses may be specified in one of two ways: + Full Name: "John Doe <john.doe\@gmail.com>" + Just Address: "john.doe\@gmail.com" + +The "Full Name" method is useful if you want a name, rather than a plain +email address, to be displayed in the recipient's From, To, or Cc fields +when they view the message. + + +${colorGreen}Multiple Recipients${colorNormal} +The -t, -cc, and -bcc options each accept multiple addresses. They may be +specified by separating them by either a white space, comma, or semi-colon +separated list. You may also specify the -t, -cc, and -bcc options multiple +times, each occurance will append the new recipients to the respective list. + +Examples: +(I used "-t" in these examples, but it can be "-cc" or "-bcc" as well) + + * Space separated list: + -t jane.doe\@yahoo.com "John Doe <john.doe\@gmail.com>" + + * Semi-colon separated list: + -t "jane.doe\@yahoo.com; John Doe <john.doe\@gmail.com>" + + * Comma separated list: + -t "jane.doe\@yahoo.com, John Doe <john.doe\@gmail.com>" + + * Multiple -t, -cc, or -bcc options: + -t "jane.doe\@yahoo.com" -t "John Doe <john.doe\@gmail.com>" + + +EOM + last CASE; + }; + + + + + + +## MESSAGE + ($topic eq 'message') && do { + print <<EOM; + +${colorBold}MESSAGE DOCUMENTATION${colorNormal} + +${colorGreen}Message Options${colorNormal} +Options related to the email message body: + -u SUBJECT + -m MESSAGE + -o message-file=FILE + -o message-content-type=<auto|text|html> + -o message-header=EMAIL HEADER + -o message-charset=CHARSET + -o message-format=raw + +-u SUBJECT + This option allows you to specify the subject for your email message. + It is not required (anymore) that the subject be quoted, although it + is recommended. The subject will be read until an argument starting + with a hyphen (-) is found. + Examples: + -u "Contact information while on vacation" + -u New Microsoft vulnerability discovered + +-m MESSAGE + This option is one of three methods that allow you to specify the message + body for your email. The message may be specified on the command line + with this -m option, read from a file with the -o message-file=FILE + option, or read from STDIN if neither of these options are present. + + It is not required (anymore) that the message be quoted, although it is + recommended. The message will be read until an argument starting with a + hyphen (-) is found. + Examples: + -m "See you in South Beach, Hawaii. -Todd" + -m Please ensure that you upgrade your systems right away + + Multi-line message bodies may be specified with the -m option by putting + a "\\n" into the message. Example: + -m "This is line 1.\\nAnd this is line 2." + + HTML messages are supported, simply begin your message with "<html>" and + sendEmail will properly label the mime header so MUAs properly render + the message. It is currently not possible without "-o message-format=raw" + to send a message with both text and html parts with sendEmail. + +-o message-file=FILE + This option is one of three methods that allow you to specify the message + body for your email. To use this option simply specify a text file + containing the body of your email message. Examples: + -o message-file=/root/message.txt + -o message-file="C:\\Program Files\\output.txt" + +-o message-content-type=<auto|text|html> + This option allows you to specify the content-type of the email. If your + email message is an html message but is being displayed as a text message + just add "-o message-content-type=html" to the command line to force it + to display as an html message. This actually just changes the Content-Type: + header. Advanced users will be happy to know that if you specify anything + other than the three options listed above it will use that as the vaule + for the Content-Type header. + +-o message-header=EMAIL HEADER + This option allows you to specify additional email headers to be included. + To add more than one message header simply use this option on the command + line more than once. If you specify a message header that sendEmail would + normally generate the one you specified will be used in it's place. + Do not use this unless you know what you are doing! + Example: + To scare a Microsoft Outlook user you may want to try this: + -o message-header="X-Message-Flag: Message contains illegal content" + Example: + To request a read-receipt try this: + -o message-header="Disposition-Notification-To: <user\@domain.com>" + Example: + To set the message priority try this: + -o message-header="X-Priority: 1" + Priority reference: 1=highest, 2=high, 3=normal, 4=low, 5=lowest + +-o message-charset=CHARSET + This option allows you to specify the character-set for the message body. + The default is iso-8859-1. + +-o message-format=raw + This option instructs sendEmail to assume the message (specified with -m, + read from STDIN, or read from the file specified in -o message-file=FILE) + is already a *complete* email message. SendEmail will not generate any + headers and will transmit the message as-is to the remote SMTP server. + Due to the nature of this option the following command line options will + be ignored when this one is used: + -u SUBJECT + -o message-header=EMAIL HEADER + -o message-charset=CHARSET + -a ATTACHMENT + + +${colorGreen}The Message Body${colorNormal} +The email message body may be specified in one of three ways: + 1) Via the -m MESSAGE command line option. + Example: + -m "This is the message body" + + 2) By putting the message body in a file and using the -o message-file=FILE + command line option. + Example: + -o message-file=/root/message.txt + + 3) By piping the message body to sendEmail when nither of the above command + line options were specified. + Example: + grep "ERROR" /var/log/messages | sendEmail -t you\@domain.com ... + +If the message body begins with "<html>" then the message will be treated as +an HTML message and the MIME headers will be written so that a HTML capable +email client will display the message in it's HTML form. +Any of the above methods may be used with the -o message-format=raw option +to deliver an already complete email message. + + +EOM + last CASE; + }; + + + + + + +## MISC + ($topic eq 'misc') && do { + print <<EOM; + +${colorBold}MISC DOCUMENTATION${colorNormal} + +${colorGreen}Misc Options${colorNormal} +Options that don't fit anywhere else: + -a ATTACHMENT [ATTACHMENT ...] + -xu USERNAME + -xp PASSWORD + -o username=USERNAME + -o password=PASSWORD + -o tls=<auto|yes|no> + -o timeout=SECONDS + -o fqdn=FQDN + +-a ATTACHMENT [ATTACHMENT ...] + This option allows you to attach any number of files to your email message. + To specify more than one attachment, simply separate each filename with a + space. Example: -a file1.txt file2.txt file3.txt + +-xu USERNAME + Alias for -o username=USERNAME + +-xp PASSWORD + Alias for -o password=PASSWORD + +-o username=USERNAME (synonym for -xu) + These options allow specification of a username to be used with SMTP + servers that require authentication. If a username is specified but a + password is not, you will be prompted to enter one at runtime. + +-o password=PASSWORD (synonym for -xp) + These options allow specification of a password to be used with SMTP + servers that require authentication. If a username is specified but a + password is not, you will be prompted to enter one at runtime. + +-o tls=<auto|yes|no> + This option allows you to specify if TLS (SSL for SMTP) should be enabled + or disabled. The default, auto, will use TLS automatically if your perl + installation has the IO::Socket::SSL and Net::SSLeay modules available, + and if the remote SMTP server supports TLS. To require TLS for message + delivery set this to yes. To disable TLS support set this to no. A debug + level of one or higher will reveal details about the status of TLS. + +-o timeout=SECONDS + This option sets the timeout value in seconds used for all network reads, + writes, and a few other things. + +-o fqdn=FQDN + This option sets the Fully Qualified Domain Name used during the initial + SMTP greeting. Normally this is automatically detected, but in case you + need to manually set it for some reason or get a warning about detection + failing, you can use this to override the default. + + +EOM + last CASE; + }; + + + + + + +## NETWORKING + ($topic eq 'networking') && do { + print <<EOM; + +${colorBold}NETWORKING DOCUMENTATION${colorNormal} + +${colorGreen}Networking Options${colorNormal} +Options related to networking: + -s SERVER[:PORT] + -b BINDADDR[:PORT] + -o tls=<auto|yes|no> + -o timeout=SECONDS + +-s SERVER[:PORT] + This option allows you to specify the SMTP server sendEmail should + connect to to deliver your email message to. If this option is not + specified sendEmail will try to connect to localhost:25 to deliver + the message. THIS IS MOST LIKELY NOT WHAT YOU WANT, AND WILL LIKELY + FAIL unless you have a email server (commonly known as an MTA) running + on your computer! + Typically you will need to specify your company or ISP's email server. + For example, if you use CableOne you will need to specify: + -s mail.cableone.net + If you have your own email server running on port 300 you would + probably use an option like this: + -s myserver.mydomain.com:300 + If you're a GMail user try: + -s smtp.gmail.com:587 -xu me\@gmail.com -xp PASSWD + +-b BINDADDR[:PORT] + This option allows you to specify the local IP address (and optional + tcp port number) for sendEmail to bind to when connecting to the remote + SMTP server. This useful for people who need to send an email from a + specific network interface or source address and are running sendEmail on + a firewall or other host with several network interfaces. + +-o tls=<auto|yes|no> + This option allows you to specify if TLS (SSL for SMTP) should be enabled + or disabled. The default, auto, will use TLS automatically if your perl + installation has the IO::Socket::SSL and Net::SSLeay modules available, + and if the remote SMTP server supports TLS. To require TLS for message + delivery set this to yes. To disable TLS support set this to no. A debug + level of one or higher will reveal details about the status of TLS. + +-o timeout=SECONDS + This option sets the timeout value in seconds used for all network reads, + writes, and a few other things. + + +EOM + last CASE; + }; + + + + + + +## OUTPUT + ($topic eq 'output') && do { + print <<EOM; + +${colorBold}OUTPUT DOCUMENTATION${colorNormal} + +${colorGreen}Output Options${colorNormal} +Options related to output: + -l LOGFILE + -v + -q + +-l LOGFILE + This option allows you to specify a log file to append to. Every message + that is displayed to STDOUT is also written to the log file. This may be + used in conjunction with -q and -v. + +-q + This option tells sendEmail to disable printing to STDOUT. In other + words nothing will be printed to the console. This does not affect the + behavior of the -l or -v options. + +-v + This option allows you to increase the debug level of sendEmail. You may + either use this option more than once, or specify more than one v at a + time to obtain a debug level higher than one. Examples: + Specifies a debug level of 1: -v + Specifies a debug level of 2: -vv + Specifies a debug level of 2: -v -v + A debug level of one is recommended when doing any sort of debugging. + At that level you will see the entire SMTP transaction (except the + body of the email message), and hints will be displayed for most + warnings and errors. The highest debug level is three. + + +EOM + last CASE; + }; + + ## Unknown option selected! + quit("ERROR => The help topic specified is not valid!", 1); + }; + +exit(1); +} + + + + + + + + + + + + + + + + + + + + + + +############################# +## ## +## MAIN PROGRAM ## +## ## +############################# + + +## Initialize +initialize(); + +## Process Command Line +processCommandLine(); +$conf{'alarm'} = $opt{'timeout'}; + +## Abort program after $conf{'alarm'} seconds to avoid infinite hangs +alarm($conf{'alarm'}) if ($^O !~ /win/i); ## alarm() doesn't work in win32 + + + + +################################################### +## Read $message from STDIN if -m was not used ## +################################################### + +if (!($message)) { + ## Read message body from a file specified with -o message-file= + if ($opt{'message-file'}) { + if (! -e $opt{'message-file'}) { + printmsg("ERROR => Message body file specified [$opt{'message-file'}] does not exist!", 0); + printmsg("HINT => 1) check spelling of your file; 2) fully qualify the path; 3) doubble quote it", 1); + quit("", 1); + } + if (! -r $opt{'message-file'}) { + printmsg("ERROR => Message body file specified can not be read due to restricted permissions!", 0); + printmsg("HINT => Check permissions on file specified to ensure it can be read", 1); + quit("", 1); + } + if (!open(MFILE, "< " . $opt{'message-file'})) { + printmsg("ERROR => Error opening message body file [$opt{'message-file'}]: $!", 0); + quit("", 1); + } + while (<MFILE>) { + $message .= $_; + } + close(MFILE); + } + + ## Read message body from STDIN + else { + alarm($conf{'alarm'}) if ($^O !~ /win/i); ## alarm() doesn't work in win32 + if ($conf{'stdout'}) { + print "Reading message body from STDIN because the '-m' option was not used.\n"; + print "If you are manually typing in a message:\n"; + print " - First line must be received within $conf{'alarm'} seconds.\n" if ($^O !~ /win/i); + print " - End manual input with a CTRL-D on its own line.\n\n" if ($^O !~ /win/i); + print " - End manual input with a CTRL-Z on its own line.\n\n" if ($^O =~ /win/i); + } + while (<STDIN>) { ## Read STDIN into $message + $message .= $_; + alarm(0) if ($^O !~ /win/i); ## Disable the alarm since at least one line was received + } + printmsg("Message input complete.", 0); + } +} + +## Replace bare LF's with CRLF's (\012 should always have \015 with it) +$message =~ s/(\015)?(\012|$)/\015\012/g; + +## Replace bare CR's with CRLF's (\015 should always have \012 with it) +$message =~ s/(\015)(\012|$)?/\015\012/g; + +## Check message for bare periods and encode them +$message =~ s/(^|$CRLF)(\.{1})($CRLF|$)/$1.$2$3/g; + +## Get the current date for the email header +my ($sec,$min,$hour,$mday,$mon,$year,$day) = gmtime(); +$year += 1900; $mon = return_month($mon); $day = return_day($day); +my $date = sprintf("%s, %s %s %d %.2d:%.2d:%.2d %s",$day, $mday, $mon, $year, $hour, $min, $sec, $conf{'timezone'}); + + + + +################################## +## Connect to the SMTP server ## +################################## +printmsg("DEBUG => Connecting to $conf{'server'}:$conf{'port'}", 1); +$SIG{'ALRM'} = sub { + printmsg("ERROR => Timeout while connecting to $conf{'server'}:$conf{'port'} There was no response after $conf{'alarm'} seconds.", 0); + printmsg("HINT => Try specifying a different mail relay with the -s option.", 1); + quit("", 1); +}; +alarm($conf{'alarm'}) if ($^O !~ /win/i); ## alarm() doesn't work in win32; +$SERVER = IO::Socket::INET->new( PeerAddr => $conf{'server'}, + PeerPort => $conf{'port'}, + LocalAddr => $conf{'bindaddr'}, + Proto => 'tcp', + Autoflush => 1, + timeout => $conf{'alarm'}, +); +alarm(0) if ($^O !~ /win/i); ## alarm() doesn't work in win32; + +## Make sure we got connected +if ( (!$SERVER) or (!$SERVER->opened()) ) { + printmsg("ERROR => Connection attempt to $conf{'server'}:$conf{'port'} failed: $@", 0); + printmsg("HINT => Try specifying a different mail relay with the -s option.", 1); + quit("", 1); +} + +## Save our IP address for later +$conf{'ip'} = $SERVER->sockhost(); +printmsg("DEBUG => My IP address is: $conf{'ip'}", 1); + + + + + + + +######################### +## Do the SMTP Dance ## +######################### + +## Read initial greeting to make sure we're talking to a live SMTP server +if (SMTPchat()) { quit($conf{'error'}, 1); } + +## We're about to use $opt{'fqdn'}, make sure it isn't empty +if (!$opt{'fqdn'}) { + ## Ok, that means we couldn't get a hostname, how about using the IP address for the HELO instead + $opt{'fqdn'} = "[" . $conf{'ip'} . "]"; +} + +## EHLO +if (SMTPchat('EHLO ' . $opt{'fqdn'})) { + printmsg($conf{'error'}, 0); + printmsg("NOTICE => EHLO command failed, attempting HELO instead"); + if (SMTPchat('HELO ' . $opt{'fqdn'})) { quit($conf{'error'}, 1); } + if ( $opt{'username'} and $opt{'password'} ) { + printmsg("WARNING => The mail server does not support SMTP authentication!", 0); + } +} +else { + + ## Determin if the server supports TLS + if ($conf{'SMTPchat_response'} =~ /STARTTLS/) { + $conf{'tls_server'} = 1; + printmsg("DEBUG => The remote SMTP server supports TLS :)", 2); + } + else { + $conf{'tls_server'} = 0; + printmsg("DEBUG => The remote SMTP server does NOT support TLS :(", 2); + } + + ## Start TLS if possible + 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')) { + quit("ERROR => TLS setup failed: " . IO::Socket::SSL::errstr(), 1); + } + printmsg("DEBUG => TLS: Using cipher: ". $SERVER->get_cipher(), 3); + printmsg("DEBUG => TLS session initialized :)", 1); + + ## Restart our SMTP session + if (SMTPchat('EHLO ' . $opt{'fqdn'})) { quit($conf{'error'}, 1); } + } + elsif ($opt{'tls'} eq 'yes' and $conf{'tls_server'} == 0) { + quit("ERROR => TLS not possible! Remote SMTP server, $conf{'server'}, does not support it.", 1); + } + + + ## Do SMTP Auth if required + if ( $opt{'username'} and $opt{'password'} ) { + if ($conf{'SMTPchat_response'} !~ /AUTH\s/) { + printmsg("NOTICE => Authentication not supported by the remote SMTP server!", 0); + } + else { + my $auth_succeeded = 0; + my $mutual_method = 0; + + # ## SASL CRAM-MD5 authentication method + # if ($conf{'SMTPchat_response'} =~ /\bCRAM-MD5\b/i) { + # printmsg("DEBUG => SMTP-AUTH: Using CRAM-MD5 authentication method", 1); + # if (SMTPchat('AUTH CRAM-MD5')) { quit($conf{'error'}, 1); } + # + # ## FIXME!! + # + # printmsg("DEBUG => User authentication was successful", 1); + # } + + ## SASL LOGIN authentication method + if ($auth_succeeded == 0 and $conf{'SMTPchat_response'} =~ /\bLOGIN\b/i) { + $mutual_method = 1; + printmsg("DEBUG => SMTP-AUTH: Using LOGIN authentication method", 1); + if (!SMTPchat('AUTH LOGIN')) { + if (!SMTPchat(base64_encode($opt{'username'}))) { + if (!SMTPchat(base64_encode($opt{'password'}))) { + $auth_succeeded = 1; + printmsg("DEBUG => User authentication was successful (Method: LOGIN)", 1); + } + } + } + if ($auth_succeeded == 0) { + printmsg("DEBUG => SMTP-AUTH: LOGIN authenticaion failed.", 1); + } + } + + ## SASL PLAIN authentication method + if ($auth_succeeded == 0 and $conf{'SMTPchat_response'} =~ /\bPLAIN\b/i) { + $mutual_method = 1; + printmsg("DEBUG => SMTP-AUTH: Using PLAIN authentication method", 1); + if (SMTPchat('AUTH PLAIN ' . base64_encode("$opt{'username'}\0$opt{'username'}\0$opt{'password'}"))) { + printmsg("DEBUG => SMTP-AUTH: PLAIN authenticaion failed.", 1); + } + else { + $auth_succeeded = 1; + printmsg("DEBUG => User authentication was successful (Method: PLAIN)", 1); + } + } + + ## If none of the authentication methods supported by sendEmail were supported by the server, let the user know + if ($mutual_method == 0) { + printmsg("WARNING => SMTP-AUTH: No mutually supported authentication methods available", 0); + } + + ## If we didn't get authenticated, log an error message and exit + if ($auth_succeeded == 0) { + quit("ERROR => ERROR => SMTP-AUTH: Authentication to $conf{'server'}:$conf{'port'} failed.", 1); + } + } + } +} + +## MAIL FROM +if (SMTPchat('MAIL FROM:<' .(returnAddressParts($from))[1]. '>')) { quit($conf{'error'}, 1); } + +## RCPT TO +my $oneRcptAccepted = 0; +foreach my $rcpt (@to, @cc, @bcc) { + my ($name, $address) = returnAddressParts($rcpt); + if (SMTPchat('RCPT TO:<' . $address . '>')) { + printmsg("WARNING => The recipient <$address> was rejected by the mail server, error follows:", 0); + $conf{'error'} =~ s/^ERROR/WARNING/o; + printmsg($conf{'error'}, 0); + } + elsif ($oneRcptAccepted == 0) { + $oneRcptAccepted = 1; + } +} +## If no recipients were accepted we need to exit with an error. +if ($oneRcptAccepted == 0) { + quit("ERROR => Exiting. No recipients were accepted for delivery by the mail server.", 1); +} + +## DATA +if (SMTPchat('DATA')) { quit($conf{'error'}, 1); } + + +############################### +## Build and send the body ## +############################### +printmsg("INFO => Sending message body",1); + +## If the message-format is raw just send the message as-is. +if ($opt{'message-format'} =~ /^raw$/i) { + print $SERVER $message; +} + +## If the message-format isn't raw, then build and send the message, +else { + + ## Message-ID: <MessageID> + if ($opt{'message-header'} !~ /^Message-ID:/iom) { + $header .= 'Message-ID: <' . $conf{'Message-ID'} . '@' . $conf{'hostname'} . '>' . $CRLF; + } + + ## From: "Name" <address@domain.com> (the pointless test below is just to keep scoping correct) + if ($from and $opt{'message-header'} !~ /^From:/iom) { + my ($name, $address) = returnAddressParts($from); + $header .= 'From: "' . $name . '" <' . $address . '>' . $CRLF; + } + + ## Reply-To: + if ($opt{'reply-to'} and $opt{'message-header'} !~ /^Reply-To:/iom) { + my ($name, $address) = returnAddressParts($opt{'reply-to'}); + $header .= 'Reply-To: "' . $name . '" <' . $address . '>' . $CRLF; + } + + ## To: "Name" <address@domain.com> + if ($opt{'message-header'} =~ /^To:/iom) { + ## The user put the To: header in via -o message-header - dont do anything + } + elsif (scalar(@to) > 0) { + $header .= "To:"; + for (my $a = 0; $a < scalar(@to); $a++) { + my $msg = ""; + + my ($name, $address) = returnAddressParts($to[$a]); + $msg = " \"$name\" <$address>"; + + ## If we're not on the last address add a comma to the end of the line. + if (($a + 1) != scalar(@to)) { + $msg .= ","; + } + + $header .= $msg . $CRLF; + } + } + ## We always want a To: line so if the only recipients were bcc'd they don't see who it was sent to + else { + $header .= "To: \"Undisclosed Recipients\" <>$CRLF"; + } + + if (scalar(@cc) > 0 and $opt{'message-header'} !~ /^Cc:/iom) { + $header .= "Cc:"; + for (my $a = 0; $a < scalar(@cc); $a++) { + my $msg = ""; + + my ($name, $address) = returnAddressParts($cc[$a]); + $msg = " \"$name\" <$address>"; + + ## If we're not on the last address add a comma to the end of the line. + if (($a + 1) != scalar(@cc)) { + $msg .= ","; + } + + $header .= $msg . $CRLF; + } + } + + if ($opt{'message-header'} !~ /^Subject:/iom) { + $header .= 'Subject: ' . $subject . $CRLF; ## Subject + } + if ($opt{'message-header'} !~ /^Date:/iom) { + $header .= 'Date: ' . $date . $CRLF; ## Date + } + if ($opt{'message-header'} !~ /^X-Mailer:/iom) { + $header .= 'X-Mailer: sendEmail-'.$conf{'version'}.$CRLF; ## X-Mailer + } + ## I wonder if I should put this in by default? + # if ($opt{'message-header'} !~ /^X-Originating-IP:/iom) { + # $header .= 'X-Originating-IP: ['.$conf{'ip'}.']'.$CRLF; ## X-Originating-IP + # } + + ## Encode all messages with MIME. + if ($opt{'message-header'} !~ /^MIME-Version:/iom) { + $header .= "MIME-Version: 1.0$CRLF"; + } + if ($opt{'message-header'} !~ /^Content-Type:/iom) { + my $content_type = 'multipart/mixed'; + if (scalar(@attachments) == 0) { $content_type = 'multipart/related'; } + $header .= "Content-Type: $content_type; boundary=\"$conf{'delimiter'}\"$CRLF"; + } + + ## Send additional message header line(s) if specified + if ($opt{'message-header'}) { + $header .= $opt{'message-header'}; + } + + ## Send the message header to the server + print $SERVER $header . $CRLF; + + ## Start sending the message body to the server + print $SERVER "This is a multi-part message in MIME format. To properly display this message you need a MIME-Version 1.0 compliant Email program.$CRLF"; + print $SERVER "$CRLF"; + + + ## Send message body + print $SERVER "--$conf{'delimiter'}$CRLF"; + ## Send a message content-type header: + ## If the message contains HTML... + if ($opt{'message-content-type'} eq 'html' or ($opt{'message-content-type'} eq 'auto' and $message =~ /^\s*(<HTML|<!DOCTYPE)/i) ) { + printmsg("Setting content-type: text/html", 1); + print $SERVER "Content-Type: text/html;$CRLF"; + } + ## Otherwise assume it's plain text... + elsif ($opt{'message-content-type'} eq 'text' or $opt{'message-content-type'} eq 'auto') { + printmsg("Setting content-type: text/plain", 1); + print $SERVER "Content-Type: text/plain;$CRLF"; + } + ## If they've specified their own content-type string... + else { + printmsg("Setting custom content-type: ".$opt{'message-content-type'}, 1); + print $SERVER "Content-Type: ".$opt{'message-content-type'}.";$CRLF"; + } + print $SERVER " charset=\"" . $opt{'message-charset'} . "\"$CRLF"; + print $SERVER "Content-Transfer-Encoding: 7bit$CRLF"; + print $SERVER $CRLF . $message; + + + + ## Send Attachemnts + if (scalar(@attachments) > 0) { + ## Disable the alarm so people on modems can send big attachments + alarm(0) if ($^O !~ /win/i); ## alarm() doesn't work in win32 + + ## Send the attachments + foreach my $filename (@attachments) { + ## This is check 2, we already checked this above, but just in case... + if ( ! -f $filename ) { + printmsg("ERROR => The file [$filename] doesn't exist! Email will be sent, but without that attachment.", 0); + } + elsif ( ! -r $filename ) { + printmsg("ERROR => Couldn't open the file [$filename] for reading: $! Email will be sent, but without that attachment.", 0); + } + else { + printmsg("DEBUG => Sending the attachment [$filename]", 1); + send_attachment($filename); + } + } + } + + + ## End the mime encoded message + print $SERVER "$CRLF--$conf{'delimiter'}--$CRLF"; +} + + +## Tell the server we are done sending the email +print $SERVER "$CRLF.$CRLF"; +if (SMTPchat()) { quit($conf{'error'}, 1); } + + + +#################### +# We are done!!! # +#################### + +## Disconnect from the server (don't SMTPchat(), it breaks when using TLS) +print $SERVER "QUIT$CRLF"; +close $SERVER; + + + + + + +####################################### +## Generate exit message/log entry ## +####################################### + +if ($conf{'debug'} or $conf{'logging'}) { + printmsg("Generating a detailed exit message", 3); + + ## Put the message together + my $output = "Email was sent successfully! From: <" . (returnAddressParts($from))[1] . "> "; + + if (scalar(@to) > 0) { + $output .= "To: "; + for ($a = 0; $a < scalar(@to); $a++) { + $output .= "<" . (returnAddressParts($to[$a]))[1] . "> "; + } + } + if (scalar(@cc) > 0) { + $output .= "Cc: "; + for ($a = 0; $a < scalar(@cc); $a++) { + $output .= "<" . (returnAddressParts($cc[$a]))[1] . "> "; + } + } + if (scalar(@bcc) > 0) { + $output .= "Bcc: "; + for ($a = 0; $a < scalar(@bcc); $a++) { + $output .= "<" . (returnAddressParts($bcc[$a]))[1] . "> "; + } + } + $output .= "Subject: [$subject] " if ($subject); + if (scalar(@attachments_names) > 0) { + $output .= "Attachment(s): "; + foreach(@attachments_names) { + $output .= "[$_] "; + } + } + $output .= "Server: [$conf{'server'}:$conf{'port'}]"; + + +###################### +# Exit the program # +###################### + + ## Print / Log the detailed message + quit($output, 0); +} +else { + ## Or the standard message + quit("Email was sent successfully!", 0); +} + diff --git a/extension/qfq/qfq/Constants.php b/extension/qfq/qfq/Constants.php index d6824f45871bce57692142efe313491b11ca6781..44075b1906ec9b7f738e48ddcfff1fdf60739ee7 100644 --- a/extension/qfq/qfq/Constants.php +++ b/extension/qfq/qfq/Constants.php @@ -112,7 +112,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_MISSING_SESSIONNAME = 1010; + const ERROR_BROKEN_PARAMETER = 1011; const ERROR_FE_USER_UID_CHANGED = 1012; const ERROR_SIP_NOT_FOUND = 1013; @@ -122,8 +122,9 @@ const ERROR_SIP_EXIST_BUT_OTHER_PARAM_GIVEN_BY_CLIENT = 1016; const ERROR_USER_NOT_LOGGED_IN = 1017; const ERROR_USER_LOGGED_IN = 1018; const ERROR_FORM_FORBIDDEN = 1019; -const ERROR_FORM_UNKNOWN_PERMISSION_MODE = 10120; +const ERROR_FORM_UNKNOWN_PERMISSION_MODE = 1020; const ERROR_MULTI_SQL_MISSING = 1021; + const ERROR_RECURSION_TOO_DEEP = 1023; const ERROR_CHECKBOXMODE_UNKNOWN = 1024; const ERROR_MISSING_SQL1 = 1025; @@ -131,12 +132,16 @@ const ERROR_CHECKBOX_EQUAL = 1026; const ERROR_MISSING_ITEM_LIST = 1027; const ERROR_UNKNOWN_FORM_RENDER = 1028; const ERROR_NAME_LABEL_EMPTY = 1029; + const ERROR_DEBUG = 1031; const ERROR_UNKNOWN_MODE = 1032; const ERROR_NOT_IMPLEMENTED = 1033; const ERROR_RESERVED_KEY_NAME = 1034; + const ERROR_UNKNOWN_FORWARD_MODE = 1036; + const ERROR_MISSING_HIDDEN_FIELD_IN_SIP = 1038; + const ERROR_MISSING_MIN_MAX = 1040; const ERROR_MIN_MAX_VIOLATION = 1041; const ERROR_UNKNOWN_CHECKTYPE = 1042; @@ -146,7 +151,6 @@ const ERROR_LOG_NOT_WRITABLE = 1045; const ERROR_UNNOWN_STORE = 1046; const ERROR_GET_STORE_ZERO = 1047; const ERROR_SET_STORE_ZERO = 1048; -const ERROR_QFQ_SESSION_MISSING = 1049; const ERROR_INVALID_OR_MISSING_PARAMETER = 1050; const ERROR_UNKNOWN_SQL_LOG_MODE = 1051; const ERROR_FORM_NOT_FOUND = 1052; @@ -155,7 +159,7 @@ const ERROR_SANATIZE_INVALID_VALUE = 1054; const ERROR_REQUIRED_VALUE_EMPTY = 1055; const ERROR_DATE_UNEXPECTED_FORMAT = 1056; const ERROR_UNEXPECTED_TYPE = 1057; -const ERROR_NOT_APPLICABLE = 108; +const ERROR_NOT_APPLICABLE = 1058; const ERROR_FORMELEMENT_TYPE = 1059; const ERROR_MISSING_OPEN_DELIMITER = 1060; const ERROR_MISSING_CLOSE_DELIMITER = 1061; @@ -269,6 +273,10 @@ const ERROR_DIRTY_RECORD_MODIFIED = 2205; // Language const ERROR_LANGUAGE_NOT_CONFIGURED_IN_QFQ = 2300; +// Session +const ERROR_MISSING_SESSIONNAME = 2400; +const ERROR_QFQ_SESSION_MISSING = 2401; +const ERROR_SESSION_BROKEN_SCRIPT_PATH = 2402; // // Store Names: Identifier @@ -370,6 +378,8 @@ const SYSTEM_SQL_LOG_FILE = '../../sql.log'; const SYSTEM_SQL_LOG_MODE = 'SQL_LOG_MODE'; // Mode, which statements to log. const SYSTEM_DATE_FORMAT = 'DATE_FORMAT'; const SYSTEM_REDIRECT_ALL_MAIL_TO = 'REDIRECT_ALL_MAIL_TO'; +const SYSTEM_MAIL_LOG = 'MAIL_LOG'; +const SYSTEM_MAIL_LOG_FILE = '../../mail.log'; const SYSTEM_SHOW_DEBUG_INFO = 'SHOW_DEBUG_INFO'; const SYSTEM_SHOW_DEBUG_INFO_YES = 'yes'; @@ -401,6 +411,9 @@ const SYSTEM_FORM_BUTTON_ON_CHANGE_CLASS = 'FORM_BUTTON_ON_CHANGE_CLASS'; const SYSTEM_BASE_URL_PRINT = 'BASE_URL_PRINT'; const SYSTEM_WKHTMLTOPDF = 'WKHTMLTOPDF'; +const SYSTEM_SEND_E_MAIL = 'sendEmail'; +const SYSTEM_SEND_E_MAIL_OPTIONS = 'SEND_E_MAIL_OPTIONS'; + const SYSTEM_EDIT_FORM_PAGE = 'EDIT_FORM_PAGE'; // computed automatically during runtime @@ -455,8 +468,12 @@ const SYSTEM_DB_UPDATE_AUTO = 'auto'; 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_DEFAULT = 'SELECT id AS periodId FROM Period WHERE start<=NOW() ORDER BY start DESC LIMIT 1'; +//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'; +const SYSTEM_FILL_STORE_SYSTEM_ERROR_MSG = 'FILL_STORE_SYSTEM_ERROR_MSG'; const SYSTEM_FORM_LANGUAGE = 'FORM_LANGUAGE'; @@ -571,26 +588,6 @@ const TOKEN_FOUND_AS_DEFAULT = 'default'; const RANDOM_LENGTH = 32; -// Report, BodyText -const TOKEN_SQL = 'sql'; -const TOKEN_HEAD = 'head'; -const TOKEN_ALT_HEAD = 'althead'; -const TOKEN_TAIL = 'tail'; -const TOKEN_RBEG = 'rbeg'; -const TOKEN_REND = 'rend'; -const TOKEN_RENR = 'renr'; -const TOKEN_RSEP = 'rsep'; -const TOKEN_FBEG = 'fbeg'; -const TOKEN_FEND = 'fend'; -const TOKEN_FSEP = 'fsep'; -const TOKEN_RBGD = 'rbgd'; -const TOKEN_DEBUG = 'debug'; -const TOKEN_FORM = CLIENT_FORM; -const TOKEN_RECORD_ID = CLIENT_RECORD_ID; -const TOKEN_DEBUG_BODYTEXT = TYPO3_DEBUG_SHOW_BODY_TEXT; - -const TOKEN_VALID_LIST = 'sql|head|althead|tail|rbeg|rend|renr|rsep|fbeg|fend|fsep|rbgd|debug|form|r|debugShowBodyText|sqlLog|sqlLogMode'; - // FORM - copy from table 'form' of processed form //const DEF_FORM_NAME = CLIENT_FORM; @@ -885,6 +882,8 @@ const FE_SENDMAIL_REPLY_TO = 'sendMailReplyTo'; // Reply to email address const FE_SENDMAIL_FLAG_AUTO_SUBMIT = 'sendMailFlagAutoSubmit'; // on|off - if 'on', suppresses OoO answers from receivers. const FE_SENDMAIL_GR_ID = 'sendMailGrId'; // gr_id: used to classify mail log entries ind table mailLog 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_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 @@ -1076,7 +1075,52 @@ 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_SRC = 10; +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'; +const SENDMAIL_TOKEN_BODY = 'b'; +const SENDMAIL_TOKEN_REPLY_TO = 'r'; +const SENDMAIL_TOKEN_FLAG_AUTO_SUBMIT = 'A'; +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_HEADER = 'h'; +const SENDMAIL_TOKEN_X_ID2 = 'y'; +const SENDMAIL_TOKEN_X_ID3 = 'z'; +const SENDMAIL_TOKEN_SRC = 'S'; + +// Report, BodyText +const TOKEN_SQL = 'sql'; +const TOKEN_HEAD = 'head'; +const TOKEN_ALT_HEAD = 'althead'; +const TOKEN_ALT_SQL = 'altsql'; +const TOKEN_TAIL = 'tail'; +const TOKEN_SHEAD = 'shead'; +const TOKEN_STAIL = 'stail'; +const TOKEN_RBEG = 'rbeg'; +const TOKEN_REND = 'rend'; +const TOKEN_RENR = 'renr'; +const TOKEN_RSEP = 'rsep'; +const TOKEN_FBEG = 'fbeg'; +const TOKEN_FEND = 'fend'; +const TOKEN_FSEP = 'fsep'; +const TOKEN_RBGD = 'rbgd'; +const TOKEN_DEBUG = 'debug'; +const TOKEN_FORM = CLIENT_FORM; +const TOKEN_RECORD_ID = CLIENT_RECORD_ID; +const TOKEN_DEBUG_BODYTEXT = TYPO3_DEBUG_SHOW_BODY_TEXT; + +const TOKEN_VALID_LIST = 'sql|head|althead|altsql|tail|shead|stail|rbeg|rend|renr|rsep|fbeg|fend|fsep|rbgd|debug|form|r|debugShowBodyText|sqlLog|sqlLogMode'; + +const TOKEN_COLUMN_CTRL = '_'; //Report: Column Token const COLUMN_PPAGE = "Page"; diff --git a/extension/qfq/qfq/Evaluate.php b/extension/qfq/qfq/Evaluate.php index 44e11c3f76b54c4ec258bdec66b61f6304098384..f4c6114314ef245a43e9d42ca5d09e8a97cac1f9 100644 --- a/extension/qfq/qfq/Evaluate.php +++ b/extension/qfq/qfq/Evaluate.php @@ -196,15 +196,14 @@ class Evaluate { $token = trim($token); - // just to extract the first token: check if this is a SQL Statement - $arr = explode(' ', $token, 2); - if ($token[0] === '!') { - $token = substr($token, 1); - $arr[0] = substr($arr[0], 1); + $token = trim(substr($token, 1)); $sqlMode = ROW_REGULAR; } + // just to extract the first token: check if this is a SQL Statement + $arr = explode(' ', $token, 2); + // SQL Statement? if (in_array(strtoupper($arr[0] . ' '), $this->sqlKeywords)) { $foundInStore = TOKEN_FOUND_IN_STORE_QUERY; @@ -215,10 +214,8 @@ class Evaluate { // explode for: <key>:<store priority>:<sanitize class>:<escape>:<default> $arr = explode(':', $token, 5); $arr = array_merge($arr, [null, null, null, null, null]); // fake isset() - $escapeTypes = $arr[3]; - if ($escapeTypes == '') { - $escapeTypes = $this->escapeTypeDefault; - } + + $escapeTypes = (empty($arr[3])) ? $this->escapeTypeDefault : $arr[3]; // search for value in stores $value = $this->store->getVar($arr[0], $arr[1], $arr[2], $foundInStore); diff --git a/extension/qfq/qfq/QuickFormQuery.php b/extension/qfq/qfq/QuickFormQuery.php index d5a52f9eb5955d71e11bc37e9a9aa43ecf7910aa..e73ce75d888eb7f8ea1958a22087347571c4187b 100644 --- a/extension/qfq/qfq/QuickFormQuery.php +++ b/extension/qfq/qfq/QuickFormQuery.php @@ -166,7 +166,7 @@ class QuickFormQuery { $updateDb = new DatabaseUpdate($this->dbArray[$this->dbIndexQfq]); $updateDb->checkNupdate($dbUpdate); - $this->store->systemStoreUpdate(); // Do this after the DB-update + $this->store->StoreSystemUpdate(); // Do this after the DB-update } /** diff --git a/extension/qfq/qfq/database/Database.php b/extension/qfq/qfq/database/Database.php index 9ceedcc6e4d63cf92f282ce2870f58c5700c2b04..69725c19d578830a8ec65ef0b518c207bb60193f 100644 --- a/extension/qfq/qfq/database/Database.php +++ b/extension/qfq/qfq/database/Database.php @@ -211,7 +211,7 @@ class Database { } elseif ($count === 0) { $result = array(); } else - throw new DbException($specificMessage . "Expected no record, got $count rows: $sql", ERROR_DB_TOO_MANY_ROWS); + throw new DbException($specificMessage . "Expected zero or one record, got $count records: $sql", ERROR_DB_TOO_MANY_ROWS); break; case ROW_EXPECT_GE_1: if ($count > 0) { diff --git a/extension/qfq/qfq/database/DatabaseUpdateData.php b/extension/qfq/qfq/database/DatabaseUpdateData.php index 247a4e61f6b1d3d32848fccd9cd2325712e7eeb6..e00226bd11123617145abb16460c0aa075439ef2 100644 --- a/extension/qfq/qfq/database/DatabaseUpdateData.php +++ b/extension/qfq/qfq/database/DatabaseUpdateData.php @@ -87,8 +87,11 @@ $UPDATE_ARRAY = array( ], '0.25.3' => [ - "CREATE TABLE `Split` (`id` INT(11) NOT NULL AUTO_INCREMENT, `tableName` VARCHAR(255) NOT NULL, `xId` INT(11) NOT NULL, `pathFileName` VARCHAR(255) NOT NULL, `modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`id`)) ENGINE = InnoDB AUTO_INCREMENT = 0 DEFAULT CHARSET = utf8;" + "ALTER TABLE `MailLog` ADD `xId2` INT NOT NULL AFTER `xId`, ADD `xId3` INT NOT NULL AFTER `xId2`", ], + + ); + diff --git a/extension/qfq/qfq/form/FormAction.php b/extension/qfq/qfq/form/FormAction.php index 3fd502547e7f245ac75344fe56332cec5ba0a1ec..e08d7b6258c55f8a669570696a7ad7f5cb6d74f1 100644 --- a/extension/qfq/qfq/form/FormAction.php +++ b/extension/qfq/qfq/form/FormAction.php @@ -252,6 +252,8 @@ class FormAction { $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]); // Mail: send new Sendmail($mail); diff --git a/extension/qfq/qfq/helper/OnArray.php b/extension/qfq/qfq/helper/OnArray.php index e8a1b1de8d7b8acd091d13a2504fe87f3c1531b7..07fdf7f98468fde6eadd65a0d0277a5c77a593fc 100644 --- a/extension/qfq/qfq/helper/OnArray.php +++ b/extension/qfq/qfq/helper/OnArray.php @@ -196,6 +196,28 @@ class OnArray { return $new; } + /** + * Remove from all keynames an optional '_'. + * + * @param array $arr + * + * @return array + */ + public static function keyNameRemoveLeadingUnderscore(array $arr) { + + foreach ($arr as $key => $value) { + if ($key[0] == TOKEN_COLUMN_CTRL) { + $newKey = substr($key, 1); + if (!empty($newKey)) { + $arr[$newKey] = $value; + unset($arr[$key]); + } + } + } + + return $arr; + } + /** * Search in array $dest for all $keyNames if they exist. If not, check if they exist in $src. If yes, copy. * diff --git a/extension/qfq/qfq/helper/Support.php b/extension/qfq/qfq/helper/Support.php index 460db77fc7beb9962e5fd473c0ff0b2c72940635..2852e41a32f59575694b4f30475b332c39c05832 100644 --- a/extension/qfq/qfq/helper/Support.php +++ b/extension/qfq/qfq/helper/Support.php @@ -1021,5 +1021,24 @@ class Support { } } + /** + * Strips the first char $c from $data if the first char is equal to $c. + * Example: with $c='_' the $data='_pId' becomes 'pId' + * + * @param $c + * @param $data + * @return string + */ + public static function stripFirstCharIf($c, $data) { + if (empty($data)) { + return $data; + } + + if ($data[0] == $c) { + $data = substr($data, 1); + } + + return $data; + } } \ No newline at end of file diff --git a/extension/qfq/qfq/report/Report.php b/extension/qfq/qfq/report/Report.php index cc4499838aa36b489507f3dc61671a3cfa179f50..142524660ba54b23551e46108af7f420c51e743c 100644 --- a/extension/qfq/qfq/report/Report.php +++ b/extension/qfq/qfq/report/Report.php @@ -357,14 +357,14 @@ class Report { } /** - * Executes the queries recursive. This Method is called for each Sublevel. + * Executes the queries recursive. This Method is called for each sublevel. * * ROOTLEVEL * This method is called once from the main method. * For the first call the method executes the rootlevels * * SUBLEVEL - * For each rootlevel the method calls it self whith the levelmode 0 + * For each rootlevel the method calls it self with the level mode 0 * If the next Level is a Sublevel it will be executed and $this->counter will be added by 1 * The sublevel calls the method again for a following sublevel * @@ -426,6 +426,7 @@ class Report { // Prepare SQL: replace variables. Actual 'line.total' or 'line.count' will recalculated: don't replace them now! unset($this->variables->resultArray[$full_level . ".line."]["total"]); unset($this->variables->resultArray[$full_level . ".line."]["count"]); + $sql = $this->variables->doVariables($this->frArray[$full_level . "." . TOKEN_SQL]); $this->store->setVar(SYSTEM_SQL_FINAL, $sql, STORE_SYSTEM); @@ -440,26 +441,28 @@ class Report { $rowTotal = isset($stat[DB_NUM_ROWS]) ? $stat[DB_NUM_ROWS] : $stat[DB_AFFECTED_ROWS]; $this->variables->resultArray[$full_level . ".line."]["total"] = $rowTotal; - if (isset($stat[DB_INSERT_ID])) { - $this->variables->resultArray[$full_level . ".line."]["insertId"] = $stat[DB_INSERT_ID]; - } + $this->variables->resultArray[$full_level . ".line."]["count"] = is_array($result) ? 1 : 0; + $this->variables->resultArray[$full_level . ".line."]["insertId"] = isset($stat[DB_INSERT_ID]) ? $stat[DB_INSERT_ID] : 0; + $content .= $this->variables->doVariables($this->frArray[$full_level . "." . TOKEN_SHEAD]); // HEAD: If there is at least one record, do 'head'. if ($rowTotal > 0) { $content .= $this->variables->doVariables($this->frArray[$full_level . "." . TOKEN_HEAD]); } - // Prepare row alteration - $arrRbgd = explode("|", $this->frArray[$full_level . "." . TOKEN_RBGD]); - if (count($arrRbgd) < 2) { - $arrRbgd[] = ''; - $arrRbgd[] = ''; - } if (is_array($result)) { + + // Prepare row alteration + $arrRbgd = explode("|", $this->frArray[$full_level . "." . TOKEN_RBGD], 2); + if (count($arrRbgd) < 2) { + $arrRbgd[] = ''; + $arrRbgd[] = ''; + } + //--------------------------------- // Process each row of resultset - $columnValueSeperator = ""; + $columnValueSeparator = ""; $rowIndex = 0; foreach ($result as $row) { // record number counter @@ -471,12 +474,15 @@ class Report { $row[$ii] = str_replace("{{" . $full_level . ".line.total}}", $rowTotal, $row[$ii]); } - // SEP set seperator (empty on first run) - $content .= $columnValueSeperator; - $columnValueSeperator = $this->variables->doVariables($this->frArray[$full_level . "." . TOKEN_RSEP]); + // SEP set separator (empty on first run) + $content .= $columnValueSeparator; + $columnValueSeparator = $this->variables->doVariables($this->frArray[$full_level . "." . TOKEN_RSEP]); + + // RBEG + $rbeg = $this->variables->doVariables($this->frArray[$full_level . "." . TOKEN_RBEG]); // RBGD: even/odd rows - $content .= str_replace(TOKEN_RBGD, $arrRbgd[$rowIndex % 2], $this->frArray[$full_level . "." . TOKEN_RBEG]); + $content .= str_replace(TOKEN_RBGD, $arrRbgd[$rowIndex % 2], $rbeg); //----------------------------- // COLUMNS: Collect all columns @@ -494,12 +500,23 @@ class Report { } } - //Print althead or tail if ($rowTotal > 0) { + // tail $content .= $this->variables->doVariables($this->frArray[$full_level . "." . TOKEN_TAIL]); } else { + // althead $content .= $this->variables->doVariables($this->frArray[$full_level . "." . TOKEN_ALT_HEAD]); + // altsql + $sql = $this->variables->doVariables($this->frArray[$full_level . "." . TOKEN_ALT_SQL]); + if (!empty($sql)) { + $result = $this->db->sql($sql, ROW_KEYS, array(), '', $keys, $stat); + foreach ($result as $row) { + $rowIndex = 0; + $content .= $this->collectRow($row, $keys, $full_level, $rowIndex); + } + } } + $content .= $this->variables->doVariables($this->frArray[$full_level . "." . TOKEN_STAIL]); ++$counter; if (isset($this->indexArray[$counter]) && is_array($this->indexArray[$counter])) { @@ -556,16 +573,24 @@ class Report { */ private function collectRow(array $row, array $keys, $full_level, $rowIndex) { $content = ""; + $assoc = array(); $fsep = ''; for ($ii = 0; $ii < count($keys); $ii++) { + // Debugging $this->store->setVar(SYSTEM_REPORT_COLUMN_INDEX, $ii + 1, STORE_SYSTEM); $this->store->setVar(SYSTEM_REPORT_COLUMN_NAME, $keys[$ii], STORE_SYSTEM); $this->store->setVar(SYSTEM_REPORT_COLUMN_VALUE, $row[$ii], STORE_SYSTEM); $flagOutput = false; $renderedColumn = $this->renderColumn($ii, $keys[$ii], $row[$ii], $full_level, $rowIndex, $flagOutput); + + $keyAssoc = Support::stripFirstCharIf(TOKEN_COLUMN_CTRL, $keys[$ii]); + if ($keyAssoc != '') { + $assoc[$keyAssoc] = $row[$ii]; + } + if ($flagOutput) { //prints $content .= $this->variables->doVariables($fsep); @@ -576,6 +601,8 @@ class Report { } } + $this->store->appendToStore(STORE_RECORD, $assoc); + return ($content); } @@ -598,7 +625,7 @@ class Report { $dummy = false; // Empty columnnames are allowed: check with isset - if (isset($columnName[0]) && $columnName[0] === "_") { + if (isset($columnName[0]) && $columnName[0] === TOKEN_COLUMN_CTRL) { $flagControl = true; $columnName = substr($columnName, 1); } @@ -696,36 +723,36 @@ class Report { break; } - $mailarr = explode("|", $columnValue, 3); + $mailConfig = explode("|", $columnValue, 3); // Fake values for tmp[1], tmp[2] to suppress access errors. - $mailarr[] = ''; - $mailarr[] = ''; + $mailConfig[] = ''; + $mailConfig[] = ''; - if (empty($mailarr[0])) { + if (empty($mailConfig[0])) { break; } - $attribute = Support::doAttribute('src', $mailarr[0]); - $attribute .= Support::doAttribute('alt', $mailarr[1]); + $attribute = Support::doAttribute('src', $mailConfig[0]); + $attribute .= Support::doAttribute('alt', $mailConfig[1]); - $content .= '<img ' . $attribute . '>' . $mailarr[2]; + $content .= '<img ' . $attribute . '>' . $mailConfig[2]; break; case "mailto": // "<email address>|[Real Name]" renders to (encrypted via JS): <a href="mailto://<email address>"><email address></a> OR <a href="mailto://<email address>">[Real Name]</a> - $mailarr = explode("|", $columnValue, 2); - if (empty($mailarr[0])) { + $mailConfig = explode("|", $columnValue, 2); + if (empty($mailConfig[0])) { break; } - $t1 = explode("@", $mailarr[0], 2); + $t1 = explode("@", $mailConfig[0], 2); $content .= "<script language=javascript><!--" . chr(10); - if (empty($mailarr[1])) { - $mailarr[1] = $mailarr[0]; + if (empty($mailConfig[1])) { + $mailConfig[1] = $mailConfig[0]; } - $content .= 'var contact = "' . substr($mailarr[1], 0, 2) . '"' . chr(10); - $content .= 'var contact1 = "' . substr($mailarr[1], 2) . '"' . chr(10); + $content .= 'var contact = "' . substr($mailConfig[1], 0, 2) . '"' . chr(10); + $content .= 'var contact1 = "' . substr($mailConfig[1], 2) . '"' . chr(10); $content .= 'var email = "' . $t1[0] . '"' . chr(10); $content .= 'var emailHost = "' . $t1[1] . '"' . chr(10); @@ -736,44 +763,57 @@ class Report { case "sendmail": // '<receiver1>,<receiver2>,...|<sender>|<subject>|<body>|<reply-to>|<flag autosubmit: on /off>' - $mailarr = explode("|", $columnValue); - if (count($mailarr) < 4) { +// $mailarr = explode("|", $columnValue); + $mailConfig = $this->sendmailConvertToken($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] = ''; - } - - $mailarr[SENDMAIL_IDX_SRC] = "Report: T3 pageId=" . $this->store->getVar('pageId', STORE_TYPO3) . +// 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) . ", T3 ttcontentId=" . $this->store->getVar('ttcontentUid', STORE_TYPO3) . ", Level=" . $full_level; - new Sendmail($mailarr); + new Sendmail($mailConfig); break; case "vertical": - // '<Text>|[angle]|[width]|[height]|[tag]' , width and heigth needs a unit (px, em ,...), 'tag' might be 'div', 'span', ... + // '<Text>|[angle]|[width]|[height]|[tag]' , width and height needs a unit (px, em ,...), 'tag' might be 'div', 'span', ... $arr = explode("|", $columnValue, 5); # angle @@ -781,11 +821,11 @@ class Report { # width $width = $arr[2] ? $arr[2] : "1em"; - $mailarr = "width:$width; "; + $extra = "width:$width; "; # height if ($arr[3]) { - $mailarr .= "height:" . $arr[3] . "; "; + $extra .= "height:" . $arr[3] . "; "; } # tag @@ -841,7 +881,7 @@ class Report { exec($cmd, $arr, $rc); - $output = implode('<BR>', $arr); + $output = implode('<br>', $arr); if ($rc != 0) { $output = $rc . " - " . $output; } @@ -1220,4 +1260,49 @@ 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 index 6d434ea69cc77af954fa54361adb2b596a1856b6..384e026d8e48a1697c7dc17ea3420750465355b0 100644 --- a/extension/qfq/qfq/report/Sendmail.php +++ b/extension/qfq/qfq/report/Sendmail.php @@ -11,6 +11,11 @@ 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. @@ -28,71 +33,169 @@ class Sendmail { * SENDMAIL_IDX_GR_ID optional: integer * SENDMAIL_IDX_X_ID optional: integer * - * @param $mailarr + * @param $mailConfig * * @throws UserFormException */ - public function __construct(array $mailarr) { - - $addBody = ''; + public function __construct(array $mailConfig) { // If there is no 'Receiver': do not send a mail. - if (!isset($mailarr[SENDMAIL_IDX_RECEIVER]) || $mailarr[SENDMAIL_IDX_RECEIVER] === '') { + if (!isset($mailConfig[SENDMAIL_IDX_RECEIVER]) || $mailConfig[SENDMAIL_IDX_RECEIVER] === '') { return; } - if (count($mailarr) < 4 || $mailarr[SENDMAIL_IDX_SENDER] === '' || $mailarr[SENDMAIL_IDX_SUBJECT] === '' || $mailarr[SENDMAIL_IDX_BODY] === '') { + 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); } - $store = Store::getInstance(''); - $redirectAllMail = $store->getVar(SYSTEM_REDIRECT_ALL_MAIL_TO, STORE_SYSTEM); + $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: ' . $mailarr[SENDMAIL_IDX_RECEIVER] . PHP_EOL; - $addBody .= 'CC: ' . $mailarr[SENDMAIL_IDX_RECEIVER_CC] . PHP_EOL; - $addBody .= 'BCC: ' . $mailarr[SENDMAIL_IDX_RECEIVER_BCC] . PHP_EOL; + $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; - $mailarr[SENDMAIL_IDX_RECEIVER] = $redirectAllMail; - $mailarr[SENDMAIL_IDX_RECEIVER_CC] = ''; - $mailarr[SENDMAIL_IDX_RECEIVER_BCC] = ''; + $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); +// $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); +// } - 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); } - $this->mailLog($mailarr, $header); + $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 $mailarr + * @param array $mailConfig * * @return string */ - private function buildHeader(array $mailarr) { + private function buildHeader(array $mailConfig) { // "\r\n" needs to be enclosed in double ticks to correctly converted to 0x0d 0x0a, - $header = "From: " . $mailarr[SENDMAIL_IDX_SENDER] . "\r\n"; + $header = "From: " . $mailConfig[SENDMAIL_IDX_SENDER] . "\r\n"; - if (isset($mailarr[SENDMAIL_IDX_REPLY_TO]) && $mailarr[SENDMAIL_IDX_REPLY_TO] != '') { - $header .= "Reply-To: " . $mailarr[SENDMAIL_IDX_REPLY_TO] . "\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($mailarr[SENDMAIL_IDX_FLAG_AUTO_SUBMIT]) || $mailarr[SENDMAIL_IDX_FLAG_AUTO_SUBMIT] === '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($mailarr[SENDMAIL_IDX_RECEIVER_CC]) && $mailarr[SENDMAIL_IDX_RECEIVER_CC] != '') { - $header .= "Cc: " . $mailarr[SENDMAIL_IDX_RECEIVER_CC] . "\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($mailarr[SENDMAIL_IDX_RECEIVER_BCC]) && $mailarr[SENDMAIL_IDX_RECEIVER_BCC] != '') { - $header .= "Bcc: " . $mailarr[SENDMAIL_IDX_RECEIVER_BCC] . "\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; @@ -101,28 +204,37 @@ class Sendmail { /** * Creates a new MailLog Record based on $mailArr / $header. * - * @param array $mailarr - * @param $header + * @param array $mailConfig * * @throws CodeException * @throws DbException */ - private function mailLog(array $mailarr, $header) { + 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[SENDMAIL_IDX_RECEIVER] = $mailarr[SENDMAIL_IDX_RECEIVER]; - $log[SENDMAIL_IDX_SENDER] = $mailarr[SENDMAIL_IDX_SENDER]; - $log[SENDMAIL_IDX_SUBJECT] = $mailarr[SENDMAIL_IDX_SUBJECT]; - $log[SENDMAIL_IDX_BODY] = $mailarr[SENDMAIL_IDX_BODY]; - $log[4] = $header; - $log[5] = empty($mailarr[SENDMAIL_IDX_GR_ID]) ? 0 : $mailarr[SENDMAIL_IDX_GR_ID]; - $log[6] = empty($mailarr[SENDMAIL_IDX_X_ID]) ? 0 : $mailarr[SENDMAIL_IDX_X_ID]; - $log[7] = empty($mailarr[SENDMAIL_IDX_SRC]) ? 0 : $mailarr[SENDMAIL_IDX_SRC]; + $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`, `src`, `modified`, `created`) VALUES ( ?, ? ,?, ?, ? ,?, ?, ?, NOW(), NOW() )', ROW_REGULAR, $log); + $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/report/Variables.php b/extension/qfq/qfq/report/Variables.php index d382759bf36b8d4703b122c2c1f879dccaf58828..355dca4c66da02994b01a66cbcbd2fd89d349e44 100644 --- a/extension/qfq/qfq/report/Variables.php +++ b/extension/qfq/qfq/report/Variables.php @@ -59,8 +59,18 @@ class Variables { * @return mixed */ public function doVariables($text) { + + if ($text == '') { + return ''; + } + // $str = preg_replace_callback("/(~([a-zA-Z0-9._])*)/", array($this, 'replaceVariables'), $text); - $str = preg_replace_callback("/{{(([a-zA-Z0-9.:_])*)}}/", array($this, 'replaceVariables'), $text); +// $str = preg_replace_callback("/{{(([a-zA-Z0-9.:_])*)}}/", array($this, 'replaceVariables'), $text); + // Process all {{x[.x].name}} + $str = preg_replace_callback('/{{\s*(([0-9]+.)+[a-zA-Z0-9_.]+)\s*}}/', array($this, 'replaceVariables'), $text); + + // Try the Stores + $str = $this->eval->parse($str); return $str; } @@ -93,11 +103,11 @@ class Variables { } // If not replaced, try the Stores - if ($data === $matches[0]) { - $dataTmp = $this->eval->parse($data); - if ($dataTmp !== false) - $data = $dataTmp; - } +// if ($data === $matches[0]) { +// $dataTmp = $this->eval->parse($data); +// if ($dataTmp !== false) +// $data = $dataTmp; +// } return $data; } diff --git a/extension/qfq/qfq/store/Config.php b/extension/qfq/qfq/store/Config.php index 51c10d244e5543867f4ffb2cadb4e8344ae05efe..42c465197b25652f73590c69059fda7a6b59dd4c 100644 --- a/extension/qfq/qfq/store/Config.php +++ b/extension/qfq/qfq/store/Config.php @@ -43,12 +43,31 @@ class Config { $config = self::renameConfigElements($config); $config = self::setDefaults($config); - + self::checkDeprecated($config); self::checkForAttack($config); return $config; } + /** + * Checks for deprecated options. + */ + private static function checkDeprecated(array $config) { + + foreach ([SYSTEM_VAR_ADD_BY_SQL] as $key) { + + if (isset($config[$key])) { + $msg = ''; + switch ($key) { + case SYSTEM_VAR_ADD_BY_SQL: + $msg = 'Replaced by: ' . SYSTEM_FILL_STORE_SYSTEM_BY_SQL . '_1|2|3'; + } + throw new qfq\UserFormException ("Deprecated option in " . CONFIG_INI . ": " . SYSTEM_VAR_ADD_BY_SQL . " - " . $msg); + } + } + } + + /** * @param array $config */ @@ -144,6 +163,7 @@ class Config { Support::setIfNotSet($config, SYSTEM_SHOW_DEBUG_INFO, SYSTEM_SHOW_DEBUG_INFO_AUTO); Support::setIfNotSet($config, SYSTEM_SQL_LOG, SYSTEM_SQL_LOG_FILE); Support::setIfNotSet($config, SYSTEM_SQL_LOG_MODE, SQL_LOG_MODE_NONE, ''); // do not worry: parse_ini_file() will replace 'none' and 'off' by ''. Set it here again. + Support::setIfNotSet($config, SYSTEM_MAIL_LOG, SYSTEM_MAIL_LOG_FILE); Support::setIfNotSet($config, F_BS_COLUMNS, '12'); Support::setIfNotSet($config, F_BS_LABEL_COLUMNS, '3'); Support::setIfNotSet($config, F_BS_INPUT_COLUMNS, '6'); @@ -187,7 +207,7 @@ class Config { Support::setIfNotSet($config, DOCUMENTATION_QFQ, DOCUMENTATION_QFQ_URL); - Support::setIfNotSet($config, SYSTEM_VAR_ADD_BY_SQL, SYSTEM_VAR_ADD_BY_SQL_DEFAULT); +// Support::setIfNotSet($config, SYSTEM_FILL_STORE_SYSTEM_BY_SQL, SYSTEM_VAR_ADD_BY_SQL_DEFAULT); return $config; } diff --git a/extension/qfq/qfq/store/Session.php b/extension/qfq/qfq/store/Session.php index 1841affef937e9e2339c4a2145a8ef541f9e2687..c0ef096bdfbc1fb2889ebe4c6a7956c5f5483a3d 100644 --- a/extension/qfq/qfq/store/Session.php +++ b/extension/qfq/qfq/store/Session.php @@ -33,8 +33,13 @@ class Session { } else { ini_set('session.cookie_httponly', 1); + $path = $this->getSitePath(); + session_set_cookie_params(0, $path); + session_name(SESSION_NAME); + session_start(); + self::$sessionId = session_id(); } @@ -44,6 +49,38 @@ class Session { } + /** + * Extract the SitePath of the current T3 installation. + * + * return: <path> with a trailing '/' + */ + private static function getSitePath() { + + if (empty($_SERVER['SCRIPT_NAME'])) { + throw new CodeException('Missing _SERVER[SCRIPT_NAME]', ERROR_SESSION_BROKEN_SCRIPT_PATH); + } + + $path = $_SERVER['SCRIPT_NAME']; + $pos = strrpos($path, '/'); + if ($pos === false) { + throw new CodeException("Broken _SERVER[SCRIPT_NAME]: $path", ERROR_SESSION_BROKEN_SCRIPT_PATH); + } + // Remove PHP script + $path = substr($path, 0, $pos + 1); + + // QFQ might be called by API - justify to the SitePath + $pos = strpos($path, 'typo3conf/ext/qfq/api'); + if ($pos !== false) { + $path = substr($path, 0, $pos); + } + + if (empty($path)) { + throw new CodeException("Broken _SERVER[SCRIPT_NAME]: $path", ERROR_SESSION_BROKEN_SCRIPT_PATH); + } + + return $path; + } + /** * Free a lock on the current session */ diff --git a/extension/qfq/qfq/store/Store.php b/extension/qfq/qfq/store/Store.php index 2192a836ed7e5091bdeafb66987dc8e49d6e81f8..4d8f7931dc9dc22d21bc2922a8af37a3e5d1c408 100644 --- a/extension/qfq/qfq/store/Store.php +++ b/extension/qfq/qfq/store/Store.php @@ -168,9 +168,9 @@ class Store { STORE_ADDITIONAL_FORM_ELEMENTS => false, ]; + self::fillStoreTypo3($bodytext); // should be filled before fillStoreSystem() to offer T3 variables + self::fillStoreClient(); // should be filled before fillStoreSystem() to offer Client variables self::fillStoreSystem($fileConfigIni); - self::fillStoreTypo3($bodytext); - self::fillStoreClient(); self::fillStoreSip(); self::fillStoreExtra(); @@ -247,6 +247,13 @@ class Store { $config[SYSTEM_SQL_LOG] = $config[SYSTEM_PATH_EXT] . '/' . $config[SYSTEM_SQL_LOG]; } + // make SQL PATH absolute. This is necessary to work in different directories correctly. + if (!empty($config[SYSTEM_MAIL_LOG]) && $config[SYSTEM_MAIL_LOG][0] !== '/') { + $config[SYSTEM_MAIL_LOG] = $config[SYSTEM_PATH_EXT] . '/' . $config[SYSTEM_MAIL_LOG]; + } + + $config[SYSTEM_SEND_E_MAIL] = $config[SYSTEM_PATH_EXT] . '/qfq/external/sendEmail'; + // In case the database credentials are given in the old style: copy them to the new style if (!isset($config[SYSTEM_DB_1_USER]) && isset($config[SYSTEM_DB_USER])) { $config[SYSTEM_DB_1_USER] = $config[SYSTEM_DB_USER]; @@ -819,26 +826,47 @@ class Store { } /** - * Read SYSTEM_VARIABLES_GET_FROM_DB from SYSTEM_STORE and if set: + * Read SYSTEM_FILL_STORE_SYSTEM_BY_SQL_1|2|3 from SYSTEM_STORE and if set: * a) fire the SQL - * b) merge all columns to SYSTEM_STORE + * b) merge all columns to STORE_SYSTEM */ - public static function systemStoreUpdate() { + public static function StoreSystemUpdate() { + $db = null; + $storeSystemAdd = array(); $storeSystem = self::getStore(STORE_SYSTEM); - if (!empty($storeSystem[SYSTEM_VAR_ADD_BY_SQL])) { - $db = new qfq\Database(); - $arr = $db->sql($storeSystem[SYSTEM_VAR_ADD_BY_SQL], ROW_EXPECT_0_1); - if (!empty($arr)) { - $storeSystem = array_merge($storeSystem, $arr); - self::setStore($storeSystem, STORE_SYSTEM, true); + for ($ii = 1; $ii <= 3; $ii++) { + if (empty($storeSystem[SYSTEM_FILL_STORE_SYSTEM_BY_SQL . "_$ii"])) { + continue; + } + + if ($db == null) { + $db = new qfq\Database(); } + + $errMsg = "More than 1 record found. " . CONFIG_INI . ": " . SYSTEM_FILL_STORE_SYSTEM_BY_SQL . "_$ii"; + $mode = ROW_EXPECT_0_1; + + // If there is an error message defined, this means there should be exactly one record. + if (!empty($storeSystem[SYSTEM_FILL_STORE_SYSTEM_ERROR_MSG . "_$ii"])) { + $mode = ROW_EXPECT_1; + $errMsg = $storeSystem[SYSTEM_FILL_STORE_SYSTEM_ERROR_MSG . "_$ii"]; + } + + $storeSystemAdd = $db->sql($storeSystem[SYSTEM_FILL_STORE_SYSTEM_BY_SQL . "_$ii"], $mode, array(), $errMsg); + $storeSystemAdd = OnArray::keyNameRemoveLeadingUnderscore($storeSystemAdd); + $storeSystem = array_merge($storeSystem, $storeSystemAdd); + + } + + if (!empty($storeSystem)) { + self::setStore($storeSystem, STORE_SYSTEM, true); } } /** - * Append an array or the first row of array of arrays to store $storeName. + * Append an array (in case of 'array of array': the first row of array) to store $storeName. * Existing values will be overwritten. * * @param $storeName diff --git a/extension/qfq/sql/formEditor.sql b/extension/qfq/sql/formEditor.sql index 7a78e5d16b3c774fbff706cc32af3181463ec071..c8ed7679bc903ab0de7caa406af0b199565b6862 100644 --- a/extension/qfq/sql/formEditor.sql +++ b/extension/qfq/sql/formEditor.sql @@ -360,6 +360,8 @@ 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 '', diff --git a/extension/qfq/tests/phpunit/DatabaseTest.php b/extension/qfq/tests/phpunit/DatabaseTest.php index 5482cae452f4b75d8e6872daae0f49867efed254..f241027edec72bbcd99489bdf98f0420c0aaa131 100644 --- a/extension/qfq/tests/phpunit/DatabaseTest.php +++ b/extension/qfq/tests/phpunit/DatabaseTest.php @@ -255,7 +255,12 @@ class DatabaseTest extends AbstractDatabaseTest { $rc = $this->dbArray[DB_INDEX_DATA_DEFAULT]->sql($sql, ROW_REGULAR, $dummy, 'fake', $dummy, $stat); // DB_NUM_ROWS | DB_INSERT_ID | DB_AFFECTED_ROWS - $this->assertEquals(10, $stat[DB_NUM_ROWS]); + $all = ''; + foreach ($rc as $val) { + $all .= '-' . implode('-', $val); + } + + $this->assertEquals(3, $stat[DB_NUM_ROWS]); } /** @@ -467,6 +472,13 @@ class DatabaseTest extends AbstractDatabaseTest { protected function setUp() { parent::setUp(); + // remove existing tables + $allTables = $this->dbArray[DB_INDEX_DATA_DEFAULT]->sql("SHOW TABLES", ROW_REGULAR); + foreach ($allTables AS $val) { + $table = current($val); + $this->dbArray[DB_INDEX_DATA_DEFAULT]->sql("DROP TABLE $table"); + } + $this->executeSQLFile(__DIR__ . '/fixtures/Generic.sql', true); } } diff --git a/extension/qfq/tests/phpunit/EvaluateTest.php b/extension/qfq/tests/phpunit/EvaluateTest.php index 820f719d755d29292fffdc67cd57a47a15ce147b..b72df041b0d0ca2359b6c79b089b9ac23933d789 100644 --- a/extension/qfq/tests/phpunit/EvaluateTest.php +++ b/extension/qfq/tests/phpunit/EvaluateTest.php @@ -124,7 +124,7 @@ class EvaluateTest extends \AbstractDatabaseTest { $this->assertEquals($expected2, $eval->parseArray($data, ['formName', 'title'])); $expected2 = $expected; - $data['formName'] = "SELECT FROM unknown garbage WITH missing parameter"; + $data['formName'] = 'SELECT FROM unknown garbage WITH missing parameter'; $expected2['formName'] = $data['formName']; $expected2['title'] = $data['title']; @@ -347,16 +347,16 @@ class EvaluateTest extends \AbstractDatabaseTest { // LDAP_ESCAPE_FILTER => array('\\', '*', '(', ')', "\x00"), // LDAP_ESCAPE_DN => array('\\', ',', '=', '+', '<', '>', ';', '"', '#'), $this->store->setVar('a', ' hello world ', STORE_FORM, true); - $this->assertEquals('\20hello world\20', $eval->substitute('a:F:all:L', $foundInStore)); -// $this->assertEquals(' hello world ', $eval->substitute('a:F:all:L', $foundInStore)); +// $this->assertEquals('\20hello world\20', $eval->substitute('a:F:all:L', $foundInStore)); + $this->assertEquals(' hello world ', $eval->substitute('a:F:all:L', $foundInStore)); $this->store->setVar('a', 'h\e,l=l+o< >w;o"r#ld', STORE_FORM, true); $this->assertEquals('h\5ce\2cl\3dl\2bo\3c \3ew\3bo\22r\23ld', $eval->substitute('a:F:all:L', $foundInStore)); $this->store->setVar('a', ' hel;lo world ', STORE_FORM, true); - $this->assertEquals('\20hel\3blo world\20', $eval->substitute('a:F:all:sL', $foundInStore)); -// $this->assertEquals(' hel\3blo world ', $eval->substitute('a:F:all:sL', $foundInStore)); +// $this->assertEquals('\20hel\3blo world\20', $eval->substitute('a:F:all:sL', $foundInStore)); + $this->assertEquals(' hel\3blo world ', $eval->substitute('a:F:all:sL', $foundInStore)); } diff --git a/extension/qfq/tests/phpunit/OnArrayTest.php b/extension/qfq/tests/phpunit/OnArrayTest.php index ef9500fb608472b4bd8b3fe5cd84035e35f8ff00..f8fa40680ed268d90833944d8210b464019f0448 100644 --- a/extension/qfq/tests/phpunit/OnArrayTest.php +++ b/extension/qfq/tests/phpunit/OnArrayTest.php @@ -151,8 +151,8 @@ class OnArrayTest extends \PHPUnit_Framework_TestCase { $this->assertEquals(array(), OnArray::getArrayItemKeyNameStartWith(array(), '')); $this->assertEquals(['a' => 'hello'], OnArray::getArrayItemKeyNameStartWith(['a' => 'hello'], '')); $this->assertEquals(array(), OnArray::getArrayItemKeyNameStartWith(['a' => 'hello'], 'b')); - $this->assertEquals([0 => 'hello'], OnArray::getArrayItemKeyNameStartWith(['a' => 'hello'], 'a')); -// $this->assertEquals(['' => 'hello'], OnArray::getArrayItemKeyNameStartWith(['a' => 'hello'], 'a')); +// $this->assertEquals([0 => 'hello'], OnArray::getArrayItemKeyNameStartWith(['a' => 'hello'], 'a')); + $this->assertEquals(['' => 'hello'], OnArray::getArrayItemKeyNameStartWith(['a' => 'hello'], 'a')); $this->assertEquals(['b' => 'hello'], OnArray::getArrayItemKeyNameStartWith(['ab' => 'hello'], 'a')); $this->assertEquals(array(), OnArray::getArrayItemKeyNameStartWith(['ba' => 'hello'], 'a')); $this->assertEquals(['a' => 'is', 'b' => 'john'], OnArray::getArrayItemKeyNameStartWith(['1_a' => 'my', '1_b' => 'name', '2_a' => 'is', '2_b' => 'john'], '2_')); @@ -167,4 +167,15 @@ class OnArrayTest extends \PHPUnit_Framework_TestCase { $this->assertEquals(['name' => "'john'", 'surname' => "'doe'"], OnArray::arrayEscapeshellarg(['name' => 'john', 'surname' => 'doe'])); $this->assertEquals(['name' => "'john'", 'sub' => ['surname' => "'doe'"]], OnArray::arrayEscapeshellarg(['name' => 'john', 'sub' => ['surname' => 'doe']])); } + + public function testArrayKeyNameRemoveLeadingUnderscore() { + $this->assertEquals(array(), OnArray::keyNameRemoveLeadingUnderscore(array())); + $this->assertEquals(['name' => 'john'], OnArray::keyNameRemoveLeadingUnderscore(['name' => 'john'])); + $this->assertEquals(['name' => 'john', 'surname' => 'doe'], OnArray::keyNameRemoveLeadingUnderscore(['name' => 'john', 'surname' => 'doe'])); + $this->assertEquals(['name' => '_john'], OnArray::keyNameRemoveLeadingUnderscore(['_name' => '_john'])); + $this->assertEquals(['name' => 'john', 'surname' => 'doe'], OnArray::keyNameRemoveLeadingUnderscore(['_name' => 'john', 'surname' => 'doe'])); + $this->assertEquals(['name' => 'john', 'surname' => 'doe'], OnArray::keyNameRemoveLeadingUnderscore(['name' => 'john', '_surname' => 'doe'])); + $this->assertEquals(['name' => 'john', 'surname' => 'doe'], OnArray::keyNameRemoveLeadingUnderscore(['_name' => 'john', '_surname' => 'doe'])); + + } } diff --git a/extension/qfq/tests/phpunit/ReportTest.php b/extension/qfq/tests/phpunit/ReportTest.php index a2b7fe06718c816320c536741a9e810fc1b97750..fb2d0badd2a5936f3dce5b658144ad70c500892a 100644 --- a/extension/qfq/tests/phpunit/ReportTest.php +++ b/extension/qfq/tests/phpunit/ReportTest.php @@ -10,6 +10,8 @@ require_once(__DIR__ . '/../../qfq/report/Report.php'); require_once(__DIR__ . '/../../qfq/Evaluate.php'); require_once(__DIR__ . '/../../qfq/store/Session.php'); +const TOTAL_COUNT_PERSON_GENERIC_SQL = 2; + /** * Created by PhpStorm. * User: crose @@ -942,19 +944,133 @@ EOF; } } + + /** + * + */ + public function testReportPageWrapper() { + + $line = <<<EOF +10.sql = SELECT firstname FROM Person ORDER BY id LIMIT 2 +10.head = <table> +10.tail = </table> +10.rbeg = <tr> +10.rend = <br> +10.renr = </tr> +10.fbeg = <td> +10.fend = </td> +10.rsep = -- +10.fsep = ++ + +10.10.sql = SELECT 'nested' FROM (SELECT '') AS fake WHERE '{{10.line.count}}'='1' +10.10.shead = Static head +10.10.stail = Static tail +10.10.head = Dynamic head +10.10.tail = Dynamic tail +10.10.althead = No record found +10.10.altsql = SELECT 'alt sql fired' + +EOF; + + $result = $this->report->process($line); + $expect = "<table><tr><td>John</td><br>Static headDynamic headnestedDynamic tailStatic tail</tr>--<tr><td>Jane</td><br>Static headNo record foundalt sql firedStatic tail</tr></table>"; + $this->assertEquals($expect, $result); + } + + /** + * + */ + public function testReportVariables() { + + $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals("normal text ", $result); + + $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id LIMIT 1\n10.10.sql = SELECT '{{10.hidden}}'"); + $this->assertEquals("normal text hidden", $result); + + $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id LIMIT 1\n10.10.sql = SELECT '{{10.unknown}}'"); + $this->assertEquals("normal text {{10.unknown}}", $result); + + $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id LIMIT 1\n10.10.sql = SELECT '{{fake}}'"); + $this->assertEquals("normal text {{fake}}", $result); + + $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id LIMIT 1\n10.10.sql = SELECT '{{fake:V}}'"); + $this->assertEquals("normal text {{fake:V}}", $result); + + $this->store->setVar('fake', 'hello world ', STORE_VAR); + $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden ' AS _hidden, 'text ' FROM Person ORDER BY id LIMIT 1\n10.10.sql = SELECT '{{fake:V}}'"); + $this->assertEquals("normal text hello world ", $result); + + $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id\n10.10.sql = SELECT '{{fake:V}}'"); + $this->assertEquals("normal text hello world normal text hello world ", $result); + + $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id\n10.10.sql = SELECT '{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.line.insertId}} '"); + $this->assertEquals("normal text hello world -1-2-0 normal text hello world -2-2-0 ", $result); + + $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id\n10.10.sql = SELECT '{{fake:V}}-{{10.line.count}}-{{10.line.total}} '"); + $this->assertEquals("normal text hello world -1-2 normal text hello world -2-2 ", $result); + + $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id\n10.10.sql = SELECT '{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.10.line.count}}-{{10.10.line.total}} '"); + $this->assertEquals("normal text hello world -1-2-1-1 normal text hello world -2-2-1-1 ", $result); + + $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id\n10.10.sql = SELECT '{{fake:V:::not found}} '"); + $this->assertEquals("normal text hello world normal text hello world ", $result); + + $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id\n10.10.sql = SELECT '{{fakeDontExist:V:::not found}} '"); + $this->assertEquals("normal text not found normal text not found ", $result); + + $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id\n10.10.sql = SELECT '{{fakeDontExist:V:::{{EDIT_FORM_PAGE:Y}}}} '"); + $this->assertEquals("normal text form normal text form ", $result); + + +// store various +// store default + // head.tail,rbeg,.seop mit variabeln +// 5.sql im 10 head abragen + } + /** * */ - public function testReportSurpress() { + public function testReportPageVariables() { + + $line = <<<EOF +10.sql = SELECT name, firstname FROM Person ORDER BY id LIMIT 2 +10.head = h:{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.line.insertId}}, +10.tail = t:{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.line.insertId}}, +10.rbeg = rb:{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.line.insertId}}, +10.rend = re:{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.line.insertId}}, +10.renr = rr:{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.line.insertId}}, +10.fbeg = -fb:{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.line.insertId}}, +10.fend = -fe:{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.line.insertId}}, +10.rsep = rs:{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.line.insertId}}, +10.fsep = fs:{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.line.insertId}}, +EOF; - $result = $this->report->process("10.sql = SELECT 'normal', 'hidden' AS _hidden, 'text' FROM Person ORDER BY id LIMIT 1"); - $this->assertEquals("normaltext", $result); + $this->store->setVar('fake', 'hello world', STORE_VAR); + $result = $this->report->process($line); + $expect = "h:hello world-1-2-0,rb:hello world-1-2-0,-fb:hello world-1-2-0,Doe-fe:hello world-1-2-0,fs:hello world-1-2-0,-fb:hello world-1-2-0,John-fe:hello world-1-2-0,re:hello world-1-2-0,rr:hello world-1-2-0,rs:hello world-1-2-0,rb:hello world-2-2-0,-fb:hello world-2-2-0,Smith-fe:hello world-2-2-0,fs:hello world-2-2-0,-fb:hello world-2-2-0,Jane-fe:hello world-2-2-0,re:hello world-2-2-0,rr:hello world-2-2-0,t:hello world-2-2-0,"; + $this->assertEquals($expect, $result); + + $line = <<<EOF +10.sql = SELECT name, firstname FROM Person ORDER BY id LIMIT 2 +10.10.sql = SELECT ' blue ' +10.10.head = h:{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.line.insertId}}, +10.10.tail = t:{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.line.insertId}}, +10.10.rbeg = rb:{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.line.insertId}}, +10.10.rend = re:{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.line.insertId}}, +10.10.renr = rr:{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.line.insertId}}, +10.10.fbeg = -fb:{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.line.insertId}}, +10.10.fend = -fe:{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.line.insertId}}, +10.10.rsep = rs:{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.line.insertId}}, +10.10.fsep = fs:{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.line.insertId}}, +EOF; - $result = $this->report->process("10.sql = SELECT 'normal', 'hidden' AS _hidden, 'text' FROM Person ORDER BY id LIMIT 1\n10.10.sql = SELECT '{{10.hidden}}'"); - $this->assertEquals("normaltexthidden", $result); + $this->store->setVar('fake', 'hello world', STORE_VAR); + $result = $this->report->process($line); + $expect = "DoeJohnh:hello world-1-2-0,rb:hello world-1-2-0,-fb:hello world-1-2-0, blue -fe:hello world-1-2-0,re:hello world-1-2-0,rr:hello world-1-2-0,t:hello world-1-2-0,SmithJaneh:hello world-2-2-0,rb:hello world-2-2-0,-fb:hello world-2-2-0, blue -fe:hello world-2-2-0,re:hello world-2-2-0,rr:hello world-2-2-0,t:hello world-2-2-0,"; + $this->assertEquals($expect, $result); - $result = $this->report->process("10.sql = SELECT 'normal', 'hidden' AS _hidden, 'text' FROM Person ORDER BY id LIMIT 1\n10.10.sql = SELECT '{{10.unknown}}'"); - $this->assertEquals("normaltext{{10.unknown}}", $result); } /** diff --git a/extension/qfq/tests/phpunit/StoreTest.php b/extension/qfq/tests/phpunit/StoreTest.php index 564401fd9d44a5343ded8ff283ad6f91210710e3..061dd4c9e61b06a2fa0e9fee5b84b23039b25557 100644 --- a/extension/qfq/tests/phpunit/StoreTest.php +++ b/extension/qfq/tests/phpunit/StoreTest.php @@ -252,11 +252,12 @@ EOT; SYSTEM_DB_INIT => 'set names utf8', SYSTEM_SQL_LOG_MODE => 'modify', - SYSTEM_DB_INDEX_DATA => 1, - SYSTEM_DB_INDEX_QFQ => 1, + SYSTEM_DB_INDEX_DATA => '1', + SYSTEM_DB_INDEX_QFQ => '1', SYSTEM_DATE_FORMAT => 'yyyy-mm-dd', SYSTEM_SHOW_DEBUG_INFO => SYSTEM_SHOW_DEBUG_INFO_NO, + F_BS_COLUMNS => '12', F_BS_LABEL_COLUMNS => '3', F_BS_INPUT_COLUMNS => '6', @@ -287,9 +288,9 @@ EOT; F_BUTTON_ON_CHANGE_CLASS => 'btn-info alert-info', SYSTEM_EDIT_FORM_PAGE => 'form', SYSTEM_SECURITY_VARS_HONEYPOT => 'email,username,password', - SYSTEM_SECURITY_ATTACK_DELAY => '5', + SYSTEM_SECURITY_ATTACK_DELAY => 5, SYSTEM_SECURITY_SHOW_MESSAGE => '0', - SYSTEM_SECURITY_GET_MAX_LENGTH => '50', + SYSTEM_SECURITY_GET_MAX_LENGTH => 50, SYSTEM_ESCAPE_TYPE_DEFAULT => 'm', SYSTEM_GFX_EXTRA_BUTTON_INFO_INLINE => '<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>', SYSTEM_GFX_EXTRA_BUTTON_INFO_BELOW => '<span class="glyphicon glyphicon-info-sign text-info" aria-hidden="true"></span>', @@ -299,8 +300,6 @@ EOT; SYSTEM_RECORD_LOCK_TIMEOUT_SECONDS => SYSTEM_RECORD_LOCK_TIMEOUT_SECONDS_DEFAULT, DOCUMENTATION_QFQ => DOCUMENTATION_QFQ_URL, - SYSTEM_VAR_ADD_BY_SQL => 'SELECT id AS periodId FROM Period WHERE start<=NOW() ORDER BY start DESC LIMIT 1', - ]; $fileName = $this->createFile($body); @@ -312,6 +311,8 @@ EOT; unset($value[SYSTEM_SQL_LOG]); unset($value[SYSTEM_PATH_EXT]); unset($value[SYSTEM_SITE_PATH]); + unset($value[SYSTEM_MAIL_LOG]); + unset($value[SYSTEM_SEND_E_MAIL]); // check default values $this->assertEquals($expect, $value, "Retrieve system store."); @@ -348,6 +349,8 @@ EOT; unset($value[SYSTEM_SQL_LOG]); unset($value[SYSTEM_PATH_EXT]); unset($value[SYSTEM_SITE_PATH]); + unset($value[SYSTEM_MAIL_LOG]); + unset($value[SYSTEM_SEND_E_MAIL]); // check default values $this->assertEquals($expect, $value, "Check explizit defined values."); diff --git a/version b/version index 166c9e29b70b1e6a84168e2e608430f62da4254a..35aa2f3c0799c4bc021a1ebe62d38c5c58582d63 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.25.2 +0.25.4