Link.php 69 KB
Newer Older
Carsten  Rose's avatar
Carsten Rose committed
1
<?php
2
3
4
/***************************************************************
 *  Copyright notice
 *
Carsten  Rose's avatar
Carsten Rose committed
5
 *  (c) 2010 Glowbase GmbH
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 *
 *  This script is part of the TYPO3 project. The TYPO3 project is
 *  free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  The GNU General Public License can be found at
 *  http://www.gnu.org/copyleft/gpl.html.
 *
 *  This script is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  This copyright notice MUST APPEAR in all copies of the script!
 ***************************************************************/

Marc Egger's avatar
Marc Egger committed
24
namespace IMATHUZH\Qfq\Core\Report;
25

26
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
Marc Egger's avatar
Marc Egger committed
27
use IMATHUZH\Qfq\Core\Helper\OnArray;
28
use IMATHUZH\Qfq\Core\Helper\Path;
29
use IMATHUZH\Qfq\Core\Helper\Sanitize;
Marc Egger's avatar
Marc Egger committed
30
31
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Helper\Token;
32
33
use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Store\Store;
34
35
36

/*
 * a:AltText
37
 * A:Attribute
38
 * b:bootstrap [0|1|<button>]
39
 * B:bullet
40
 * c:class  [n|i|e|<class>]
41
42
43
 * C:checkbox    [name]
 * d:download
 * D:delete
44
45
 * e:encryption 0|1
 * E:edit
46
 * f:
47
48
49
 * F:File
 * g:target
 * G:Glyph
50
 * h:
51
 * H:Help
Carsten  Rose's avatar
Carsten Rose committed
52
 * i:icon (Font Awesome, t)
53
 * I:information
54
55
56
57
58
59
 * j:
 * J:
 * k:
 * K:
 * l:
 * L:
60
61
 * m:mailto
 * M:Mode
62
 * n:GET/POST Rest Call
63
64
 * N:new
 * o:ToolTip
65
 * O:Monitor
66
 * p:page
67
 * P:picture       [file]
68
 * q:question  <text>
69
 * Q:
70
71
 * r:render
 * R:right
Carsten  Rose's avatar
Carsten Rose committed
72
 * s:sip
73
74
75
76
77
 * S:Show
 * t:text
 * T:Thumbnail
 * u:url
 * U:URL Param
78
79
 * v:
 * V:
80
 * w:websocket
81
82
 * W:Dimension
 * x:Delete
83
 * X:
84
 * y:Copy to clipboard
85
86
87
 * Y:
 * z:DropDown Menu
 * Z:
88
 *
89
90
 */

91
92
93
94
/**
 * Class Link
 * @package qfq
 */
