Passed
Push — master ( cf235e...a68ace )
by
unknown
16:24
created

AbstractTreeView::getRecord()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Backend\Tree\View;
17
18
use TYPO3\CMS\Backend\Routing\UriBuilder;
19
use TYPO3\CMS\Backend\Utility\BackendUtility;
20
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
21
use TYPO3\CMS\Core\Database\ConnectionPool;
22
use TYPO3\CMS\Core\Database\Query\QueryHelper;
23
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
24
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
25
use TYPO3\CMS\Core\Imaging\Icon;
26
use TYPO3\CMS\Core\Imaging\IconFactory;
27
use TYPO3\CMS\Core\Localization\LanguageService;
28
use TYPO3\CMS\Core\Utility\GeneralUtility;
29
30
/**
31
 * Base class for creating a browsable array/page/folder tree in HTML
32
 */
33
abstract class AbstractTreeView
34
{
35
    // EXTERNAL, static:
36
37
    // Holds the current script to reload to.
38
    /**
39
     * @var string
40
     */
41
    public $thisScript = '';
42
43
    // Used if the tree is made of records (not folders for ex.)
44
    /**
45
     * @var string
46
     */
47
    public $title = 'no title';
48
49
    /**
50
     * Needs to be initialized with $GLOBALS['BE_USER']
51
     * Done by default in init()
52
     *
53
     * @var \TYPO3\CMS\Core\Authentication\BackendUserAuthentication|string
54
     */
55
    public $BE_USER = '';
56
57
    /**
58
     * Database table to get the tree data from.
59
     * Leave blank if data comes from an array.
60
     *
61
     * @var string
62
     */
63
    public $table = 'pages';
64
65
    /**
66
     * Defines the field of $table which is the parent id field (like pid for table pages).
67
     *
68
     * @var string
69
     */
70
    public $parentField = 'pid';
71
72
    /**
73
     * WHERE clause used for selecting records for the tree. Is set by function init.
74
     *
75
     * @see init()
76
     * @var string
77
     */
78
    public $clause = '';
79
80
    /**
81
     * Field for ORDER BY. Is set by function init.
82
     *
83
     * @see init()
84
     * @var string
85
     */
86
    public $orderByFields = '';
87
88
    /**
89
     * Default set of fields selected from the tree table.
90
     * Make SURE that these fields names listed herein are actually possible to select from $this->table (if that variable is set to a TCA table name)
91
     *
92
     * @see addField()
93
     * @var array
94
     */
95
    public $fieldArray = [
96
        'uid',
97
        'pid',
98
        'title',
99
        'is_siteroot',
100
        'doktype',
101
        'nav_title',
102
        'mount_pid',
103
        'php_tree_stop',
104
        't3ver_state',
105
        'hidden',
106
        'starttime',
107
        'endtime',
108
        'fe_group',
109
        'module',
110
        'extendToSubpages',
111
        'nav_hide',
112
        't3ver_wsid',
113
    ];
114
115
    /**
116
     * List of other fields which are ALLOWED to set (here, based on the "pages" table!)
117
     *
118
     * @see addField()
119
     * @var string
120
     */
121
    public $defaultList = 'uid,pid,tstamp,sorting,deleted,perms_userid,perms_groupid,perms_user,perms_group,perms_everybody,crdate,cruser_id';
122
123
    /**
124
     * If 1, HTML code is also accumulated in ->tree array during rendering of the tree
125
     *
126
     * @var int
127
     */
128
    public $makeHTML = 1;
129
130
    // *********
131
    // Internal
132
    // *********
133
    // For record trees:
134
    // one-dim array of the uid's selected.
135
    /**
136
     * @var array
137
     */
138
    public $ids = [];
139
140
    // The hierarchy of element uids
141
    /**
142
     * @var array
143
     */
144
    public $ids_hierarchy = [];
145
146
    // The hierarchy of versioned element uids
147
    /**
148
     * @var array
149
     */
150
    public $orig_ids_hierarchy = [];
151
152
    // Temporary, internal array
153
    /**
154
     * @var array
155
     */
156
    public $buffer_idH = [];
157
158
    // For both types
159
    // Tree is accumulated in this variable
160
    /**
161
     * @var array
162
     */
163
    public $tree = [];
164
165
    /**
166
     * Constructor
167
     */
168
    public function __construct()
169
    {
170
        $this->title = $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'];
171
        $this->determineScriptUrl();
172
    }
173
174
    /**
175
     * Sets the script url depending on being a module or script request
176
     */
177
    protected function determineScriptUrl()
178
    {
179
        $this->thisScript = (string)GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoutePath(
180
            $GLOBALS['TYPO3_REQUEST']->getAttribute('route')->getPath()
181
        );
182
    }
183
184
    /**
185
     * @return string
186
     */
187
    protected function getThisScript()
188
    {
189
        return strpos($this->thisScript, '?') === false ? $this->thisScript . '?' : $this->thisScript . '&';
190
    }
191
192
    /**
193
     * Initialize the tree class. Needs to be overwritten
194
     *
195
     * @param string $clause Record WHERE clause
196
     * @param string $orderByFields Record ORDER BY field
197
     */
198
    public function init($clause = '', $orderByFields = '')
199
    {
200
        // Setting BE_USER by default
201
        $this->BE_USER = $GLOBALS['BE_USER'];
202
        // Setting clause
203
        if ($clause) {
204
            $this->clause = $clause;
205
        }
206
        if ($orderByFields) {
207
            $this->orderByFields = $orderByFields;
208
        }
209
    }
210
211
    /**
212
     * Adds a fieldname to the internal array ->fieldArray
213
     *
214
     * @param string $field Field name to
215
     * @param bool $noCheck If set, the fieldname will be set no matter what. Otherwise the field name must either be found as key in $GLOBALS['TCA'][$table]['columns'] or in the list ->defaultList
216
     */
217
    public function addField($field, $noCheck = false)
218
    {
219
        if ($noCheck || is_array($GLOBALS['TCA'][$this->table]['columns'][$field]) || GeneralUtility::inList($this->defaultList, $field)) {
220
            $this->fieldArray[] = $field;
221
        }
222
    }
223
224
    /**
225
     * Resets the tree, recs, ids, ids_hierarchy and orig_ids_hierarchy internal variables. Use it if you need it.
226
     */
227
    public function reset()
228
    {
229
        $this->tree = [];
230
        $this->ids = [];
231
        $this->ids_hierarchy = [];
232
        $this->orig_ids_hierarchy = [];
233
    }
234
235
    /*******************************************
236
     *
237
     * rendering parts
238
     *
239
     *******************************************/
240
    /**
241
     * Generate the plus/minus icon for the browsable tree.
242
     *
243
     * @param array $row Record for the entry
244
     * @param int $a The current entry number
245
     * @param int $c The total number of entries. If equal to $a, a "bottom" element is returned.
246
     * @param int $nextCount The number of sub-elements to the current element.
247
     * @param bool $isOpen The element was expanded to render subelements if this flag is set.
248
     * @return string Image tag with the plus/minus icon.
249
     * @internal
250
     * @see \TYPO3\CMS\Backend\Tree\View\PageTreeView::PMicon()
251
     */
252
    public function PMicon($row, $a, $c, $nextCount, $isOpen)
253
    {
254
        if ($nextCount) {
255
            return $this->PM_ATagWrap($row['uid'], $isOpen);
256
        }
257
        return '';
258
    }
259
260
    /**
261
     * Wrap the plus/minus icon in a link
262
     *
263
     * @param string $bMark If set, the link will have an anchor point (=$bMark) and a name attribute (=$bMark)
264
     * @param bool $isOpen
265
     * @return string Link-wrapped input string
266
     * @internal
267
     */
268
    public function PM_ATagWrap($bMark = '', $isOpen = false)
269
    {
270
        $anchor = $bMark ? '#' . $bMark : '';
271
        $name = $bMark ? ' name="' . $bMark . '"' : '';
272
        $aUrl = $this->getThisScript() . $anchor;
273
        return '<a class="list-tree-control ' . ($isOpen ? 'list-tree-control-open' : 'list-tree-control-closed') . '" href="' . htmlspecialchars($aUrl) . '"' . $name . '><i class="fa"></i></a>';
274
    }
275
276
    /**
277
     * Adds attributes to image tag.
278
     *
279
     * @param string $icon Icon image tag
280
     * @param string $attr Attributes to add, eg. ' border="0"'
281
     * @return string Image tag, modified with $attr attributes added.
282
     */
283
    public function addTagAttributes($icon, $attr)
284
    {
285
        return preg_replace('/ ?\\/?>$/', '', $icon) . ' ' . $attr . ' />';
286
    }
287
288
    /*******************************************
289
     *
290
     * tree handling
291
     *
292
     *******************************************/
293
    /**
294
     * Returns TRUE/FALSE if the next level for $id should be expanded - based on
295
     * data in $this->stored[][] and ->expandAll flag.
296
     * Used in subclasses
297
     *
298
     * @param int $id Record id/key
299
     * @return bool
300
     * @internal
301
     * @see \TYPO3\CMS\Backend\Tree\View\PageTreeView::expandNext()
302
     */
303
    public function expandNext($id)
304
    {
305
        return false;
306
    }
307
308
    /******************************
309
     *
310
     * Functions that might be overwritten by extended classes
311
     *
312
     ********************************/
313
    /**
314
     * Returns the root icon for a tree/mountpoint (defaults to the globe)
315
     *
316
     * @param array $rec Record for root.
317
     * @return string Icon image tag.
318
     */
319
    public function getRootIcon($rec)
320
    {
321
        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
322
        return $iconFactory->getIcon('apps-pagetree-root', Icon::SIZE_SMALL)->render();
323
    }
324
325
    /**
326
     * Get the icon markup for the row
327
     *
328
     * @param array $row The row to get the icon for
329
     * @return string The icon markup, wrapped into a span tag, with the records title as title attribute
330
     */
331
    public function getIcon($row): string
332
    {
333
        if (is_int($row)) {
0 ignored issues
show
introduced by
The condition is_int($row) is always false.
Loading history...
334
            trigger_error(
335
                'Calling ' . __METHOD__ . ' with argument $row containing the records uid is deprecated and will be removed in v12. Use the full row instead.',
336
                E_USER_DEPRECATED
337
            );
338
339
            $row = BackendUtility::getRecord($this->table, $row);
340
            if ($row === null) {
341
                return '';
342
            }
343
        }
344
        $title = $this->getTitleAttrib($row);
345
        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
346
        $icon = $row['is_siteroot'] ? $iconFactory->getIcon('apps-pagetree-folder-root', Icon::SIZE_SMALL) : $iconFactory->getIconForRecord($this->table, $row, Icon::SIZE_SMALL);
347
        return '<span title="' . $title . '">' . $icon->render() . '</span>';
348
    }
349
350
    /**
351
     * Returns the title for the input record. If blank, a "no title" label (localized) will be returned.
352
     * Do NOT htmlspecialchar the string from this function - has already been done.
353
     *
354
     * @param array $row The input row array (where the key "title" is used for the title)
355
     * @param int $titleLen Title length (30)
356
     * @return string The title.
357
     */
358
    public function getTitleStr($row, $titleLen = 30)
359
    {
360
        $title = htmlspecialchars(GeneralUtility::fixed_lgd_cs($row['title'], $titleLen));
361
        return trim($title) === '' ? '<em>[' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title')) . ']</em>' : $title;
362
    }
