Passed
Push — master ( d67a70...8e4d7f )
by
unknown
28:10 queued 12:52
created

StandardContentPreviewRenderer::renderText()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 5
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Backend\Preview;
19
20
use Psr\Log\LoggerAwareInterface;
21
use Psr\Log\LoggerAwareTrait;
22
use TYPO3\CMS\Backend\Routing\UriBuilder;
23
use TYPO3\CMS\Backend\Utility\BackendUtility;
24
use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem;
25
use TYPO3\CMS\Backend\View\PageLayoutView;
26
use TYPO3\CMS\Backend\View\PageLayoutViewDrawFooterHookInterface;
27
use TYPO3\CMS\Backend\View\PageLayoutViewDrawItemHookInterface;
28
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
29
use TYPO3\CMS\Core\Core\Environment;
30
use TYPO3\CMS\Core\Imaging\Icon;
31
use TYPO3\CMS\Core\Imaging\IconFactory;
32
use TYPO3\CMS\Core\Localization\LanguageService;
33
use TYPO3\CMS\Core\Service\FlexFormService;
34
use TYPO3\CMS\Core\Utility\GeneralUtility;
35
use TYPO3\CMS\Fluid\View\StandaloneView;
36
37
/**
38
 * Class StandardContentPreviewRenderer
39
 *
40
 * Legacy preview rendering refactored from PageLayoutView.
41
 * Provided as default preview rendering mechanism via
42
 * StandardPreviewRendererResolver which detects the renderer
43
 * based on TCA configuration.
44
 *
45
 * Can be replaced and/or subclassed by custom implementations
46
 * by changing this TCA configuration.
47
 *
48
 * See also PreviewRendererInterface documentation.
49
 */
50
class StandardContentPreviewRenderer implements PreviewRendererInterface, LoggerAwareInterface
51
{
52
    use LoggerAwareTrait;
53
54
    /**
55
     * Menu content types defined by TYPO3
56
     *
57
     * @var string[]
58
     */
59
    private const MENU_CONTENT_TYPES = [
60
        'menu_abstract',
61
        'menu_categorized_content',
62
        'menu_categorized_pages',
63
        'menu_pages',
64
        'menu_recently_updated',
65
        'menu_related_pages',
66
        'menu_section',
67
        'menu_section_pages',
68
        'menu_sitemap',
69
        'menu_sitemap_pages',
70
        'menu_subpages',
71
    ];
72
73
    public function renderPageModulePreviewHeader(GridColumnItem $item): string
74
    {
75
        $record = $item->getRecord();
76
        $itemLabels = $item->getContext()->getItemLabels();
77
78
        $outHeader = '';
79
80
        if ($record['header']) {
81
            $infoArr = [];
82
            $this->getProcessedValue($item, 'header_position,header_layout,header_link', $infoArr);
83
            $hiddenHeaderNote = '';
84
            // If header layout is set to 'hidden', display an accordant note:
85
            if ($record['header_layout'] == 100) {
86
                $hiddenHeaderNote = ' <em>[' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:header_layout.I.6')) . ']</em>';
87
            }
88
            $outHeader = $record['date']
89
                ? htmlspecialchars($itemLabels['date'] . ' ' . BackendUtility::date($record['date'])) . '<br />'
90
                : '';
91
            $outHeader .= '<strong>' . $this->linkEditContent($this->renderText($record['header']), $record)
92
                . $hiddenHeaderNote . '</strong><br />';
93
        }
94
95
        return $outHeader;
96
    }
97
98
    public function renderPageModulePreviewContent(GridColumnItem $item): string
99
    {
100
        $out = '';
101
        $record = $item->getRecord();
102
103
        $contentTypeLabels = $item->getContext()->getContentTypeLabels();
104
        $languageService = $this->getLanguageService();
105
106
        $drawItem = true;
107
        $hookPreviewContent = '';
108
        // Hook: Render an own preview of a record
109
        if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem'])) {
110
            $pageLayoutView = PageLayoutView::createFromPageLayoutContext($item->getContext());
111
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem'] ?? [] as $className) {
112
                $hookObject = GeneralUtility::makeInstance($className);
113
                if (!$hookObject instanceof PageLayoutViewDrawItemHookInterface) {
114
                    throw new \UnexpectedValueException($className . ' must implement interface ' . PageLayoutViewDrawItemHookInterface::class, 1582574553);
115
                }
116
                $hookObject->preProcess($pageLayoutView, $drawItem, $previewHeader, $hookPreviewContent, $record);
117
            }
118
            $item->setRecord($record);
119
        }
120
121
        if (!$drawItem) {
122
            return $hookPreviewContent;
123
        }
124
        // Check if a Fluid-based preview template was defined for this CType
125
        // and render it via Fluid. Possible option:
126
        // mod.web_layout.tt_content.preview.media = EXT:site_mysite/Resources/Private/Templates/Preview/Media.html