95
96
class Link {

Carsten  Rose's avatar
Carsten Rose committed
97
    /**
98
     * @var Sip
Carsten  Rose's avatar
Carsten Rose committed
99
100
101
     */
    private $sip = null;

102
103
104
105
106
    /**
     * @var Store
     */
    private $store = null;

107
108
109
110
111
    /**
     * @var Thumbnail
     */
    private $thumbnail = null;

112
113
    private $dbIndexData = false;

Carsten  Rose's avatar
Carsten Rose committed
114
    private $phpUnit;
115
    private $renderControl = array();
116
//    private $linkClassSelector = array(TOKEN_CLASS_INTERNAL => "internal ", TOKEN_CLASS_EXTERNAL => "external ");
117
118
//    private $cssLinkClassInternal = '';
//    private $cssLinkClassExternal = '';
Carsten  Rose's avatar
Carsten Rose committed
119
    private $ttContentUid = '';
120

121
    private $callTable = [
Carsten  Rose's avatar
Carsten Rose committed
122
123
124
        TOKEN_URL => 'buildUrl',
        TOKEN_MAIL => 'buildMail',
        TOKEN_PAGE => 'buildPage',
125
        TOKEN_COPY_TO_CLIPBOARD => 'buildCopyToClipboard',
Carsten  Rose's avatar
Carsten Rose committed
126
        TOKEN_DOWNLOAD => 'buildDownload',
127
        TOKEN_DROPDOWN => 'buildDropdown',
Carsten  Rose's avatar
Carsten Rose committed
128
129
130
131
132
133
134
135
136
137
138
139
        TOKEN_TOOL_TIP => 'buildToolTip',
        TOKEN_PICTURE => 'buildPicture',
        TOKEN_BULLET => 'buildBullet',
        TOKEN_CHECK => 'buildCheck',
        TOKEN_DELETE => 'buildDeleteIcon',
        TOKEN_ACTION_DELETE => 'buildActionDelete',
        TOKEN_EDIT => 'buildEdit',
        TOKEN_HELP => 'buildHelp',
        TOKEN_INFO => 'buildInfo',
        TOKEN_NEW => 'buildNew',
        TOKEN_SHOW => 'buildShow',
        TOKEN_FILE => 'buildFile',
140
        TOKEN_FILE_DEPRECATED => 'buildFile',
Carsten  Rose's avatar
Carsten Rose committed
141
        TOKEN_GLYPH => 'buildGlyph',
142
        TOKEN_BOOTSTRAP_BUTTON => 'buildBootstrapButton',
143
144
    ];

145
    private $tableVarName = [
Carsten  Rose's avatar
Carsten Rose committed
146
147
148
        TOKEN_URL => NAME_URL,
        TOKEN_MAIL => NAME_MAIL,
        TOKEN_PAGE => NAME_PAGE,
149
        TOKEN_UID => NAME_UID,
150
        TOKEN_SOURCE => NAME_SOURCE,
151
        TOKEN_DROPDOWN => NAME_DROPDOWN,
Carsten  Rose's avatar
Carsten Rose committed
152
153
154
155
        TOKEN_DOWNLOAD => NAME_DOWNLOAD,
        TOKEN_DOWNLOAD_MODE => NAME_DOWNLOAD_MODE,
        TOKEN_TEXT => NAME_TEXT,
        TOKEN_ALT_TEXT => NAME_ALT_TEXT,
156
        TOKEN_BOOTSTRAP_BUTTON => NAME_BOOTSTRAP_BUTTON,
Carsten  Rose's avatar
Carsten Rose committed
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
        TOKEN_TOOL_TIP => NAME_TOOL_TIP,
        TOKEN_PICTURE => NAME_IMAGE,
        TOKEN_BULLET => NAME_IMAGE,
        TOKEN_CHECK => NAME_IMAGE,
        TOKEN_DELETE => NAME_IMAGE,
        TOKEN_EDIT => NAME_IMAGE,
        TOKEN_HELP => NAME_IMAGE,
        TOKEN_INFO => NAME_IMAGE,
        TOKEN_NEW => NAME_IMAGE,
        TOKEN_SHOW => NAME_IMAGE,
        TOKEN_GLYPH => NAME_IMAGE,
        TOKEN_RENDER => NAME_RENDER,
        TOKEN_TARGET => NAME_TARGET,
        TOKEN_CLASS => NAME_LINK_CLASS,
        TOKEN_QUESTION => NAME_QUESTION,
        TOKEN_ENCRYPTION => NAME_ENCRYPTION,
        TOKEN_SIP => NAME_SIP,
        TOKEN_URL_PARAM => NAME_URL_PARAM,
        TOKEN_RIGHT => NAME_RIGHT,
        TOKEN_ACTION_DELETE => NAME_ACTION_DELETE,
        TOKEN_FILE => NAME_FILE,
178
        TOKEN_FILE_DEPRECATED => NAME_FILE,
179
180
        TOKEN_THUMBNAIL => NAME_THUMBNAIL,
        TOKEN_THUMBNAIL_DIMENSION => NAME_THUMBNAIL_DIMENSION,
181
        TOKEN_COPY_TO_CLIPBOARD => NAME_COPY_TO_CLIPBOARD,
182
        TOKEN_ATTRIBUTE => NAME_ATTRIBUTE,
183
184
185
186
187
188
189
190

        TOKEN_MONITOR => NAME_MONITOR,
        // The following don't need a renaming: already 'long'
        TOKEN_L_FILE => TOKEN_L_FILE,
        TOKEN_L_TAIL => TOKEN_L_TAIL,
        TOKEN_L_APPEND => TOKEN_L_APPEND,
        TOKEN_L_INTERVAL => TOKEN_L_INTERVAL,
        TOKEN_L_HTML_ID => TOKEN_L_HTML_ID,
191
192
    ];

Carsten  Rose's avatar
Carsten Rose committed
193
    // Used to find double definitions.
194
    private $tokenMapping = [
Carsten  Rose's avatar
Carsten Rose committed
195
196
197
        TOKEN_URL => LINK_ANCHOR,
        TOKEN_MAIL => LINK_ANCHOR,
        TOKEN_PAGE => LINK_ANCHOR,
198
        TOKEN_UID => LINK_ANCHOR,
Carsten  Rose's avatar
Carsten Rose committed
199
        TOKEN_DOWNLOAD => LINK_ANCHOR,
200
        TOKEN_FILE => NAME_FILE,
201
        TOKEN_COPY_TO_CLIPBOARD => LINK_ANCHOR,
Carsten  Rose's avatar
Carsten Rose committed
202

203
        TOKEN_PICTURE => LINK_PICTURE,
Carsten  Rose's avatar
Carsten Rose committed
204
205
206
207
208
209
210
211
212
        TOKEN_BULLET => LINK_PICTURE,
        TOKEN_CHECK => LINK_PICTURE,
        TOKEN_DELETE => LINK_PICTURE,
        TOKEN_EDIT => LINK_PICTURE,
        TOKEN_HELP => LINK_PICTURE,
        TOKEN_INFO => LINK_PICTURE,
        TOKEN_NEW => LINK_PICTURE,
        TOKEN_SHOW => LINK_PICTURE,
        TOKEN_GLYPH => LINK_PICTURE,
213
    ];
214

215
216
217
    /**
     * __construct
     *
218
     * @param Sip $sip
219
     * @param string $dbIndexData
220
     * @param bool $phpUnit
Marc Egger's avatar
Marc Egger committed
221
222
223
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
224
     */
225
    public function __construct(Sip $sip, $dbIndexData = DB_INDEX_DEFAULT, $phpUnit = false) {
226
227

        #TODO: rewrite $phpUnit to: "if (!defined('PHPUNIT_QFQ')) {...}"
Carsten  Rose's avatar
Carsten Rose committed
228
        $this->phpUnit = $phpUnit;
229
230
231
232
233

        if ($phpUnit) {
            $_SERVER['REQUEST_URI'] = 'localhost';
        }

Carsten  Rose's avatar
Carsten Rose committed
234
        $this->sip = $sip;
Carsten  Rose's avatar
Carsten Rose committed
235
        $this->store = Store::getInstance('', $phpUnit);
236
237
//        $this->cssLinkClassInternal = $this->store->getVar(SYSTEM_CSS_LINK_CLASS_INTERNAL, STORE_SYSTEM);
//        $this->cssLinkClassExternal = $this->store->getVar(SYSTEM_CSS_LINK_CLASS_EXTERNAL, STORE_SYSTEM);
Carsten  Rose's avatar
Carsten Rose committed
238
        $this->ttContentUid = $this->store->getVar(TYPO3_TT_CONTENT_UID, STORE_TYPO3);
239
        $this->dbIndexData = $dbIndexData;
240
241
242
        /*
         * mode:
         * 0: no output
243
244
         * 1: <span title='...'>text</span>   (no href)
         * 2: <span title='...'>url</span>    (no href)
245
246
         * 3: <a href=url>url</a>
         * 4: <a href=url>Text</a>
247
248
         * 5: text
         * 6: url
249
         * 8: SIP only - 's=badcaffee1234'
250
         *
Carsten  Rose's avatar
Carsten Rose committed
251
         *  r=render mode, u=url, t:text and/or image.
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
         *
         *                  [r][u][t] = mode
         */

        $this->renderControl[0][0][0] = 0;
        $this->renderControl[0][0][1] = 0;
        $this->renderControl[0][1][0] = 3;
        $this->renderControl[0][1][1] = 4;

        $this->renderControl[1][0][0] = 0;
        $this->renderControl[1][0][1] = 1;
        $this->renderControl[1][1][0] = 3;
        $this->renderControl[1][1][1] = 4;

        $this->renderControl[2][0][0] = 0;
        $this->renderControl[2][0][1] = 0;
        $this->renderControl[2][1][0] = 0;
        $this->renderControl[2][1][1] = 4;

        $this->renderControl[3][0][0] = 0;
        $this->renderControl[3][0][1] = 1;
        $this->renderControl[3][1][0] = 2;
        $this->renderControl[3][1][1] = 1;

        $this->renderControl[4][0][0] = 0;
        $this->renderControl[4][0][1] = 1;
        $this->renderControl[4][1][0] = 2;
        $this->renderControl[4][1][1] = 2;

        $this->renderControl[5][0][0] = 0;
        $this->renderControl[5][0][1] = 0;
        $this->renderControl[5][1][0] = 0;
        $this->renderControl[5][1][1] = 0;
285

286
287
288
289
290
        $this->renderControl[6][0][0] = 0;
        $this->renderControl[6][0][1] = 5;
        $this->renderControl[6][1][0] = 0;
        $this->renderControl[6][1][1] = 5;

291
292
293
294
        $this->renderControl[7][0][0] = 0;
        $this->renderControl[7][0][1] = 0;
        $this->renderControl[7][1][0] = 6;
        $this->renderControl[7][1][1] = 6;
295

296
297
298
299
        $this->renderControl[8][0][0] = 0;
        $this->renderControl[8][0][1] = 0;
        $this->renderControl[8][1][0] = 8;
        $this->renderControl[8][1][1] = 8;
300
    }
301

302
    /**
303
     * In render mode 3,4,5 there is no '<a href ...>'. Nevertheless, tooltip and BS Button should be displayed.
304
     * Do this by applying a '<span>' attribute around the text.
305
     *
306
307
308
     * @param array $vars
     * @param       $keyName
     *
309
     * @return mixed|string
Marc Egger's avatar
Marc Egger committed
310
     * @throws \CodeException
311
     */
312
    private function wrapLinkTextOnly(array $vars, $keyName) {
313
314

        $text = $vars[$keyName];
315
        if ($vars[NAME_BOOTSTRAP_BUTTON] == '' && $vars[FINAL_TOOL_TIP] == '' && $vars[NAME_ATTRIBUTE] == '') {
316
317
318
            return $text;
        }

319
        $attributes = Support::doAttribute('title', $vars[FINAL_TOOL_TIP]);
320
321
322
        if ($vars[NAME_ATTRIBUTE] != '') {
            $attributes .= $vars[NAME_ATTRIBUTE] . ' ';
        }
323
324

        if ($vars[NAME_BOOTSTRAP_BUTTON] != '') {
325
            $attributes .= Support::doAttribute('class', [$vars[NAME_BOOTSTRAP_BUTTON], 'disabled']);
326
327
        }

328
329
330
331
332
        return Support::wrapTag("<span $attributes>", $text);
    }

