Link.php 65.7 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\Sanitize;
Marc Egger's avatar
Marc Egger committed
29
30
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Helper\Token;
31
32
use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Store\Store;
33
34
35

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

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

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

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

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

111
112
    private $dbIndexData = false;

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

120
    private $callTable = [
Carsten  Rose's avatar
Carsten Rose committed
121
122
123
        TOKEN_URL => 'buildUrl',
        TOKEN_MAIL => 'buildMail',
        TOKEN_PAGE => 'buildPage',
124
        TOKEN_COPY_TO_CLIPBOARD => 'buildCopyToClipboard',
Carsten  Rose's avatar
Carsten Rose committed
125
        TOKEN_DOWNLOAD => 'buildDownload',
126
        TOKEN_DROPDOWN => 'buildDropdown',
Carsten  Rose's avatar
Carsten Rose committed
127
128
129
130
131
132
133
134
135
136
137
138
        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',
139
        TOKEN_FILE_DEPRECATED => 'buildFile',
Carsten  Rose's avatar
Carsten Rose committed
140
        TOKEN_GLYPH => 'buildGlyph',
141
        TOKEN_BOOTSTRAP_BUTTON => 'buildBootstrapButton',
142
143
    ];

144
    private $tableVarName = [
Carsten  Rose's avatar
Carsten Rose committed
145
146
147
        TOKEN_URL => NAME_URL,
        TOKEN_MAIL => NAME_MAIL,
        TOKEN_PAGE => NAME_PAGE,
148
        TOKEN_UID => NAME_UID,
149
        TOKEN_DROPDOWN => NAME_DROPDOWN,
Carsten  Rose's avatar
Carsten Rose committed
150
151
152
153
        TOKEN_DOWNLOAD => NAME_DOWNLOAD,
        TOKEN_DOWNLOAD_MODE => NAME_DOWNLOAD_MODE,
        TOKEN_TEXT => NAME_TEXT,
        TOKEN_ALT_TEXT => NAME_ALT_TEXT,
154
        TOKEN_BOOTSTRAP_BUTTON => NAME_BOOTSTRAP_BUTTON,
Carsten  Rose's avatar
Carsten Rose committed
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
        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,
176
        TOKEN_FILE_DEPRECATED => NAME_FILE,
177
178
        TOKEN_THUMBNAIL => NAME_THUMBNAIL,
        TOKEN_THUMBNAIL_DIMENSION => NAME_THUMBNAIL_DIMENSION,
179
        TOKEN_COPY_TO_CLIPBOARD => NAME_COPY_TO_CLIPBOARD,
180
        TOKEN_ATTRIBUTE => NAME_ATTRIBUTE,
181
182
183
184
185
186
187
188

        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,
189
190
    ];

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

201
        TOKEN_PICTURE => LINK_PICTURE,
Carsten  Rose's avatar
Carsten Rose committed
202
203
204
205
206
207
208
209
210
        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,
211
    ];
212

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

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

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

Carsten  Rose's avatar
Carsten Rose committed
232
        $this->sip = $sip;
Carsten  Rose's avatar
Carsten Rose committed
233
        $this->store = Store::getInstance('', $phpUnit);
234
235
//        $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
236
        $this->ttContentUid = $this->store->getVar(TYPO3_TT_CONTENT_UID, STORE_TYPO3);
237
        $this->dbIndexData = $dbIndexData;
