Support.php 50.6 KB
Newer Older
1
2
3
4
5
6
7
8
<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 1/28/16
 * Time: 8:05 AM
 */

Marc Egger's avatar
Marc Egger committed
9
namespace IMATHUZH\Qfq\Core\Helper;
10

11

Marc Egger's avatar
Marc Egger committed
12
use IMATHUZH\Qfq\Core\Store\Store;
13

14
15
16
const LONG_CURLY_OPEN = '#/+open+/#';
const LONG_CURLY_CLOSE = '#/+close+/#';

17
18
19
20
/**
 * Class Support
 * @package qfq
 */
21
22
class Support {

23
24
25
26
27
    /**
     * @var Store
     */
    private static $store = null;

28
    /**
Carsten  Rose's avatar
Carsten Rose committed
29
30
     * @param array $queryArray Empty or prefilled assoc array with url parameter
     * @param string $mode PARAM_T3_NO_ID, PARAM_T3_ALL
Marc Egger's avatar
Marc Egger committed
31
32
33
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
34
     */
35
    public static function appendTypo3ParameterToArray(array &$queryArray, $mode = PARAM_T3_ALL) {
36

37
        self::$store = Store::getInstance();
38

39
        if ($mode === PARAM_T3_ALL) {
40
            $queryArray['id'] = self::$store->getVar(TYPO3_PAGE_ID, STORE_TYPO3);
41
42
43
        }

        // TYPE
44
        $tmp = self::$store->getVar(TYPO3_PAGE_TYPE, STORE_TYPO3);
45
46
47
        if ($tmp !== false && $tmp != 0) {
            $queryArray['type'] = $tmp;
        }
48

49
        // Language
50
        $tmp = self::$store->getVar(TYPO3_PAGE_LANGUAGE, STORE_TYPO3);
51
52
53
        if ($tmp !== false && $tmp != 0) {
            $queryArray['L'] = $tmp;
        }
54
55
    }

56
57
58
59
60
61
62
63
64
65
    /**
     * Check $uri if 'type' and/or 'L' is missing. If yes, append it.
     *
     * Do nothing if $uri is empty.
     * Do nothing if $uri is absolute (=starts with http)
     *
     * @param $uri
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
66
     * @throws \UserReportException
67
68
69
     */
    public static function appendTypo3ParameterToUrl($uri) {

70
71
72
        self::$store = Store::getInstance();


73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
        if ($uri == '') {
            return '';
        }

        // Full URL: no processing
        $protocol = explode(':', $uri, 2);
        if ($protocol[0] == 'http' || $protocol[0] == 'https') {
            return $uri;
        }

        $arr = KeyValueStringParser::parse($uri, '=', '&');
        $type = self::$store->getVar(TYPO3_PAGE_TYPE, STORE_TYPO3);
        $language = self::$store->getVar(TYPO3_PAGE_LANGUAGE, STORE_TYPO3);

        if ($type != 0 && $type !== false && !isset($arr['type'])) {
            $uri .= '&type=' . $type;
        }

        if ($language != 0 && $language !== false && !isset($arr['L'])) {
            $uri .= '&L=' . $language;
        }

        return $uri;
    }

98
99
100
101
    /**
     * Build the form log filename. Depending on $formLog=FORM_LOG_ALL one file for all BE_USER, or $formLog=FORM_LOG_SESSION one file per BE_USER.
     *
     * @param $formName
102
     * @param $formLogMode
103
     * @return string
Marc Egger's avatar
Marc Egger committed
104
105
106
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
107
     */
108
    public static function getFormLogFileName($formName, $formLogMode) {
109

110
        self::$store = Store::getInstance();
111

112
        switch ($formLogMode) {
113
114
115
116
            case FORM_LOG_ALL:
                $perBeSession = '';
                break;
            case FORM_LOG_SESSION:
117
                $perBeSession = self::$store->getVar(TYPO3_BE_USER, STORE_TYPO3) . '.';
118
                if (empty($perBeSession)) {
Marc Egger's avatar
Marc Egger committed
119
                    throw new \UserFormException('formLog: no BE User logged in', ERROR_NO_BE_USER_LOGGED);
120
                }
121
122
                break;
            default:
Marc Egger's avatar
Marc Egger committed
123
                throw new \CodeException('Unknown mode: ' . $formLogMode, ERROR_UNKNOWN_TOKEN);
124
125
        }

126
        $filename = SYSTEM_FILEADMIN_PROTECTED_LOG . '/' . $formName . "." . $perBeSession . "log";
127
128

        return sanitize::safeFilename($filename, false, true);
129
130
    }

131
    /**
132
133
     * Builds a urlencoded query string of an assoc array.
     *
134
     * @param array $queryArray
Carsten  Rose's avatar
Carsten Rose committed
135
     *
136
     * @return string Querystring (e.g.: id=23&type=99
137
138
139
140
141
142
143
144
145
146
147
148
149
150
     */
    public static function arrayToQueryString(array $queryArray) {
        $items = array();

        foreach ($queryArray as $key => $value) {
            if (is_int($key)) {
                $items[] = urlencode($value);
            } else {
                $items[] = $key . '=' . urlencode($value);
            }
        }

        return implode('&', $items);
    }
151
152