    /**
     * Renders a (BS)-Dropdown menu.
333
     *
334
     * @param string $str
335
336
337
     *      'z|t:menu|b|o:click me'  - the menu button to click on. A text 'menu', a BS button, a tooltip 'click me'.
     *      '||p:detail&pId=1&s|t:Person 1'     - Page id=detail with pId=1 will be opened in the browser.
     *      '||d:file.pdf|p:detail&pId=1&_sip=1||t:Person as PDF' - Page id=detail with pId=1 will downloaded as a PDF.
338
     *
339
     * @param array $rcMenuEntryStrArr
340
341
     * @return string
     */
342
    private function processDropdown($str, array &$rcMenuEntryStrArr) {
343

344
        $rcMenuEntryStrArr = array();
345
        $tokenCollect = array();
346

347
348
        // Split 'z|t:menu|b|o:click me||p:detail&pId=1&s|t:Person 1||...'. Add '||' to take care that the last element is flushed.
        $paramArr = KeyValueStringParser::explodeEscape(PARAM_DELIMITER, $str . '||');
349

350
        // Iterate over token. Find delimiter to separate dropdown definition and all individual menu entries.
351
352
353
354
355
        foreach ($paramArr as $tokenStr) {
            $tokenArr = explode(PARAM_TOKEN_DELIMITER, $tokenStr, 2);
            switch ($tokenArr[0] ?? '') {
                // Indicator to start menu entry: force a flush of existing token and start a new round.
                case '':
356
357
                    // New menu entry.
                    if (!empty($tokenCollect)) {
358
                        $rcMenuEntryStrArr[] = implode(PARAM_DELIMITER, $tokenCollect);
359
                    }
360
361
362
363
364
365
366
367
                    $tokenCollect = array();
                    break;

                default:
                    $tokenCollect[] = $tokenStr;
            }
        }

368
369
        // First entry is the dropdown button, all others are the menu entries
        $dropdownButtonStr = array_shift($rcMenuEntryStrArr);
370

371
        return $dropdownButtonStr;
372
373
374
    }