238
239
240
        /*
         * mode:
         * 0: no output
241
242
         * 1: <span title='...'>text</span>   (no href)
         * 2: <span title='...'>url</span>    (no href)
243
244
         * 3: <a href=url>url</a>
         * 4: <a href=url>Text</a>
245
246
         * 5: text
         * 6: url
247
         * 8: SIP only - 's=badcaffee1234'
248
         *
Carsten  Rose's avatar
Carsten Rose committed
249
         *  r=render mode, u=url, t:text and/or image.
250
251
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
         *
         *                  [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;
283

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

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

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

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

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

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

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

        return Support::wrapTag("<span $attributes>", $text);
327
328
329

    }

330
    /**
331
332
     * Renders a BS-dropdown menu.
     *
333
     * @param string $str
334
335
336
     *      '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.
337
338
     *
     * @return string
Marc Egger's avatar
Marc Egger committed
339
340
341
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
342
343
     */
    private function processDropdown($str) {
344

345
346
347
        $menuEntryStrArr = array();
        $menuEntryLinkArr = array();
        $tokenCollect = array();
348

349
        $htmlId = Support::uniqIdQfq('dd_');
350
351
352
353
354
355
356

        $paramArr = KeyValueStringParser::explodeEscape(PARAM_DELIMITER, $str);

        // Iterate over token. Find delimiter to separate dropdown definition and all menu entries.
        foreach ($paramArr as $tokenStr) {
            $tokenArr = explode(PARAM_TOKEN_DELIMITER, $tokenStr, 2);
            switch ($tokenArr[0] ?? '') {
357

358
                case TOKEN_DROPDOWN:
359
                    $tokenCollect[] = ''; // In case there are no further options given, it's necessary to have at least one
360
361
362
363
                    break;

                // Indicator to start menu entry: force a flush of existing token and start a new round.
                case '':
364
365
366
367
                    // New menu entry.
                    if (!empty($tokenCollect)) {
                        $menuEntryStrArr[] = implode(PARAM_DELIMITER, $tokenCollect);
                    }
368
                    $tokenCollect = array();
369
//                    $tokenCollect[] = $tokenStr;
370
371
372
373
374
375
376
                    break;

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

377
        // Flush remaining element.
378
379
        $menuEntryStrArr[] = implode(PARAM_DELIMITER, $tokenCollect);

380
        // Remove first array element - that's the menu, not a menu entry. Render the menu.
381
382
383
384
385
386
387
388
        $flagMenuEnabled = false;
        $dropdownSymbol = $this->renderDropdownSymbol(array_shift($menuEntryStrArr), $htmlId, $flagMenuEnabled);

        if ($flagMenuEnabled) {
            // For each menu entry get the link
            foreach ($menuEntryStrArr as $str) {
                $menuEntryLinkArr[] = $this->renderLink($str);
            }
389
390
        }

391
        return $this->renderDropdown($dropdownSymbol, $menuEntryLinkArr, $htmlId, $flagMenuEnabled);
392
393
394
395

    }

    /**
396
397
     * https://getbootstrap.com/docs/3.4/components/#dropdowns
     *
398
399
400
401
402
403
404
405
406
407
408
409
410
411
     * 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>
     * End
     *
412
413
414
415
     * @param string $dropdownSymbol
     * @param array $menuEntryLinkArr
     * @param string $htmlId
     * @param bool $flagMenuEnabled
416
417
     * @return string
     */
418
419
    private function renderDropdown(string $dropdownSymbol, array $menuEntryLinkArr, $htmlId, $flagMenuEnabled) {
        $ul = '';
420
421
        $li = '';
        $attribute = '';
422

423
424
425
        if ($flagMenuEnabled) {
            $tmp = '';
            foreach ($menuEntryLinkArr as $link) {
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
                $attribute = '';

                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;

                    case '===':
                        // Header
                        $link = substr($link, 3);
                        $attribute = ' class="dropdown-header"';
                        break;

                    default:
                        break;
                }
                $li .= '<li' . $attribute . '>' . $link . '</li>';
454
455
            }

456
            $ul = Support::wrapTag('<ul style="max-height: 70vh; overflow-y: auto" class="dropdown-menu" aria-labelledby="' . $htmlId . '">', $li);
457
458
459
460
461
462
463
464
        }

        return Support::wrapTag('<span class="dropdown">', $dropdownSymbol . $ul);
    }

    /**
     * @param string $dropdownStr
     * @param string $htmlId
Marc Egger's avatar
Marc Egger committed
465
     * @param $flagMenuEnabled
466
     * @return string
Marc Egger's avatar
Marc Egger committed
467
468
469
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
470
471
472
473
474
475
476
     */
    private function renderDropdownSymbol($dropdownStr, $htmlId, &$flagMenuEnabled) {
        $class = '';

        $paramArr = KeyValueStringParser::parse($dropdownStr, PARAM_TOKEN_DELIMITER, PARAM_DELIMITER);
        foreach ([TOKEN_URL, TOKEN_MAIL, TOKEN_PAGE, TOKEN_DOWNLOAD, TOKEN_COPY_TO_CLIPBOARD] as $token) {
            if (isset($paramArr[$token])) {
Marc Egger's avatar
Marc Egger committed
477
                throw new \UserReportException("Dropdown / broken definition: Token '$token' can't be part of " . TOKEN_DROPDOWN, ERROR_INVALID_VALUE);
478
            }
479
480
        }

481
482
483
484
485
486
487
488
        // All render mode <3 means: enabled
        $flagMenuEnabled = ($paramArr[TOKEN_RENDER] ?? 0) < 3;

        // Misuse the link class to render the menu symbol: this gives tooltip, glyph, button, text ... - set render mode to '3' = 'no link'
        $paramArr[TOKEN_RENDER] = '3';

        if (!isset($paramArr[TOKEN_GLYPH])) {
            $paramArr[TOKEN_GLYPH] = 'glyphicon-option-vertical';
489
490
        }

491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
        if (isset($paramArr[TOKEN_BOOTSTRAP_BUTTON])) {
            $vars = $this->buildBootstrapButton(array(), $paramArr[TOKEN_BOOTSTRAP_BUTTON]);
            $class = ' ' . ($vars[NAME_BOOTSTRAP_BUTTON] ?? '');
        }

        if (!isset($paramArr[TOKEN_ATTRIBUTE])) {
            $paramArr[TOKEN_ATTRIBUTE] = 'data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"';
        }

        if (!$flagMenuEnabled) {
            $class .= ' disabled';
            // In case there is no button: set the text an glyphicon to 'muted'
            if (($vars[NAME_BOOTSTRAP_BUTTON] ?? '') === '') {

                if (($paramArr[TOKEN_TEXT] ?? '') != '') {
                    $paramArr[TOKEN_TEXT] = Support::wrapTag('<span class="text-muted">', $paramArr[TOKEN_TEXT]);
                }

                if (($paramArr[TOKEN_GLYPH] ?? '') != '') {
                    $paramArr[TOKEN_GLYPH] .= ' text-muted';
                }
            }
        }

        $paramArr[TOKEN_ATTRIBUTE] .= ' class="dropdown-toggle' . $class . '"';
        $paramArr[TOKEN_ATTRIBUTE] .= ' id="' . $htmlId . '"';

        return $this->renderLink(KeyValueStringParser::unparse($paramArr, PARAM_TOKEN_DELIMITER, PARAM_DELIMITER));
519
520
    }

521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
    /**
     * @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]);
540
541
        $urlParts = array_merge(['scheme' => 'ws', 'host' => '', 'port' => 80, 'path' => ''], $urlParts);
        if (empty($urlParts['host'])) {
542
543
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'Target URL incomplete',
                    ERROR_MESSAGE_TO_DEVELOPER =>
544
545
546
                        'host: ' . $urlParts['host'] . ', ' .
                        'port: ' . $urlParts['port'] . ', ' .
                        'path: ' . $urlParts['path']])
547
548
549
                , ERROR_MISSING_VALUE);
        }

550
551
552
553
554
555
556
557
        // Check for wss >> ssl
        if ($urlParts['scheme'] == 'wss') {
            $urlParts['host'] = 'ssl://' . $urlParts['host'];
            if ($urlParts['port'] == 0) {
                $urlParts['port'] = 443;
            }
        }

558
        // Open Socket
559
560
561
        $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,
562
                    ERROR_MESSAGE_TO_DEVELOPER =>
563
564
565
                        'host: ' . $urlParts['host'] . ', ' .
                        'port: ' . $urlParts['port'] . ', ' .
                        'path: ' . $urlParts['path']])
566
567
568
569
570
571
572
573
                , ERROR_MISSING_VALUE);
        }

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

        return $answer;
    }

574
    /**
575
     * Build the whole link.
576
     *
577
     * @param string $str Qualifier with params. 'report'-syntax. F.e.:  u:www.example.com|P:home.gif|t:Home"
Carsten  Rose's avatar
Carsten Rose committed
578
     *
579
     * @return string The complete HTML encoded Link like
580
     *           <a href='http://example.com' class='external'><img src='icon.gif' title='help text'>Description</a>
Marc Egger's avatar
Marc Egger committed
581
582
583
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
584
     */
Carsten  Rose's avatar
Carsten Rose committed
585
    public function renderLink($str) {
586

587
        $tokenGiven = array();
588
        $link = "";
589

590
        if (empty($str)) {
591
            return '';
592
        }
593

594
595
596
597
598
599
600
601
602
603
604
605
        switch ($str[0] ?? '') {
            case TOKEN_DROPDOWN:
                // Check for dropdown menu
                return $this->processDropdown($str);
                break;
            case TOKEN_WEBSOCKET:
                return $this->processWebSocket($str);
                break;
            default:
                break;
        }

606
607
608
609
        if (($str[0] ?? '') == TOKEN_DROPDOWN) {
            return $this->processDropdown($str);
        }

610
        $vars = $this->fillParameter($str, $tokenGiven);
Carsten  Rose's avatar
Carsten Rose committed
611
        $vars = $this->processParameter($vars, $tokenGiven);
612
        $mode = $this->getModeRender($vars, $tokenGiven);
613

Carsten  Rose's avatar
Carsten Rose committed
614
615
616
617
        if (isset($tokenGiven[TOKEN_DOWNLOAD]) && $tokenGiven[TOKEN_DOWNLOAD] === true) {
            $this->store->setVar(SYSTEM_DOWNLOAD_POPUP, DOWNLOAD_POPUP_REQUEST, STORE_SYSTEM);
        }

618
        // 0-6 URL, plain email
619
        // 10-14 encrypted email
620
        // 20-24 delete / ajax
621
622
623
624
625
626
627
628
629
630
        switch ($mode) {
            // 0: No Output
            case '0':
            case '10':
            case '20':
                break;

            // 1: 'text'
            case '1':
            case '11':
631
                $link = $this->wrapLinkTextOnly($vars, FINAL_CONTENT);
632
633
634
635
636
                break;

            // 2: 'url'
            case '2':
            case '12':
637
                $link = $this->wrapLinkTextOnly($vars, FINAL_HREF);
638
639
                break;

640
            // 3: <a href=url ...>url</a>
641
642
            case '3':
            case '13':
643
                $link = Support::wrapTag($vars[FINAL_ANCHOR], $vars[FINAL_HREF]);
644
645
                break;

646
            // 4: <a href=url ...>Text</a>
647
648
            case '4':
            case '14':
649
                $link = Support::wrapTag($vars[FINAL_ANCHOR], $vars[FINAL_CONTENT]);
650
                break;
651

652
653
654
655
            case '21':
            case '22':
            case '23':
            case '24':
656
657
658
659
                //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;
660
661
662

            // 5: plain text, no <span> around
            case '5':
663
                $link = $vars[NAME_TEXT];
664
665
666
                break;
            case '15':
            case '25':
Marc Egger's avatar
Marc Egger committed
667
                throw new \UserReportException ("Mode not implemented. internal render mode=$mode", ERROR_UNKNOWN_MODE);
668

669
            // 6: plain url, no <span> around
670
671
672
673
674
            case '6':
                $link = $vars[FINAL_HREF];
                break;
            case '16':
            case '26':
Marc Egger's avatar
Marc Egger committed
675
                throw new \UserReportException ("Mode not implemented. internal render mode=$mode", ERROR_UNKNOWN_MODE);
676
                break;
677
678
679
            case '8':
                $link = substr($vars[FINAL_HREF], 12); // strip 'index.php?s='
                break;
680
681

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

685
686
687
        return $link;
    }

688
    /**
689
     * Order $param. Parameter with priority are hardcoded at the moment.
690
691
     *
     * @param array $param
Carsten  Rose's avatar
Carsten Rose committed
692
     *
693
694
695
696
697
698
699
     * @return array
     */
    private function paramPriority(array $param) {
        $prio = array();
        $regular = array();

        foreach ($param as $value) {
700
701
702
703
704

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

705
            $key = substr($value, 0, 2);
706
707

            if (strlen($key) == 1) {
708
                $key .= PARAM_TOKEN_DELIMITER;
709
710
            }

711
            if ($key == TOKEN_DOWNLOAD . PARAM_TOKEN_DELIMITER) {
712
713
714
                $prio[] = $value;
            } else {
                $regular[] = $value;
715
716
717
718
719
            }
        }

        return array_merge($prio, $regular);
    }
720

721
    /**
722
     * Iterate over all given token. Check for double definition.
723
     *
724
     * @param string $str
725
     * @param array $rcTokenGiven - return an array with found token.
Carsten  Rose's avatar
Carsten Rose committed
726
     *
727
     * @return array
Marc Egger's avatar
Marc Egger committed
728
729
730
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
731
     */
732
    public function fillParameter($str, array &$rcTokenGiven) {
733

Carsten  Rose's avatar
Carsten Rose committed
734
        // Define all possible vars: no more isset().
735
736
        $vars = $this->initVars();
        $flagArray = array();
737

Carsten  Rose's avatar
Carsten Rose committed
738
        // str="u:http://www.example.com|c:i|t:Hello World|q:Do you really want to delete the record 25:warn:yes:no"
739
        $param = KeyValueStringParser::explodeEscape(PARAM_DELIMITER, $str);
740

741
742
        $param = $this->paramPriority($param);

Carsten  Rose's avatar
Carsten Rose committed
743
        // Parse all parameter, fill variables.
744
        foreach ($param as $item) {
745

746
            // Skip empty entries
747
748
749
            if ($item === '') {
                continue;
            }
750

751
752
753
754
            // u:www.example.com
            $arr = explode(":", $item, 2);
            $key = isset($arr[0]) ? $arr[0] : '';
            $value = isset($arr[1]) ? $arr[1] : '';
755

756
            // Bookkeeping defined parameter.
757
            if (isset($rcTokenGiven[$key])) {
Marc Egger's avatar
Marc Egger committed
758
                throw new \UserReportException ("Multiple definitions for key '$key'", ERROR_MULTIPLE_DEFINITION);
759
            }
760
            $rcTokenGiven[$key] = true;
761
762

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

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

770
771
772
773
774
            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);

775
            $value = $this->checkValue($key, $value);
776

777
            // Store value
778
            if ((isset($rcTokenGiven[TOKEN_DOWNLOAD]) || isset($rcTokenGiven[TOKEN_COPY_TO_CLIPBOARD])) &&
779
780
                ($key == TOKEN_PAGE || $key == TOKEN_URL || $key == TOKEN_URL_PARAM || $key == TOKEN_UID ||
                    $key == TOKEN_FILE || $key == TOKEN_FILE_DEPRECATED)) {
Carsten  Rose's avatar
Carsten Rose committed
781

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

784
                unset($rcTokenGiven[$key]); // Skip Bookkeeping for TOKEN_URL_PARAM | TOKEN_FILE | TOKEN_URL.
Carsten  Rose's avatar
Carsten Rose committed
785
786
                continue;
            } else {
787
                // 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'].
788
789
790
                if ($key != TOKEN_GLYPH) {
                    $vars[$keyName] = $value;
                }
Carsten  Rose's avatar
Carsten Rose committed
791
792
            }

793
            // Check for double anchor or picture definition.
794
795
796
797
            if (isset($this->tokenMapping[$key])) {
                $type = $this->tokenMapping[$key];

                if (isset($flagArray[$type])) {
Marc Egger's avatar
Marc Egger committed
798
                    throw new \UserReportException ("Multiple definitions of url/mail/page/download or picture", ERROR_MULTIPLE_DEFINITION);
799
800
801
802
803
804
805
                }
                $flagArray[$type] = true;

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

808
809
810
811
            if (isset($this->callTable[$key])) {
                $build = $this->callTable[$key];
                $vars = $this->$build($vars, $value);
            }
812
813
        }

814
        // Download Link needs some extra work
Carsten  Rose's avatar
Carsten Rose committed
815
        if (isset($rcTokenGiven[TOKEN_DOWNLOAD]) && $rcTokenGiven[TOKEN_DOWNLOAD]) {
816
            $vars = $this->buildDownloadLate($vars);
817
818
        }

819
        // CopyToClipboard (Download) Link needs some extra work
Carsten  Rose's avatar
Carsten Rose committed
820
        if (isset($rcTokenGiven[TOKEN_COPY_TO_CLIPBOARD]) && $rcTokenGiven[TOKEN_COPY_TO_CLIPBOARD]) {
821
822
823
            $vars = $this->buildCopyToClipboardLate($vars);
        }

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

829
        // Final Checks
830
        $this->checkParam($rcTokenGiven, $vars);
831

832
833
834
        return $vars;
    }

835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
    /**
     * 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 => '',
861
            NAME_COLLECT_ELEMENTS => array(),
862
            NAME_COPY_TO_CLIPBOARD => '',
863
            NAME_ATTRIBUTE => '',
864
865
866

            NAME_RENDER => '0',
            NAME_RIGHT => 'l',
867
            NAME_SIP => false,
868
869
870
            NAME_ENCRYPTION => '0',
            NAME_DELETE => '',

871
872
            NAME_MONITOR => '0',

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

            NAME_ACTION_DELETE => '',

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

    }
888
889

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

900
901
902
903
        switch ($key) {
            case TOKEN_ENCRYPTION:
            case TOKEN_SIP:
                if ($value !== '0' && $value !== '1') {
Marc Egger's avatar
Marc Egger committed
904
                    throw new \UserReportException ("Invalid value for token '$key': '$value''", ERROR_INVALID_VALUE);
905
906
                }
                break;
907
908
909
910
911
912
913
            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
914
                        throw new \UserReportException ("Invalid value for token '$key': '$value''", ERROR_INVALID_VALUE);
915
916
                }
                break;
917
918
919
920
921
            case TOKEN_BOOTSTRAP_BUTTON:
                if ($value == '1') {
                    $value = 'btn-default';
                }
                break;
922
923
924
925
926
927
            default:
        }

        return $value;
    }

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

        foreach ($tokenGiven as $token => $value) {
            if (isset($this->tokenMapping[$token])) {
                switch ($this->tokenMapping[$token]) {
                    case LINK_ANCHOR:
                        $countLinkAnchor++;
                        break;
                    case LINK_PICTURE:
                        $countLinkPicture++;
                        break;
950
951
952
953
954
                    case NAME_FILE:
                    case NAME_URL:
                    case NAME_PAGE:
                        $countSources++;
                        break;
955
956
957
958
959
960
961
                    default:
                        break;
                }
            }
        }

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

965
        if ($countLinkPicture > 1) {
Marc Egger's avatar
Marc Egger committed
966
            throw new \UserReportException ("Multiple definitions for token picture/bullet/check/edit...delete'" . TOKEN_PAGE . "'", ERROR_MULTIPLE_DEFINITION);
967
968
969
        }

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

973
        if (isset($tokenGiven[TOKEN_DOWNLOAD]) && count($vars[NAME_COLLECT_ELEMENTS]) == 0 && $countSources == 0) {
Marc Egger's avatar
Marc Egger committed
974
            throw new \UserReportException ("Missing element sources for download", ERROR_MISSING_REQUIRED_PARAMETER);
Carsten  Rose's avatar
Carsten Rose committed
975
        }
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] = $this->doThumbnail($vars);
Carsten  Rose's avatar
Carsten Rose committed
996
        $vars[FINAL_CONTENT] = $this->doContent($vars, $vars[FINAL_CONTENT_PURE]); // must be called after doSymbol()
Carsten  Rose's avatar
Carsten Rose committed
997
        $vars[FINAL_QUESTION] = $this->doQuestion($vars);
998
        $vars[FINAL_ANCHOR] = $this->doAnchor($vars);
999

1000
        return $vars;
1001
    }
1002

Carsten  Rose's avatar
Carsten Rose committed
1003
    /**
1004
1005
1006
     * Determine DownloadMode: explicit given or detected by given download sources.
     * Do some basic checks if parameter are correct.
     *
Carsten  Rose's avatar
Carsten Rose committed
1007
     * @param array $vars
Carsten  Rose's avatar
Carsten Rose committed
1008
     *
1009
     * @return string - DOWNLOAD_MODE_PDF | DOWNLOAD_MODE_ZIP | DOWNLOAD_MODE_FILE | DOWNLOAD_MODE_EXCEL
Marc Egger's avatar
Marc Egger committed
1010
     * @throws \UserFormException
Carsten  Rose's avatar
Carsten Rose committed
1011
1012
1013
     */
    private function getDownloadModeNCheck(array $vars) {

1014
        $cnt = count($vars[NAME_COLLECT_ELEMENTS]);
Carsten  Rose's avatar
Carsten Rose committed
1015
1016
1017
1018
1019
        $mode = $vars[NAME_DOWNLOAD_MODE];

        // Determine default.
        if ($mode == '') {
            if ($cnt == 1) {
1020
                $mode = (substr($vars[NAME_COLLECT_ELEMENTS][0], 0, 1) == TOKEN_FILE_DEPRECATED) ? DOWNLOAD_MODE_FILE : DOWNLOAD_MODE_PDF;
Carsten  Rose's avatar
Carsten Rose committed
1021
1022
1023
1024
1025
1026
1027
1028
            } else {
                $mode = DOWNLOAD_MODE_PDF;
            }
        }

        // Do some checks.
        switch ($mode) {
            case DOWNLOAD_MODE_PDF:
1029
            case DOWNLOAD_MODE_SAVE_PDF:
Carsten  Rose's avatar
Carsten Rose committed
1030
            case DOWNLOAD_MODE_ZIP:
1031
            case DOWNLOAD_MODE_EXCEL:
Carsten  Rose's avatar
Carsten Rose committed
1032
1033
1034
                break;
            case DOWNLOAD_MODE_FILE:
                if ($cnt > 1) {
Marc Egger's avatar
Marc Egger committed
1035
                    throw new \UserFormException("With 'downloadMode' = 'file' only one element source is allowed.", ERROR_DOWNLOAD_UNEXPECTED_NUMBER_OF_SOURCES);
Carsten  Rose's avatar
Carsten Rose committed
1036
1037
1038
                }
                break;
            default:
Marc Egger's avatar
Marc Egger committed
1039
                throw new \UserFormException("Unknown mode: $mode", ERROR_UNKNOWN_MODE);
Carsten  Rose's avatar
Carsten Rose committed
1040
1041
1042
1043