363
364
    /**
365
     * Returns the value for the image "title" attribute
366
     *
367
     * @param array $row The input row array (where the key "title" is used for the title)
368
     * @return string The attribute value (is htmlspecialchared() already)
369
     */
370
    public function getTitleAttrib($row)
371
    {
372
        return htmlspecialchars($row['title']);
373
    }
374
375
    /********************************
376
     *
377
     * tree data building
378
     *
379
     ********************************/
380
    /**
381
     * Fetches the data for the tree
382
     *
383
     * @param int $uid item id for which to select subitems (parent id)
384
     * @param int $depth Max depth (recursivity limit)
385
     * @param string $depthData HTML-code prefix for recursive calls.
386
     * @return int The count of items on the level
387
     */
388
    public function getTree($uid, $depth = 999, $depthData = '')
389
    {
390
        // Buffer for id hierarchy is reset:
391
        $this->buffer_idH = [];
392
        // Init vars
393
        $depth = (int)$depth;
394
        $HTML = '';
395
        $a = 0;
396
        $res = $this->getDataInit($uid);
397
        $c = $this->getDataCount($res);
398
        $crazyRecursionLimiter = 9999;
399
        $idH = [];
400
        // Traverse the records:
401
        while ($crazyRecursionLimiter > 0 && ($row = $this->getDataNext($res))) {
402
            /** @var array $row */
403
            if (!$this->getBackendUser()->isInWebMount($this->table === 'pages' ? $row : $row['pid'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getBackendUser()-...' ? $row : $row['pid']) of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
404
                // Current record is not within web mount => skip it
405
                continue;
406
            }
407
408
            $a++;
409
            $crazyRecursionLimiter--;
410
            $newID = $row['uid'];
411
            if ($newID == 0) {
412
                throw new \RuntimeException('Endless recursion detected: TYPO3 has detected an error in the database. Please fix it manually (e.g. using phpMyAdmin) and change the UID of ' . $this->table . ':0 to a new value. See https://forge.typo3.org/issues/16150 to get more information about a possible cause.', 1294586383);
413
            }
414
            // Reserve space.
415
            $this->tree[] = [];
416
            end($this->tree);
417
            // Get the key for this space
418
            $treeKey = key($this->tree);
419
            // Accumulate the id of the element in the internal arrays
420
            $this->ids[] = ($idH[$row['uid']]['uid'] = $row['uid']);
421
            $this->ids_hierarchy[$depth][] = $row['uid'];
422
            $this->orig_ids_hierarchy[$depth][] = $row['_ORIG_uid'] ?: $row['uid'];
423
424
            // Make a recursive call to the next level
425
            $nextLevelDepthData = $depthData . '<span class="treeline-icon treeline-icon-' . ($a === $c ? 'clear' : 'line') . '"></span>';
426
            $hasSub = $this->expandNext($newID) && !$row['php_tree_stop'];
427
            if ($depth > 1 && $hasSub) {
428
                $nextCount = $this->getTree($newID, $depth - 1, $nextLevelDepthData);
429
                if (!empty($this->buffer_idH)) {
430
                    $idH[$row['uid']]['subrow'] = $this->buffer_idH;
431
                }
432
                // Set "did expand" flag
433
                $isOpen = true;
434
            } else {
435
                $nextCount = $this->getCount($newID);
436
                // Clear "did expand" flag
437
                $isOpen = false;
438
            }
439
            // Set HTML-icons, if any:
440
            if ($this->makeHTML) {
441
                $HTML = $this->PMicon($row, $a, $c, $nextCount, $isOpen);
442
            }
443
            // Finally, add the row/HTML content to the ->tree array in the reserved key.
444
            $this->tree[$treeKey] = [
445
                'row' => $row,
446
                'HTML' => $HTML,
447
                'invertedDepth' => $depth,
448
                'depthData' => $depthData,
449
                'hasSub' => $nextCount && $hasSub,
450
                'isFirst' => $a === 1,
451
                'isLast' => $a === $c,
452
            ];
453
        }
454
455
        $this->getDataFree($res);
456
        $this->buffer_idH = $idH;
457
        return $c;
458
    }
459
460
    /********************************
461
     *
462
     * Data handling
463
     * Works with records and arrays
464
     *
465
     ********************************/
466
    /**
467
     * Returns the number of records having the parent id, $uid
468
     *
469
     * @param int $uid Id to count subitems for
470
     * @return int
471
     * @internal
472
     */
473
    public function getCount($uid)
474
    {
475
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
476
        $queryBuilder->getRestrictions()
477
                ->removeAll()
478
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
479
                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getBackendUser()->workspace));
480
        $count = $queryBuilder
481
                ->count('uid')
482
                ->from($this->table)
483
                ->where(
484
                    $queryBuilder->expr()->eq(
485
                        $this->parentField,
486
                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
487
                    ),
488
                    QueryHelper::stripLogicalOperatorPrefix($this->clause)
489
                )
490
                ->execute()
491
                ->fetchColumn();
492
493
        return (int)$count;
494
    }