    /**
375
376
     * https://getbootstrap.com/docs/3.4/components/#dropdowns
     *
377
378
379
380
381
382
383
384
385
386
387
388
     * Start
     * <span class="dropdown">
     *   <span class="glyphicon glyphicon-option-vertical dropdown-toggle" id="dropdownMenu11" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
     *   </span>
     *   <ul class="dropdown-menu" aria-labelledby="dropdownMenu11">
     *    <li><a href="#">Action</a></li>
     *    <li><a href="#">Another action</a></li>
     *    <li><a href="#">Something else here</a></li>
     *    <li role="separator" class="divider"></li>
     *    <li><a href="#">Separated link</a></li>
     *   </ul>
     * </span>
389
390
391
392
393
394
395
396
397
398
399
400
401
     *
     * <div class="btn-group">
     *   <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
     *     <span class="glyphicon glyphicon-option-vertical"></span>Button text
     *   </button>
     *   <ul class="dropdown-menu" aria-labelledby="dropdownMenu11">
     *    <li><a href="#">Action</a></li>
     *    <li><a href="#">Another action</a></li>
     *    <li><a href="#">Something else here</a></li>
     *    <li role="separator" class="divider"></li>
     *    <li><a href="#">Separated link</a></li>
     *   </ul>
     * </div>
402
403
     * End
     *
404
405
     * @param array $menuEntryStrArr
     * @param $htmlId
406
     * @return string
Marc Egger's avatar
Marc Egger committed
407
     * @throws \CodeException
408
     * @throws \DbException
Marc Egger's avatar
Marc Egger committed
409
410
     * @throws \UserFormException
     * @throws \UserReportException
411
     */
412
413
    private function renderDropdownUl(array $menuEntryStrArr, $htmlId) {
        $li = '';
414

415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
        foreach ($menuEntryStrArr as $str) {
            $attribute = '';

            $link = $this->renderLink($str);

            switch (substr($link, 0, 3)) {
                case '---':
                    $link = substr($link, 3);
                    if ($link == '') {
                        # Separator
                        $attribute = ' role="separator" class="divider"';
                    } else {
                        # Disabled
                        $attribute = ' class="disabled"';
                        if (false === strstr($link, '<a ')) {
                            # If there is no '<a>'-tag, the 'disabled' class is broken - set a fake one.
                            $link = Support::wrapTag('<a href="#">', $link);
                        }
                    }
                    break;
435

436
437
438
439
440
                case '===':
                    // Header
                    $link = substr($link, 3);
                    $attribute = ' class="dropdown-header"';
                    break;
441

442
443
                default:
                    break;
444
            }
445
446
            // Menu entries
            $li .= '<li' . $attribute . '>' . $link . '</li>';
447
448
        }

449
450
        // Wrapped Menu entries
        return Support::wrapTag('<ul style="max-height: 70vh; overflow-y: auto" class="dropdown-menu" aria-labelledby="' . $htmlId . '">', $li);
451
452
    }

453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
    /**
     * @param $str
     * @return string
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function processWebSocket($str) {

        $websocket = new WebSocket();

        $answer = '';

        // str="w:wss://antmedia.math.uzh.ch:6334/test|t:<payload>|timeout:..."
        $param = KeyValueStringParser::parse($str, PARAM_TOKEN_DELIMITER, PARAM_DELIMITER);
        if (empty($param[TOKEN_WEBSOCKET]) || empty($param[TOKEN_TEXT])) {
            throw new \UserReportException("Missing Websocket target or text to send", ERROR_MISSING_VALUE);
        }

        $urlParts = parse_url($param[TOKEN_WEBSOCKET]);
472
473
        $urlParts = array_merge(['scheme' => 'ws', 'host' => '', 'port' => 80, 'path' => ''], $urlParts);
        if (empty($urlParts['host'])) {
474
475
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'Target URL incomplete',
                    ERROR_MESSAGE_TO_DEVELOPER =>
476
477
478
                        'host: ' . $urlParts['host'] . ', ' .
                        'port: ' . $urlParts['port'] . ', ' .
                        'path: ' . $urlParts['path']])
479
480
481
                , ERROR_MISSING_VALUE);
        }

482
483
484
485
486
487
488
489
        // Check for wss >> ssl
        if ($urlParts['scheme'] == 'wss') {
            $urlParts['host'] = 'ssl://' . $urlParts['host'];
            if ($urlParts['port'] == 0) {
                $urlParts['port'] = 443;
            }
        }

490
        // Open Socket
491
492
493
        $errorMsg = '';
        if (false === $websocket->connect($urlParts['host'], $urlParts['port'], $urlParts['path'], '', $errorMsg)) {
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'Failed connect websocket: ' . $errorMsg,
494
                    ERROR_MESSAGE_TO_DEVELOPER =>
495
496
497
                        'host: ' . $urlParts['host'] . ', ' .
                        'port: ' . $urlParts['port'] . ', ' .
                        'path: ' . $urlParts['path']])
498
499
500
501
502
503
504
505
                , ERROR_MISSING_VALUE);
        }

        $answer = $websocket->sendData($param[TOKEN_TEXT]);

        return $answer;
    }

506
    /**
507
     * Build the whole link.
508
     *
509
     * @param string $str Qualifier with params. 'report'-syntax. F.e.:  u:www.example.com|P:home.gif|t:Home"
510
     * @param string $strDefault Same as $str, but might give some defaults if corresponding values in $str are missing.
Carsten  Rose's avatar
Carsten Rose committed
511
     *
512
     * @return string The complete HTML encoded Link like
513
     *           <a href='http://example.com' class='external'><img src='icon.gif' title='help text'>Description</a>
Marc Egger's avatar
Marc Egger committed
514
     * @throws \CodeException
515
     * @throws \DbException
Marc Egger's avatar
Marc Egger committed
516
517
     * @throws \UserFormException
     * @throws \UserReportException
518
     */
519
    public function renderLink($str, $strDefault = '') {
520

521
        $tokenGiven = array();
522
        $link = "";
523
524
525
        $ddHtmlId = '';
        $rcMenuEntryStrArr = array();
        $vars = $this->initVars();
526

527
        if (empty($str)) {
528
            return '';
529
        }
530

531
        // Special cases
532
533
        switch ($str[0] ?? '') {
            case TOKEN_DROPDOWN:
534
535
536
                $ddHtmlId = Support::uniqIdQfq('dd_');
                // Parse and split $str to  '$rcMenuEntryStrArr' and 'remaining button'
                $str = $this->processDropdown($str, $rcMenuEntryStrArr);
537
                break;
538

539
540
            case TOKEN_WEBSOCKET:
                return $this->processWebSocket($str);
541

542
543
544
545
            case TOKEN_REST_CLIENT:
                $restClient = new RestClient();
                return $restClient->process($str);

546
547
548
549
            default:
                break;
        }

550
551
        // General processing
        $vars = $this->fillParameter($vars, $str, $tokenGiven, $strDefault);
Carsten  Rose's avatar
Carsten Rose committed
552
        $vars = $this->processParameter($vars, $tokenGiven);
553
        $mode = $this->getModeRender($vars, $tokenGiven);
554

Carsten  Rose's avatar
Carsten Rose committed
555
556
557
558
        if (isset($tokenGiven[TOKEN_DOWNLOAD]) && $tokenGiven[TOKEN_DOWNLOAD] === true) {
            $this->store->setVar(SYSTEM_DOWNLOAD_POPUP, DOWNLOAD_POPUP_REQUEST, STORE_SYSTEM);
        }

559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
        if (($vars[NAME_DROPDOWN] ?? '') == '1') {
            $ul = '';
            // Render menu items only if the menu is active.
            if ($vars[NAME_RENDER] == 0) {
                $ul = $this->renderDropdownUl($rcMenuEntryStrArr, $ddHtmlId);
            }

            // Tooltip and attributes
            $attributes = Support::doAttribute('title', $vars[FINAL_TOOL_TIP]);
            if ($vars[NAME_ATTRIBUTE] != '') {
                $attributes .= $vars[NAME_ATTRIBUTE] . ' ';
            }

            // Bootstrap or plain
            if ($vars[NAME_BOOTSTRAP_BUTTON] == '0') {
                $tag = '<span class="dropdown" ' . $attributes . '>';
            } else {
                $tag = '<div class="btn-group" ' . $attributes . '>';
            }

            return Support::wrapTag($tag, $vars[FINAL_CONTENT] . $ul);
        }

582
        // 0-6 URL, plain email
583
        // 10-14 encrypted email
584
        // 20-24 delete / ajax
585
586
587
588
589
590
591
592
593
594
        switch ($mode) {
            // 0: No Output
            case '0':
            case '10':
            case '20':
                break;

            // 1: 'text'
            case '1':
            case '11':
595
                $link = $this->wrapLinkTextOnly($vars, FINAL_CONTENT);
596
597
598
599
600
                break;

            // 2: 'url'
            case '2':
            case '12':
601
                $link = $this->wrapLinkTextOnly($vars, FINAL_HREF);
602
603
                break;

604
            // 3: <a href=url ...>url</a>
605
606
            case '3':
            case '13':
607
                $link = Support::wrapTag($vars[FINAL_ANCHOR], $vars[FINAL_HREF]);
608
609
                break;

610
            // 4: <a href=url ...>Text</a>
611
612
            case '4':
            case '14':
613
                $link = Support::wrapTag($vars[FINAL_ANCHOR], $vars[FINAL_CONTENT]);
614
                break;
615

616
617
618
619
            case '21':
            case '22':
            case '23':
            case '24':
620
621
622
623
                //TODO: Alter Code, umstellen auf JS Client von RO. Vorlage koennte 'Delete' in Subrecord sein.
                $link = "<a href=\"javascript: void(0);\" onClick=\"var del = new FR.Delete({recordId:'',sip:'',forward:'" .
                    $vars[NAME_PAGE] . "'});\" " . $vars[NAME_LINK_CLASS] . ">" . $vars[NAME_TEXT] . "</a>";
                break;
624
625
626

            // 5: plain text, no <span> around
            case '5':
627
                $link = $vars[NAME_TEXT];
628
629
630
                break;
            case '15':
            case '25':
Marc Egger's avatar
Marc Egger committed
631
                throw new \UserReportException ("Mode not implemented. internal render mode=$mode", ERROR_UNKNOWN_MODE);
632

633
            // 6: plain url, no <span> around
634
635
636
637
638
            case '6':
                $link = $vars[FINAL_HREF];
                break;
            case '16':
            case '26':
Marc Egger's avatar
Marc Egger committed
639
                throw new \UserReportException ("Mode not implemented. internal render mode=$mode", ERROR_UNKNOWN_MODE);
640
                break;
641
            case '8':
642
                $link = substr($vars[FINAL_HREF], -SIP_TOKEN_LENGTH); // get only the last 13 characters (the sip)
643
                break;
644
645

            default:
Marc Egger's avatar
Marc Egger committed
646
                throw new \UserReportException ("Mode not implemented. internal render mode=$mode", ERROR_UNKNOWN_MODE);
647
648
        }

649
650
651
        return $link;
    }

652
    /**
653
654
     * $str and $strDefault are standard QFQ link format strings like 'p:{{pageAlias:T}}|t:linktext|b|s|...'
     * Parameter missing in $str and given in $strDefault will used.
655
     *
656
657
     * Split Parameter string in num Array (assoc is not possible cause for 'download', multiple sources with same key are possible).
     * Reorder param to bring prio token (currently only 'd') to top.
Carsten  Rose's avatar
Carsten Rose committed
658
     *
659
660
     * @param $str
     * @param $strDefault
661
662
     * @return array
     */
663
    private function paramPreparation($str, $strDefault = '') {
664
665
666
        $prio = array();
        $regular = array();

667
668
669
670
671
672
673
        // str="u:http://www.example.com|c:i|t:Hello World|q:Do you really want to delete the record 25:warn:yes:no"
        // Return a numbered array with strings like [ 0 => 'p:..', 1 => 't:...' , ...]
        $param = KeyValueStringParser::explodeEscape(PARAM_DELIMITER, $str);

        // Return an assoc array like [ 'd' => 'file.pdf', 'p' => 'content', ... ]
        $assocDefault = KeyValueStringParser::explodeKvpSimple($strDefault, PARAM_TOKEN_DELIMITER, PARAM_DELIMITER);

674
        foreach ($param as $value) {
675
676
677
678
679

            if ($value == '') {
                continue;
            }

680
681
682
            $arr = explode(PARAM_TOKEN_DELIMITER, $value, 2);
            $key = $arr[0];
            $value = $arr[1] ?? '';
683

684
685
686
687
            if ($key == TOKEN_DOWNLOAD) {
                $prio[] = [$key => $value ?? ''];
            } else {
                $regular[] = [$key => $value ?? ''];
688
689
            }

690
691
692
            // Explicit given arg: remove from default
            if (isset($assocDefault[$key])) {
                unset ($assocDefault[$key]);
693
694
695
            }
        }

696
697
698
699
700
701
702
703
704
705
706
707
        // Apply defaults, if not already given

        // First check if there is a prio item - currently only TOKEN_DOWNLOAD is of this type.
        if (isset($assocDefault[TOKEN_DOWNLOAD])) {
            $prio[] = [TOKEN_DOWNLOAD => $assocDefault[TOKEN_DOWNLOAD]];
            unset ($assocDefault[TOKEN_DOWNLOAD]);
        }
        // Append all remaining defaults to regular
        foreach ($assocDefault as $key => $value) {
            $regular[] = [$key => $value];
        }

708
709
        return array_merge($prio, $regular);
    }
710

711
    /**
712
     * Iterate over all given token. Check for double definition.
713
     *
714
     * @param $vars
715
     * @param string $str
716
     * @param array $rcTokenGiven - return an array with found token.
Carsten  Rose's avatar
Carsten Rose committed
717
     *
718
     * @param string $strDefault
719
     * @return array
Marc Egger's avatar
Marc Egger committed
720
     * @throws \CodeException
721
     * @throws \DbException
Marc Egger's avatar
Marc Egger committed
722
723
     * @throws \UserFormException
     * @throws \UserReportException
724
     */
725
    public function fillParameter($vars, $str, array &$rcTokenGiven, $strDefault = '') {
726

727
        $rcTokenGiven = array();
728
729
730
731
732
        if ($vars == array()) {
            // Define all possible vars: no more isset().
            $vars = $this->initVars();
        }

733
        $flagArray = array();
734

735
        $items = $this->paramPreparation($str, $strDefault);
736

Carsten  Rose's avatar
Carsten Rose committed
737
        // Parse all parameter, fill variables.
738
739
740
741
        foreach ($items as $item) {

            $value = reset($item);
            $key = key($item);
742

743
            // Skip empty entries
744
            if (empty($key)) {
745
746
                continue;
            }
747

748
            // Bookkeeping defined parameter.
749
            if (isset($rcTokenGiven[$key])) {
Marc Egger's avatar
Marc Egger committed
750
                throw new \UserReportException ("Multiple definitions for key '$key'", ERROR_MULTIPLE_DEFINITION);
751
            }
752

753
            $rcTokenGiven[$key] = true;
754
755

            if (!isset($this->tableVarName[$key])) {
756
                $msg[ERROR_MESSAGE_TO_USER] = "Unknown link qualifier: '$key' - did you forget the one character qualifier?";
Marc Egger's avatar
Marc Egger committed
757
                $msg[ERROR_MESSAGE_TO_DEVELOPER] = $str;
Marc Egger's avatar
Marc Egger committed
758
                throw new \UserReportException (json_encode($msg), ERROR_UNKNOWN_LINK_QUALIFIER);
759
            }
Carsten  Rose's avatar
Carsten Rose committed
760

761
762
            $keyName = $this->tableVarName[$key]; // convert token to name

763
764
765
766
767
            if ($key == TOKEN_PAGE && $value == '') {
                $value = $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3); // If no pageid|pagealias is defined, take current page
            }
            $value = Token::checkForEmptyValue($key, $value);

768
            $value = $this->checkValue($key, $value);
769

770
            // Store value
771
            if ((isset($rcTokenGiven[TOKEN_DOWNLOAD]) || isset($rcTokenGiven[TOKEN_COPY_TO_CLIPBOARD])) &&
772
773
                ($key == TOKEN_PAGE || $key == TOKEN_URL || $key == TOKEN_URL_PARAM || $key == TOKEN_UID
                    || $key == TOKEN_SOURCE || $key == TOKEN_FILE || $key == TOKEN_FILE_DEPRECATED)) {
Carsten  Rose's avatar
Carsten Rose committed
774

775
                $vars[NAME_COLLECT_ELEMENTS][] = $key . ':' . $value;
Carsten  Rose's avatar
Carsten Rose committed
776

777
                unset($rcTokenGiven[$key]); // Skip Bookkeeping for TOKEN_URL_PARAM | TOKEN_FILE | TOKEN_URL.
Carsten  Rose's avatar
Carsten Rose committed
778
779
                continue;
            } else {
780
                // TOKEN_GLYPH should not be treated as an regular image. Same applies to the other Glyph symbols, but those don't have a value, and therefore do not fill $vars['image'].
781
782
783
                if ($key != TOKEN_GLYPH) {
                    $vars[$keyName] = $value;
                }
Carsten  Rose's avatar
Carsten Rose committed
784
785
            }

786
            // Check for double anchor or picture definition.
787
788
789
790
            if (isset($this->tokenMapping[$key])) {
                $type = $this->tokenMapping[$key];

                if (isset($flagArray[$type])) {
Marc Egger's avatar
Marc Egger committed
791
                    throw new \UserReportException ("Multiple definitions of url/mail/page/download or picture", ERROR_MULTIPLE_DEFINITION);
792
793
794
795
796
797
798
                }
                $flagArray[$type] = true;

//                if ($type === LINK_PICTURE) {
//                    $build = 'build' . strtoupper($keyName[0]) . substr($keyName, 1);
//                    $this->$build($vars, $value);
//                }
799
800
            }

801
802
803
804
            if (isset($this->callTable[$key])) {
                $build = $this->callTable[$key];
                $vars = $this->$build($vars, $value);
            }
805
806
        }

807
        // Download Link needs some extra work
808
        if ($rcTokenGiven[TOKEN_DOWNLOAD] ?? false) {
809
            $vars = $this->buildDownloadLate($vars);
810
811
        }

812
        // CopyToClipboard (Download) Link needs some extra work
813
        if ($rcTokenGiven[TOKEN_COPY_TO_CLIPBOARD] ?? false) {
814
815
816
            $vars = $this->buildCopyToClipboardLate($vars);
        }

817
818
819
820
821
        // CopyToClipboard (Download) Link needs some extra work
        if ($rcTokenGiven[TOKEN_DROPDOWN] ?? false) {
            $vars = $this->buildDropdownLate($vars);
        }

822
823
824
825
826
        // Check for special default setting.
        if ($vars[NAME_SIP] === false) {
            $vars[NAME_SIP] = "0";
        }

827
        // Final Checks
828
        $this->checkParam($rcTokenGiven, $vars);
829

830
831
832
        return $vars;
    }

