1<?php
2namespace TYPO3\CMS\Frontend\ContentObject;
3
4/*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17use TYPO3\CMS\Core\Database\RelationHandler;
18use TYPO3\CMS\Core\TimeTracker\TimeTracker;
19use TYPO3\CMS\Core\Utility\GeneralUtility;
20use TYPO3\CMS\Frontend\Category\Collection\CategoryCollection;
21use TYPO3\CMS\Frontend\Page\PageRepository;
22
23/**
24 * Contains RECORDS class object.
25 */
26class RecordsContentObject extends AbstractContentObject
27{
28    /**
29     * List of all items with table and uid information
30     *
31     * @var array
32     */
33    protected $itemArray = [];
34
35    /**
36     * List of all selected records with full data, arranged per table
37     *
38     * @var array
39     */
40    protected $data = [];
41
42    /**
43     * Rendering the cObject, RECORDS
44     *
45     * @param array $conf Array of TypoScript properties
46     * @return string Output
47     */
48    public function render($conf = [])
49    {
50        // Reset items and data
51        $this->itemArray = [];
52        $this->data = [];
53
54        $theValue = '';
55        $originalRec = $GLOBALS['TSFE']->currentRecord;
56        // If the currentRecord is set, we register, that this record has invoked this function.
57        // It's should not be allowed to do this again then!!
58        if ($originalRec) {
59            ++$GLOBALS['TSFE']->recordRegister[$originalRec];
60        }
61
62        $tables = isset($conf['tables.']) ? $this->cObj->stdWrap($conf['tables'], $conf['tables.']) : $conf['tables'];
63        if ($tables) {
64            $tablesArray = array_unique(GeneralUtility::trimExplode(',', $tables, true));
65            // Add tables which have a configuration (note that this may create duplicate entries)
66            if (is_array($conf['conf.'])) {
67                foreach ($conf['conf.'] as $key => $value) {
68                    if (substr($key, -1) !== '.' && !in_array($key, $tablesArray)) {
69                        $tablesArray[] = $key;
70                    }
71                }
72            }
73
74            // Get the data, depending on collection method.
75            // Property "source" is considered more precise and thus takes precedence over "categories"
76            $source = isset($conf['source.']) ? $this->cObj->stdWrap($conf['source'], $conf['source.']) : $conf['source'];
77            $categories = isset($conf['categories.']) ? $this->cObj->stdWrap($conf['categories'], $conf['categories.']) : $conf['categories'];
78            if ($source) {
79                $this->collectRecordsFromSource($source, $tablesArray);
80            } elseif ($categories) {
81                $relationField = isset($conf['categories.']['relation.']) ? $this->cObj->stdWrap($conf['categories.']['relation'], $conf['categories.']['relation.']) : $conf['categories.']['relation'];
82                $this->collectRecordsFromCategories($categories, $tablesArray, $relationField);
83            }
84            $itemArrayCount = count($this->itemArray);
85            if ($itemArrayCount > 0) {
86                /** @var ContentObjectRenderer $cObj */
87                $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
88                $cObj->setParent($this->cObj->data, $this->cObj->currentRecord);
89                $this->cObj->currentRecordNumber = 0;
90                $this->cObj->currentRecordTotal = $itemArrayCount;
91                foreach ($this->itemArray as $val) {
92                    $row = $this->data[$val['table']][$val['id']];
93                    // Perform overlays if necessary (records coming from category collections are already overlaid)
94                    if ($source) {
95                        // Versioning preview
96                        $this->getPageRepository()->versionOL($val['table'], $row);
97                        // Language overlay
98                        if (is_array($row)) {
99                            $row = $this->getPageRepository()->getLanguageOverlay($val['table'], $row);
100                        }
101                    }
102                    // Might be unset during the overlay process
103                    if (is_array($row)) {
104                        $dontCheckPid = isset($conf['dontCheckPid.']) ? $this->cObj->stdWrap($conf['dontCheckPid'], $conf['dontCheckPid.']) : $conf['dontCheckPid'];
105                        if (!$dontCheckPid) {
106                            $row = $this->cObj->checkPid($row['pid']) ? $row : '';
107                        }
108                        if ($row && !$GLOBALS['TSFE']->recordRegister[$val['table'] . ':' . $val['id']]) {
109                            $renderObjName = $conf['conf.'][$val['table']] ?: '<' . $val['table'];
110                            $renderObjKey = $conf['conf.'][$val['table']] ? 'conf.' . $val['table'] : '';
111                            $renderObjConf = $conf['conf.'][$val['table'] . '.'];
112                            $this->cObj->currentRecordNumber++;
113                            $cObj->parentRecordNumber = $this->cObj->currentRecordNumber;
114                            $GLOBALS['TSFE']->currentRecord = $val['table'] . ':' . $val['id'];
115                            $this->cObj->lastChanged($row['tstamp']);
116                            $cObj->start($row, $val['table']);
117                            $tmpValue = $cObj->cObjGetSingle($renderObjName, $renderObjConf, $renderObjKey);
118                            $theValue .= $tmpValue;
119                        }
120                    }
121                }
122            }
123        }
124        $wrap = isset($conf['wrap.']) ? $this->cObj->stdWrap($conf['wrap'], $conf['wrap.']) : $conf['wrap'];
125        if ($wrap) {
126            $theValue = $this->cObj->wrap($theValue, $wrap);
127        }
128        if (isset($conf['stdWrap.'])) {
129            $theValue = $this->cObj->stdWrap($theValue, $conf['stdWrap.']);
130        }
131        // Restore
132        $GLOBALS['TSFE']->currentRecord = $originalRec;
133        if ($originalRec) {
134            --$GLOBALS['TSFE']->recordRegister[$originalRec];
135        }
136        return $theValue;
137    }
138
139    /**
140     * Collects records according to the configured source
141     *
142     * @param string $source Source of records
143     * @param array $tables List of tables
144     */
145    protected function collectRecordsFromSource($source, array $tables)
146    {
147        /** @var RelationHandler $loadDB*/
148        $loadDB = GeneralUtility::makeInstance(RelationHandler::class);
149        $loadDB->setFetchAllFields(true);
150        $loadDB->start($source, implode(',', $tables));
151        foreach ($loadDB->tableArray as $table => $v) {
152            if (isset($GLOBALS['TCA'][$table])) {
153                $loadDB->additionalWhere[$table] = $this->getPageRepository()->enableFields($table);
154            }
155        }
156        $this->data = $loadDB->getFromDB();
157        reset($loadDB->itemArray);
158        $this->itemArray = $loadDB->itemArray;
159    }
160
161    /**
162     * Collects records for all selected tables and categories.
163     *
164     * @param string $selectedCategories Comma-separated list of categories
165     * @param array $tables List of tables
166     * @param string $relationField Name of the field containing the categories relation
167     */
168    protected function collectRecordsFromCategories($selectedCategories, array $tables, $relationField)
169    {
170        $selectedCategories = array_unique(GeneralUtility::intExplode(',', $selectedCategories, true));
171
172        // Loop on all selected tables
173        foreach ($tables as $table) {
174
175            // Get the records for each selected category
176            $tableRecords = [];
177            $categoriesPerRecord = [];
178            foreach ($selectedCategories as $aCategory) {
179                try {
180                    $collection = CategoryCollection::load(
181                        $aCategory,
182                        true,
183                        $table,
184                        $relationField
185                    );
186                    if ($collection->count() > 0) {
187                        // Add items to the collection of records for the current table
188                        foreach ($collection as $item) {
189                            $tableRecords[$item['uid']] = $item;
190                            // Keep track of all categories a given item belongs to
191                            if (!isset($categoriesPerRecord[$item['uid']])) {
192                                $categoriesPerRecord[$item['uid']] = [];
193                            }
194                            $categoriesPerRecord[$item['uid']][] = $aCategory;
195                        }
196                    }
197                } catch (\Exception $e) {
198                    $message = sprintf(
199                        'Could not get records for category id %d. Error: %s (%d)',
200                        $aCategory,
201                        $e->getMessage(),
202                        $e->getCode()
203                    );
204                    $this->getTimeTracker()->setTSlogMessage($message, 2);
205                }
206            }
207            // Store the resulting records into the itemArray and data results array
208            if (!empty($tableRecords)) {
209                $this->data[$table] = [];
210                foreach ($tableRecords as $record) {
211                    $this->itemArray[] = [
212                        'id' => $record['uid'],
213                        'table' => $table
214                    ];
215                    // Add to the record the categories it belongs to
216                    $record['_categories'] = implode(',', $categoriesPerRecord[$record['uid']]);
217                    $this->data[$table][$record['uid']] = $record;
218                }
219            }
220        }
221    }
222
223    /**
224     * @return TimeTracker
225     */
226    protected function getTimeTracker()
227    {
228        return GeneralUtility::makeInstance(TimeTracker::class);
229    }
230
231    /**
232     * @return PageRepository
233     */
234    protected function getPageRepository(): PageRepository
235    {
236        return $GLOBALS['TSFE']->sys_page ?: GeneralUtility::makeInstance(PageRepository::class);
237    }
238}
239