    /**
Carsten  Rose's avatar
Carsten Rose committed
153
154
     * Extract Tag(s) from $tag (eg: <div><input class="form-control">, might contain further attributes) and wrap it
     * around
Carsten  Rose's avatar
Carsten Rose committed
155
     * $value. If $flagOmitEmpty==true && $value=='': return ''.
156
     *
Carsten  Rose's avatar
Carsten Rose committed
157
158
     * @param string $tag
     * @param string $value
159
     * @param bool|false $omitIfValueEmpty
Carsten  Rose's avatar
Carsten Rose committed
160
     *
161
162
163
     * @return string
     */
    public static function wrapTag($tag, $value, $omitIfValueEmpty = false) {
164
165
166
167
168
169
170
171
172
173
174
175

        $tag = trim($tag);

        if ($tag == '' || ($omitIfValueEmpty && $value == "")) {
            return $value;
        }

        $tagArray = explode('>', $tag, 2);
        if (count($tagArray) > 1 && $tagArray[1] != '') {
            $value = self::wrapTag($tagArray[1], $value, $omitIfValueEmpty);
            $tag = $tagArray[0] . '>';
        }
176
177
178
179
180
181
182
183
184
185

        // a) <div class="container-fluid">, b) <label>
        $arr = explode(' ', $tag);

        $tagPlain = (count($arr) === 1) ? substr($arr[0], 1, strlen($arr[0]) - 2) : substr($arr[0], 1);
        $closing = '</' . $tagPlain . '>';

        return $tag . $value . $closing;
    }

186
187
188
189
190
    /**
     * @param $glyphIcon
     * @param string $value
     * @return string
     */
191
192
193
194
    public static function renderGlyphIcon($glyphIcon, $value = '') {
        return Support::wrapTag("<span class='" . GLYPH_ICON . " $glyphIcon'>", $value);
    }

195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
    /**
     * Removes '$tag' and closing $tag from $value, if they are the outermost.
     * E.g.  unWrapTag('<p>', '<p>Hello World</p>')    returns  'Hello World'
     *
     * @param string $tag
     * @param string $value
     *
     * @return string
     */
    public static function unWrapTag($tag, $value) {

        if ($tag == '' || $value == '') {
            return $value;
        }

        $lenTag = strlen($tag);
        $lenValue = strlen($value);

        if ($lenValue < $lenTag + $lenTag + 1) {
            return $value;
        }

        $closeTag = $tag[0] . '/' . substr($tag, 1);

        if (substr($value, 0, $lenTag) == $tag && substr($value, $lenValue - $lenTag - 1) == $closeTag) {
            $value = substr($value, $lenTag, $lenValue - $lenTag - $lenTag - 1);
        }

        return $value;
    }

226
227
228
229
230
    /**
     * Wraps some $inner fragment with a CSS styled $tooltipText . CSS is configured in 'Resources/Public/qfq-jqw.css'.
     *
     * Based on: http://www.w3schools.com/howto/howto_css_tooltip.asp
     *
231
     * @param string $htmlId
232
     * @param string $tooltipText
Carsten  Rose's avatar
Carsten Rose committed
233
     *
234
     * @return string
Marc Egger's avatar
Marc Egger committed
235
     * @throws \CodeException
236
237
238
239
240
241
     */
    public static function doTooltip($htmlId, $tooltipText) {

        return "<img " . self::doAttribute('id', $htmlId) . " src='" . GFX_INFO . "' title=\"" . htmlentities($tooltipText) . "\">";
    }

242
243
    /**
     * Format's an attribute: $type=$value. If $flagOmitEmpty==true && $value=='': return ''.
244
     * Escape double tick - assumes that attributes will always be enclosed by double ticks.
245
     * Add's a space at the end.
246
     *
Carsten  Rose's avatar
Carsten Rose committed
247
     * @param string $type
248
     * @param string|array $value
249
250
     * @param bool $flagOmitEmpty true|false
     * @param string $modeEscape ESCAPE_WITH_BACKSLASH | ESCAPE_WITH_HTML_QUOTE
Carsten  Rose's avatar
Carsten Rose committed
251
     *
Carsten  Rose's avatar
Carsten Rose committed
252
     *
253
     * @return string correctly formatted attribute. Space at the end.
Marc Egger's avatar
Marc Egger committed
254
     * @throws \CodeException
255
     */
256
    public static function doAttribute($type, $value, $flagOmitEmpty = true, $modeEscape = ESCAPE_WITH_HTML_QUOTE) {
257
258
259
260
261
262

        // several attributes might be given as an array - concat to a string
        if (is_array($value)) {
            $value = implode(' ', $value);
        }

263
        if ($flagOmitEmpty && trim($value) === "") {
264
            return '';
265
266
        }

267
        switch (strtolower($type)) {
268
269
            case 'size':
            case 'maxlength':
270
                // empty or '0' for attributes of type 'size' or 'maxlength' result in unsuable input elements: skip this.
271
272
273
274
                if ($value === '' || $value == 0) {
                    return '';
                }
                break;
275
276
277
278
279
            // Bad idea to do urlencode on this place: it will convert ?, &, ... which are necessary for a proper URL.
            // Instead the value of a parameter needs to encode. Unfortunately, it's too late on this place.
//            case 'href':
//                $value = urlencode($value);
//                break;
280
281
282
            default:
                break;
        }
283

284
285
        $value = self::escapeDoubleTick(trim($value), $modeEscape);

286
287
288
289
290
        return $type . '="' . $value . '" ';
    }

