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