Evaluate.php 14.7 KB
Newer Older
Carsten  Rose's avatar
Carsten Rose committed
1
2
3
4
5
6
7
8
9
10
11
12
<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 1/12/16
 * Time: 4:36 PM
 */

namespace qfq;

use qfq;

13
14
require_once(__DIR__ . '/../core/store/Store.php');
require_once(__DIR__ . '/../core/database/Database.php');
15
require_once(__DIR__ . '/../core/typo3/FePassword.php');
16
require_once(__DIR__ . '/helper/Support.php');
17
require_once(__DIR__ . '/helper/OnString.php');
18
require_once(__DIR__ . '/helper/KeyValueStringParser.php');
19
require_once(__DIR__ . '/report/Link.php');
Carsten  Rose's avatar
Carsten Rose committed
20

21
const EVALUATE_DB_INDEX_DEFAULT = 0;
Carsten  Rose's avatar
Carsten Rose committed
22
23
24
25
/**
 * Class Evaluate
 * @package qfq
 */
Carsten  Rose's avatar
Carsten Rose committed
26
class Evaluate {
27
28
29
    /**
     * @var Store
     */
Carsten  Rose's avatar
Carsten Rose committed
30
    private $store = null;
31

32
    /**
33
     * @var Database[]
34
     */
35
    private $dbArray = array();
36

37
38
39
40
41
    /**
     * @var Link
     */
    private $link = null;

42
    private $dbIndex = EVALUATE_DB_INDEX_DEFAULT;
Carsten  Rose's avatar
Carsten Rose committed
43
44
45
46
    private $startDelimiter = '';
    private $startDelimiterLength = 0;
    private $endDelimiter = '';
    private $endDelimiterLength = 0;
47
48
    private $sqlKeywords = array('SELECT ', 'INSERT ', 'DELETE ', 'UPDATE ', 'SHOW ', 'REPLACE ', 'TRUNCATE ', 'DESCRIBE ', 'EXPLAIN ', 'SET ');

49
    private $escapeTypeDefault = '';
50

51

52
//    private $debugStack = array();
Carsten  Rose's avatar
Carsten Rose committed
53
54


55
56
    /**
     * @param \qfq\Store $store
Carsten  Rose's avatar
Carsten Rose committed
57
58
59
     * @param Database $db
     * @param string $startDelimiter
     * @param string $endDelimiter
60
     * @throws CodeException
Carsten  Rose's avatar
Carsten Rose committed
61
     * @throws UserFormException
62
     */
Carsten  Rose's avatar
Carsten Rose committed
63
64
    public function __construct(Store $store, Database $db, $startDelimiter = '{{', $endDelimiter = '}}') {
        $this->store = $store;
65
66

        $this->dbArray[EVALUATE_DB_INDEX_DEFAULT] = $db;
Carsten  Rose's avatar
Carsten Rose committed
67
68
69
70
        $this->startDelimiter = $startDelimiter;
        $this->startDelimiterLength = strlen($startDelimiter);
        $this->endDelimiter = $endDelimiter;
        $this->endDelimiterLength = strlen($endDelimiter);
71
        $this->escapeTypeDefault = $this->store->getVar(F_ESCAPE_TYPE_DEFAULT, STORE_SYSTEM);
72
73
74
        if (empty($this->escapeTypeDefault) || $this->escapeTypeDefault == TOKEN_ESCAPE_CONFIG) {
            $this->escapeTypeDefault = $this->store->getVar(SYSTEM_ESCAPE_TYPE_DEFAULT, STORE_SYSTEM);
        }
Carsten  Rose's avatar
Carsten Rose committed
75
76
77
    }

