[TASK] Unify TypoScript-related helper methods
[Packages/TYPO3.CMS.git] / typo3 / sysext / css_styled_content / Classes / Controller / CssStyledContentController.php
1 <?php
2 namespace TYPO3\CMS\CssStyledContent\Controller;
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
17 use TYPO3\CMS\Core\TypoScript\TypoScriptService;
18 use TYPO3\CMS\Core\Utility\GeneralUtility;
19 use TYPO3\CMS\Core\Utility\MathUtility;
20
21 /**
22 * Plugin class - instantiated from TypoScript.
23 * Rendering some content elements from tt_content table.
24 */
25 class CssStyledContentController extends \TYPO3\CMS\Frontend\Plugin\AbstractPlugin
26 {
27 /**
28 * Same as class name
29 *
30 * @var string
31 */
32 public $prefixId = 'tx_cssstyledcontent_pi1';
33
34 /**
35 * Path to this script relative to the extension dir.
36 *
37 * @var string
38 */
39 public $scriptRelPath = 'Classes/Controller/CssStyledContentController.php';
40
41 /**
42 * The extension key
43 *
44 * @var string
45 */
46 public $extKey = 'css_styled_content';
47
48 /**
49 * @var array
50 */
51 public $conf = [];
52
53 /***********************************
54 * Rendering of Content Elements:
55 ***********************************/
56
57 /**
58 * Rendering the "Table" type content element, called from TypoScript (tt_content.table.20)
59 *
60 * @param string $content Content input. Not used, ignore.
61 * @param array $conf TypoScript configuration
62 * @return string HTML output.
63 */
64 public function render_table($content, $conf)
65 {
66 // Look for hook before running default code for function
67 if ($hookObj = $this->hookRequest('render_table')) {
68 return $hookObj->render_table($content, $conf);
69 } else {
70 // Init FlexForm configuration
71 $this->pi_initPIflexForm();
72 // Get bodytext field content
73 $field = isset($conf['field']) && trim($conf['field']) ? trim($conf['field']) : 'bodytext';
74 $content = trim($this->cObj->data[$field]);
75 if ($content === '') {
76 return '';
77 }
78 // Get configuration
79 $caption = trim($this->cObj->data['table_caption']);
80 $useTfoot = trim($this->cObj->data['table_tfoot']);
81 $headerPosition = trim($this->cObj->data['table_header_position']);
82 switch ($headerPosition) {
83 case '1':
84 $headerPos = 'top';
85 break;
86 case '2':
87 $headerPos = 'left';
88 break;
89 default:
90 $headerPos = '';
91 break;
92 }
93 $tableClass = trim($this->cObj->data['table_class']);
94 $delimiter = trim($this->cObj->data['table_delimiter']);
95 if ($delimiter) {
96 $delimiter = chr((int)$delimiter);
97 } else {
98 $delimiter = '|';
99 }
100 $quotedInput = trim($this->cObj->data['table_enclosure']);
101 if ($quotedInput) {
102 $quotedInput = chr((int)$quotedInput);
103 } else {
104 $quotedInput = '';
105 }
106 // Generate id prefix for accessible header
107 $headerScope = $headerPos === 'top' ? 'col' : 'row';
108 $headerIdPrefix = $headerScope . $this->cObj->data['uid'] . '-';
109 // Split into single lines (will become table-rows):
110 $rows = GeneralUtility::trimExplode(LF, $content);
111 reset($rows);
112 // Find number of columns to render:
113 $cols = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange(
114 $this->cObj->data['cols'] ? $this->cObj->data['cols'] : count(str_getcsv(current($rows), $delimiter, $quotedInput)),
115 0,
116 100
117 );
118 // Traverse rows (rendering the table here)
119 $rCount = count($rows);
120 foreach ($rows as $k => $v) {
121 $cells = str_getcsv($v, $delimiter, $quotedInput);
122 $newCells = [];
123 for ($a = 0; $a < $cols; $a++) {
124 if (trim($cells[$a]) === '') {
125 $cells[$a] = ' ';
126 }
127 $cells[$a] = preg_replace('|<br */?>|i', LF, $cells[$a]);
128 if ($headerPos === 'top' && !$k || $headerPos === 'left' && !$a) {
129 $scope = ' scope="' . $headerScope . '"';
130 $scope .= ' id="' . $headerIdPrefix . ($headerScope === 'col' ? $a : $k) . '"';
131 $newCells[$a] = '<th' . $scope . '>' . $this->cObj->stdWrap($cells[$a], $conf['innerStdWrap.']) . '</th>';
132 } else {
133 if (empty($headerPos)) {
134 $accessibleHeader = '';
135 } else {
136 $accessibleHeader = ' headers="' . $headerIdPrefix . ($headerScope === 'col' ? $a : $k) . '"';
137 }
138 $newCells[$a] = '<td' . $accessibleHeader . '>' . $this->cObj->stdWrap($cells[$a], $conf['innerStdWrap.']) . '</td>';
139 }
140 }
141 $rows[$k] = '<tr>' . implode('', $newCells) . '</tr>';
142 }
143 $addTbody = 0;
144 $tableContents = '';
145 if ($caption) {
146 $tableContents .= '
147 <caption>' . $caption . '</caption>';
148 }
149 if ($headerPos === 'top' && $rows[0]) {
150 $tableContents .= '<thead>' . $rows[0] . '</thead>';
151 unset($rows[0]);
152 $addTbody = 1;
153 }
154 if ($useTfoot) {
155 $tableContents .= '<tfoot>' . $rows[$rCount - 1] . '</tfoot>';
156 unset($rows[$rCount - 1]);
157 $addTbody = 1;
158 }
159 $tmpTable = implode('', $rows);
160 if ($addTbody) {
161 $tmpTable = '<tbody>' . $tmpTable . '</tbody>';
162 }
163 $tableContents .= $tmpTable;
164 // Set header type:
165 $type = (int)$this->cObj->data['layout'];
166 // Table tag params.
167 $tableTagParams = [];
168 $tableTagParams['class'] = 'contenttable contenttable-' . $type . ($tableClass ? ' contenttable-' . $tableClass : '');
169 // Compile table output:
170 $out = '<table ' . GeneralUtility::implodeAttributes($tableTagParams) . '>' . $tableContents . '</table>';
171 // Return value
172 return $out;
173 }
174 }
175
176 /**
177 * Returns an array containing width relations for $colCount columns.
178 *
179 * Tries to use "colRelations" setting given by TS.
180 * uses "1:1" column relations by default.
181 *
182 * @param array $conf TS configuration for img
183 * @param int $colCount number of columns
184 * @return array
185 */
186 protected function getImgColumnRelations($conf, $colCount)
187 {
188 $relations = [];
189 $equalRelations = array_fill(0, $colCount, 1);
190 $colRelationsTypoScript = trim($this->cObj->stdWrap($conf['colRelations'], $conf['colRelations.']));
191 if ($colRelationsTypoScript) {
192 // Try to use column width relations given by TS
193 $relationParts = explode(':', $colRelationsTypoScript);
194 // Enough columns defined?
195 if (count($relationParts) >= $colCount) {
196 $out = [];
197 for ($a = 0; $a < $colCount; $a++) {
198 $currentRelationValue = (int)$relationParts[$a];
199 if ($currentRelationValue >= 1) {
200 $out[$a] = $currentRelationValue;
201 } else {
202 GeneralUtility::devLog('colRelations used with a value smaller than 1 therefore colRelations setting is ignored.', $this->extKey, 2);
203 unset($out);
204 break;
205 }
206 }
207 if (max($out) / min($out) <= 10) {
208 $relations = $out;
209 } else {
210 GeneralUtility::devLog(
211 'The difference in size between the largest and smallest colRelation was not within' .
212 ' a factor of ten therefore colRelations setting is ignored..',
213 $this->extKey,
214 2
215 );
216 }
217 }
218 }
219 return $relations ?: $equalRelations;
220 }
221
222 /**
223 * Returns an array containing the image widths for an image row with $colCount columns.
224 *
225 * @param array $conf TS configuration of img
226 * @param int $colCount number of columns
227 * @param int $netW max usable width for images (without spaces and borders)
228 * @return array
229 */
230 protected function getImgColumnWidths($conf, $colCount, $netW)
231 {
232 $columnWidths = [];
233 $colRelations = $this->getImgColumnRelations($conf, $colCount);
234 $accumWidth = 0;
235 $accumDesiredWidth = 0;
236 $relUnitCount = array_sum($colRelations);
237 for ($a = 0; $a < $colCount; $a++) {
238 // This much width is available for the remaining images in this row (int)
239 $availableWidth = $netW - $accumWidth;
240 // Theoretical width of resized image. (float)
241 $desiredWidth = $netW / $relUnitCount * $colRelations[$a];
242 // Add this width. $accumDesiredWidth becomes the desired horizontal position
243 $accumDesiredWidth += $desiredWidth;
244 // Calculate width by comparing actual and desired horizontal position.
245 // this evenly distributes rounding errors across all images in this row.
246 $suggestedWidth = round($accumDesiredWidth - $accumWidth);
247 // finalImgWidth may not exceed $availableWidth
248 $finalImgWidth = (int)min($availableWidth, $suggestedWidth);
249 $accumWidth += $finalImgWidth;
250 $columnWidths[$a] = $finalImgWidth;
251 }
252 return $columnWidths;
253 }
254
255 /**
256 * Rendering the text w/ image content element, called from TypoScript (tt_content.textpic.20)
257 *
258 * @param string $content Content input. Not used, ignore.
259 * @param array $conf TypoScript configuration. See TSRef "IMGTEXT". This function aims to be compatible.
260 * @return string HTML output.
261 */
262 public function render_textpic($content, $conf)
263 {
264 // Look for hook before running default code for function
265 if (method_exists($this, 'hookRequest') && ($hookObj = $this->hookRequest('render_textpic'))) {
266 return $hookObj->render_textpic($content, $conf);
267 }
268 $renderMethod = $this->cObj->stdWrap($conf['renderMethod'], $conf['renderMethod.']);
269 // Render using the default IMGTEXT code (table-based)
270 if (!$renderMethod || $renderMethod === 'table') {
271 return $this->cObj->cObjGetSingle('IMGTEXT', $conf);
272 }
273
274 $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
275
276 $restoreRegisters = false;
277 if (isset($conf['preRenderRegisters.'])) {
278 $restoreRegisters = true;
279 $this->cObj->cObjGetSingle('LOAD_REGISTER', $conf['preRenderRegisters.']);
280 }
281 // Specific configuration for the chosen rendering method
282 if (is_array($conf['rendering.'][$renderMethod . '.'])) {
283 $conf = array_replace_recursive($conf, $conf['rendering.'][$renderMethod . '.']);
284 }
285 // Image or Text with Image?
286 if (is_array($conf['text.'])) {
287 $content = $this->cObj->stdWrap($this->cObj->cObjGet($conf['text.'], 'text.'), $conf['text.']);
288 }
289 $imgList = trim($this->cObj->stdWrap($conf['imgList'], $conf['imgList.']));
290 if (!$imgList) {
291 // No images, that's easy
292 if ($restoreRegisters) {
293 $this->cObj->cObjGetSingle('RESTORE_REGISTER', []);
294 }
295 return $content;
296 }
297 $imgs = GeneralUtility::trimExplode(',', $imgList, true);
298 if (empty($imgs)) {
299 // The imgList was not empty but did only contain empty values
300 if ($restoreRegisters) {
301 $this->cObj->cObjGetSingle('RESTORE_REGISTER', []);
302 }
303 return $content;
304 }
305 $imgStart = (int)$this->cObj->stdWrap($conf['imgStart'], $conf['imgStart.']);
306 $imgCount = count($imgs) - $imgStart;
307 $imgMax = (int)$this->cObj->stdWrap($conf['imgMax'], $conf['imgMax.']);
308 if ($imgMax) {
309 $imgCount = MathUtility::forceIntegerInRange($imgCount, 0, $imgMax);
310 }
311 $imgPath = $this->cObj->stdWrap($conf['imgPath'], $conf['imgPath.']);
312 // Does we need to render a "global caption" (below the whole image block)?
313 $renderGlobalCaption = !$conf['captionSplit'] && !$conf['imageTextSplit'] && is_array($conf['caption.']);
314 if ($imgCount == 1) {
315 // If we just have one image, the caption relates to the image, so it is not "global"
316 $renderGlobalCaption = false;
317 }
318 $imgListContainsReferenceUids = (bool)(isset($conf['imgListContainsReferenceUids.'])
319 ? $this->cObj->stdWrap($conf['imgListContainsReferenceUids'], $conf['imgListContainsReferenceUids.'])
320 : $conf['imgListContainsReferenceUids']);
321 // Use the calculated information (amount of images, if global caption is wanted) to choose a different rendering method for the images-block
322 $this->frontendController->register['imageCount'] = $imgCount;
323 $this->frontendController->register['renderGlobalCaption'] = $renderGlobalCaption;
324 $fallbackRenderMethod = '';
325 if ($conf['fallbackRendering']) {
326 $fallbackRenderMethod = $this->cObj->cObjGetSingle($conf['fallbackRendering'], $conf['fallbackRendering.']);
327 }
328 if ($fallbackRenderMethod && is_array($conf['rendering.'][$fallbackRenderMethod . '.'])) {
329 $conf = array_replace_recursive($conf, $conf['rendering.'][$fallbackRenderMethod . '.']);
330 }
331 // Set the accessibility mode which uses a different type of markup, used 4.7+
332 $accessibilityMode = false;
333 if (strpos(strtolower($renderMethod), 'caption') || strpos(strtolower($fallbackRenderMethod), 'caption')) {
334 $accessibilityMode = true;
335 }
336 // Global caption
337 $globalCaption = '';
338 if ($renderGlobalCaption) {
339 $globalCaption = $this->cObj->stdWrap($this->cObj->cObjGet($conf['caption.'], 'caption.'), $conf['caption.']);
340 }
341 // Positioning
342 $position = $this->cObj->stdWrap($conf['textPos'], $conf['textPos.']);
343 // 0,1,2 = center,right,left
344 $imagePosition = $position & 7;
345 // 0,8,16,24 (above,below,intext,intext-wrap)
346 $contentPosition = $position & 24;
347 $textMargin = (int)$this->cObj->stdWrap($conf['textMargin'], $conf['textMargin.']);
348 if (!$conf['textMargin_outOfText'] && $contentPosition < 16) {
349 $textMargin = 0;
350 }
351 $colspacing = (int)$this->cObj->stdWrap($conf['colSpace'], $conf['colSpace.']);
352 $border = (int)$this->cObj->stdWrap($conf['border'], $conf['border.']) ? 1 : 0;
353 $borderThickness = (int)$this->cObj->stdWrap($conf['borderThick'], $conf['borderThick.']);
354 $borderThickness = $borderThickness ?: 1;
355 $borderSpace = $conf['borderSpace'] && $border ? (int)$conf['borderSpace'] : 0;
356 // Generate cols
357 $cols = (int)$this->cObj->stdWrap($conf['cols'], $conf['cols.']);
358 $colCount = $cols > 1 ? $cols : 1;
359 if ($colCount > $imgCount) {
360 $colCount = $imgCount;
361 }
362 $rowCount = ceil($imgCount / $colCount);
363 // Generate rows
364 $rows = (int)$this->cObj->stdWrap($conf['rows'], $conf['rows.']);
365 if ($rows > 1) {
366 $rowCount = $rows;
367 if ($rowCount > $imgCount) {
368 $rowCount = $imgCount;
369 }
370 $colCount = $rowCount > 1 ? ceil($imgCount / $rowCount) : $imgCount;
371 }
372 // Max Width
373 $maxW = (int)$this->cObj->stdWrap($conf['maxW'], $conf['maxW.']);
374 $maxWInText = (int)$this->cObj->stdWrap($conf['maxWInText'], $conf['maxWInText.']);
375 $fiftyPercentWidthInText = round($maxW / 100 * 50);
376 // in Text
377 if ($contentPosition >= 16) {
378 if (!$maxWInText) {
379 // If maxWInText is not set, it's calculated to the 50% of the max
380 $maxW = $fiftyPercentWidthInText;
381 } else {
382 $maxW = $maxWInText;
383 }
384 }
385 // max usuable width for images (without spacers and borders)
386 $netW = $maxW - $colspacing * ($colCount - 1) - $colCount * $border * ($borderThickness + $borderSpace) * 2;
387 // Specify the maximum width for each column
388 $columnWidths = $this->getImgColumnWidths($conf, $colCount, $netW);
389 $image_frames = (int)$this->cObj->stdWrap($conf['image_frames.']['key'], $conf['image_frames.']['key.']);
390 // EqualHeight
391 $equalHeight = (int)$this->cObj->stdWrap($conf['equalH'], $conf['equalH.']);
392 if ($equalHeight) {
393 $relations_cols = [];
394 // contains the individual width of all images after scaling to $equalHeight
395 $imgWidths = [];
396 for ($a = 0; $a < $imgCount; $a++) {
397 $imgKey = $a + $imgStart;
398
399 /** @var $file \TYPO3\CMS\Core\Resource\File */
400 if (MathUtility::canBeInterpretedAsInteger($imgs[$imgKey])) {
401 if ($imgListContainsReferenceUids) {
402 $file = $this->getResourceFactory()->getFileReferenceObject((int)$imgs[$imgKey])->getOriginalFile();
403 } else {
404 $file = $this->getResourceFactory()->getFileObject((int)$imgs[$imgKey]);
405 }
406 } else {
407 $file = $this->getResourceFactory()->getFileObjectFromCombinedIdentifier($imgPath . $imgs[$imgKey]);
408 }
409
410 // relationship between the original height and the wished height
411 $rel = $file->getProperty('height') / $equalHeight;
412 // if relations is zero, then the addition of this value is omitted as the image is not expected to display because of some error.
413 if ($rel) {
414 $imgWidths[$a] = $file->getProperty('width') / $rel;
415 // counts the total width of the row with the new height taken into consideration.
416 $relations_cols[(int)floor($a / $colCount)] += $imgWidths[$a];
417 }
418 }
419 }
420 // Fetches pictures
421 $splitArr = [];
422 $splitArr['imgObjNum'] = $conf['imgObjNum'];
423 $splitArr = $typoScriptService->explodeConfigurationForOptionSplit($splitArr, (int)$imgCount);
424 // Contains the width of every image row
425 $imageRowsFinalWidths = [];
426 // Array index of $imgsTag will be the same as in $imgs, but $imgsTag only contains the images that are actually shown
427 $imgsTag = [];
428 $origImages = [];
429 $rowIdx = 0;
430 for ($a = 0; $a < $imgCount; $a++) {
431 $imgKey = $a + $imgStart;
432 // If the image cannot be interpreted as integer (therefore filename and no FAL id), add the image path
433 if (MathUtility::canBeInterpretedAsInteger($imgs[$imgKey])) {
434 $totalImagePath = (int)$imgs[$imgKey];
435 $this->initializeCurrentFileInContentObjectRenderer($totalImagePath, $imgListContainsReferenceUids);
436 } else {
437 $totalImagePath = $imgPath . $imgs[$imgKey];
438 }
439 // register IMG_NUM is kept for backwards compatibility
440 $this->frontendController->register['IMAGE_NUM'] = $imgKey;
441 $this->frontendController->register['IMAGE_NUM_CURRENT'] = $imgKey;
442 $this->frontendController->register['ORIG_FILENAME'] = $totalImagePath;
443 $this->cObj->data[$this->cObj->currentValKey] = $totalImagePath;
444 $imgObjNum = (int)$splitArr[$a]['imgObjNum'];
445 $imgConf = $conf[$imgObjNum . '.'];
446 if ($equalHeight) {
447 if ($a % $colCount == 0) {
448 // A new row starts
449 // Reset accumulated net width
450 $accumWidth = 0;
451 // Reset accumulated desired width
452 $accumDesiredWidth = 0;
453 $rowTotalMaxW = $relations_cols[$rowIdx];
454 if ($rowTotalMaxW > $netW && $netW > 0) {
455 $scale = $rowTotalMaxW / $netW;
456 } else {
457 $scale = 1;
458 }
459 $desiredHeight = $equalHeight / $scale;
460 $rowIdx++;
461 }
462 // This much width is available for the remaining images in this row (int)
463 $availableWidth = $netW - $accumWidth;
464 // Theoretical width of resized image. (float)
465 $desiredWidth = $imgWidths[$a] / $scale;
466 // Add this width. $accumDesiredWidth becomes the desired horizontal position
467 $accumDesiredWidth += $desiredWidth;
468 // Calculate width by comparing actual and desired horizontal position.
469 // this evenly distributes rounding errors across all images in this row.
470 $suggestedWidth = round($accumDesiredWidth - $accumWidth);
471 // finalImgWidth may not exceed $availableWidth
472 $finalImgWidth = (int)min($availableWidth, $suggestedWidth);
473 $accumWidth += $finalImgWidth;
474 $imgConf['file.']['width'] = $finalImgWidth;
475 $imgConf['file.']['height'] = round($desiredHeight);
476 // other stuff will be calculated accordingly:
477 unset($imgConf['file.']['maxW']);
478 unset($imgConf['file.']['maxH']);
479 unset($imgConf['file.']['minW']);
480 unset($imgConf['file.']['minH']);
481 unset($imgConf['file.']['width.']);
482 unset($imgConf['file.']['maxW.']);
483 unset($imgConf['file.']['maxH.']);
484 unset($imgConf['file.']['minW.']);
485 unset($imgConf['file.']['minH.']);
486 } else {
487 $imgConf['file.']['maxW'] = $columnWidths[$a % $colCount];
488 }
489 $titleInLink = $this->cObj->stdWrap($imgConf['titleInLink'], $imgConf['titleInLink.']);
490 $titleInLinkAndImg = $this->cObj->stdWrap($imgConf['titleInLinkAndImg'], $imgConf['titleInLinkAndImg.']);
491 $oldATagParms = $this->frontendController->ATagParams;
492 if ($titleInLink) {
493 // Title in A-tag instead of IMG-tag
494 $titleText = trim($this->cObj->stdWrap($imgConf['titleText'], $imgConf['titleText.']));
495 if ($titleText) {
496 // This will be used by the IMAGE call later:
497 $this->frontendController->ATagParams .= ' title="' . htmlspecialchars($titleText) . '"';
498 }
499 }
500
501 // hook to allow custom rendering of a single element
502 // This hook is needed to render alternative content which is not just a plain image,
503 // like showing other FAL content, like videos, things which need to be embedded as JS, ...
504 $customRendering = '';
505 if (isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['css_styled_content']['pi1_hooks']['render_singleMediaElement'])
506 && is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['css_styled_content']['pi1_hooks']['render_singleMediaElement'])) {
507 $hookParameters = [
508 'file' => $totalImagePath,
509 'imageConfiguration' => $imgConf
510 ];
511
512 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['css_styled_content']['pi1_hooks']['render_singleMediaElement'] as $reference) {
513 $customRendering = \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction($reference, $hookParameters, $this);
514 // if there is a renderer found, don't run through the other renderers
515 if (!empty($customRendering)) {
516 break;
517 }
518 }
519 }
520
521 if (!empty($customRendering)) {
522 $imgsTag[$imgKey] = $customRendering;
523 } elseif ($imgConf || $imgConf['file']) {
524 if ($image_frames) {
525 if (is_array($conf['image_frames.'][$image_frames . '.'])) {
526 $imgConf['file.']['m.'] = $conf['image_frames.'][$image_frames . '.'];
527 }
528 }
529 if ($titleInLink && !$titleInLinkAndImg) {
530 // Check if the image will be linked
531 $link = $this->cObj->imageLinkWrap('', $this->cObj->getCurrentFile() ?: $totalImagePath, $imgConf['imageLinkWrap.']);
532 if ($link) {
533 // Title in A-tag only (set above: ATagParams), not in IMG-tag
534 unset($imgConf['titleText']);
535 unset($imgConf['titleText.']);
536 $imgConf['emptyTitleHandling'] = 'removeAttr';
537 }
538 }
539 $imgsTag[$imgKey] = $this->cObj->cObjGetSingle('IMAGE', $imgConf);
540 } else {
541 // currentValKey !!!
542 $imgsTag[$imgKey] = $this->cObj->cObjGetSingle('IMAGE', ['file' => $totalImagePath]);
543 }
544 // Restore our ATagParams
545 $this->frontendController->ATagParams = $oldATagParms;
546 // Store the original filepath
547 $origImages[$imgKey] = $this->frontendController->lastImageInfo;
548 if ($this->frontendController->lastImageInfo[0] == 0) {
549 $imageRowsFinalWidths[(int)floor($a / $colCount)] += $this->cObj->data['imagewidth'];
550 } else {
551 $imageRowsFinalWidths[(int)floor($a / $colCount)] += $this->frontendController->lastImageInfo[0];
552 }
553 }
554 // How much space will the image-block occupy?
555 $imageBlockWidth = max($imageRowsFinalWidths) + $colspacing * ($colCount - 1) + $colCount * $border * ($borderSpace + $borderThickness) * 2;
556 $this->frontendController->register['rowwidth'] = $imageBlockWidth;
557 $this->frontendController->register['rowWidthPlusTextMargin'] = $imageBlockWidth + $textMargin;
558 // Edit icons:
559 if (!is_array($conf['editIcons.'])) {
560 $conf['editIcons.'] = [];
561 }
562 $editIconsHTML = $conf['editIcons'] && $this->frontendController->beUserLogin ? $this->cObj->editIcons('', $conf['editIcons'], $conf['editIcons.']) : '';
563 $imageWrapCols = 1;
564 // User wants to separate the rows, but only do that if we do have rows
565 $separateRows = $this->cObj->stdWrap($conf['separateRows'], $conf['separateRows.']);
566 if ($rowCount == 1) {
567 $separateRows = 0;
568 }
569 if ($accessibilityMode) {
570 $imagesInColumns = round($imgCount / ($rowCount * $colCount), 0, PHP_ROUND_HALF_UP);
571 // Apply optionSplit to the list of classes that we want to add to each column
572 $addClassesCol = $conf['addClassesCol'];
573 if (isset($conf['addClassesCol.'])) {
574 $addClassesCol = $this->cObj->stdWrap($addClassesCol, $conf['addClassesCol.']);
575 }
576 $addClassesColConf = $typoScriptService->explodeConfigurationForOptionSplit(['addClassesCol' => $addClassesCol], $colCount);
577 // Apply optionSplit to the list of classes that we want to add to each image
578 $addClassesImage = $conf['addClassesImage'];
579 if (isset($conf['addClassesImage.'])) {
580 $addClassesImage = $this->cObj->stdWrap($addClassesImage, $conf['addClassesImage.']);
581 }
582 $addClassesImageConf = $typoScriptService->explodeConfigurationForOptionSplit(['addClassesImage' => $addClassesImage], $imagesInColumns);
583 $rows = [];
584 $currentImage = 0;
585 // Iterate over the rows
586 for ($rowCounter = 1; $rowCounter <= $rowCount; $rowCounter++) {
587 $rowColumns = [];
588 // Iterate over the columns
589 for ($columnCounter = 1; $columnCounter <= $colCount; $columnCounter++) {
590 $columnImages = [];
591 // Iterate over the amount of images allowed in a column
592 for ($imagesCounter = 1; $imagesCounter <= $imagesInColumns; $imagesCounter++) {
593 $image = null;
594 $splitCaption = null;
595 $imageMarkers = ($captionMarkers = []);
596 $single = '&nbsp;';
597 // Set the key of the current image
598 $imageKey = $currentImage + $imgStart;
599 // Register IMAGE_NUM_CURRENT for the caption
600 $this->frontendController->register['IMAGE_NUM_CURRENT'] = $imageKey;
601 $this->cObj->data[$this->cObj->currentValKey] = $origImages[$imageKey]['origFile'];
602 if (MathUtility::canBeInterpretedAsInteger($imgs[$imageKey])) {
603 $this->initializeCurrentFileInContentObjectRenderer((int)$imgs[$imageKey], $imgListContainsReferenceUids);
604 } elseif (!isset($imgs[$imageKey])) {
605 // If not all columns in the last row are filled $imageKey gets larger than
606 // the array. In that case we clear the current file.
607 $this->cObj->setCurrentFile(null);
608 }
609 // Get the image if not an empty cell
610 if (isset($imgsTag[$imageKey])) {
611 $image = $this->cObj->stdWrap($imgsTag[$imageKey], $conf['imgTagStdWrap.']);
612 // Add the edit icons
613 if ($editIconsHTML) {
614 $image .= $this->cObj->stdWrap($editIconsHTML, $conf['editIconsStdWrap.']);
615 }
616 // Wrap the single image
617 $single = $this->cObj->stdWrap($image, $conf['singleStdWrap.']);
618 // Get the caption
619 if (!$renderGlobalCaption) {
620 $imageMarkers['caption'] = $this->cObj->stdWrap($this->cObj->cObjGet($conf['caption.'], 'caption.'), $conf['caption.']);
621 $imageMarkers['caption'] = $this->templateService->substituteMarkerArray($imageMarkers['caption'], $captionMarkers, '###|###', 1, 1);
622 }
623 if ($addClassesImageConf[$imagesCounter - 1]['addClassesImage']) {
624 $imageMarkers['classes'] = ' ' . $addClassesImageConf[$imagesCounter - 1]['addClassesImage'];
625 }
626 }
627 $columnImages[] = $this->templateService->substituteMarkerArray($single, $imageMarkers, '###|###', 1, 1);
628 $currentImage++;
629 }
630 $rowColumn = $this->cObj->stdWrap(implode(LF, $columnImages), $conf['columnStdWrap.']);
631 // Start filling the markers for columnStdWrap
632 $columnMarkers = [];
633 if ($addClassesColConf[$columnCounter - 1]['addClassesCol']) {
634 $columnMarkers['classes'] = ' ' . $addClassesColConf[$columnCounter - 1]['addClassesCol'];
635 }
636 $rowColumns[] = $this->templateService->substituteMarkerArray($rowColumn, $columnMarkers, '###|###', 1, 1);
637 }
638 if ($rowCounter == $rowCount) {
639 $rowConfiguration = $conf['lastRowStdWrap.'];
640 } else {
641 $rowConfiguration = $conf['rowStdWrap.'];
642 }
643 $row = $this->cObj->stdWrap(implode(LF, $rowColumns), $rowConfiguration);
644 // Start filling the markers for columnStdWrap
645 $rowMarkers = [];
646 $rows[] = $this->templateService->substituteMarkerArray($row, $rowMarkers, '###|###', 1, 1);
647 }
648 $images = $this->cObj->stdWrap(implode(LF, $rows), $conf['allStdWrap.']);
649 // Start filling the markers for allStdWrap
650 $allMarkers = [];
651 $classes = [];
652 // Add the global caption to the allStdWrap marker array if set
653 if ($globalCaption) {
654 $allMarkers['caption'] = $globalCaption;
655 }
656 // Set the margin for image + text, no wrap always to avoid multiple stylesheets
657 $noWrapMargin = (int)(($maxWInText ? $maxWInText : $fiftyPercentWidthInText) + (int)$this->cObj->stdWrap($conf['textMargin'], $conf['textMargin.']));
658 $this->addPageStyle('.csc-textpic-intext-right-nowrap .csc-textpic-text', 'margin-right: ' . $noWrapMargin . 'px;');
659 $this->addPageStyle('.csc-textpic-intext-left-nowrap .csc-textpic-text', 'margin-left: ' . $noWrapMargin . 'px;');
660 // Beside Text where the image block width is not equal to maxW
661 if ($contentPosition == 24 && $maxW != $imageBlockWidth) {
662 $noWrapMargin = $imageBlockWidth + $textMargin;
663 // Beside Text, Right
664 if ($imagePosition == 1) {
665 $this->addPageStyle('.csc-textpic-intext-right-nowrap-' . $noWrapMargin . ' .csc-textpic-text', 'margin-right: ' . $noWrapMargin . 'px;');
666 $classes[] = 'csc-textpic-intext-right-nowrap-' . $noWrapMargin;
667 } elseif ($imagePosition == 2) {
668 $this->addPageStyle('.csc-textpic-intext-left-nowrap-' . $noWrapMargin . ' .csc-textpic-text', 'margin-left: ' . $noWrapMargin . 'px;');
669 $classes[] = 'csc-textpic-intext-left-nowrap-' . $noWrapMargin;
670 }
671 }
672 // Add the border class if needed
673 if ($border) {
674 $classes[] = $conf['borderClass'] ?: 'csc-textpic-border';
675 }
676 // Add the class for equal height if needed
677 if ($equalHeight) {
678 $classes[] = 'csc-textpic-equalheight';
679 }
680 $addClasses = $this->cObj->stdWrap($conf['addClasses'], $conf['addClasses.']);
681 if ($addClasses) {
682 $classes[] = $addClasses;
683 }
684 if ($classes) {
685 $class = ' ' . implode(' ', $classes);
686 }
687 // Fill the markers for the allStdWrap
688 $images = $this->templateService->substituteMarkerArray($images, $allMarkers, '###|###', 1, 1);
689 } else {
690 // Apply optionSplit to the list of classes that we want to add to each image
691 $addClassesImage = $conf['addClassesImage'];
692 if (isset($conf['addClassesImage.'])) {
693 $addClassesImage = $this->cObj->stdWrap($addClassesImage, $conf['addClassesImage.']);
694 }
695 $addClassesImageConf = $typoScriptService->explodeConfigurationForOptionSplit(['addClassesImage' => $addClassesImage], $colCount);
696 // Render the images
697 $images = '';
698 for ($c = 0; $c < $imageWrapCols; $c++) {
699 $tmpColspacing = $colspacing;
700 if ($c == $imageWrapCols - 1 && $imagePosition == 2 || $c == 0 && ($imagePosition == 1 || $imagePosition == 0)) {
701 // Do not add spacing after column if we are first column (left) or last column (center/right)
702 $tmpColspacing = 0;
703 }
704 $thisImages = '';
705 $allRows = '';
706 $maxImageSpace = 0;
707 $imgsTagCount = count($imgsTag);
708 for ($i = $c; $i < $imgsTagCount; $i = $i + $imageWrapCols) {
709 $imgKey = $i + $imgStart;
710 $colPos = $i % $colCount;
711 if ($separateRows && $colPos == 0) {
712 $thisRow = '';
713 }
714 // Render one image
715 if ($origImages[$imgKey][0] == 0) {
716 $imageSpace = $this->cObj->data['imagewidth'] + $border * ($borderSpace + $borderThickness) * 2;
717 } else {
718 $imageSpace = $origImages[$imgKey][0] + $border * ($borderSpace + $borderThickness) * 2;
719 }
720 $this->frontendController->register['IMAGE_NUM'] = $imgKey;
721 $this->frontendController->register['IMAGE_NUM_CURRENT'] = $imgKey;
722 $this->frontendController->register['ORIG_FILENAME'] = $origImages[$imgKey]['origFile'];
723 $this->frontendController->register['imagewidth'] = $origImages[$imgKey][0];
724 $this->frontendController->register['imagespace'] = $imageSpace;
725 $this->frontendController->register['imageheight'] = $origImages[$imgKey][1];
726 if (MathUtility::canBeInterpretedAsInteger($imgs[$imgKey])) {
727 $this->initializeCurrentFileInContentObjectRenderer(intval($imgs[$imgKey]), $imgListContainsReferenceUids);
728 }
729 if ($imageSpace > $maxImageSpace) {
730 $maxImageSpace = $imageSpace;
731 }
732 $thisImage = '';
733 $thisImage .= $this->cObj->stdWrap($imgsTag[$imgKey], $conf['imgTagStdWrap.']);
734 if (!$renderGlobalCaption) {
735 $thisImage .= $this->cObj->stdWrap($this->cObj->cObjGet($conf['caption.'], 'caption.'), $conf['caption.']);
736 }
737 if ($editIconsHTML) {
738 $thisImage .= $this->cObj->stdWrap($editIconsHTML, $conf['editIconsStdWrap.']);
739 }
740 $thisImage = $this->cObj->stdWrap($thisImage, $conf['oneImageStdWrap.']);
741 $classes = '';
742 if ($addClassesImageConf[$colPos]['addClassesImage']) {
743 $classes = ' ' . $addClassesImageConf[$colPos]['addClassesImage'];
744 }
745 $thisImage = str_replace('###CLASSES###', $classes, $thisImage);
746 if ($separateRows) {
747 $thisRow .= $thisImage;
748 } else {
749 $allRows .= $thisImage;
750 }
751 $this->frontendController->register['columnwidth'] = $maxImageSpace + $tmpColspacing;
752 // Close this row at the end (colCount), or the last row at the final end
753 if ($separateRows && $i + 1 === count($imgsTag)) {
754 // Close the very last row with either normal configuration or lastRow stdWrap
755 $allRows .= $this->cObj->stdWrap(
756 $thisRow,
757 is_array($conf['imageLastRowStdWrap.']) ? $conf['imageLastRowStdWrap.'] : $conf['imageRowStdWrap.']
758 );
759 } elseif ($separateRows && $colPos == $colCount - 1) {
760 $allRows .= $this->cObj->stdWrap($thisRow, $conf['imageRowStdWrap.']);
761 }
762 }
763 $thisImages .= $allRows;
764 $images .= $thisImages;
765 }
766 // Add the global caption, if not split
767 if ($globalCaption) {
768 $images .= $globalCaption;
769 }
770 // CSS-classes
771 $borderClass = '';
772 if ($border) {
773 $borderClass = $conf['borderClass'] ?: 'csc-textpic-border';
774 }
775 // Multiple classes with all properties, to be styled in CSS
776 $class = '';
777 $class .= $borderClass ? ' ' . $borderClass : '';
778 $class .= $equalHeight ? ' csc-textpic-equalheight' : '';
779 $addClasses = $this->cObj->stdWrap($conf['addClasses'], $conf['addClasses.']);
780 $class .= $addClasses ? ' ' . $addClasses : '';
781 // Do we need a width in our wrap around images?
782 $imgWrapWidth = '';
783 if ($position == 0 || $position == 8) {
784 // For 'center' we always need a width: without one, the margin:auto trick won't work
785 $imgWrapWidth = $imageBlockWidth;
786 }
787 if ($rowCount > 1) {
788 // For multiple rows we also need a width, so that the images will wrap
789 $imgWrapWidth = $imageBlockWidth;
790 }
791 if ($globalCaption) {
792 // If we have a global caption, we need the width so that the caption will wrap
793 $imgWrapWidth = $imageBlockWidth;
794 }
795 // Wrap around the whole image block
796 $this->frontendController->register['totalwidth'] = $imgWrapWidth;
797 if ($imgWrapWidth) {
798 $images = $this->cObj->stdWrap($images, $conf['imageStdWrap.']);
799 } else {
800 $images = $this->cObj->stdWrap($images, $conf['imageStdWrapNoWidth.']);
801 }
802 }
803
804 $output = str_replace(
805 [
806 '###TEXT###',
807 '###IMAGES###',
808 '###CLASSES###'
809 ],
810 [
811 $content,
812 $images,
813 $class
814 ],
815 $this->cObj->cObjGetSingle($conf['layout'], $conf['layout.'])
816 );
817
818 if ($restoreRegisters) {
819 $this->cObj->cObjGetSingle('RESTORE_REGISTER', []);
820 }
821
822 return $output;
823 }
824
825 /**
826 * Loads the file / file reference object and sets it in the
827 * currentFile property of the ContentObjectRenderer.
828 *
829 * This makes the file data available during image rendering.
830 *
831 * @param int $fileUid The UID of the file or file reference (depending on $treatAsReference) that should be loaded.
832 * @param bool $treatAsReference If TRUE the given UID will be used to load a file reference otherwise it will be used to load a regular file.
833 */
834 protected function initializeCurrentFileInContentObjectRenderer($fileUid, $treatAsReference)
835 {
836 $resourceFactory = \TYPO3\CMS\Core\Resource\ResourceFactory::getInstance();
837 if ($treatAsReference) {
838 $imageFile = $resourceFactory->getFileReferenceObject($fileUid);
839 } else {
840 $imageFile = $resourceFactory->getFileObject($fileUid);
841 }
842 $this->cObj->setCurrentFile($imageFile);
843 }
844
845 /***********************************
846 * Rendering of Content Element properties
847 ***********************************/
848
849 /**
850 * Add top or bottom margin to the content element
851 *
852 * Constructs and adds a class to the content element. This class selector
853 * and its declaration are added to the specific page styles.
854 *
855 * @param string $content Content input. Not used, ignore.
856 * @param array $configuration TypoScript configuration
857 * @return string The class name
858 */
859 public function renderSpace($content, array $configuration)
860 {
861 // Look for hook before running default code for function
862 if (method_exists($this, 'hookRequest') && ($hookObject = $this->hookRequest('renderSpace'))) {
863 return $hookObject->renderSpace($content, $configuration);
864 }
865 if (isset($configuration['space']) && in_array($configuration['space'], ['before', 'after'])) {
866 $constant = (int)$configuration['constant'];
867 if ($configuration['space'] === 'before') {
868 $value = $constant + $this->cObj->data['spaceBefore'];
869 $declaration = 'margin-top: ' . $value . 'px !important;';
870 } else {
871 $value = $constant + $this->cObj->data['spaceAfter'];
872 $declaration = 'margin-bottom: ' . $value . 'px !important;';
873 }
874 if (!empty($value)) {
875 if ($configuration['classStdWrap.']) {
876 $className = $this->cObj->stdWrap($value, $configuration['classStdWrap.']);
877 } else {
878 $className = $value;
879 }
880 $selector = '.' . trim($className);
881 $this->addPageStyle($selector, $declaration);
882 return $className;
883 }
884 }
885 }
886
887 /************************************
888 * Helper functions
889 ************************************/
890
891 /**
892 * Returns table attributes for tables. Not used anymore.
893 *
894 * @param array $conf TypoScript configuration array
895 * @param int $type The "layout" type
896 * @return array Array with attributes inside.
897 */
898 public function getTableAttributes($conf, $type)
899 {
900 // Initializing:
901 $tableTagParams_conf = $conf['tableParams_' . $type . '.'];
902 $border = $this->cObj->data['table_border'] ? (int)$this->cObj->data['table_border'] : $tableTagParams_conf['border'];
903 $cellSpacing = $this->cObj->data['table_cellspacing'] ? (int)$this->cObj->data['table_cellspacing'] : $tableTagParams_conf['cellspacing'];
904 $cellPadding = $this->cObj->data['table_cellpadding'] ? (int)$this->cObj->data['table_cellpadding'] : $tableTagParams_conf['cellpadding'];
905 $summary = trim(htmlspecialchars($this->pi_getFFvalue($this->cObj->data['pi_flexform'], 'acctables_summary')));
906 // Create table attributes and classes array:
907 $tableTagParams = ($classes = []);
908 // Table attributes for all doctypes except HTML5
909 if ($this->frontendController->config['config']['doctype'] !== 'html5') {
910 $tableTagParams['border'] = $border;
911 $tableTagParams['cellspacing'] = $cellSpacing;
912 $tableTagParams['cellpadding'] = $cellPadding;
913 if ($summary) {
914 $tableTagParams['summary'] = $summary;
915 }
916 } else {
917 if ($border) {
918 // Border property has changed, now with class
919 $borderClass = 'contenttable-border-' . $border;
920 $borderDeclaration = 'border-width: ' . $border . 'px; border-style: solid;';
921 $this->addPageStyle('.' . $borderClass, $borderDeclaration);
922 $classes[] = $borderClass;
923 }
924 if ($cellSpacing) {
925 // Border attribute for HTML5 is 1 when there is cell spacing
926 $tableTagParams['border'] = 1;
927 // Use CSS3 border-spacing in class to have cell spacing
928 $cellSpacingClass = 'contenttable-cellspacing-' . $cellSpacing;
929 $cellSpacingDeclaration = 'border-spacing: ' . $cellSpacing . 'px;';
930 $this->addPageStyle('.' . $cellSpacingClass, $cellSpacingDeclaration);
931 $classes[] = $cellSpacingClass;
932 }
933 if ($cellPadding) {
934 // Cell padding property has changed, now with class
935 $cellPaddingClass = 'contenttable-cellpadding-' . $cellPadding;
936 $cellSpacingSelector = '.' . $cellPaddingClass . ' td, .' . $cellPaddingClass . ' th';
937 $cellPaddingDeclaration = 'padding: ' . $cellPadding . 'px;';
938 $this->addPageStyle($cellSpacingSelector, $cellPaddingDeclaration);
939 $classes[] = $cellPaddingClass;
940 }
941 }
942 // Background color is class
943 if (isset($conf['color.'][$this->cObj->data['table_bgColor']]) && !empty($conf['color.'][$this->cObj->data['table_bgColor']])) {
944 $classes[] = 'contenttable-color-' . $this->cObj->data['table_bgColor'];
945 }
946 if (!empty($classes)) {
947 $tableTagParams['class'] = ' ' . implode(' ', $classes);
948 }
949 // Return result:
950 return $tableTagParams;
951 }
952
953 /**
954 * Add a style to the page, specific for this page
955 *
956 * The selector can be a contextual selector, like '#id .class p'
957 * The presence of the selector is checked to avoid multiple entries of the
958 * same selector.
959 *
960 * @param string $selector The selector
961 * @param string $declaration The declaration
962 */
963 protected function addPageStyle($selector, $declaration)
964 {
965 if (!isset($this->frontendController->tmpl->setup['plugin.']['tx_cssstyledcontent.']['_CSS_PAGE_STYLE'])) {
966 $this->frontendController->tmpl->setup['plugin.']['tx_cssstyledcontent.']['_CSS_PAGE_STYLE'] = [];
967 }
968 if (!isset($this->frontendController->tmpl->setup['plugin.']['tx_cssstyledcontent.']['_CSS_PAGE_STYLE'][$selector])) {
969 $this->frontendController->tmpl->setup['plugin.']['tx_cssstyledcontent.']['_CSS_PAGE_STYLE'][$selector] = TAB . $selector . ' { ' . $declaration . ' }';
970 }
971 }
972
973 /**
974 * Returns an object reference to the hook object if any
975 *
976 * @param string $functionName Name of the function you want to call / hook key
977 * @return object|NULL Hook object, if any. Otherwise NULL.
978 */
979 public function hookRequest($functionName)
980 {
981 // Hook: menuConfig_preProcessModMenu
982 if ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['css_styled_content']['pi1_hooks'][$functionName]) {
983 $hookObj = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['css_styled_content']['pi1_hooks'][$functionName]);
984 if (method_exists($hookObj, $functionName)) {
985 $hookObj->pObj = $this;
986 return $hookObj;
987 }
988 }
989 }
990
991 /**
992 * Get the ResourceFactory
993 *
994 * @return \TYPO3\CMS\Core\Resource\ResourceFactory
995 */
996 protected function getResourceFactory()
997 {
998 return \TYPO3\CMS\Core\Resource\ResourceFactory::getInstance();
999 }
1000 }