diff --git a/extension/Documentation/Manual.rst b/extension/Documentation/Manual.rst index e3a76d546549b6bc29d7ad97e5634ebc7b210b12..c902098b84287bea3c8b6eb6f0815145a226aa01 100644 --- a/extension/Documentation/Manual.rst +++ b/extension/Documentation/Manual.rst @@ -472,6 +472,9 @@ config.qfq.php | LDAP_1_RDN | LDAP_1_RDN='ou=Admin,ou=example,dc=com' | Credentials for non-anonymous LDAP access. At the moment only one set of | | LDAP_1_PASSWORD | LDAP_1_PASSWORD=mySecurePassword | | +-------------------------------+-------------------------------------------------------+----------------------------------------------------------------------------+ +| T3_DB_NAME | T3_DB_NAME=specialt3dbname | Only necessary for inline report editing if the t3 database is not | +| | | anologous to the Data db name (but ending in _t3) - see `inline-report`_. | ++-------------------------------+-------------------------------------------------------+----------------------------------------------------------------------------+ @@ -503,11 +506,11 @@ Example: *typo3conf/config.qfq.php*: :: After parsing the configuration, the following variables will be set automatically in STORE_SYSTEM: -+----------------+-----------------------------------------------------------------------------------+ -| _dbNameData | Can be used to dynamically access the current selected database: {{dbNameData:Y}} | -+----------------+-----------------------------------------------------------------------------------+ -| _dbNameQfq | Can be used to dynamically access the current selected database: {{dbNameQfq:Y}} | -+----------------+-----------------------------------------------------------------------------------+ ++----------------+------------------------------------------------------------------------------------+ +| _dbNameData | Can be used to dynamically access the current selected database: {{_dbNameData:Y}} | ++----------------+------------------------------------------------------------------------------------+ +| _dbNameQfq | Can be used to dynamically access the current selected database: {{_dbNameQfq:Y}} | ++----------------+------------------------------------------------------------------------------------+ .. _`CustomVariables`: @@ -4992,6 +4995,29 @@ The parsed bodytext could be displayed by activating 'showDebugInfo' (:ref:`debu A small symbol with a tooltip will be shown, where the content record will be displayed on the webpage. Note: :ref:`debug` information will only be shown with *showDebugInfo: yes* in configuration_. + +.. _`inline-report`: + +Inline Report editing +--------------------- + +For quick changes it can be bothersome to go to the TYPO3 backend to update the page content and reload the page. +For this reason, QFQ offers an inline report editing feature whenever there is a TYPO3 BE user logged in. A small +link symbol will appear on the right-hand side of each report record. Please note that the TYPO3 Frontend cache +is also deleted upon each inline report save. + +In order for the inline report editing to work, QFQ needs to be able to access the T3 database. By default this database +is assumed to be accessible with the same credentials as specified with indexData and is assumed to be named similarly to +the indexData db name, but ending in _t3 instead of _db (e.g., mydb_db and mydb_t3). +For a standard installation and db setup, this should be the case. + +You can however specify a custom T3 db name in config-qfq-php_: + +:: + + T3_DB_NAME = customT3DbName + + Structure --------- diff --git a/extension/qfq/api/save.php b/extension/qfq/api/save.php index 71fcac1370f6e54d5946b24df78a543eada87cfb..054c54120a3bd9d9728e11d1478dd6cfa097f320 100644 --- a/extension/qfq/api/save.php +++ b/extension/qfq/api/save.php @@ -59,6 +59,11 @@ try { $data = $qfq->saveForm(); + if (isset($data[REPORT_SAVE])) { + // Redirect to previous page + header("Location: {$_SERVER['HTTP_REFERER']}"); + } + $arr = $qfq->getForwardMode(); $answer[API_REDIRECT] = $arr[API_REDIRECT]; $answer[API_REDIRECT_URL] = $arr[API_REDIRECT_URL]; diff --git a/extension/qfq/qfq/Constants.php b/extension/qfq/qfq/Constants.php index 05c2315b18be00814296767ac2a3515d477ad83f..09e7ca56ef951315f040836e0bd41634a463bb5c 100644 --- a/extension/qfq/qfq/Constants.php +++ b/extension/qfq/qfq/Constants.php @@ -45,6 +45,8 @@ const FORM_BUTTON_DELETE = 'delete'; const FORM_BUTTON_CLOSE = 'close'; const FORM_BUTTON_SAVE = 'save'; +const REPORT_SAVE = 'reportSave'; + const F_BS_COLUMNS = 'bsColumns'; const F_BS_LABEL_COLUMNS = 'bsLabelColumns'; @@ -409,6 +411,8 @@ const SYSTEM_DB_1_SERVER = 'DB_1_SERVER'; const SYSTEM_DB_1_PASSWORD = 'DB_1_PASSWORD'; const SYSTEM_DB_1_NAME = 'DB_1_NAME'; +const SYSTEM_T3_DB_NAME = 'T3_DB_NAME'; + const SYSTEM_DB_INIT = 'init'; const SYSTEM_DB_INDEX_DATA = "indexData"; @@ -1205,7 +1209,10 @@ const INDEX_PHP = 'index.php'; // QuickFormQuery.php const T3DATA_BODYTEXT = 'bodytext'; +const T3DATA_BODYTEXT_RAW = 'bodytext-raw'; const T3DATA_UID = 'uid'; +const T3DATA_HEADER = 'header'; +const REPORT_INLINE_BODYTEXT = 'bodytext'; // Special Column to check for uploads const COLUMN_PATH_FILE_NAME = 'pathFileName'; diff --git a/extension/qfq/qfq/QuickFormQuery.php b/extension/qfq/qfq/QuickFormQuery.php index 762801973cf06c2dfd7ac11344ae5cb2d95e7cb2..e514a04ccb08b6d152b728dfef19d941e208442d 100644 --- a/extension/qfq/qfq/QuickFormQuery.php +++ b/extension/qfq/qfq/QuickFormQuery.php @@ -23,6 +23,7 @@ use qfq; require_once(__DIR__ . '/store/Store.php'); +require_once(__DIR__ . '/store/Sip.php'); require_once(__DIR__ . '/store/FillStoreForm.php'); require_once(__DIR__ . '/store/Session.php'); require_once(__DIR__ . '/Constants.php'); @@ -151,6 +152,7 @@ class QuickFormQuery { } $btp = new BodytextParser(); + $t3data[T3DATA_BODYTEXT_RAW] = $t3data[T3DATA_BODYTEXT]; $t3data[T3DATA_BODYTEXT] = $btp->process($t3data[T3DATA_BODYTEXT]); $this->t3data = $t3data; @@ -1306,7 +1308,7 @@ class QuickFormQuery { /** * Load record $id from $table and saves them in $store * - * @param string $table tablename from where to load record wiht $recordId + * @param array $table tablename * @param string $recordId record ID of current record * @param string $store name of store where to save the record * @@ -1362,12 +1364,95 @@ class QuickFormQuery { private function doReport() { $report = new Report($this->t3data, $this->eval, $this->phpUnit); + $html = ''; - $html = $report->process($this->t3data['bodytext']); + if ($this->store->getVar(TYPO3_BE_USER, STORE_TYPO3, SANITIZE_ALLOW_ALNUMX)) { + $html .= $this->buildInlineReport(); + } + $html .= $report->process($this->t3data[T3DATA_BODYTEXT]); return $html; } + /** Constructs a form to directly edit qfq content elements inline. + * + * @return string - the html code + * @throws CodeException + * @throws DbException + * @throws DownloadException + * @throws UserFormException + * @throws UserReportException + */ + private function buildInlineReport() { + $uid = $this->t3data[T3DATA_UID]; + $bodytext = $this->t3data[T3DATA_BODYTEXT_RAW]; + $header = $this->t3data[T3DATA_HEADER]; + + $icon = Support::renderGlyphIcon(GLYPH_ICON_TASKS); + $showFormJs = '$("#tt-content-edit-' . $uid . '").toggleClass("hidden")'; + $toggleBtn = Support::wrapTag("<a href='#' onclick='$showFormJs' style='float:right;'>", $icon); + + $saveBtnAttributes = Support::doAttribute('class', 'btn btn-default') . + Support::doAttribute('id', "tt-content-save-$uid") . + Support::doAttribute('type', 'submit') . + Support::doAttribute('style', 'float:right; margin:-5px;') . + Support::doAttribute('title', 'Save & Reload'); + $saveBtnIcon = Support::renderGlyphIcon(GLYPH_ICON_CHECK); + $saveBtn = Support::wrapTag("<button $saveBtnAttributes>", $saveBtnIcon); + $header = "QFQ Page Content '$header'"; + $headerBar = Support::wrapTag("<div class='col-md-12 qfq-form-title'>", $header . $saveBtn); + + $ttContentCode = Support::htmlEntityEncodeDecode(MODE_ENCODE, $bodytext); + $codeBoxAttributes = Support::doAttribute('style', "width:100%;") . + Support::doAttribute('id', "tt-content-code-$uid") . + Support::doAttribute('rows',20) . + Support::doAttribute('name', REPORT_INLINE_BODYTEXT); + $codeBox = Support::wrapTag("<textarea $codeBoxAttributes>", $ttContentCode); + + $form = join(' ', [$headerBar, $codeBox]); + $sipObj = new Sip; + $action = $sipObj->queryStringToSip(API_DIR . "/save.php?uid=$uid&" . REPORT_SAVE . "=1"); + $formAttributes = Support::doAttribute('id', "tt-content-edit-$uid") . + Support::doAttribute('class', 'hidden') . + Support::doAttribute('method', 'post') . + Support::doAttribute('action', $action); + $form = Support::wrapTag("<form $formAttributes>", $form); + + return $toggleBtn . $form; + } + + /** + * @throws CodeException + * @throws DbException + * @throws UserFormException + */ + public function saveReport() { + $uid = $this->store->getVar(T3DATA_UID, STORE_SIP . STORE_ZERO, SANITIZE_ALLOW_DIGIT); + if ($uid == 0) { + // Check if it was called with a SIP (containing a uid) + // If not, this might be an attack => cancel. + return; + } + + $bodytext = Support::htmlEntityEncodeDecode(MODE_DECODE, $_POST[REPORT_INLINE_BODYTEXT]); + $dbT3 = $this->store->getVar(SYSTEM_T3_DB_NAME, STORE_SYSTEM . STORE_EMPTY, SANITIZE_ALLOW_ALNUMX); + if ($dbT3 == '') { + $dbData = $this->store->getVar(SYSTEM_DB_NAME_DATA, STORE_SYSTEM, SANITIZE_ALLOW_ALNUMX); + $dbT3 = substr($dbData, 0, strrpos($dbData, "_") + 1) . 't3'; + } + + // Update bodytext + $sql = "UPDATE $dbT3.tt_content SET bodytext = ?, tstamp = UNIX_TIMESTAMP(NOW()) WHERE uid = ?"; + $this->dbArray[$this->dbIndexData]->sql($sql, ROW_REGULAR, [$bodytext, $uid]); + + // Clear cache + // Need to truncate cf_cache_pages because it is used to restore page-specific cache + $sql = "DELETE FROM $dbT3.cf_cache_pages WHERE 1"; + $this->dbArray[$this->dbIndexData]->sql($sql); + + $this->formSpec[F_FORWARD_MODE] = 'auto'; + } + /** * Save the current form. * @@ -1379,8 +1464,13 @@ class QuickFormQuery { * @throws UserReportException */ public function saveForm() { - - $json = $this->doForm(FORM_SAVE); + if ($this->store->getVar(REPORT_SAVE, STORE_SIP . STORE_ZERO) == '1') { + $this->saveReport(); + $json = array(); + $json[REPORT_SAVE] = 1; + } else { + $json = $this->doForm(FORM_SAVE); + } return $json; } diff --git a/extension/qfq/qfq/Save.php b/extension/qfq/qfq/Save.php index a2beebf72a57d385261483f1a21463fdeda359d0..826fee366406c722892a5302736bf6998e9bce20 100644 --- a/extension/qfq/qfq/Save.php +++ b/extension/qfq/qfq/Save.php @@ -309,7 +309,7 @@ class Save { $sql .= '`' . $column . '` = ?, '; } - $sql = substr($sql, 0, strlen($sql) - 2) . ' WHERE id = ?'; + $sql = substr($sql, 0, strlen($sql) - 2) . " WHERE id = ?"; $values[] = $recordId; $rc = $this->db->sql($sql, ROW_REGULAR, array_values($values)); diff --git a/extension/qfq/qfq/form/Dirty.php b/extension/qfq/qfq/form/Dirty.php index 52b6c317653d193cef9d9e002ff1ed1ea50b43cc..07be2e27512cf8173df7cd873f8efb631cc04673 100644 --- a/extension/qfq/qfq/form/Dirty.php +++ b/extension/qfq/qfq/form/Dirty.php @@ -152,26 +152,25 @@ class Dirty { $formDirtyMode = $tableVars[F_DIRTY_MODE]; $rcMd5 = ''; - // Check for changed record. Compute $rcMd5 - $flagModified = $this->isRecordModified($tableName, $recordId, $recordHashMd5, $rcMd5); - if (($recordHashMd5 != '') && $flagModified) { - return [API_STATUS => API_ANSWER_STATUS_CONFLICT, API_MESSAGE => 'The record has been modified in the meantime. Please reload the form, edit and save again.']; - } - - $feUser = $this->session->get(SESSION_FE_USER); + if ($formDirtyMode == DIRTY_MODE_NONE) { + $answer = [API_STATUS => 'success', API_MESSAGE => '']; + } else { + // Check for changed record. Compute $rcMd5 + $flagModified = $this->isRecordModified($tableName, $recordId, $recordHashMd5, $rcMd5); + if (($recordHashMd5 != '') && $flagModified) { + return [API_STATUS => API_ANSWER_STATUS_CONFLICT, API_MESSAGE => 'The record has been modified in the meantime. Please reload the form, edit and save again.']; + } - // Look for already existing dirty record. - $recordDirty = $this->getRecordDirty($tableName, $recordId); + // Look for already existing dirty record. + $recordDirty = $this->getRecordDirty($tableName, $recordId); - if (count($recordDirty) == 0) { - if ($formDirtyMode == DIRTY_MODE_NONE) { - $answer = [API_STATUS => 'success', API_MESSAGE => '']; - } else { + if (count($recordDirty) == 0) { // No dirty record found. + $feUser = $this->session->get(SESSION_FE_USER); $answer = $this->writeDirty($this->client[SIP_SIP], $recordId, $tableVars, $feUser, $rcMd5); + } else { + $answer = $this->conflict($recordDirty, $formDirtyMode); } - } else { - $answer = $this->conflict($recordDirty, $formDirtyMode); } return $answer; @@ -277,8 +276,9 @@ class Dirty { * @param string $tableName * @param int $recordId * @param string $recordHashMd5 - timestamp e.g. '2017-07-27 14:06:56' - * * @param $rcMd5 + * @param string $tableId + * * @return bool true if $recordHashMd5 is different from current record md5 hash. */ private function isRecordModified($tableName, $recordId, $recordHashMd5, &$rcMd5) { @@ -359,8 +359,9 @@ class Dirty { $answer = [API_STATUS => API_ANSWER_STATUS_SUCCESS, API_MESSAGE => '']; - if ($recordId == 0) { - return $answer; // New records never have a recordDirty nor a conflict. + if ($recordId == 0 || // New records never have a recordDirty nor a conflict. + $dirtyMode == DIRTY_MODE_NONE) { // mode none -> no lock checking + return $answer; } // Check if the record has changed in the meantime. @@ -371,13 +372,9 @@ class Dirty { $lockStatus = $this->getCheckDirty($tableName, $recordId, $rcRecordDirty, $rcMsg); if (empty($rcRecordDirty)) { - if ($dirtyMode == DIRTY_MODE_NONE) { - return $answer; // only situation where it's ok that there is no dirtyRecord. - } if ($formMode == FORM_DELETE) { return $answer; } - // This is pessimistic, but secure. // throw new UserFormException("Missing record lock: please reload the form, edit and save again.", ERROR_DIRTY_MISSING_LOCK); diff --git a/extension/qfq/qfq/helper/Support.php b/extension/qfq/qfq/helper/Support.php index ab94a4fded1821c1501ea1ac039aed0a15298208..bb040cd78af7440ddcfe28c9b3a1310b74fbbb23 100644 --- a/extension/qfq/qfq/helper/Support.php +++ b/extension/qfq/qfq/helper/Support.php @@ -146,6 +146,10 @@ class Support { return $tag . $value . $closing; } + public static function renderGlyphIcon($glyphIcon, $value = '') { + return Support::wrapTag("<span class='" . GLYPH_ICON . " $glyphIcon'>", $value); + } + /** * Removes '$tag' and closing $tag from $value, if they are the outermost. * E.g. unWrapTag('<p>', '<p>Hello World</p>') returns 'Hello World'