    /**
78
     * Evaluate a whole array or an array of arrays.
79
     *
Carsten  Rose's avatar
Carsten Rose committed
80
     * @param       $tokenArray
81
82
     * @param array $skip Optional Array with keynames, which will not be evaluated.
     * @param array $debugStack
Carsten  Rose's avatar
Carsten Rose committed
83
     *
84
     * @return array
Carsten  Rose's avatar
Carsten Rose committed
85
86
     * @throws CodeException
     * @throws DbException
87
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
88
     * @throws UserReportException
89
     */
90
    public function parseArray($tokenArray, array $skip = array(), &$debugStack = array()) {
91
92
        $arr = array();

93
        // In case there is an Element 'fillStoreVar', process that first.
94
        if (!empty($tokenArray[FE_FILL_STORE_VAR]) && is_string($tokenArray[FE_FILL_STORE_VAR])) {
95

96
97
98
            $arr = $this->parse($tokenArray[FE_FILL_STORE_VAR], ROW_REGULAR, 0, $debugStack);
            if (!empty($arr)) {
                $this->store::appendToStore($arr[0], STORE_VAR);
99
100
101
102
            }
            unset($tokenArray[FE_FILL_STORE_VAR]);
        }

103
        foreach ($tokenArray as $key => $value) {
104
105

            if (array_search($key, $skip) !== false) {
106
                $arr[$key] = $value;
107
108
109
                continue;
            }

110
            if (is_array($value)) {
111
                $arr[] = $this->parseArray($value, $skip);
112
            } else {
113
                $value = Support::handleEscapeSpaceComment($value);
114

115
                $arr[$key] = $this->parse($value, ROW_IMPLODE_ALL, 0, $debugStack);
116
            }
117
118
119
120
121
122
        }

        return $arr;
    }