    /**
     * Escapes Double Ticks ("), which are not already escaped.
291
292
293
     * modeEscape: ESCAPE_WITH_BACKSLASH | ESCAPE_WITH_HTML_QUOTE
     *
     * TinyMCE: Encoding JS Attributes (keys & values) for TinyMCE needs to be encapsulated in '&quot;' instead of '\"'.
294
     *
Carsten  Rose's avatar
Carsten Rose committed
295
     * @param        $str
296
     * @param string $modeEscape
Carsten  Rose's avatar
Carsten Rose committed
297
     *
298
     * @return string
Marc Egger's avatar
Marc Egger committed
299
     * @throws \CodeException
300
     */
301
    public static function escapeDoubleTick($str, $modeEscape = ESCAPE_WITH_BACKSLASH) {
302
303
304
305
306
        $newStr = '';

        for ($ii = 0; $ii < strlen($str); $ii++) {
            if ($str[$ii] === '"') {
                if ($ii === 0 || $str[$ii - 1] != '\\') {
307
308
309
310
311
312
313
314
                    switch ($modeEscape) {
                        case ESCAPE_WITH_BACKSLASH:
                            $newStr .= '\\';
                            break;
                        case ESCAPE_WITH_HTML_QUOTE:
                            $newStr .= '&quot;';
                            continue 2;
                        default:
Marc Egger's avatar
Marc Egger committed
315
                            throw new \CodeException('Unknown modeEscape=' . $modeEscape, ERROR_UNKNOWN_ESCAPE_MODE);
316
                    }
317
318
319
320
321
322
                }
            }
            $newStr .= $str[$ii];
        }

        return $newStr;
323
324
    }

325
326
327
328
    /**
     * Format's an attribute and inserts them at the beginning of the $htmlTag:
     * If $flagOmitEmpty==true && $value=='': insert nothing
     *
Carsten  Rose's avatar
Carsten Rose committed
329
330
     * @param string $htmlTag with open and closing angle.
     * @param string $type
331
     * @param string|array $value
Carsten  Rose's avatar
Carsten Rose committed
332
333
     * @param bool $flagOmitEmpty
     * @param string $modeEscape
Carsten  Rose's avatar
Carsten Rose committed
334
     *
335
     * @return string correctly fomratted attribute. Space at the end.
Marc Egger's avatar
Marc Egger committed
336
     * @throws \CodeException
337
338
339
340
341
342
343
344
     */
    public static function insertAttribute($htmlTag, $type, $value, $flagOmitEmpty = true, $modeEscape = ESCAPE_WITH_BACKSLASH) {
        $htmlTag = trim($htmlTag);

        // e.g. '<div class=...' will be exploded to '<div' and 'class=...'
        $parts = explode(' ', $htmlTag, 2);
        if (count($parts) < 2) {
            if (strlen($htmlTag) < 3) {
Marc Egger's avatar
Marc Egger committed
345
                throw new \CodeException('HTML Token too short (<3 chars):' . $htmlTag, ERROR_HTML_TOKEN_TOO_SHORT);
346
347
348
349
350
351
352
353
354
355
356
            } else {
                $parts[0] = substr($htmlTag, 0, -1);
                $parts[1] = '>';
            }
        }

        $attr = self::doAttribute($type, $value, $flagOmitEmpty, $modeEscape);

        return $parts[0] . ' ' . $attr . $parts[1];
    }

357
    /**
358
     * Search for the parameter $needle in $haystack. The arguments has to be separated by ','.
359
     *
360
     * Returns false if not found, or index (starting with 0) of found place. Be careful: use unary operator to compare for 'false'
361
     *
Carsten  Rose's avatar
Carsten Rose committed
362
363
     * @param string $needle
     * @param string $haystack
Carsten  Rose's avatar
Carsten Rose committed
364
     *
365
     * @return boolean     true if found, else false
366
     */
367
368
369
370
    public static function findInSet($needle, $haystack) {
        $arr = explode(',', $haystack);

        return array_search($needle, $arr) !== false;
371
    }
372

373
    /**
374
     * Converts a dateTime String to the international format:
375
376
377
378
     * 1.2.79 > 1979-02-01 00:00:00
     * 01.02.13 3:24 >  1979-02-01 03:24:00
     * 1.2.1979 14:21:5 > 1979-02-01 14:21:05
     *
Carsten  Rose's avatar
Carsten Rose committed
379
     * @param string $dateTimeString
Carsten  Rose's avatar
Carsten Rose committed
380
     *
381
     * @return string
Marc Egger's avatar
Marc Egger committed
382
     * @throws \UserFormException
383
     */
384
    public static function dateTimeGermanToInternational($dateTimeString) {
385
386
387
        $dateRaw = '';
        $timeRaw = '';

388
389
390
391
392

//        const REGEXP_DATE_INT_ = '^\d{2,4}-\d{2}-\d{2}$';
//        const REGEXP_DATE_GER = '^\d{1,2}\.\d{1,2}\.\d{2}(\d{2})?$';
//        const REGEXP_TIME = '^\d{1,2}:\d{1,2}(:\d{1,2})?$';

393
394
395
396
397
398
        $tmpArr = explode(' ', $dateTimeString);
        switch (count($tmpArr)) {
            case 0:
                return '';

            case 1:
399
                if (strpos($tmpArr[0], ':') === false) {
400
                    $dateRaw = $tmpArr[0];
401
402
                } else {
                    $timeRaw = $tmpArr[0];
403
404
405
406
407
408
409
410
411
                }
                break;

            case 2:
                $dateRaw = $tmpArr[0];
                $timeRaw = $tmpArr[1];
                break;

            default:
Marc Egger's avatar
Marc Egger committed
412
                throw new \UserFormException('Date/time format not recognised.', ERROR_DATE_TIME_FORMAT_NOT_RECOGNISED);
413
414
415
                break;
        }

416
        if ($dateRaw === '' || $dateRaw === '0000-00-00' || $dateRaw === '00.00.0000') {
417
418
            $date = '0000-00-00';
        } else {
419
            // International format: YYYY-MM-DD
420
421

            if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateRaw) === 1) {
422
423
424
                $date = $dateRaw;

                // German format: 1.1.01 - 11.12.1234
425
            } elseif (preg_match('/^\d{1,2}\.\d{1,2}\.\d{2}(\d{2})?$/', $dateRaw) === 1) {
426
427
428
429
430
431
432
433
434
435
                $tmpArr = explode('.', $dateRaw);

                if ($tmpArr[2] < 70) {
                    $tmpArr[2] = 2000 + $tmpArr[2];
                } elseif ($tmpArr[2] < 100) {
                    $tmpArr[2] = 1900 + $tmpArr[2];
                }
                $date = sprintf("%04d-%02d-%02d", $tmpArr[2], $tmpArr[1], $tmpArr[0]);

            } else {
Marc Egger's avatar
Marc Egger committed
436
                throw new \UserFormException('Date/time format not recognised.', ERROR_DATE_TIME_FORMAT_NOT_RECOGNISED);
437
438
439
            }
        }