833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
    /**
     * Cleans and make existing the standard vars used every time to render a link.
     *
     * @return array
     */
    private function initVars() {

        return [
            NAME_MAIL => '',
            NAME_URL => '',
            NAME_PAGE => '',

            NAME_TEXT => '',
            NAME_ALT_TEXT => '',
            NAME_BOOTSTRAP_BUTTON => '',
            NAME_IMAGE => '',
            NAME_IMAGE_TITLE => '',
            NAME_GLYPH => '',
            NAME_GLYPH_TITLE => '',
            NAME_QUESTION => '',
            NAME_TARGET => '',
            NAME_TOOL_TIP => '',
            NAME_TOOL_TIP_JS => '',
            NAME_URL_PARAM => '',
            NAME_EXTRA_CONTENT_WRAP => '',
            NAME_DOWNLOAD_MODE => '',
859
            NAME_COLLECT_ELEMENTS => array(),
860
            NAME_COPY_TO_CLIPBOARD => '',
861
            NAME_ATTRIBUTE => '',
862
863
864

            NAME_RENDER => '0',
            NAME_RIGHT => 'l',
865
            NAME_SIP => false,
866
867
868
            NAME_ENCRYPTION => '0',
            NAME_DELETE => '',

869
870
            NAME_MONITOR => '0',

871
            NAME_LINK_CLASS => '', // class name
872
            NAME_LINK_CLASS_DEFAULT => '', // Depending of 'as page' or 'as url'. Only used if class is not explicit set.
873
874
875
876
877
878
879
880
881

            NAME_ACTION_DELETE => '',

            FINAL_HREF => '',
            FINAL_CONTENT => '',
            FINAL_SYMBOL => '',
            FINAL_TOOL_TIP => '',
            FINAL_CLASS => '',
            FINAL_QUESTION => '',
882
            FINAL_THUMBNAIL => '',
883
884
        ];
    }