127
        $infoArr = [];
128
        $this->getProcessedValue($item, 'header_position,header_layout,header_link', $infoArr);
129
        $tsConfig = BackendUtility::getPagesTSconfig($record['pid'])['mod.']['web_layout.']['tt_content.']['preview.'] ?? [];
130
        if (!empty($tsConfig[$record['CType']]) || !empty($tsConfig[$record['CType'] . '.'])) {
131
            $fluidPreview = $this->renderContentElementPreviewFromFluidTemplate($record);
132
            if ($fluidPreview !== null) {
133
                return $fluidPreview;
134
            }
135
        }
136
137
        // Draw preview of the item depending on its CType
138
        switch ($record['CType']) {
139
            case 'header':
140
                if ($record['subheader']) {
141
                    $out .= $this->linkEditContent($this->renderText($record['subheader']), $record) . '<br />';
142
                }
143
                break;
144
            case 'uploads':
145
                if ($record['media']) {
146
                    $out .= $this->linkEditContent($this->getThumbCodeUnlinked($record, 'tt_content', 'media'), $record) . '<br />';
147
                }
148
                break;
149
            case 'shortcut':
150
                if (!empty($record['records'])) {
151
                    $shortcutContent = [];
152
                    $recordList = explode(',', $record['records']);
153
                    foreach ($recordList as $recordIdentifier) {
154
                        $split = BackendUtility::splitTable_Uid($recordIdentifier);
155
                        $tableName = empty($split[0]) ? 'tt_content' : $split[0];
156
                        $shortcutRecord = BackendUtility::getRecord($tableName, $split[1]);
0 ignored issues
show
Bug introduced by
$split[1] of type string is incompatible with the type integer expected by parameter $uid of TYPO3\CMS\Backend\Utilit...endUtility::getRecord(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

156
                        $shortcutRecord = BackendUtility::getRecord($tableName, /** @scrutinizer ignore-type */ $split[1]);
Loading history...
157
                        if (is_array($shortcutRecord)) {
158
                            $icon = $this->getIconFactory()->getIconForRecord($tableName, $shortcutRecord, Icon::SIZE_SMALL)->render();
159
                            $icon = BackendUtility::wrapClickMenuOnIcon(
160
                                $icon,
161
                                $tableName,
162
                                $shortcutRecord['uid'],
163
                                '1'
164
                            );
165
                            $shortcutContent[] = $icon
166
                                . htmlspecialchars(BackendUtility::getRecordTitle($tableName, $shortcutRecord));
167
                        }
168
                    }
169
                    $out .= implode('<br />', $shortcutContent) . '<br />';
170
                }
171
                break;
172
            case 'list':
173
                $hookOut = '';
174
                if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info'])) {
175
                    $pageLayoutView = PageLayoutView::createFromPageLayoutContext($item->getContext());
176
                    $_params = ['pObj' => &$pageLayoutView, 'row' => $record];
177
                    foreach (
178
                        $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info'][$record['list_type']] ??
179
                        $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info']['_DEFAULT'] ??
180
                        [] as $_funcRef
181
                    ) {
182
                        $hookOut .= GeneralUtility::callUserFunction($_funcRef, $_params, $pageLayoutView);
183
                    }
184
                }
185
186
                if ((string)$hookOut !== '') {
187
                    $out .= $hookOut;
188
                } elseif (!empty($record['list_type'])) {
189
                    $label = BackendUtility::getLabelFromItemListMerged($record['pid'], 'tt_content', 'list_type', $record['list_type']);
190
                    if (!empty($label)) {
191
                        $out .= $this->linkEditContent('<strong>' . htmlspecialchars($languageService->sL($label)) . '</strong>', $record) . '<br />';
192
                    } else {
193
                        $message = sprintf($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'), $record['list_type']);
194
                        $out .= '<span class="label label-warning">' . htmlspecialchars($message) . '</span>';
195
                    }
196
                } elseif (!empty($record['select_key'])) {
197
                    $out .= htmlspecialchars($languageService->sL(BackendUtility::getItemLabel('tt_content', 'select_key')))
198
                        . ' ' . htmlspecialchars($record['select_key']) . '<br />';
199
                } else {
200
                    $out .= '<strong>' . $languageService->getLL('noPluginSelected') . '</strong>';
201
                }
202
                $out .= htmlspecialchars($languageService->sL(BackendUtility::getLabelFromItemlist('tt_content', 'pages', $record['pages']))) . '<br />';
203
                break;
204
            default:
205
                $contentTypeLabel = (string)($contentTypeLabels[$record['CType']] ?? '');
206
                if ($contentTypeLabel === '') {
207
                    $message = sprintf(
208
                        $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'),
209
                        $record['CType']
210
                    );
211
                    $out .= '<span class="label label-warning">' . htmlspecialchars($message) . '</span>';
212
                    break;
213
                }
214
                // Handle menu content types
215
                if (in_array($record['CType'], self::MENU_CONTENT_TYPES, true)) {
216
                    $out .= $this->linkEditContent('<strong>' . htmlspecialchars($contentTypeLabel) . '</strong>', $record);
217
                    if ($record['CType'] !== 'menu_sitemap' && (($record['pages'] ?? false) || ($record['selected_categories'] ?? false))) {
218
                        // Show pages/categories if menu type is not "Sitemap"
219
                        $out .= ':' . $this->linkEditContent($this->generateListForMenuContentTypes($record), $record) . '<br />';
220
                    }
221
                    break;
222
                }
223
                $out .= $this->linkEditContent('<strong>' . htmlspecialchars($contentTypeLabel) . '</strong>', $record) . '<br />';
224
                if ($record['bodytext']) {
225
                    $out .= $this->linkEditContent($this->renderText($record['bodytext']), $record) . '<br />';
226
                }
227
                if ($record['image']) {
228
                    $out .= $this->linkEditContent($this->getThumbCodeUnlinked($record, 'tt_content', 'image'), $record) . '<br />';
229
                }
230
        }
231
232
        return $out;
233
    }