440
        if ($timeRaw === '' || $timeRaw === '00:00:00') {
441
442
            $time = '00:00:00';
        } else {
443
            if (preg_match('/^\d{1,2}:\d{1,2}(:\d{1,2})?$/', $timeRaw) !== 1) {
Marc Egger's avatar
Marc Egger committed
444
                throw new \UserFormException('Date/time format not recognised.', ERROR_DATE_TIME_FORMAT_NOT_RECOGNISED);
445
446
            }

447
448
449
450
451
452
453
454
455
            $tmpArr = explode(':', $timeRaw);
            switch (count($tmpArr)) {
                case 2:
                    $time = sprintf("%02d:%02d:00", $tmpArr[0], $tmpArr[1]);
                    break;
                case 3:
                    $time = sprintf("%02d:%02d:%02d", $tmpArr[0], $tmpArr[1], $tmpArr[2]);
                    break;
                default:
Marc Egger's avatar
Marc Egger committed
456
                    throw new \UserFormException('Date/time format not recognised.', ERROR_DATE_TIME_FORMAT_NOT_RECOGNISED);
457
458
459
460
461
462
                    break;
            }
        }

        return $date . ' ' . $time;
    }
463

464
465
466
    /**
     * @param string $type date | datetime | time
     * @param string $format FORMAT_DATE_INTERNATIONAL | FORMAT_DATE_GERMAN
467
     * @param string $timeIsOptional
Carsten  Rose's avatar
Carsten Rose committed
468
     *
469
     * @return string
Marc Egger's avatar
Marc Egger committed
470
     * @throws \UserFormException
471
     */
