Files
cms-frontend/Classes/ContentObject/ImageContentObject.php
T

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;
}
}