885
886

    /**
887
888
     * Validate value for token
     *
889
890
     * @param $key
     * @param $value
Carsten  Rose's avatar
Carsten Rose committed
891
     *
892
     * @return mixed
Marc Egger's avatar
Marc Egger committed
893
     * @throws \UserReportException
894
895
     */
    private function checkValue($key, $value) {
896

897
898
899
900
        switch ($key) {
            case TOKEN_ENCRYPTION:
            case TOKEN_SIP:
                if ($value !== '0' && $value !== '1') {
Marc Egger's avatar
Marc Egger committed
901
                    throw new \UserReportException ("Invalid value for token '$key': '$value''", ERROR_INVALID_VALUE);
902
903
                }
                break;
904
905
906
907
908
909
910
            case TOKEN_ACTION_DELETE:
                switch ($value) {
                    case TOKEN_ACTION_DELETE_AJAX:
                    case TOKEN_ACTION_DELETE_REPORT:
                    case TOKEN_ACTION_DELETE_CLOSE:
                        break;
                    default:
Marc Egger's avatar
Marc Egger committed
911
                        throw new \UserReportException ("Invalid value for token '$key': '$value''", ERROR_INVALID_VALUE);
912
913
                }
                break;
914
915
916
917
918
            case TOKEN_BOOTSTRAP_BUTTON:
                if ($value == '1') {
                    $value = 'btn-default';
                }
                break;
919
920
921
922
923
924
            default:
        }

        return $value;
    }