    /**
Carsten  Rose's avatar
Carsten Rose committed
123
124
     * Recursive evaluation of 'line'. Constant string, Variables or SQL Query or all of them. All queries will be
     * fired. In case of an 'INSERT' statement, return the last_insert_id().
125
126
     *
     * Token to replace have to be enclosed by '{{' and '}}'
127
     *
128
     * @param string $line
129
     * @param string $sqlMode ROW_IMPLODE | ROW_REGULAR | ... - might be overwritten in $line by '{{!...'
130
     * @param int $recursion
Carsten  Rose's avatar
Carsten Rose committed
131
     *
132
133
     * @param array $debugStack
     * @param string $foundInStore
134
     * @return array|mixed|null|string
135
136
     * @throws CodeException
     * @throws DbException
137
     * @throws UserFormException
138
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
139
     */
140
    public function parse($line, $sqlMode = ROW_IMPLODE_ALL, $recursion = 0, &$debugStack = array(), &$foundInStore = '') {
141

142
        $flagTokenReplaced = false;
Carsten  Rose's avatar
Carsten Rose committed
143
144

        if ($recursion > 4) {
145
146
            throw new qfq\UserFormException(
                json_encode([ERROR_MESSAGE_TO_USER => 'Recursion too deep', ERROR_MESSAGE_SUPPORT => "Level: $recursion, Line: $line"]),
147
                ERROR_RECURSION_TOO_DEEP);
Carsten  Rose's avatar
Carsten Rose committed
148
149
        }

150
        $result = $line;
Carsten  Rose's avatar
Carsten Rose committed
151

152
        $debugIndent = str_repeat(' ', $recursion);
153
        $debugLocal[] = $debugIndent . "Parse: $result";
Carsten  Rose's avatar
Carsten Rose committed
154

155
        $posFirstClose = strpos($result, $this->endDelimiter);
156

157
158
        // Variables like 'fillStoreVar' might contain SQL statements. Put them in store in case a DB exception is thrown.
        $this->store->setVar(SYSTEM_SQL_RAW, $line, STORE_SYSTEM);
159

160
161
162
        while ($posFirstClose !== false) {

            $posMatchOpen = strrpos(substr($result, 0, $posFirstClose), $this->startDelimiter);
Carsten  Rose's avatar
Carsten Rose committed
163
            if ($posMatchOpen === false) {
164
                throw new UserFormException(
165
                    json_encode([ERROR_MESSAGE_TO_USER => 'Missing open delimiter', ERROR_MESSAGE_SUPPORT => "Text: $result"]),
166
                    ERROR_MISSING_OPEN_DELIMITER);
Carsten  Rose's avatar
Carsten Rose committed
167
168
            }

169
170
171
            $pre = substr($result, 0, $posMatchOpen);
            $post = substr($result, $posFirstClose + $this->endDelimiterLength);
            $match = substr($result, $posMatchOpen + $this->startDelimiterLength, $posFirstClose - $posMatchOpen - $this->startDelimiterLength);
Carsten  Rose's avatar
Carsten Rose committed
172

173
            $evaluated = $this->substitute($match, $foundInStore, $sqlMode);
174

175
176
177
            // newline
            $debugLocal[] = '';

178
            $debugLocal[] = $debugIndent . "Replace: $match";
Carsten  Rose's avatar
Carsten Rose committed
179

180
            if ($foundInStore === '') {
181
182

                // Encode the non replaceable part as preparation not to process again. Recode them at the end.
183
                $evaluated = Support::encryptDoubleCurlyBraces($this->startDelimiter . $match . $this->endDelimiter);
184
                $debugLocal[] = $debugIndent . "BY: <nothing found - not replaced>";
185

186
187
188
            } else {

                $flagTokenReplaced = true;
Carsten  Rose's avatar
Carsten Rose committed
189

190
191
192
                // If an array is returned, break everything and return this assoc array.
                if (is_array($evaluated)) {
                    $result = $evaluated;
193
                    $debugLocal[] = $debugIndent . "BY: array(" . count($result) . ")";
194
195
                    break;
                }
196

197
                $debugLocal[] = $debugIndent . "BY: $evaluated";
Carsten  Rose's avatar
Carsten Rose committed
198

199
200
                // More to substitute in the new evaluated result? Start recursion just with the new result..
                if (strpos($evaluated, $this->endDelimiter) !== false) {
201
                    $evaluated = $this->parse($evaluated, ROW_IMPLODE_ALL, $recursion + 1, $debugLocal, $foundInStore);
202
203
                }
            }
204
            $result = $pre . $evaluated . $post;
205

206
207
            $posFirstClose = strpos($result, $this->endDelimiter);
        }
Carsten  Rose's avatar
Carsten Rose committed
208

209
210
        $result = Support::decryptDoubleCurlyBraces($result);

211
        if ($flagTokenReplaced === true) {
212
213
214
            if (is_array($result)) {
                $str = "array(" . count($result) . ")";
            } else {
215
                $str = "$result";
216
217
            }
            $debugLocal[] = $debugIndent . "FINAL: " . $str;
218

219
            $debugStack = $debugLocal;
Carsten  Rose's avatar
Carsten Rose committed
220
221
        }

222
        return $result;
Carsten  Rose's avatar
Carsten Rose committed
223
224
    }

225
226
227
228
229
230
231
232
233
    /**
     * @param $arrToken
     * @param $dbIndex
     * @param $foundInStore
     * @return string
     * @throws CodeException
     * @throws UserFormException
     * @throws UserReportException
     */
234
    private function inlineLink($arrToken, $dbIndex, &$foundInStore) {
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255

        $token = OnString::trimQuote(trim(implode(' ', $arrToken)));

        if ($this->link === null) {
            $this->link = new Link($this->store->getSipInstance(), $dbIndex);
        }

        $foundInStore = TOKEN_FOUND_AS_COLUMN;

        return $this->link->renderLink($token);
    }

