getMimeType() === 'video/youtube' || $file->getExtension() === 'youtube') && $this->getOnlineMediaHelper($file) !== false; } /** * Get online media helper * * @return false|OnlineMediaHelperInterface */ protected function getOnlineMediaHelper(FileInterface $file) { if ($this->onlineMediaHelper === null) { $orgFile = $file; if ($orgFile instanceof FileReference) { $orgFile = $orgFile->getOriginalFile(); } if ($orgFile instanceof File) { $this->onlineMediaHelper = GeneralUtility::makeInstance(OnlineMediaHelperRegistry::class)->getOnlineMediaHelper($orgFile); } else { $this->onlineMediaHelper = false; } } return $this->onlineMediaHelper; } /** * Render for given File(Reference) html output * * @param int|string $width TYPO3 known format; examples: 220, 200m or 200c * @param int|string $height TYPO3 known format; examples: 220, 200m or 200c */ public function render(FileInterface $file, int|string $width, int|string $height, array $options = []): string { $options = $this->collectOptions($options, $file); $src = $this->createYouTubeUrl($options, $file); if (empty($src)) { return ''; } $attributes = $this->collectIframeAttributes($width, $height, $options); return sprintf( '', $options['srcAttribute'] ?? 'src', htmlspecialchars($src, ENT_QUOTES | ENT_HTML5), empty($attributes) ? '' : ' ' . $this->implodeAttributes($attributes) ); } /** * @return array */ protected function collectOptions(array $options, FileInterface $file) { // Check for an autoplay option at the file reference itself, if not overridden yet. if (!isset($options['autoplay']) && $file instanceof FileReference) { $autoplay = $file->getProperty('autoplay'); if ($autoplay !== null) { $options['autoplay'] = $autoplay; } } $showPlayerControls = 1; $options['controls'] = (int)!empty($options['controls'] ?? $showPlayerControls); if (!isset($options['allow'])) { $options['allow'] = 'fullscreen'; if (!empty($options['autoplay'])) { $options['allow'] = 'autoplay; fullscreen'; } } return $options; } protected function createYouTubeUrl(array $options, FileInterface $file): string { $videoId = $this->getVideoIdFromFile($file); if (empty($videoId)) { return ''; } $urlParams = ['autohide=1']; $urlParams[] = 'controls=' . $options['controls']; if (!empty($options['autoplay'])) { $urlParams[] = 'autoplay=1'; // If autoplay is enabled, enforce mute=1, see https://developer.chrome.com/blog/autoplay/ $urlParams[] = 'mute=1'; } if (!empty($options['modestbranding'])) { $urlParams[] = 'modestbranding=1'; } if (!empty($options['loop'])) { $urlParams[] = 'loop=1&playlist=' . rawurlencode($videoId); } if (isset($options['relatedVideos'])) { $urlParams[] = 'rel=' . (int)(bool)$options['relatedVideos']; } if (!isset($options['enablejsapi']) || !empty($options['enablejsapi'])) { // @todo: This renderer has a dependency to Request / TypoScript. Model this explicitly. $urlParams[] = 'enablejsapi=1&origin=' . rawurlencode( ($GLOBALS['TYPO3_REQUEST'] ?? null)?->getAttribute('normalizedParams')?->getRequestHost() ?? '' ); } $youTubeUrl = sprintf( 'https://www.youtube%s.com/embed/%s?%s', !isset($options['no-cookie']) || !empty($options['no-cookie']) ? '-nocookie' : '', rawurlencode($videoId), implode('&', $urlParams) ); return $youTubeUrl; } /** * @return string */ protected function getVideoIdFromFile(FileInterface $file) { if ($file instanceof FileReference) { $orgFile = $file->getOriginalFile(); } else { $orgFile = $file; } return $this->getOnlineMediaHelper($file)->getOnlineMediaId($orgFile); } /** * @param int|string $width * @param int|string $height * @return array pairs of key/value; not yet html-escaped */ protected function collectIframeAttributes($width, $height, array $options) { $attributes = []; $attributes['allowfullscreen'] = true; if (isset($options['additionalAttributes']) && is_array($options['additionalAttributes'])) { $attributes = array_merge($attributes, $options['additionalAttributes']); } if (isset($options['data']) && is_array($options['data'])) { array_walk($options['data'], static function (string|int $value, string $key) use (&$attributes): void { $attributes['data-' . $key] = $value; }); } if ((int)$width > 0) { $attributes['width'] = (int)$width; } if ((int)$height > 0) { $attributes['height'] = (int)$height; } if ($this->shouldIncludeFrameBorderAttribute()) { $attributes['frameborder'] = 0; } foreach (['class', 'dir', 'id', 'lang', 'style', 'title', 'accesskey', 'tabindex', 'onclick', 'poster', 'preload', 'allow'] as $key) { if (!empty($options[$key])) { $attributes[$key] = $options[$key]; } } return $attributes; } /** * @internal */ protected function implodeAttributes(array $attributes): string { $attributeList = []; foreach ($attributes as $name => $value) { $name = preg_replace('/[^\p{L}0-9_.-]/u', '', $name); if ($value === true) { $attributeList[] = $name; } else { $attributeList[] = $name . '="' . htmlspecialchars($value, ENT_QUOTES | ENT_HTML5) . '"'; } } return implode(' ', $attributeList); } /** * HTML5 deprecated the "frameborder" attribute as everything should be done via styling. * * @todo: This renderer has a dependency to Request / TypoScript. Model this explicitly. */ protected function shouldIncludeFrameBorderAttribute(): bool { return DocType::createFromRequest($GLOBALS['TYPO3_REQUEST'] ?? null)->shouldIncludeFrameBorderAttribute(); } }