472
    public static function dateTimeRegexp($type, $format, $timeIsOptional = '0') {
473
474
475

        if ($format === FORMAT_DATE_GERMAN) {
            // yyyy-mm-dd | 0000-00-00
Carsten  Rose's avatar
Carsten Rose committed
476
            $date = '(([1-9]|0[1-9]|1[0-9]|2[0-9]|3[01])\.([1-9]|0[1-9]|1[012])\.([0-9]{4}|[0-9]{2})|00\.00\.(00){1,2})';
477
478
        } else {
            // FORMAT_DATE_INTERNATIONAL: [d]d.[m]m.[yy]yy | 00.00.0000
Carsten  Rose's avatar
Carsten Rose committed
479
            $date = '([0-9]{4}-([1-9]|0[1-9]|1[012])-([1-9]|0[1-9]|1[0-9]|2[0-9]|3[01])|0000-00-00)';
480
481
482
483
484
485
486
487
488
489
        }

        // hh:mm[:ss]
        $time = '(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){1,2}';

        switch ($type) {
            case 'date':
                $pattern = $date;
                break;
            case 'datetime':
490
491
492
493
                if ($timeIsOptional == '1')
                    $pattern = $date . '( ' . $time . ')?';
                else
                    $pattern = $date . ' ' . $time;
494
495
496
497
498
                break;
            case 'time':
                $pattern = $time;
                break;
            default:
Marc Egger's avatar
Marc Egger committed
499
                throw new \UserFormException("Unknown mode: '$type'", ERROR_UNKNOWN_MODE);
500
501
502
503
504
505
506
507
508
509
510
        }

        return '^' . $pattern . '$';
    }

    /**
     * Parses $dateTimeString. If a date is given, detect international or german format automatically.
     * Convert to $dateFormat. 'yy' and 'd' will be converted to 'yyyy' and 'dd'
     * Returned value will be 'date only', 'datetime' oder 'time only', depending on the input value.
     *
     * @param string $dateTimeString
Carsten  Rose's avatar
Carsten Rose committed
511
     * @param string $dateFormat FORMAT_DATE_INTERNATIONAL | FORMAT_DATE_GERMAN
Carsten  Rose's avatar
Carsten Rose committed
512
513
514
     * @param string $showZero
     * @param string $showTime
     * @param string $showSeconds
Carsten  Rose's avatar
Carsten Rose committed
515
     *
516
     * @return string
Marc Egger's avatar
Marc Egger committed
517
     * @throws \UserFormException
518
519
520
521
522
523
524
     */
    public static function convertDateTime($dateTimeString, $dateFormat, $showZero, $showTime, $showSeconds) {
        $givenDate = '';
        $givenTime = '';
        $newDate = '';
        $newTime = '';
        $delim = '';
Carsten  Rose's avatar
Carsten Rose committed
525
        $flagDateAndTime = false;
526
527
528
529
530
531
532
533
534

        $dateTimeString = trim($dateTimeString);


        switch ($dateTimeString) {
            case '':
            case '0':
            case '0000-00-00 00:00:00':
            case '0000-00-00':
535
536
            case '00.00.0000 00:00:00':
            case '00.00.0000':
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
            case '00:00:00':
                return (self::dateTimeZero($dateFormat, $showZero, $showTime, $showSeconds));
                break;

            default:
                break;
        }

        // Main convert
        $arr = explode(' ', $dateTimeString);
        switch (count($arr)) {
            case 1:
                if (strpos($dateTimeString, ':') === false) {
                    $givenDate = $dateTimeString;
                } else {
                    $givenTime = $dateTimeString;
                }
                break;

            case 2:
                $givenDate = $arr[0];
                $givenTime = $arr[1];
Carsten  Rose's avatar
Carsten Rose committed
559
                $flagDateAndTime = true;
560
561
562
563
564
            default:
        }

        // Date
        if ($givenDate != '') {
Carsten  Rose's avatar
Carsten Rose committed
565
            if ($givenDate == '0') {
566
                $givenDate = '0000-00-00';
Carsten  Rose's avatar
Carsten Rose committed
567
            }
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582

            $arr = self::splitDateToArray($givenDate);

            switch ($dateFormat) {
                case FORMAT_DATE_INTERNATIONAL:
                    $newDate = sprintf("%04d-%02d-%02d", $arr[0], $arr[1], $arr[2]);
                    break;
                case FORMAT_DATE_GERMAN:
                    $newDate = sprintf("%02d.%02d.%04d", $arr[2], $arr[1], $arr[0]);
                default:
            }
        }

        // Time
        if ($givenTime != '') {
Carsten  Rose's avatar
Carsten Rose committed
583
            if ($givenTime == '0') {
584
                $givenTime = '0:0:0';
Carsten  Rose's avatar
Carsten Rose committed
585
            }
586
587
588
589
590
591
592
593
594
595
596
597
598

            $arr = explode(':', $givenTime);
            if (count($arr) < 3) {
                $arr[2] = 0;
            }

            if ($showSeconds == 1) {
                $newTime = sprintf("%02d:%02d:%02d", $arr[0], $arr[1], $arr[2]);
            } else {
                $newTime = sprintf("%02d:%02d", $arr[0], $arr[1]);
            }
        }

Carsten  Rose's avatar
Carsten Rose committed
599
        if ($flagDateAndTime) {
600
601
602
603
604
605
606
607
608
609
            $delim = ' ';
        }

        return $newDate . $delim . $newTime;
    }

    /**
     * Returns a representation of 0 in a choosen variant.
     *
     * @param string $dateFormat FORMAT_DATE_INTERNATIONAL | FORMAT_DATE_GERMAN
610
     * @param string $showZero
Carsten  Rose's avatar
Carsten Rose committed
611
     * @param string $showTime '0' | '1'
612
     * @param string $showSeconds '0' | '1'
Carsten  Rose's avatar
Carsten Rose committed
613
     *
614
615
616
617
     * @return string
     */
    private static function dateTimeZero($dateFormat, $showZero, $showTime, $showSeconds) {

618
        if ($showZero != '1') {
619
            return '';
620
        }
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639

        // $dateFormat (INT/GER),  $showTime, $showSeconds
        $arr[0][0][0] = '0000-00-00';
        $arr[0][0][1] = '0000-00-00';
        $arr[0][1][0] = '0000-00-00 00:00';
        $arr[0][1][1] = '0000-00-00 00:00:00';
        $arr[1][0][0] = '00.00.0000';
        $arr[1][0][1] = '00.00.0000';
        $arr[1][1][0] = '00.00.0000 00:00';
        $arr[1][1][1] = '00.00.0000 00:00:00';

        $showFormat = ($dateFormat === FORMAT_DATE_INTERNATIONAL) ? 0 : 1;

        return $arr[$showFormat][$showTime][$showSeconds];
    }

    /**
     * Split date FORMAT_DATE_GERMAN | FORMAT_DATE_INTERNATIONAL to array with arr[0]=yyyy, arr[1]=mm, arr[2]=dd.
     *
Carsten  Rose's avatar
Carsten Rose committed
640
     * @param string $dateString
Carsten  Rose's avatar
Carsten Rose committed
641
     *
642
     * @return array
Marc Egger's avatar
Marc Egger committed
643
     * @throws \UserFormException
644
645
646
647
648
649
650
     */
    private static function splitDateToArray($dateString) {

        if (strpos($dateString, '-') === false) {
            // FORMAT_DATE_GERMAN
            $arr = explode('.', $dateString);
            if (count($arr) != 3) {
Marc Egger's avatar
Marc Egger committed
651
                throw new \UserFormException("Unexpected format for date: $dateString", ERROR_DATE_UNEXPECTED_FORMAT);
652
653
654
655
656
657
658
659
            }
            $tmp = $arr[0];
            $arr[0] = $arr[2];
            $arr[2] = $tmp;
        } else {
            // FORMAT_DATE_INTERNATIONAL
            $arr = explode('-', $dateString);
            if (count($arr) != 3) {
Marc Egger's avatar
Marc Egger committed
660
                throw new \UserFormException("Unexpected format for date: $dateString", ERROR_DATE_UNEXPECTED_FORMAT);
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
            }
        }

        // Adjust yy to yyyy. See https://dev.mysql.com/doc/refman/5.7/en/datetime.html > Dates containing two-digit year values ...
        if ($arr[0] < 100) {
            $add = ($arr[0] < 70) ? 2000 : 1900;
            $arr[0] = $add + $arr[0];
        }

        return $arr;
    }

    /**
     * Calculates the placeholder for date/dateTime/local input fields.
     *
     * @param array $formElement
Carsten  Rose's avatar
Carsten Rose committed
677
     *
678
     * @return string
Marc Egger's avatar
Marc Egger committed
679
     * @throws \UserFormException
680
681
682
683
     */
    public static function getDateTimePlaceholder(array $formElement) {

        $timePattern = ($formElement[FE_SHOW_SECONDS] == 1) ? 'hh:mm:ss' : 'hh:mm';
684
        switch ($formElement[FE_TYPE]) {
685
            case 'date':
686
                $placeholder = $formElement[FE_DATE_FORMAT];
687
688
                break;
            case 'datetime':
689
                $placeholder = $formElement[FE_DATE_FORMAT] . ' ' . $timePattern;
690
691
692
693
694
                break;
            case 'time':
                $placeholder = $timePattern;
                break;
            default:
Marc Egger's avatar
Marc Egger committed
695
                throw new \UserFormException("Unexpected Formelement type: '" . $formElement[FE_TYPE] . "'", ERROR_FORMELEMENT_TYPE);
696
697
698
699
700
701
        }

        return $placeholder;
    }