234
235
    /**
236
     * Render a footer for the record
237
     *
238
     * @param GridColumnItem $item
239
     * @return string
240
     */
241
    public function renderPageModulePreviewFooter(GridColumnItem $item): string
242
    {
243
        $content = '';
244
        $info = [];
245
        $record = $item->getRecord();
246
        $this->getProcessedValue($item, 'starttime,endtime,fe_group,space_before_class,space_after_class', $info);
247
248
        if (!empty($GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']) && !empty($record[$GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']])) {
249
            $info[] = htmlspecialchars($record[$GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']]);
250
        }
251
252
        // Call drawFooter hooks
253
        if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawFooter'])) {
254
            $pageLayoutView = PageLayoutView::createFromPageLayoutContext($item->getContext());
255
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawFooter'] ?? [] as $className) {
256
                $hookObject = GeneralUtility::makeInstance($className);
257
                if (!$hookObject instanceof PageLayoutViewDrawFooterHookInterface) {
258
                    throw new \UnexpectedValueException($className . ' must implement interface ' . PageLayoutViewDrawFooterHookInterface::class, 1582574541);
259
                }
260
                $hookObject->preProcess($pageLayoutView, $info, $record);
261
            }
262
            $item->setRecord($record);
263
        }
264
265
        if (!empty($info)) {
266
            $content = implode('<br>', $info);
267
        }
268
269
        if (!empty($content)) {
270
            $content = '<div class="t3-page-ce-footer">' . $content . '</div>';
271
        }
272
273
        return $content;
274
    }
275
276
    public function wrapPageModulePreview(string $previewHeader, string $previewContent, GridColumnItem $item): string
277
    {
278
        $content = '<span class="exampleContent">' . $previewHeader . $previewContent . '</span>';
279
        if ($item->isDisabled()) {
280
            return '<span class="text-muted">' . $content . '</span>';
281
        }
282
        return $content;
283
    }
284
285
    protected function getProcessedValue(GridColumnItem $item, string $fieldList, array &$info): void
286
    {
287
        $itemLabels = $item->getContext()->getItemLabels();
288
        $record = $item->getRecord();
289
        $fieldArr = explode(',', $fieldList);
290
        foreach ($fieldArr as $field) {
291
            if ($record[$field]) {
292
                $info[] = '<strong>' . htmlspecialchars((string)($itemLabels[$field] ?? '')) . '</strong> '
293
                    . htmlspecialchars(BackendUtility::getProcessedValue('tt_content', $field, $record[$field]) ?? '');
294
            }
295
        }
296
    }
297
298
    protected function renderContentElementPreviewFromFluidTemplate(array $row): ?string