495
496
    /**
497
     * Returns root record for uid (<=0)
498
     *
499
     * @return array Array with title/uid keys with values of $this->title/0 (zero)
500
     */
501
    public function getRootRecord()
502
    {
503
        return ['title' => $this->title, 'uid' => 0];
504
    }
505
506
    /**
507
     * Getting the tree data: Selecting/Initializing data pointer to items for a certain parent id.
508
     * For tables: This will make a database query to select all children to "parent"
509
     * For arrays: This will return key to the ->dataLookup array
510
     *
511
     * @param int $parentId parent item id
512
     *
513
     * @return mixed Data handle (Tables: An sql-resource, arrays: A parentId integer. -1 is returned if there were NO subLevel.)
514
     * @internal
515
     */
516
    public function getDataInit($parentId)
517
    {
518
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
519
        $queryBuilder->getRestrictions()
520
                ->removeAll()
521
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
522
                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getBackendUser()->workspace));
523
        $queryBuilder
524
                ->select(...$this->fieldArray)
525
                ->from($this->table)
526
                ->where(
527
                    $queryBuilder->expr()->eq(
528
                        $this->parentField,
529
                        $queryBuilder->createNamedParameter($parentId, \PDO::PARAM_INT)
530
                    ),
531
                    QueryHelper::stripLogicalOperatorPrefix($this->clause)
532
                );
