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

namespace qfq;

use qfq;

require_once(__DIR__ . '/../qfq/store/Store.php');
14
require_once(__DIR__ . '/../qfq/database/Database.php');
15
require_once(__DIR__ . '/helper/Support.php');
16
17
require_once(__DIR__ . '/helper/OnString.php');
require_once(__DIR__ . '/report/Link.php');
Carsten  Rose's avatar
Carsten Rose committed
18

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

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

35
36
37
38
39
    /**
     * @var Link
     */
    private $link = null;

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

48

49
//    private $debugStack = array();
Carsten  Rose's avatar
Carsten Rose committed
50
51


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

        $this->dbArray[EVALUATE_DB_INDEX_DEFAULT] = $db;
Carsten  Rose's avatar
Carsten Rose committed
64
65
66
67
        $this->startDelimiter = $startDelimiter;
        $this->startDelimiterLength = strlen($startDelimiter);
        $this->endDelimiter = $endDelimiter;
        $this->endDelimiterLength = strlen($endDelimiter);
68
        $this->escapeTypeDefault = $this->store->getVar(F_ESCAPE_TYPE_DEFAULT, STORE_SYSTEM);
69
70
71
        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
72
73
74
    }

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

        foreach ($tokenArray as $key => $value) {
91
92

            if (array_search($key, $skip) !== false) {
93
                $arr[$key] = $value;
94
95
96
                continue;
            }

97
            if (is_array($value)) {
98
                $arr[] = $this->parseArray($value, $skip);
99
            } else {
100
                $value = Support::handleEscapeSpaceComment($value);
101

102
                $arr[$key] = $this->parse($value, 0, $debugStack);
103
            }
104
105
106
107
108
109
        }

        return $arr;
    }

    /**
Carsten  Rose's avatar
Carsten Rose committed
110
111
     * 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().
112
113
     *
     * Token to replace have to be enclosed by '{{' and '}}'
114
     *
Carsten  Rose's avatar
Carsten Rose committed
115
     * @param     $line
116
     * @param int $recursion
Carsten  Rose's avatar
Carsten Rose committed
117
     *
118
119
     * @param array $debugStack
     * @param string $foundInStore
120
     * @return array|mixed|null|string
121
122
     * @throws CodeException
     * @throws DbException
123
     * @throws UserFormException
124
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
125
     */
126
    public function parse($line, $recursion = 0, &$debugStack = array(), &$foundInStore = '') {
127
        $flagTokenReplaced = false;
Carsten  Rose's avatar
Carsten Rose committed
128
129

        if ($recursion > 4) {
130
            throw new qfq\UserFormException("Recursion too deep ($recursion). Line: $line", ERROR_RECURSION_TOO_DEEP);
Carsten  Rose's avatar
Carsten Rose committed
131
132
        }

133
        $result = $line;
Carsten  Rose's avatar
Carsten Rose committed
134

135
        $debugIndent = str_repeat(' ', $recursion);
136
        $debugLocal[] = $debugIndent . "PARSE: $result";
Carsten  Rose's avatar
Carsten Rose committed
137

138
        $posFirstClose = strpos($result, $this->endDelimiter);
139

140
//        $this->store->setVar(SYSTEM_SQL_RAW, $line, STORE_SYSTEM);
141

142
143
144
        while ($posFirstClose !== false) {

            $posMatchOpen = strrpos(substr($result, 0, $posFirstClose), $this->startDelimiter);
Carsten  Rose's avatar
Carsten Rose committed
145
            if ($posMatchOpen === false) {
Carsten  Rose's avatar
Carsten Rose committed
146
                throw new UserFormException("Missing open delimiter: $result", ERROR_MISSING_OPEN_DELIMITER);
Carsten  Rose's avatar
Carsten Rose committed
147
148
            }

149
150
151
            $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
152

153
            $evaluated = $this->substitute($match, $foundInStore);
154

155
156
157
158
            // newline
            $debugLocal[] = '';

            $debugLocal[] = $debugIndent . "REPLACE: $match";
Carsten  Rose's avatar
Carsten Rose committed
159

160
            if ($foundInStore === '') {
161
162

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

166
167
168
            } else {

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

170
171
172
                // If an array is returned, break everything and return this assoc array.
                if (is_array($evaluated)) {
                    $result = $evaluated;
173
                    $debugLocal[] = $debugIndent . "BY: array(" . count($result) . ")";
174
175
                    break;
                }
176

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

179
180
181
182
183
                // More to substitute in the new evaluated result? Start recursion just with the new result..
                if (strpos($evaluated, $this->endDelimiter) !== false) {
                    $evaluated = $this->parse($evaluated, $recursion + 1, $debugLocal, $foundInStore);
                }
            }
184
            $result = $pre . $evaluated . $post;
185

186
187
            $posFirstClose = strpos($result, $this->endDelimiter);
        }
Carsten  Rose's avatar
Carsten Rose committed
188

189
190
        $result = Support::decryptDoubleCurlyBraces($result);

191
        if ($flagTokenReplaced === true) {
192
193
            $debugLocal[] = $debugIndent . "FINAL: " . is_array($result) ? "array(" . count($result) . ")" : "$result";

194
            $debugStack = $debugLocal;
Carsten  Rose's avatar
Carsten Rose committed
195
196
        }

197
        return $result;
Carsten  Rose's avatar
Carsten Rose committed
198
199
200
201
    }

    /**
     * Tries to substitute $token.
202
     * Token might be:
203
     *   a) a SQL statement to fire
204
     *   b) fetch from a store. Syntax: 'form', 'form:C', 'form:SC0', 'form:S:alnumx', 'form:F:all:s','form:F:all:s:default'
205
     *
206
     * The token have to be _without_ Delimiter '{{' , '}}'
207
     * If neither a) or b) match, return the token itself.
Carsten  Rose's avatar
Carsten Rose committed
208
     *
209
     * @param string $token
Carsten  Rose's avatar
Carsten Rose committed
210
211
212
     * @param string $foundInStore Returns the name of the store where $key has been found. If $key is not found,
     *                             return ''.
     *
213
     * @return array|null|string
Carsten  Rose's avatar
Carsten Rose committed
214
     * @throws CodeException
215
     * @throws DbException
216
     * @throws UserFormException
217
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
218
     */
