Commit 201e1510 authored by Rafael Ostertag's avatar Rafael Ostertag
Browse files

Merge remote-tracking branch 'origin/crose_work' into raos_work

parents bd343f14 89879621
......@@ -60,19 +60,31 @@ DELETE
* class=record-delete
* Button: data-sip={{SIP}}
* SIP values:
* SIP_RECORD_ID: Mandatory.
* SIP_TABLE: Either SIP_TABLE or SIP_FORM has to be given.
* SIP_FORM: Either SIP_TABLE or SIP_FORM has to be given. Not implemented now.
* SIP_TARGET_URL: Only with SIP_MODE_ANSWER=MODE_HTML - Url to redirect browser to.
* SIP_MODE_ANSWER: MODE_JSON / MODE_HTML. If not given, this means MODE_JSON.
* Three possible variants with delete links:
* Form: main record
* (1) Form: main record
* HTML Code:
<button id="delete-button" type="button" class="btn btn-default navbar-btn" ><span class="glyphicon glyphicon-trash"></span></button>
* Form: subrecord, one delete button per record
* Report: typially inside a table, but maybe different.
* (2) Form: subrecord, one delete button per record
* HTML Code:
<button type="button" class="record-delete" data-sip={{SIP}} ><span class="glyphicon glyphicon-trash"></span></button>
* (3) Report: typially inside a table, but maybe different.
<button type="button" class="record-delete" data-sip={{SIP}} ><span class="glyphicon glyphicon-trash"></span></button>
Upload
-----------------
......@@ -87,7 +99,7 @@ Upload
Upload to server, before 'save'
...............................
* If a user open's a file for upload via the browse button, that file is immediately transmitted to the server. The user will see a turning wheel during until the upload finished.
* If a user open's a file for upload via the browse button, that file is immediately transmitted to the server. The user will see a turning wheel until the upload finished.
* After successfull upload the 'Browse' button disappears and the filename, plus the delete button, will be displayed (client logic).
* The uploaded file will be checked: maxsize, mime type, check script.
* The uploaded file is still temporary. It has been renamed from $_SESSION['X'][<uploadSip>][FILES_TMP_NAME] to $_SESSION['X'][<uploadSip>][FILES_TMP_NAME].cached
......@@ -100,13 +112,15 @@ Upload to server, before 'save'
Form save
.........
* Before building the insert/update, process all 'uploads'.
* Get every uniq sipUpload to every upload formElement. Get the corresponding temporary uploaded filename.
* If $_SESSION['X'][<uploadSip>]['flagDelete']='1' is set, delete prefious uploaded file.
* Calculate <destination>
* mv <file>.cached <destination>
* clientvalue[<feName>] = <destination>
* delete $_SESSION['X'][<uploadSip>]
* Step 1: insert /update the record.
* Step 2: process all 'uploads'.
* Get every uniq sipUpload to every upload formElement. Get the corresponding temporary uploaded filename.
* If $_SESSION['X'][<uploadSip>]['flagDelete']='1' is set, delete prefious uploaded file.
* Calculate <destination>
* mv <file>.cached <destination>
* clientvalue[<feName>] = <destination>
* delete $_SESSION['X'][<uploadSip>]
* Step 3: update record with final `FileDestination'
Formelement type: DATE / DATETIME / TIME
----------------------------------------
......@@ -132,8 +146,8 @@ Formelement type: DATE / DATETIME / TIME
* datetime format: 'DATE TIME'
Debug / Log / Errormessages
===========================
Debug / Log
===========
* Before firing a SQL or doing processing of an FormElement, set some debugging / error variables:
......@@ -157,6 +171,20 @@ Debug / Log / Errormessages
$this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM) === 'yes'
Errormessages & Eceptions
=========================
* Exception types:
* Code
* Db
* User Form
* user Report
* plus an Errorhandler which throws exceptions
* Exceptions inside of an API call delivers the error code and msg as JSON to the client.
* Typo3 suppress E_NOTICE (e.g. undefined index). To catch E_NOTICE in QFQ, it will be temporaly enabled in QfqCongroller.php.
Stores
======
......
Neue Versionsnummer
===================
1) In folgenden Files anpassen:
* extension/Documentation/_make/conf.py: release, version
* extension/Documentation/Settings.yml: version
* extension/ext_emconf.php: version
2) Im Projectverzeichnis:
make t3sphinx (nicht sicher ob das noetig ist)
3) Neuen Tag vergeben: git tag v0.4
4) Alle Files, inkl. Tags, in GIT einchecken.
5) Per PhpStorm Sync aller Files auf VM qfq
6) In T3 Instanz Dokumentation rendern lassen.
T3 6.2: Admin Tools > Extension Manager > QFQ > Doku HTML: rechts oben 'Render Documentation'
......@@ -14,11 +14,17 @@ require_once(__DIR__ . '/../../qfq/qfq/exceptions/CodeException.php');
require_once(__DIR__ . '/../../qfq/qfq/exceptions/DbException.php');
class QfqController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController {
public function showAction() {
public function showAction() {
try {
$contentObject = $this->configurationManager->getContentObject();
// By T3 default 'E_NOTICE' is unset. E.g. 'Undefined Index' will throw an exception.
// QFQ like to see those 'E_NOTICE'
$origErrorReporting = error_reporting();
error_reporting($origErrorReporting | E_NOTICE);
$qfq = new \qfq\QuickFormQuery($contentObject->data);
$html = $qfq->process();
......@@ -34,6 +40,9 @@ class QfqController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController {
$html = "Generic Exception: " . $e->getMessage();
}
// Restore has to be outside of try/catch - E_NOTICE needs to unset for further T3 handling after an QFQ Exception.
error_reporting($origErrorReporting);
$this->view->assign('qfqOutput', $html);
return $this->view->render();
}
......
......@@ -90,8 +90,6 @@ Setup a *report* to manage all *forms*: Create a Typo3 page and insert a content
+------------------------+----------------------------------+----------------------------------------------------------------------------+
| DB_INIT | DB_INIT=set names utf8 | Global init for using the database. |
+------------------------+----------------------------------+----------------------------------------------------------------------------+
| SESSION_NAME | SESSION_NAME=qfq | PHP Session name, by default 'qfq' |
+------------------------+----------------------------------+----------------------------------------------------------------------------+
| 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 is a lot |
......@@ -117,7 +115,6 @@ Example: *<ext_dir>/config.ini*
DB_NAME = qfq_db
DB_NAME_TEST = qfq_db_test
DB_INIT = set names utf8
SESSION_NAME = qfq
SQL_LOG = sql.log
SHOW_DEBUG_INFO = auto
CSS_LINK_CLASS_INTERNAL = internal
......
......@@ -6,8 +6,8 @@
conf.py:
copyright: 2016
project: QFQ Extension
version: 0.3
release: 0.3.0
version: 0.4
release: 0.4.0
latex_documents:
- - Index
- qfq.tex
......
......@@ -57,9 +57,9 @@ copyright = u'2016, Carsten Rose'
# built documents.
#
# The short X.Y version.
version = '0.3'
version = '0.4'
# The full version, including alpha/beta/rc tags.
release = '0.3.0'
release = '0.4.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
......
<p>
<f:format.raw value="{qfqOutput}"/>
</p>
\ No newline at end of file
<f:format.raw value="{qfqOutput}"/>
......@@ -8,8 +8,6 @@ DB_NAME_TEST = <TESTDB>
DB_INIT = set names utf8
SESSION_NAME = qfq
SQL_LOG = sql.log
; all, modify
SQL_LOG_MODE = modify
......
......@@ -10,5 +10,5 @@ $EM_CONF[$_EXTKEY] = array(
'dependencies' => 'fluid,extbase',
'clearcacheonload' => true,
'state' => 'alpha',
'version' => '0.3'
'version' => '0.4'
);
\ No newline at end of file
......@@ -47,18 +47,14 @@ require_once(__DIR__ . '/../qfq/Constants.php');
$answer = array();
$answer[API_REDIRECT] = API_ANSWER_REDIRECT_NO;
$answer[API_MESSAGE] = '';
$answer[API_MESSAGE] = 'Something failed';
$answer[API_STATUS] = API_ANSWER_STATUS_ERROR;
try {
$qfq = new \qfq\QuickFormQuery(['bodytext' => '']);
$qfq->delete();
$answer[API_MESSAGE] = 'delete: success';
$answer[API_REDIRECT] = API_ANSWER_REDIRECT_CLIENT;
$answer[API_STATUS] = API_ANSWER_STATUS_SUCCESS;
$result = $qfq->delete();
} catch (qfq\UserFormException $e) {
$answer[API_MESSAGE] = $e->formatMessage();
......@@ -70,6 +66,16 @@ try {
$answer[API_MESSAGE] = "Generic Exception: " . $e->getMessage();
}
header("Content-Type: application/json");
echo json_encode($answer);
// Fallbak. in case an exception has been thrown and $result is not filled.
if (!isset($result[MSG_HEADER]) && !isset($result[MSG_CONTENT])) {
$result[MSG_HEADER] = "Content-Type: application/json";
$result[MSG_CONTENT] = json_encode($answer);
}
if (isset($result[MSG_HEADER]) && $result[MSG_HEADER] !== '')
header($result[MSG_HEADER]);
echo $result[MSG_CONTENT];
This diff is collapsed.
......@@ -12,21 +12,27 @@ const NESTING_TOKEN_OPEN = '#&nesting-open-&#';
const NESTING_TOKEN_CLOSE = '#&nesting-close&#';
const NESTING_TOKEN_LENGTH = 17;
class BodytextParser {
class BodytextParser {
/**
* @param $bodytext
* @return mixed
*/
public function process($bodytext) {
$bodytext = $this->trimAndRemoveCommentAndEmptyLine($bodytext);
$nestingOpen = '';
$nestingClose = '';
$bodytext = $this->trimAndRemoveCommentAndEmptyLine($bodytext, $nestingOpen, $nestingClose);
// Encrypt double curly braces to prevent false positives with nesting: form = {{form}}\n
$bodytext = Support::encryptDoubleCurlyBraces($bodytext);
$bodytext = $this->encryptNestingDelimeter($bodytext);
$bodytext = $this->joinLine($bodytext);
$bodytext = $this->unNest($bodytext);
$bodytext = $this->trimAndRemoveCommentAndEmptyLine($bodytext);
$bodytext = $this->joinLine($bodytext, $nestingOpen, $nestingClose);
$bodytext = $this->encryptNestingDelimeter($bodytext, $nestingOpen, $nestingClose);
$bodytext = $this->unNest($bodytext, $nestingOpen, $nestingClose);
$bodytext = $this->trimAndRemoveCommentAndEmptyLine($bodytext, $nestingOpen, $nestingClose);
$bodytext = Support::decryptDoubleCurlyBraces($bodytext);
if (strpos($bodytext, NESTING_TOKEN_OPEN) !== false) {
......@@ -42,55 +48,125 @@ class BodytextParser {
* @return string
*/
private function trimAndRemoveCommentAndEmptyLine($bodytext) {
private function trimAndRemoveCommentAndEmptyLine($bodytext, &$nestingOpen, &$nestingClose) {
$data = array();
$src = explode(PHP_EOL, $bodytext);
if ($src === false) {
return '';
}
$firstLine = trim($src[0]);
foreach ($src as $row) {
$row = trim($row);
if ($row === '' || $row[0] === '#') {
continue;
}
$data[] = $row;
}
$this->setNestingToken($firstLine, $nestingOpen, $nestingClose);
return implode(PHP_EOL, $data);
}
/**
* Encrypt '{\n' and '}\n' by more complex token.
* Set the 'nesting token for this tt-conten record. Valid tokens are {}, <>, [], ().
* If the first line of bodytext is a comment line and the last char of that line is a valid token: set that one.
* If not: set {} as nesting token.
*
* @param $bodytext
* @return mixed
* Example:
* # Some nice text - no token found, take {}
* # ] - []
* # Powefull QFQ: < - <>
*
* @param $firstLine
* @param $nestingOpen
* @param $nestingClose
*/
private function encryptNestingDelimeter($bodytext) {
// Take care that a trailing '}' will be recognised: add '\n'
if (substr($bodytext, -1) === '}') {
$bodytext .= "\n";
private function setNestingToken($firstLine, &$nestingOpen, &$nestingClose) {
if ($nestingOpen !== '') {
return; // tokens already set or not bodytext: do not change.
}
$bodytext = str_replace("{\n", NESTING_TOKEN_OPEN, $bodytext);
$bodytext = str_replace("}\n", NESTING_TOKEN_CLOSE, $bodytext);
return $bodytext;
// Nothing defined: set default {}.
if ($firstLine === false || $firstLine === '' || $firstLine[0] !== '#') {
$nestingOpen = '{';
$nestingClose = '}';
return;
}
$pos = 0;
$tokenList = '{}<>[]()';
// Definition: first line of bodytext, has to be a comment line. If the last char is one of the valid token: set that one.
// Nothing found: set {}.
if ($firstLine[0] === '#') {
$token = substr($firstLine, -1);
$pos = strpos($tokenList, $token);
if ($pos === false) {
$pos = 0;
} else {
if ($pos % 2 === 1) {
$pos -= 1;
}
}
}
$nestingOpen = substr($tokenList, $pos, 1);
$nestingClose = substr($tokenList, $pos + 1, 1);
}
/**
* Join lines, which do not begin with '<level>.<keyword>[ ]='
* Join lines. Preservers Nesting.
*
* Iterates over all lines.
* Is a line a 'new line'?
* no: concat it to the last one.
* yes: flush the buffer, start a new 'new line'
*
* New Line Trigger:
* a: {
* b: }
* c: 20
* d: 20.30
*
* e: 5 {
* f: 5.10 {
*
* g: head =
* h: 10.20.head =
*
* c,d,e,f: ^\d+(\.\d+)*(\s*{)?$
* g,h: ^(\d+\.)*(sql|head)\s*=
*
* @param array $bodytextArray
* @return string
*/
private function joinLine($bodytext) {
private function joinLine($bodytext, $nestingOpen, $nestingClose) {
$data = array();
$bodytextArray = explode(PHP_EOL, $bodytext);
$nestingOpenRegexp = $nestingOpen;
if ($nestingOpen === '(' || $nestingOpen === '[') {
$nestingOpenRegexp = '\\' . $nestingOpen;
}
$full = '';
foreach ($bodytextArray as $row) {
// Valid 'new line' starts indicators: form, <level>, <level.sublevel>, <level>.<keyword>, {, <level> {, }
if ((1 === preg_match('/^\s*(\d*(\.)?)*\s*(head|althead|tail|sql|rbeg|rend|renr|rsep|fbeg|fend|fsep|' .
TYPO3_FORM . '|' . TYPO3_DEBUG_SHOW_BODY_TEXT . '|' . TYPO3_RECORD_ID . ') *=/', $row))
|| (1 === preg_match('/^\s*(\d*(\.)?)*\s*({|})\s*/', $row))
|| (1 === preg_match('/^\s*(\d+(\.)?)+/', $row))
// if ((1 === preg_match('/^\s*(\d*(\.)?)*\s*(' . TOKEN_VALID_LIST . ') *=/', $row))
// || (1 === preg_match('/^\s*(\d*(\.)?)*\s*(' . $nestingOpen . '|' . $nestingClose . ')/', $row))
// || (1 === preg_match('/^\s*(\d+(\.)?)+/', $row))
if (($row == $nestingOpen || $row == $nestingClose)
|| (1 === preg_match('/^\d+(\.\d+)*(\s*' . $nestingOpenRegexp . ')?$/', $row))
|| (1 === preg_match('/^(\d+\.)*(' . TOKEN_VALID_LIST . ')\s*=/', $row))
) {
// if there is already something: save this
......@@ -114,14 +190,50 @@ class BodytextParser {
return implode(PHP_EOL, $data);
}
//PREG_SPLIT_DELIM_CAPTURE
/**
* Encrypt $nestingOpen and $nestingClose by a more complex token. This makes it easy to search later for '}' or '{'
*
* Valid open (complete line): {, 10 {, 10.20 {
* Valid close (complete line): }
*
* @param $bodytext
* @return mixed
*/
private function encryptNestingDelimeter($bodytext, $nestingOpen, $nestingClose) {
if ($nestingOpen === '(' || $nestingOpen === '[') {
$nestingOpen = '\\' . $nestingOpen;
$nestingClose = '\\' . $nestingClose;
}
$bodytext = preg_replace('/^((\d+)(\.\d+)*\s*)?(' . $nestingOpen . ')$/m', '$1' . NESTING_TOKEN_OPEN, $bodytext);
$bodytext = preg_replace('/^' . $nestingClose . '$/m', '$1' . NESTING_TOKEN_CLOSE, $bodytext);
return $bodytext;
}
/**
* Unnest all level.
*
* Input:
* 10 {
* sql = SELECT
* 20.sql = INSERT ..
* 30 {
* sql = DELETE
* }
* }
*
* Output:
* 10.sql = SELECT
* 10.20.sql = INSERT
* 10.20.30.sql = DELETE
*
* @param $bodytext
* @return mixed|string
* @throws UserFormException
*/
private function unNest($bodytext) {
private function unNest($bodytext, $nestingOpen, $nestingClose) {
// Replace '\{' | '\}' by internal token. All remaining '}' | '{' means: 'nested'
// $bodytext = str_replace('\{', '#&[_#', $bodytext);
......@@ -135,7 +247,7 @@ class BodytextParser {
$posMatchOpen = strrpos(substr($result, 0, $posFirstClose), NESTING_TOKEN_OPEN);
if ($posMatchOpen === false) {
$result = $this->decryptNestingDelimeter($result);
$result = $this->decryptNestingDelimeter($result, $nestingOpen, $nestingClose);
throw new \qfq\UserFormException("Missing open delimiter: $result", ERROR_MISSING_OPEN_DELIMITER);
}
......@@ -164,8 +276,10 @@ class BodytextParser {
// Split nested content in single rows
$lines = explode(PHP_EOL, $match);
foreach ($lines as $line) {
if ($line !== '') {
$pre .= $level . '.' . $line . PHP_EOL;
}
}
$result = $pre . $post;
$posFirstClose = strpos($result, NESTING_TOKEN_CLOSE);
......@@ -184,10 +298,11 @@ class BodytextParser {
* @param $bodytext
* @return mixed
*/
private function decryptNestingDelimeter($bodytext) {
private function decryptNestingDelimeter($bodytext, $nestingOpen, $nestingClose) {
$bodytext = str_replace(NESTING_TOKEN_OPEN, "$nestingOpen\n", $bodytext);
$bodytext = str_replace(NESTING_TOKEN_CLOSE, "$nestingClose\n", $bodytext);
$bodytext = str_replace(NESTING_TOKEN_OPEN, "{\n", $bodytext);
$bodytext = str_replace(NESTING_TOKEN_CLOSE, "}\n", $bodytext);
return $bodytext;
}
......
......@@ -137,7 +137,7 @@ class BuildFormBootstrap extends AbstractBuildForm {
$toolTip = "Edit form" . PHP_EOL . PHP_EOL . OnArray::toString($this->store->getStore(STORE_SIP), ' = ', PHP_EOL, "'");
$url = $this->createFormEditUrl();
$buttonEditForm = $this->buildButtonAnchor('form-edit-button', $url, $toolTip, 'glyphicon-wrench');
$buttonEditForm = $this->buildButtonAnchor('form-edit-button', $url, $toolTip, GLYPH_ICON_TOOL);
}
// Button: Save
......@@ -145,10 +145,10 @@ class BuildFormBootstrap extends AbstractBuildForm {
$toolTip = 'Save';
if ($this->showDebugInfo) {
$toolTip .= PHP_EOL . "table = '" . $this->formSpec['tableName'] . "'" . PHP_EOL . "r = '" . $recordId . "'";
$toolTip .= PHP_EOL . "table = '" . $this->formSpec[F_TABLE_NAME] . "'" . PHP_EOL . "r = '" . $recordId . "'";
}
$buttonSave = $this->buildButtonCode('save-button', $toolTip, 'glyphicon-ok');
$buttonSave = $this->buildButtonCode('save-button', $toolTip, GLYPH_ICON_CHECK);
}
// Button: Close
......@@ -156,10 +156,10 @@ class BuildFormBootstrap extends AbstractBuildForm {
$toolTip = 'Close';
if ($this->showDebugInfo) {
$toolTip .= PHP_EOL . "table = '" . $this->formSpec['tableName'] . "'" . PHP_EOL . "r = '" . $recordId . "'";
$toolTip .= PHP_EOL . "table = '" . $this->formSpec[F_TABLE_NAME] . "'" . PHP_EOL . "r = '" . $recordId . "'";
}
$buttonClose = $this->buildButtonCode('close-button', 'Close', 'glyphicon-remove');
$buttonClose = $this->buildButtonCode('close-button', 'Close', GLYPH_ICON_CLOSE);
}
// Button: Delete
......@@ -167,11 +167,11 @@ class BuildFormBootstrap extends AbstractBuildForm {
$toolTip = 'Delete';
if ($this->showDebugInfo && $recordId > 0) {
$toolTip .= PHP_EOL . "table = '" . $this->formSpec['tableName'] . "'" . PHP_EOL . "r = '" . $recordId . "'";
$toolTip .= PHP_EOL . "table = '" . $this->formSpec[F_TABLE_NAME] . "'" . PHP_EOL . "r = '" . $recordId . "'";
}
$disabled = ($recordId > 0) ? '' : 'disabled';
$buttonDelete = $this->buildButtonCode('delete-button', $toolTip, 'glyphicon-trash', $disabled);
$buttonDelete = $this->buildButtonCode('delete-button', $toolTip, GLYPH_ICON_DELETE, $disabled);
}
// Button: New
......@@ -179,7 +179,7 @@ class BuildFormBootstrap extends AbstractBuildForm {
$toolTip = 'New';
$url = $this->deriveNewRecordUrlFromExistingSip($toolTip);
$buttonNew = $this->buildButtonAnchor('form-new-button', $url, $toolTip, 'glyphicon-plus');
$buttonNew = $this->buildButtonAnchor('form-new-button', $url, $toolTip, GLYPH_ICON_NEW);
}
$html = Support::wrapTag('<div class="btn-group" role="group">', $buttonEditForm);
......@@ -201,7 +201,7 @@ class BuildFormBootstrap extends AbstractBuildForm {
* @return string
*/
private function buildButtonAnchor($id, $url, $title, $icon, $disabled = '') {
return "<a href='$url' id='$id' class='btn btn-default navbar-btn $disabled' " . Support::doAttribute('title', $title) . "><span class='glyphicon $icon'></span></a>";
return "<a " . Support::doAttribute('href', $url) . " id='$id' class='btn btn-default navbar-btn $disabled' " . Support::doAttribute('title', $title) . "><span class='glyphicon $icon'></span></a>";
}
/**
......@@ -243,7 +243,7 @@ class BuildFormBootstrap extends AbstractBuildForm {
}
// Anker for pill navigation
$a = '<a href="#' . $this->createAnker($formElement['id']) . '" data-toggle="tab">' . $formElement['label'] . '</a>';
$a = '<a ' . Support::doAttribute('href', '#' . $this->createAnker($formElement['id'])) . ' data-toggle="tab">' . $formElement['label'] . '</a>';
if ($ii <= $maxVisiblePill) {
$pillButton .= '<li role="presentation" ' . $active . '>' . $a . '</li>';
......@@ -315,7 +315,7 @@ class BuildFormBootstrap extends AbstractBuildForm {
$tabId = $this->getTabId();
if (0 < ($recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP))) {
$deleteUrl = $this->createDeleteUrl($this->formSpec['tableName'], $recordId);
$deleteUrl = $this->createDeleteUrl($this->formSpec[F_TABLE_NAME], $recordId);
}
$actionUpload = FILE_ACTION . '=' . FILE_ACTION_UPLOAD;
......@@ -353,7 +353,7 @@ EOF;
* @param $value
* @return mixed
*/
public function buildPill(array $formElement, $htmlFormElementId, $value, &$json) {
public function buildPill(array $formElement, $htmlFormElementId, $value, array &$json) {
$html = '';
// save parent processed FE's
$tmpStore = $this->feSpecNative;
......