702
703
704
    /**
     * Encrypt curly braces by an uncommon string. Helps preventing unwished action on curly braces.
     *
Carsten  Rose's avatar
Carsten Rose committed
705
     * @param string $text
Carsten  Rose's avatar
Carsten Rose committed
706
     *
707
708
     * @return mixed
     */
709
    public static function encryptDoubleCurlyBraces($text) {
710
711
        $text = str_replace('{{', LONG_CURLY_OPEN, $text);
        $text = str_replace('}}', LONG_CURLY_CLOSE, $text);
712
713
714
715
716
717
718

        return $text;
    }

    /**
     * Decrypt curly braces by an uncommon string. Helps preventing unwished action on curly braces
     *
Carsten  Rose's avatar
Carsten Rose committed
719
     * @param string $text
Carsten  Rose's avatar
Carsten Rose committed
720
     *
721
722
723
     * @return mixed
     */
    public static function decryptDoubleCurlyBraces($text) {
724
725
726
727
728

        if (!is_string($text)) {
            return $text;
        }

729
730
        $text = str_replace(LONG_CURLY_OPEN, '{{', $text);
        $text = str_replace(LONG_CURLY_CLOSE, '}}', $text);
731
732
733
734

        return $text;
    }

735
736
    /**
     * @param int $length Length of the required hash string
Carsten  Rose's avatar
Carsten Rose committed
737
     *
738
739
740
741
742
743
744
745
746
747
748
749
     * @return string       A random alphanumeric hash
     */
    public static function randomAlphaNum($length) {
        $possible_characters = "abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        $string = "";
        while (strlen($string) < $length) {
            $string .= substr($possible_characters, rand() % (strlen($possible_characters)), 1);
        }

        return ($string);
    }

750
    /**
Carsten  Rose's avatar
Carsten Rose committed
751
752
     * Concatenate URL and Parameter. Depending of if there is a '?' in URL or not,  append the param with '?' or '&'..
     *
Carsten  Rose's avatar
Carsten Rose committed
753
     * @param string $url
754
     * @param string|array $param
Carsten  Rose's avatar
Carsten Rose committed
755
     *
756
757
758
     * @return string
     */
    public static function concatUrlParam($url, $param) {
759
760
761
762
763

        if (is_array($param)) {
            $param = implode('&', $param);
        }

764
        if ($param == '') {
765
            return $url;
766
        }
767

768
        $token = ((strpos($url, '?') === false) && (strpos($url, '&')) === false) ? '?' : '&';
Carsten  Rose's avatar
Carsten Rose committed
769

770
771
        return $url . $token . $param;
    }
772