299
    {
300
        $tsConfig = BackendUtility::getPagesTSconfig($row['pid'])['mod.']['web_layout.']['tt_content.']['preview.'] ?? [];
301
        $fluidTemplateFile = '';
302
303
        if ($row['CType'] === 'list' && !empty($row['list_type'])
304
            && !empty($tsConfig['list.'][$row['list_type']])
305
        ) {
306
            $fluidTemplateFile = $tsConfig['list.'][$row['list_type']];
307
        } elseif (!empty($tsConfig[$row['CType']])) {
308
            $fluidTemplateFile = $tsConfig[$row['CType']];
309
        }
310
311
        if ($fluidTemplateFile) {
312
            $fluidTemplateFile = GeneralUtility::getFileAbsFileName($fluidTemplateFile);
313
            if ($fluidTemplateFile) {
314
                try {
315
                    $view = GeneralUtility::makeInstance(StandaloneView::class);
316
                    $view->setTemplatePathAndFilename($fluidTemplateFile);
317
                    $view->assignMultiple($row);
318
                    if (!empty($row['pi_flexform'])) {
319
                        $flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
320
                        $view->assign('pi_flexform_transformed', $flexFormService->convertFlexFormContentToArray($row['pi_flexform']));
321
                    }
322
                    return $view->render();
323
                } catch (\Exception $e) {
324
                    $this->logger->warning(sprintf(
325
                        'The backend preview for content element %d can not be rendered using the Fluid template file "%s": %s',
326
                        $row['uid'],
327
                        $fluidTemplateFile,
328
                        $e->getMessage()
329
                    ));
330
331
                    if ($GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] && $this->getBackendUser()->isAdmin()) {
332
                        $view = GeneralUtility::makeInstance(StandaloneView::class);
333
                        $view->assign('error', [
334
                            'message' => str_replace(Environment::getProjectPath(), '', $e->getMessage()),
335
                            'title' => 'Error while rendering FluidTemplate preview using ' . str_replace(Environment::getProjectPath(), '', $fluidTemplateFile),
336
                        ]);
337
                        $view->setTemplateSource('<f:be.infobox title="{error.title}" state="2">{error.message}</f:be.infobox>');
338
                        return $view->render();
339
                    }
340
                }
341
            }
342
        }
343
        return null;
344
    }
345
346
    /**
347
     * Create thumbnail code for record/field but not linked
348
     *
349
     * @param mixed[] $row Record array
350
     * @param string $table Table (record is from)
351
     * @param string $field Field name for which thumbnail are to be rendered.
352
     * @return string HTML for thumbnails, if any.
353
     */
354
    protected function getThumbCodeUnlinked($row, $table, $field): string
355
    {
356
        return BackendUtility::thumbCode($row, $table, $field, '', '', null, 0, '', '', false);
357
    }
358
359
    /**
360
     * Processing of larger amounts of text (usually from RTE/bodytext fields) with word wrapping etc.
361
     *
362
     * @param string $input Input string
363
     * @return string Output string
364
     */
365
    protected function renderText(string $input): string
366
    {
367
        $input = strip_tags($input);
368
        $input = GeneralUtility::fixed_lgd_cs($input, 1500);
369
        return nl2br(htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8', false));
370
    }
371
372
    /**
373
     * Generates a list of selected pages or categories for the menu content types
374
     *
375
     * @param array $record row from pages
376
     * @return string
377
     */
378
    protected function generateListForMenuContentTypes(array $record): string
379
    {
380
        $table = 'pages';
381
        $field = 'pages';
382
        // get categories instead of pages
383
        if (strpos($record['CType'], 'menu_categorized') !== false) {
384
            $table = 'sys_category';
385
            $field = 'selected_categories';
386
        }
387
        if (trim($record[$field]) === '') {
388
            return '';
389
        }
390
        $content = '';
391
        $uidList = explode(',', $record[$field]);
392
        foreach ($uidList as $uid) {
393
            $uid = (int)$uid;
394
            $pageRecord = BackendUtility::getRecord($table, $uid, 'title');
395
            $content .= '<br>' . htmlspecialchars($pageRecord['title']) . ' (' . $uid . ')';
396
        }
397
        return $content;
398
    }
399
400
    /**
401
     * Will create a link on the input string and possibly a big button after the string which links to editing in the RTE.
402
     * Used for content element content displayed so the user can click the content / "Edit in Rich Text Editor" button
403
     *
404
     * @param string $linkText String to link. Must be prepared for HTML output.
405
     * @param array $row The row.
406
     * @return string If the whole thing was editable $str is return with link around. Otherwise just $str.
407
     */
408
    protected function linkEditContent(string $linkText, $row): string
409
    {
410
        $backendUser = $this->getBackendUser();
411
        if ($backendUser->check('tables_modify', 'tt_content') && $backendUser->recordEditAccessInternals('tt_content', $row)) {
412
            $urlParameters = [
413
                'edit' => [
414
                    'tt_content' => [
415
                        $row['uid'] => 'edit'
416
                    ]
417
                ],
418
                'returnUrl' => $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri() . '#element-tt_content-' . $row['uid']
419
            ];
420
            $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
421
            $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
422
            return '<a href="' . htmlspecialchars($url) . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">' . $linkText . '</a>';
423
        }
424
        return $linkText;
425
    }
426
427
    protected function getBackendUser(): BackendUserAuthentication
428
    {
429
        return $GLOBALS['BE_USER'];
430
    }
431
432
    protected function getLanguageService(): LanguageService
433
    {
434
        return $GLOBALS['LANG'];
435
    }
436
437
    protected function getIconFactory(): IconFactory
438
    {
439
        return GeneralUtility::makeInstance(IconFactory::class);
440
    }
441
}
442