219
    public function substitute($token, &$foundInStore = '') {
Carsten  Rose's avatar
Carsten Rose committed
220
221
222
        $sqlMode = ROW_IMPLODE_ALL;

        $token = trim($token);
223
224
225
        $dbIndex = $this->dbIndex;

        // Check if the $token starts with '[<int>]...' - yes: open the necessary database.
226
        if (strlen($token) > 2 && $token[0] === '[') {
227
228
229
230
231
232
233
234
235
236
            if ($token[2] !== ']') {
                throw new UserFormException("Missing token ']' in '$token' on position 3", ERROR_TOKEN_MISSING);
            }
            $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
237

238
239
240
241
        if ($token === '') {
            return '';
        }

Carsten  Rose's avatar
Carsten Rose committed
242
        if ($token[0] === '!') {
243
            $token = trim(substr($token, 1));
Carsten  Rose's avatar
Carsten Rose committed
244
245
246
            $sqlMode = ROW_REGULAR;
        }

247
248
        // Extract token: check if this is a SQL Statement
        $arrToken = explode(' ', $token);
249

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

254
            return $this->dbArray[$dbIndex]->sql($token, $sqlMode);
Carsten  Rose's avatar
Carsten Rose committed
255
256
        }

257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
        // Variable Type '... AS LINK'
        $countToken = count($arrToken);
        if ($countToken > 2 && strcasecmp($arrToken[$countToken - 2], 'as') == 0) {

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

            if (strcasecmp($type, 'link') == 0) {

                $str = OnString::trimQuote(substr($token, 0, strlen($token) - 8));  // strlen('_as_link')=8

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

                $foundInStore = TOKEN_FOUND_AS_COLUMN;

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

277
        // explode for: <key>:<store priority>:<sanitize class>:<escape>:<default>
278
279
        $arrToken = explode(':', $token, 5);
        $arrToken = array_merge($arrToken, [null, null, null, null, null]); // fake isset()
280

281
        $escapeTypes = (empty($arrToken[3])) ? $this->escapeTypeDefault : $arrToken[3];
Carsten  Rose's avatar
Carsten Rose committed
282
283

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

286
287
        // escape ticks
        if (is_string($value)) {
288
            // Process all escape requests in the given order.
289
            for ($ii = 0; $ii < strlen($escapeTypes); $ii++) {
290
                $escape = $escapeTypes[$ii];
291
292
293
                if ($escape == TOKEN_ESCAPE_CONFIG) {
                    $escape = $this->escapeTypeDefault;
                }
294
295
296
297
298
299
300
                switch ($escape) {
                    case TOKEN_ESCAPE_SINGLE_TICK:
                        $value = str_replace("'", "\\'", $value);
                        break;
                    case TOKEN_ESCAPE_DOUBLE_TICK:
                        $value = str_replace('"', '\\"', $value);
                        break;
301
                    case TOKEN_ESCAPE_LDAP_FILTER:
302
303
                        $value = Support::ldap_escape($value, null, LDAP_ESCAPE_FILTER);
                        break;
304
                    case TOKEN_ESCAPE_LDAP_DN:
305
306
                        $value = Support::ldap_escape($value, null, LDAP_ESCAPE_DN);
                        break;
307
                    case TOKEN_ESCAPE_MYSQL:
308
                        $value = $this->dbArray[$dbIndex]->realEscapeString($value);
309
310
311
                        break;
                    case TOKEN_ESCAPE_NONE: // do nothing
                        break;
312
                    default:
313
                        throw new UserFormException("Unknown Escape qualifier: $escape", UNKNOWN_TYPE);
314
315
                        break;
                }
316
317
318
            }
        }

319
        // Not found and a default is given: take the default.
320
        if ($foundInStore == '' && !empty($arrToken[4])) {
321
            $foundInStore = TOKEN_FOUND_AS_DEFAULT;
322
            $value = str_replace('\\:', ':', $arrToken[4]);
323
        }
324
325

        return $value;
Carsten  Rose's avatar
Carsten Rose committed
326
    }
327

328
329
330
    /**
     * @return string
     */
331
332
333
//    public function getDebug() {
//        return '<pre>' . implode("\n", $this->debugStack) . '</pre>';
//    }
334
}