773
774
775
776
777
778
779
780
781
782
    /**
     * Concatenate $host, $path and $query. A given $host will always append a '/' if none is given.
     * End of  $path, there is no assumption to append '/' - the user needs full control.
     * $query: trailing 'index.php' will be skipped.
     *
     * If HOST and PATH is combined in a var, pass the value as $hostOrPath, leave $host empty.
     *
     * @param string $host
     * @param string $hostOrPath
     * @param string $query
Carsten  Rose's avatar
Carsten Rose committed
783
     *
784
     * @return string
Marc Egger's avatar
Marc Egger committed
785
     * @throws \CodeException
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
     */
    public static function mergeUrlComponents($host, $hostOrPath, $query) {
        $url = '';


        if ($host != '' && substr($host, -1, 1) != '/') {
            $host .= '/';
        }

        if ($host != '' && substr($hostOrPath, 0, 1) == '/') {
            $hostOrPath = substr($hostOrPath, 1);
        }

        if ($host != '' || $hostOrPath != '') {
            $url = $host . $hostOrPath;
        }

        if (substr($query, 0, 9) == 'index.php') {
            $query = substr($query, 9);
        }

        if (false !== strpos(substr($query, 1), '?')) {
Marc Egger's avatar
Marc Egger committed
808
            throw new \CodeException('Found a "?" after the beginning of the query - this is forbidden', ERROR_BROKEN_PARAMETER);
809
810
811
812
813
814
815
816
817
818
        }

        if ($query != '') {
            $url = $url . '?' . $query;
            $url = str_replace('??', '?', $url);
        }

        return $url;
    }

819
820
821
822
    /**
     * Set Defaults for the current formElement.
     *
     * @param array $formElement
823
     * @param array $formSpec
824
     * @return array
825
     *
Marc Egger's avatar
Marc Egger committed
826
827
828
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
829
     */
830
    public static function setFeDefaults(array $formElement, array $formSpec = array()) {
831
832
833
834
835

        $store = Store::getInstance();

        // Some Defaults
        self::setIfNotSet($formElement, FE_SHOW_SECONDS, '0');
836
        self::setIfNotSet($formElement, FE_TIME_IS_OPTIONAL, '0');
837
        self::setIfNotSet($formElement, FE_SHOW_ZERO, '0');
838
        self::setIfNotSet($formElement, FE_HIDE_ZERO, '0');
839
840
        self::setIfNotSet($formElement, FE_DATE_FORMAT, $store->getVar(SYSTEM_DATE_FORMAT, STORE_SYSTEM));

841
842
843
        self::setIfNotSet($formElement, FE_HTML_BEFORE);
        self::setIfNotSet($formElement, FE_HTML_AFTER);

844
        self::setIfNotSet($formElement, FE_DATA_REFERENCE, ($formElement[FE_NAME] == '' ? $formElement[FE_ID] : $formElement[FE_NAME]));
845

Carsten  Rose's avatar
Carsten Rose committed
846
        self::setIfNotSet($formElement, FE_SUBRECORD_TABLE_CLASS, SUBRECORD_TABLE_CLASS_DEFAULT);
847

848
        if (isset($formSpec[F_BS_LABEL_COLUMNS])) {
849
850
851
852
853
854
            self::setIfNotSet($formElement, F_BS_LABEL_COLUMNS, $formSpec[F_BS_LABEL_COLUMNS], '');
            self::setIfNotSet($formElement, F_BS_INPUT_COLUMNS, $formSpec[F_BS_INPUT_COLUMNS], '');
            self::setIfNotSet($formElement, F_BS_NOTE_COLUMNS, $formSpec[F_BS_NOTE_COLUMNS], '');
        }

        if ($formElement[FE_MODE_SQL] != '') {
855
            $formElement[FE_MODE] = $formElement[FE_MODE_SQL];
856
        }
857

858
859
        if (isset($formSpec[F_MODE_GLOBAL])) {
            $formElement[FE_MODE] = self::applyFormModeToFormElement($formElement[FE_MODE], $formSpec[F_MODE_GLOBAL]);
860
861
        }

862
        // set typeAheadPedantic
863
        if (isset($formElement[FE_TYPEAHEAD_PEDANTIC]) && $formElement[FE_TYPEAHEAD_PEDANTIC] === '') {
864
865
866
867
868
869
            $formElement[FE_TYPEAHEAD_PEDANTIC] = '1'; // support legacy option of 'typeAheadPedantic' without a value
        }
        if (isset($formElement[FE_TYPEAHEAD_LDAP]) || isset($formElement[FE_TYPEAHEAD_SQL])) {
            self::setIfNotSet($formElement, FE_TYPEAHEAD_PEDANTIC, '1');
        }

870
871
        // Will be used to change dynamicUpdate behaviour
        if (isset($formElement[FE_WRAP_ROW_LABEL_INPUT_NOTE])) {
872
873
            $formElement[FE_FLAG_ROW_OPEN_TAG] = self::findInSet('row', $formElement[FE_WRAP_ROW_LABEL_INPUT_NOTE]);
            $formElement[FE_FLAG_ROW_CLOSE_TAG] = self::findInSet('/row', $formElement[FE_WRAP_ROW_LABEL_INPUT_NOTE]);
874
875
876
877
878
        } else {
            $formElement[FE_FLAG_ROW_OPEN_TAG] = true;
            $formElement[FE_FLAG_ROW_CLOSE_TAG] = false;
        }

879
880
        self::setIfNotSet($formElement, FE_INPUT_EXTRA_BUTTON_INFO_CLASS, $store->getVar(FE_INPUT_EXTRA_BUTTON_INFO_CLASS, STORE_SYSTEM));

881
        // For specific FE hard coded 'checkType'
882
883
884
885
        switch ($formElement[FE_TYPE]) {
            case FE_TYPE_IMAGE_CUT:
                $formElement[FE_CHECK_TYPE] = SANITIZE_ALLOW_ALLBUT;
                break;
886
887
888
            case FE_TYPE_ANNOTATE:
                $formElement[FE_CHECK_TYPE] = SANITIZE_ALLOW_ALL;
                break;
889
890
891
892
            default:
                break;
        }

893
894
        self::setIfNotSet($formElement, FE_MIN);
        self::setIfNotSet($formElement, FE_MAX);
895
896
897
898
        self::setIfNotSet($formElement, FE_DECIMAL_FORMAT);

        self::setIfNotSet($formElement, F_FE_DATA_PATTERN_ERROR);

899
900
        $typeSpec = $store->getVar($formElement[FE_NAME], STORE_TABLE_COLUMN_TYPES);
        self::adjustFeToColumnDefinition($formElement, $typeSpec);
901
902
903
904
905
906
907
908
909

        return $formElement;
    }

    /**
     * Adjusts several FE parameters using smart guesses based on the table column definition and other parameters.
     *
     * @param array $formElement
     * @param $typeSpec
Marc Egger's avatar
Marc Egger committed
910
     * @throws \UserFormException
911
     */
