304 lines
14 KiB
PHP
304 lines
14 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of the TYPO3 CMS project.
|
|
*
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, either version 2
|
|
* of the License, or any later version.
|
|
*
|
|
* For the full copyright and license information, please read the
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
*
|
|
* The TYPO3 project - inspiring people to share!
|
|
*/
|
|
|
|
namespace TYPO3\CMS\Frontend\ContentObject;
|
|
|
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
|
use Psr\Log\LoggerInterface;
|
|
use Psr\Log\LogLevel;
|
|
use TYPO3\CMS\Core\Page\AssetCollector;
|
|
use TYPO3\CMS\Core\Resource\File;
|
|
use TYPO3\CMS\Core\Resource\FileReference;
|
|
use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
|
|
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
|
|
use TYPO3\CMS\Core\Type\DocType;
|
|
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
use TYPO3\CMS\Frontend\ContentObject\Event\ModifyImageSourceCollectionEvent;
|
|
use TYPO3\CMS\Frontend\Page\FrontendUrlPrefix;
|
|
|
|
/**
|
|
* Contains IMAGE class object.
|
|
*/
|
|
class ImageContentObject extends AbstractContentObject
|
|
{
|
|
public function __construct(
|
|
protected readonly MarkerBasedTemplateService $markerTemplateService,
|
|
protected readonly TimeTracker $timeTracker,
|
|
protected readonly LoggerInterface $logger,
|
|
) {}
|
|
|
|
/**
|
|
* Rendering the cObject, IMAGE
|
|
*
|
|
* @param mixed $conf Array of TypoScript properties (marked as "mixed" currently because we don't know what we're receiving)
|
|
* @return string Output
|
|
*/
|
|
public function render($conf = []): string
|
|
{
|
|
if (!empty($conf['if.']) && !$this->cObj->checkIf($conf['if.'])) {
|
|
return '';
|
|
}
|
|
|
|
$theValue = $this->cImage($conf['file'] ?? '', is_array($conf) ? $conf : []);
|
|
if (isset($conf['stdWrap.'])) {
|
|
$theValue = $this->cObj->stdWrap($theValue, $conf['stdWrap.']);
|
|
}
|
|
return $theValue;
|
|
}
|
|
|
|
/**
|
|
* Returns a <img> tag with the image file defined by $file and processed according to the properties in the TypoScript array.
|
|
* Mostly this function is a sub-function to the IMAGE function which renders the IMAGE cObject in TypoScript.
|
|
*
|
|
* @param string|File|FileReference|null $file File TypoScript resource
|
|
* @param array $conf TypoScript configuration properties
|
|
* @return string HTML <img> tag, (possibly wrapped in links and other HTML) if any image found.
|
|
*/
|
|
protected function cImage($file, array $conf): string
|
|
{
|
|
$imageResource = $this->cObj->getImgResource($file, $conf['file.'] ?? []);
|
|
if ($imageResource === null) {
|
|
return '';
|
|
}
|
|
// $info['originalFile'] will be set, when the file is processed by FAL.
|
|
// In that case the URL is final and we must not add a prefix
|
|
if ($imageResource->getOriginalFile() === null && is_file($imageResource->getFullPath())) {
|
|
$absRefPrefix = GeneralUtility::makeInstance(FrontendUrlPrefix::class)->getUrlPrefix($this->request);
|
|
$source = $absRefPrefix . str_replace('%2F', '/', rawurlencode($imageResource->getPublicUrl()));
|
|
} else {
|
|
$source = $imageResource->getPublicUrl();
|
|
}
|
|
// A file whose physical resource is gone (sys_file.missing=1) resolves to
|
|
// an image resource without public URL. Render nothing in this case, just
|
|
// like for an image resource that could not be resolved at all.
|
|
if ($source === null) {
|
|
$identifier = $imageResource->getOriginalFile()?->getIdentifier() ?: $imageResource->getFullPath();
|
|
$this->logger->warning('The image "{file}" has no public URL, the file is probably missing, and won\'t be included in frontend output', [
|
|
'file' => $identifier,
|
|
]);
|
|
$this->timeTracker->setTSlogMessage(
|
|
'The image "' . $identifier . '" has no public URL, the file is probably missing. It is not rendered.',
|
|
LogLevel::WARNING
|
|
);
|
|
return '';
|
|
}
|
|
GeneralUtility::makeInstance(AssetCollector::class)->addMedia(
|
|
$source,
|
|
$imageResource->getLegacyImageResourceInformation()
|
|
);
|
|
|
|
$layoutKey = (string)$this->cObj->stdWrapValue('layoutKey', $conf);
|
|
$imageTagTemplate = $this->getImageTagTemplate($layoutKey, $conf);
|
|
$sourceCollection = $this->getImageSourceCollection($layoutKey, $conf, $file);
|
|
|
|
$altParam = $this->getAltParam($conf);
|
|
$params = $this->cObj->stdWrapValue('params', $conf);
|
|
if ($params !== '' && $params[0] !== ' ') {
|
|
$params = ' ' . $params;
|
|
}
|
|
|
|
$imageTagValues = [
|
|
'width' => $imageResource->getWidth(),
|
|
'height' => $imageResource->getHeight(),
|
|
'src' => htmlspecialchars($source),
|
|
'params' => $params,
|
|
'altParams' => $altParam,
|
|
'sourceCollection' => $sourceCollection,
|
|
'selfClosingTagSlash' => DocType::createFromRequest($this->request)->isXmlCompliant() ? ' /' : '',
|
|
];
|
|
|
|
$theValue = $this->markerTemplateService->substituteMarkerArray($imageTagTemplate, $imageTagValues, '###|###', true, true);
|
|
|
|
$linkWrap = (string)$this->cObj->stdWrapValue('linkWrap', $conf);
|
|
if ($linkWrap !== '') {
|
|
$theValue = $this->linkWrap($theValue, $linkWrap);
|
|
} elseif ($conf['imageLinkWrap'] ?? false) {
|
|
$originalFile = urldecode($imageResource->getFullPath());
|
|
$theValue = $this->cObj->imageLinkWrap($theValue, $originalFile, $conf['imageLinkWrap.']);
|
|
}
|
|
$wrap = $this->cObj->stdWrapValue('wrap', $conf);
|
|
if ((string)$wrap !== '') {
|
|
$theValue = $this->cObj->wrap($theValue, $conf['wrap']);
|
|
}
|
|
return $theValue;
|
|
}
|
|
|
|
/**
|
|
* Returns the html-template for rendering the image-Tag if no template is defined via typoscript the
|
|
* default <img> tag template is returned
|
|
*
|
|
* @param string $layoutKey rendering key
|
|
* @param array $conf TypoScript configuration properties
|
|
*/
|
|
protected function getImageTagTemplate($layoutKey, $conf): string
|
|
{
|
|
if ($layoutKey && isset($conf['layout.']) && isset($conf['layout.'][$layoutKey . '.'])) {
|
|
return $this->cObj->stdWrapValue('element', $conf['layout.'][$layoutKey . '.']);
|
|
}
|
|
return '<img src="###SRC###" width="###WIDTH###" height="###HEIGHT###" ###PARAMS### ###ALTPARAMS### ###SELFCLOSINGTAGSLASH###>';
|
|
}
|
|
|
|
/**
|
|
* Render alternate sources for the image tag. If no source collection is given an empty string is returned.
|
|
*
|
|
* @param string $layoutKey rendering key
|
|
* @param array $conf TypoScript configuration properties
|
|
* @param string|File|FileReference|null $file
|
|
* @return string
|
|
*/
|
|
protected function getImageSourceCollection(string $layoutKey, array $conf, $file)
|
|
{
|
|
$sourceCollection = '';
|
|
if ($layoutKey
|
|
&& isset($conf['sourceCollection.']) && $conf['sourceCollection.']
|
|
&& (
|
|
isset($conf['layout.'][$layoutKey . '.']['source']) && $conf['layout.'][$layoutKey . '.']['source']
|
|
|| isset($conf['layout.'][$layoutKey . '.']['source.']) && $conf['layout.'][$layoutKey . '.']['source.']
|
|
)
|
|
) {
|
|
// find active sourceCollection
|
|
$activeSourceCollections = [];
|
|
foreach ($conf['sourceCollection.'] as $sourceCollectionKey => $sourceCollectionConfiguration) {
|
|
if (str_ends_with($sourceCollectionKey, '.')) {
|
|
if (empty($sourceCollectionConfiguration['if.']) || $this->cObj->checkIf($sourceCollectionConfiguration['if.'])) {
|
|
$activeSourceCollections[] = $sourceCollectionConfiguration;
|
|
}
|
|
}
|
|
}
|
|
|
|
// apply option split to configurations
|
|
$typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
|
|
$srcLayoutOptionSplitted = $typoScriptService->explodeConfigurationForOptionSplit((array)$conf['layout.'][$layoutKey . '.'], count($activeSourceCollections));
|
|
$eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
|
|
|
|
$isXmlCompliant = DocType::createFromRequest($this->request)->isXmlCompliant();
|
|
|
|
// render sources
|
|
foreach ($activeSourceCollections as $key => $sourceConfiguration) {
|
|
$sourceLayout = $this->cObj->stdWrapValue('source', $srcLayoutOptionSplitted[$key] ?? []);
|
|
|
|
$sourceRenderConfiguration = [
|
|
'file' => $file,
|
|
'file.' => $conf['file.'] ?? null,
|
|
];
|
|
|
|
$imageQuality = $this->cObj->stdWrapValue('quality', $sourceConfiguration ?? []);
|
|
if ($imageQuality) {
|
|
$sourceRenderConfiguration['file.']['params'] = '-quality ' . (int)$imageQuality;
|
|
}
|
|
|
|
$pixelDensity = (int)$this->cObj->stdWrapValue('pixelDensity', $sourceConfiguration, 1);
|
|
$dimensionKeys = ['width', 'height', 'maxW', 'minW', 'maxH', 'minH', 'maxWidth', 'maxHeight', 'XY'];
|
|
foreach ($dimensionKeys as $dimensionKey) {
|
|
$dimension = (string)$this->cObj->stdWrapValue($dimensionKey, $sourceConfiguration);
|
|
if ($dimension === '') {
|
|
$dimension = (string)$this->cObj->stdWrapValue($dimensionKey, $conf['file.'] ?? []);
|
|
}
|
|
if ($dimension !== '') {
|
|
if (str_contains($dimension, 'c') && ($dimensionKey === 'width' || $dimensionKey === 'height')) {
|
|
$dimensionParts = explode('c', $dimension, 2);
|
|
$dimension = ((int)$dimensionParts[0] * $pixelDensity) . 'c';
|
|
if ($dimensionParts[1]) {
|
|
$dimension .= $dimensionParts[1];
|
|
}
|
|
} elseif ($dimensionKey === 'XY') {
|
|
$dimensionParts = GeneralUtility::intExplode(',', $dimension);
|
|
$dimension = $dimensionParts[0] * $pixelDensity;
|
|
if ($dimensionParts[1]) {
|
|
$dimension .= ',' . $dimensionParts[1] * $pixelDensity;
|
|
}
|
|
} else {
|
|
$dimension = (int)$dimension * $pixelDensity;
|
|
}
|
|
$sourceRenderConfiguration['file.'][$dimensionKey] = $dimension;
|
|
// Remove the stdWrap properties for dimension as they have been processed already above.
|
|
unset($sourceRenderConfiguration['file.'][$dimensionKey . '.']);
|
|
}
|
|
}
|
|
$imageResource = $this->cObj->getImgResource($sourceRenderConfiguration['file'], $sourceRenderConfiguration['file.']);
|
|
if ($imageResource !== null) {
|
|
$sourceConfiguration['width'] = $imageResource->getWidth();
|
|
$sourceConfiguration['height'] = $imageResource->getHeight();
|
|
|
|
$urlPrefix = '';
|
|
// Prepend 'absRefPrefix' to file path only if file was not processed by FAL, e.g. GIFBUILDER
|
|
if ($imageResource->getOriginalFile() === null && is_file($imageResource->getFullPath())) {
|
|
$urlPrefix = GeneralUtility::makeInstance(FrontendUrlPrefix::class)->getUrlPrefix($this->request);
|
|
}
|
|
|
|
$sourceConfiguration['src'] = htmlspecialchars($urlPrefix . $imageResource->getPublicUrl());
|
|
$sourceConfiguration['selfClosingTagSlash'] = $isXmlCompliant ? ' /' : '';
|
|
|
|
$oneSourceCollection = $this->markerTemplateService->substituteMarkerArray($sourceLayout, $sourceConfiguration, '###|###', true, true);
|
|
|
|
$sourceCollection .= $eventDispatcher->dispatch(
|
|
new ModifyImageSourceCollectionEvent($oneSourceCollection, $sourceCollection, (array)$sourceConfiguration, $sourceRenderConfiguration, $this->cObj)
|
|
)->getSourceCollection();
|
|
}
|
|
}
|
|
}
|
|
return $sourceCollection;
|
|
}
|
|
|
|
/**
|
|
* Wraps the input string by the $wrap value and implements the "linkWrap" data type as well.
|
|
*
|
|
* The "linkWrap" data type means that this function will find any integer encapsulated
|
|
* in {} (curly braces) in the first wrap part and substitute it with the corresponding page
|
|
* uid from the rootline where the found integer is pointing to the key in the rootline.
|
|
*
|
|
* @param string $content Input string
|
|
* @param string $wrap A string where the first two parts separated by "|" (vertical line) will be wrapped around the input string
|
|
*/
|
|
protected function linkWrap(string $content, string $wrap): string
|
|
{
|
|
$wrapArr = explode('|', $wrap);
|
|
if (preg_match('/\\{([0-9]*)\\}/', $wrapArr[0], $reg)) {
|
|
$localRootLine = $this->request->getAttribute('frontend.page.information')->getLocalRootLine();
|
|
$uid = $localRootLine[$reg[1]]['uid'] ?? null;
|
|
if ($uid) {
|
|
$wrapArr[0] = str_replace($reg[0], $uid, $wrapArr[0]);
|
|
}
|
|
}
|
|
return trim($wrapArr[0]) . $content . trim($wrapArr[1] ?? '');
|
|
}
|
|
|
|
/**
|
|
* An abstraction method which creates an alt or title parameter for an HTML img, applet, area or input element and the FILE content element.
|
|
* From the $conf array it implements the properties "altText" and "titleText"
|
|
*
|
|
* @param array $conf TypoScript configuration properties
|
|
* @return string Parameter string containing alt and title parameters (if any)
|
|
*/
|
|
protected function getAltParam(array $conf): string
|
|
{
|
|
$altText = trim((string)$this->cObj->stdWrapValue('altText', $conf));
|
|
$titleText = trim((string)$this->cObj->stdWrapValue('titleText', $conf));
|
|
|
|
// "alt":
|
|
$altParam = ' alt="' . htmlspecialchars($altText) . '"';
|
|
// "title":
|
|
$emptyTitleHandling = $this->cObj->stdWrapValue('emptyTitleHandling', $conf);
|
|
// Choices: 'keepEmpty' | 'useAlt' | 'removeAttr'
|
|
if ($titleText || $emptyTitleHandling === 'keepEmpty') {
|
|
$altParam .= ' title="' . htmlspecialchars($titleText) . '"';
|
|
} elseif ($emptyTitleHandling === 'useAlt') {
|
|
$altParam .= ' title="' . htmlspecialchars($altText) . '"';
|
|
}
|
|
return $altParam;
|
|
}
|
|
}
|