925
926
927
928
    /**
     * Check for double definition.
     *
     * @param array $tokenGiven
Carsten  Rose's avatar
Carsten Rose committed
929
     *
930
     * @param array $vars
Marc Egger's avatar
Marc Egger committed
931
     * @throws \UserReportException
932
     */
933
    private function checkParam(array $tokenGiven, array &$vars) {
934
935
        $countLinkAnchor = 0;
        $countLinkPicture = 0;
936
        $countSources = 0;
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953

        foreach ($tokenGiven as $token => $value) {
            if (isset($this->tokenMapping[$token])) {
                switch ($this->tokenMapping[$token]) {
                    case LINK_ANCHOR:
                        $countLinkAnchor++;
                        break;
                    case LINK_PICTURE:
                        $countLinkPicture++;
                        break;
                    default:
                        break;
                }
            }
        }

        if ($countLinkAnchor > 1) {
Marc Egger's avatar
Marc Egger committed
954
            throw new \UserReportException ("Multiple URL / PAGE / MAILTO or COPY_TO_CLIPBOARD definition", ERROR_MULTIPLE_URL_PAGE_MAILTO_DEFINITION);
955
        }
956

957
        if ($countLinkPicture > 1) {
Marc Egger's avatar
Marc Egger committed
958
            throw new \UserReportException ("Multiple definitions for token picture/bullet/check/edit...delete'" . TOKEN_PAGE . "'", ERROR_MULTIPLE_DEFINITION);
959
960
961
        }

        if (isset($tokenGiven[TOKEN_MAIL]) && isset($tokenGiven[TOKEN_TARGET])) {
Marc Egger's avatar
Marc Egger committed
962
            throw new \UserReportException ("Token Mail and Target at the same time not possible'" . TOKEN_PAGE . "'", ERROR_MULTIPLE_DEFINITION);
963
        }
Carsten  Rose's avatar
Carsten Rose committed
964

965
        if (isset($tokenGiven[TOKEN_DOWNLOAD]) && $vars[NAME_SIP] == "1" && count($vars[NAME_COLLECT_ELEMENTS]) == 0) {
Marc Egger's avatar
Marc Egger committed
966
            throw new \UserReportException ("Missing element sources for download", ERROR_MISSING_REQUIRED_PARAMETER);
Carsten  Rose's avatar
Carsten Rose committed
967
        }
968

969
970
971
972
        if (isset($tokenGiven[TOKEN_DOWNLOAD]) && $vars[NAME_SIP] == "0" && count($vars[NAME_COLLECT_ELEMENTS]) > 0) {
            throw new \UserReportException ("For persistent downloads, sources are not necessary/allowed in the link definition. Instead, please define the sources in QFQ extension setup > file.", ERROR_MISSING_REQUIRED_PARAMETER);
        }

973
974
975
        if (isset($tokenGiven[TOKEN_ACTION_DELETE])) {
            $this->checkDeleteParam($vars);
        }
976
    }
977
978

    /**
979
     * Compute final link parameter.
980
     *
Carsten  Rose's avatar
Carsten Rose committed
981
     * @param array $vars
Carsten  Rose's avatar
Carsten Rose committed
982
     * @param array $tokenGiven
Carsten  Rose's avatar
Carsten Rose committed
983
     *
984
     * @return array
Marc Egger's avatar
Marc Egger committed
985
986
987
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
988
     */
Carsten  Rose's avatar
Carsten Rose committed
989
    private function processParameter(array $vars, array $tokenGiven) {
990

Carsten  Rose's avatar
Carsten Rose committed
991
        $vars[FINAL_HREF] = $this->doHref($vars, $tokenGiven); // must be called before doToolTip()
992
993
994
        $vars[FINAL_TOOL_TIP] = $this->doToolTip($vars);
        $vars[FINAL_CLASS] = $this->doCssClass($vars);
        $vars[FINAL_SYMBOL] = $this->doSymbol($vars);
995
        $vars[FINAL_THUMBNAIL]