912
    public static function adjustFeToColumnDefinition(array &$formElement, $typeSpec) {
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936

        self::adjustMaxLength($formElement, $typeSpec);
        self::adjustDecimalFormat($formElement, $typeSpec);

        // Make educated guesses about the desired $min, $max, $checkType, and $inputType

        // $typeSpec = 'tinyint(3) UNSIGNED NOT NULL' | 'int(11) NOT NULL'
        $arr = explode(' ', $typeSpec, 2);
        if (empty($arr[1])) {
            $sign = 'signed';
        } else {
            $arr = explode(' ', $arr[1], 2);
            $sign = $arr[0] == 'unsigned' ? 'unsigned' : 'signed';
        }

        $arr = explode('(', $typeSpec, 2);
        $token = $arr[0];

        # s: signed, u: unsigned.
        # s-min, s-max, s-checktype, u-min, u-max, u-checktype
        $control = [
            'tinyint' => [-128, 127, SANITIZE_ALLOW_NUMERICAL, 0, 255, SANITIZE_ALLOW_DIGIT],
            'smallint' => [-32768, 32767, SANITIZE_ALLOW_NUMERICAL, 0, 65535, SANITIZE_ALLOW_DIGIT],
            'mediumint' => [-8388608, 8388607, SANITIZE_ALLOW_NUMERICAL, 0, 16777215, SANITIZE_ALLOW_DIGIT],
937
            'int' => [-2147483648, 2147483647, SANITIZE_ALLOW_NUMERICAL, 0, 4294967295, SANITIZE_ALLOW_DIGIT],
938
939
940
941
942
            'bigint' => [-9223372036854775808, 9223372036854775807, SANITIZE_ALLOW_NUMERICAL, 0, 18446744073709551615, SANITIZE_ALLOW_DIGIT],
        ];

        $min = '';
        $max = '';
943
        $checkType = SANITIZE_ALLOW_ALNUMX;
944
        $inputType = '';
945
        $isANumber = true;
946

947
948
949
950
951
952
953
954
955
956
957
958
959
        switch ($formElement[FE_TYPE]) {
            case FE_TYPE_PASSWORD:
            case FE_TYPE_NOTE:
                $checkType = SANITIZE_ALLOW_ALL;
                break;

            case FE_TYPE_EDITOR:
            case FE_TYPE_TEXT:
                if ($formElement[FE_ENCODE] === FE_ENCODE_SPECIALCHAR)
                    $checkType = SANITIZE_ALLOW_ALL;
                break;
        }

960
961
962
963
964
965
        switch ($token) {
            case 'tinyint':
            case 'smallint':
            case 'mediumint':
            case 'int':
            case 'bigint':
966
                $inputType = HTML_INPUT_TYPE_NUMBER;
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
                $arr = $control[$token];
                if ($sign == 'signed') {
                    $min = $arr[0];
                    $max = $arr[1];
                    $checkType = $arr[2];
                } else {
                    $min = $arr[3];
                    $max = $arr[4];
                    $checkType = $arr[5];
                }
                break;

            case 'decimal':
            case 'float':
            case 'double':
                $checkType = SANITIZE_ALLOW_NUMERICAL;
                break;

            case 'bit':
986
                $inputType = HTML_INPUT_TYPE_NUMBER;
987
988
                $checkType = SANITIZE_ALLOW_DIGIT;
                break;
989
990
991
992

            default:
                $isANumber = false;
                break;
993
994
        }

995
        // Numbers don't need a maxLength because they are being handled by min/max and/or decimalFormat
996
        if ($isANumber) {
997
            $formElement[FE_MAX_LENGTH] = '';
998
        }
999

1000
        if (!empty($formElement[FE_TYPEAHEAD_SQL]) || !empty($formElement[FE_TYPEAHEAD_LDAP])) {