    /**
     * @param $arrToken
     * @param $dbIndex
     * @param $foundInStore
     * @return string
     * @throws CodeException
     * @throws UserFormException
     * @throws UserReportException
     */
256
    private function inlineDataDndApi($arrToken, $dbIndex, &$foundInStore) {
257
258

        $token = OnString::trimQuote(trim(implode(' ', $arrToken)));
259
        if (empty($token)) {
260
261
262
263
264
265
266
267
268
            throw new UserReportException('Missing form name for "data-dnd-api"', ERROR_MISSING_FORM);
        }

        if ($this->link === null) {
            $this->link = new Link($this->store->getSipInstance(), $dbIndex);
        }

        $foundInStore = TOKEN_FOUND_AS_COLUMN;

269
        $s = $this->link->renderLink('U:' . $token . '|s|r:8');
270
271
272
273

        // Flag to add DND JS code later on.
        $this->store->setVar(SYSTEM_DRAG_AND_DROP_JS, 'true', STORE_SYSTEM);

274
        // data-dnd-api="typo3conf/ext/qfq/qfq/api/dragAndDrop.php?s={{'U:form=<form name>[&paramX=<any value>]|s|r:8' AS _link}}"
275
276
277
        return DND_DATA_DND_API . '="' . API_DIR . '/' . API_DRAG_AND_DROP_PHP . '?s=' . $s . '"';
    }

Carsten  Rose's avatar
Carsten Rose committed
278
279
    /**
     * Tries to substitute $token.
280
     * Token might be:
281
     *   a) a SQL statement to fire
282
     *   b) fetch from a store. Syntax: '\[db index\]form:[store]:[sanitize]:[escape]:[default]:[type violate message]', ''
283
     *
284
     * The token have to be *without* Delimiter '{{' , '}}'
285
     * If neither a) or b) match, return the token itself.
Carsten  Rose's avatar
Carsten Rose committed
286
     *
287
     * @param string $token
288
     * @param string $foundInStore Returns the name of the store where $key has been found. If $key is not found, return ''.
289
     * @param string $sqlMode - ROW_IMPLODE | ROW_REGULAR | ... - might be overwritten in $line by '{{!...'
Carsten  Rose's avatar
Carsten Rose committed
290
     *
291
     * @return array|null|string
Carsten  Rose's avatar
Carsten Rose committed
292
     * @throws CodeException
293
     * @throws DbException
294
     * @throws UserFormException
295
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
296
     */
297
    public function substitute($token, &$foundInStore = '', $sqlMode = ROW_IMPLODE_ALL) {
Carsten  Rose's avatar
Carsten Rose committed
298
299

        $token = trim($token);
300
301
302
        $dbIndex = $this->dbIndex;

        // Check if the $token starts with '[<int>]...' - yes: open the necessary database.
303
        if (strlen($token) > 2 && $token[0] === '[') {
304
            if ($token[2] !== ']') {
305
306
307
                throw new UserFormException(json_encode(
                    [ERROR_MESSAGE_TO_USER => "Missing token ']' on position 3",
                        ERROR_MESSAGE_SUPPORT => "In string '$token'"]), ERROR_TOKEN_MISSING);
308
309
310
311
312
313
314
315
            }
            $dbIndex = $token[1];
            $token = trim(substr($token, 3));

            if (empty($this->dbArray[$dbIndex])) {
                $this->dbArray[$dbIndex] = new Database($dbIndex);
            }
        }
Carsten  Rose's avatar
Carsten Rose committed
316

317
318
319
320
        if ($token === '') {
            return '';
        }

321
        // Get SQL column / row separated
Carsten  Rose's avatar
Carsten Rose committed
322
        if ($token[0] === '!') {
323
            $token = trim(substr($token, 1));
Carsten  Rose's avatar
Carsten Rose committed
324
325
326
            $sqlMode = ROW_REGULAR;
        }

327
        // Extract token: check if this is a 'variable', 'SQL Statement', 'link', 'data-dnd-api'
328
        $arrToken = explode(' ', $token);
329

330
331
        // Variable Type 'SQL Statement'
        if (in_array(strtoupper($arrToken[0] . ' '), $this->sqlKeywords)) {
332
            $foundInStore = TOKEN_FOUND_IN_STORE_QUERY;
Carsten  Rose's avatar
Carsten Rose committed
333

334
            return $this->dbArray[$dbIndex]->sql($token, $sqlMode);
Carsten  Rose's avatar
Carsten Rose committed
335
336
        }

337
        // Variable Type '... AS _link', '... as data-dnd-api'
338
339
340
341
342
        $countToken = count($arrToken);
        if ($countToken > 2 && strcasecmp($arrToken[$countToken - 2], 'as') == 0) {

            $type = OnString::stripFirstCharIf('_', $arrToken[$countToken - 1]);

343
344
            array_pop($arrToken); // remove 'link' | 'data-dnd-api'
            array_pop($arrToken); // remove 'as'
Carsten  Rose's avatar
Carsten Rose committed
345

346
347
            if (strcasecmp($type, COLUMN_LINK) == 0) {
                return ($this->inlineLink($arrToken, $dbIndex, $foundInStore));
348
            }
349

350
351
            if ($type == DND_DATA_DND_API) {
                return ($this->inlineDataDndApi($arrToken, $dbIndex, $foundInStore));
352
353
354
355

            }
        }

356
357
        // explode for: <key>:<store priority>:<sanitize class>:<escape>:<default>:<type violate message>
        $arrToken = array_merge(KeyValueStringParser::explodeEscape(':', $token, 6), [null, null, null, null, null, null]);
358

359
        $escapeTypes = (empty($arrToken[3])) ? $this->escapeTypeDefault : $arrToken[3];
360
        $typeMessageViolate = ($arrToken[5] === null || $arrToken[5] === '') ? SANITIZE_TYPE_MESSAGE_VIOLATE_CLASS : $arrToken[5];
Carsten  Rose's avatar
Carsten Rose committed
361
362

        // search for value in stores
363
        $value = $this->store->getVar($arrToken[0], $arrToken[1], $arrToken[2], $foundInStore, $typeMessageViolate);
Carsten  Rose's avatar
Carsten Rose committed
364

365
366
        // escape ticks
        if (is_string($value)) {
367
            // Process all escape requests in the given order.
368
            for ($ii = 0; $ii < strlen($escapeTypes); $ii++) {
369
                $escape = $escapeTypes[$ii];
370
371
372
                if ($escape == TOKEN_ESCAPE_CONFIG) {
                    $escape = $this->escapeTypeDefault;
                }
373
374
375
376
377
378
379
                switch ($escape) {
                    case TOKEN_ESCAPE_SINGLE_TICK:
                        $value = str_replace("'", "\\'", $value);
                        break;
                    case TOKEN_ESCAPE_DOUBLE_TICK:
                        $value = str_replace('"', '\\"', $value);
                        break;
380
381
382
                    case TOKEN_ESCAPE_COLON:
                        $value = str_replace(':', '\\:', $value);
                        break;
383
                    case TOKEN_ESCAPE_LDAP_FILTER:
384
385
                        $value = Support::ldap_escape($value, null, LDAP_ESCAPE_FILTER);
                        break;
386
                    case TOKEN_ESCAPE_LDAP_DN:
387
388
                        $value = Support::ldap_escape($value, null, LDAP_ESCAPE_DN);
                        break;
389
                    case TOKEN_ESCAPE_MYSQL:
390
                        $value = $this->dbArray[$dbIndex]->realEscapeString($value);
391
392
393
                        break;
                    case TOKEN_ESCAPE_NONE: // do nothing
                        break;
394
395
396
397
                    case TOKEN_ESCAPE_PASSWORD_T3FE:
                        $fePassword = new FePassword();
                        $value = $fePassword->getHash($value);
                        break;
398
                    default:
399
                        throw new UserFormException("Unknown escape qualifier: $escape", UNKNOWN_TYPE);
400
401
                        break;
                }
402
403
404
            }
        }

405
        // Not found and a default is given: take the default.
406
        if ($foundInStore == '' && !empty($arrToken[4])) {
407
            $foundInStore = TOKEN_FOUND_AS_DEFAULT;
408
            $value = str_replace('\\:', ':', $arrToken[4]);
409
        }
410
411

        return $value;
Carsten  Rose's avatar
Carsten Rose committed
412
    }
413

414
415
416
    /**
     * @return string
     */
417
418
419
//    public function getDebug() {
//        return '<pre>' . implode("\n", $this->debugStack) . '</pre>';
//    }
420
}