533
534
        foreach (QueryHelper::parseOrderBy($this->orderByFields) as $orderPair) {
535
            [$fieldName, $order] = $orderPair;
536
            $queryBuilder->addOrderBy($fieldName, $order);
537
        }
538
539
        return $queryBuilder->execute();
540
    }
541
542
    /**
543
     * Getting the tree data: Counting elements in resource
544
     *
545
     * @param mixed $res Data handle
546
     * @return int number of items
547
     * @internal
548
     * @see getDataInit()
549
     */
550
    public function getDataCount(&$res)
551
    {
552
        return $res->rowCount();
553
    }
554
555
    /**
556
     * Getting the tree data: next entry
557
     *
558
     * @param mixed $res Data handle
559
     *
560
     * @return array|bool item data array OR FALSE if end of elements.
561
     * @internal
562
     * @see getDataInit()
563
     */
564
    public function getDataNext(&$res)
565
    {
566
        while ($row = $res->fetch()) {
567
            BackendUtility::workspaceOL($this->table, $row, $this->getBackendUser()->workspace, true);
568
            if (is_array($row)) {
569
                break;
570
            }
571
        }
572
        return $row;
573
    }
574
575
    /**
576
     * Getting the tree data: frees data handle
577
     *
578
     * @param mixed $res Data handle
579
     * @internal
580
     */
581
    public function getDataFree(&$res)
582
    {
583
        $res->closeCursor();
584
    }
585
586
    /**
587
     * @return LanguageService
588
     */
589
    protected function getLanguageService()
590
    {
591
        return $GLOBALS['LANG'];
592
    }
593
594
    /**
595
     * @return BackendUserAuthentication
596
     */
597
    protected function getBackendUser()
598
    {
599
        return $GLOBALS['BE_USER'];
600
    }
601
}
602