*/ // must be run within Dokuwiki if(!defined('DOKU_INC')) { die(); } /** * Exception Class * * Class InvalidYouTubeEmbed */ class InvalidEmbed extends Exception { public function errorMessage(): string { return $this->getMessage(); } } class syntax_plugin_externalembed extends DokuWiki_Syntax_Plugin { /** * @return string Syntax mode type */ public function getType(): string { return 'substition'; } /** * @return string Paragraph type */ public function getPType(): string { return 'block'; } /** * @return int Sort order - Low numbers go before high numbers */ public function getSort(): int { return 2; } /** * Connect lookup pattern to lexer. * * @param string $mode Parser mode */ public function connectTo($mode) { $this->Lexer->addEntryPattern('{{external_embed>', $mode, 'plugin_externalembed'); } public function postConnect() { $this->Lexer->addExitPattern('}}', 'plugin_externalembed'); } /** * Handle matches of the externalembed syntax * * @param string $match The match of the syntax * @param int $state The state of the handler * @param int $pos The position in the document * @param Doku_Handler $handler The handler * * @return array Data for the renderer * @noinspection PhpMissingParamTypeInspection */ function handle($match, $state, $pos, $handler): array { switch($state) { case DOKU_LEXER_EXIT: case DOKU_LEXER_ENTER : /** @var array $data */ return array(); case DOKU_LEXER_SPECIAL: case DOKU_LEXER_MATCHED : break; case DOKU_LEXER_UNMATCHED : if(!empty($match)) { try { //get and define config variables define('YT_API_KEY', $this->getConf('YT_API_KEY')); define('THUMBNAIL_CACHE_TIME', $this->getConf('THUMBNAIL_CACHE_TIME') * 60 * 60); define('PLAYLIST_CACHE_TIME', $this->getConf('PLAYLIST_CACHE_TIME')); define('DEFAULT_PRIVACY_DISCLAIMER', $this->getConf('DEFAULT_PRIVACY_DISCLAIMER')); // cam be empty $disclaimers = array(); define('DOMAIN_WHITELIST', $this->getDomains($this->getConf('DOMAIN_WHITELIST'), $disclaimers)); define('DISCLAIMERS', $disclaimers); //can be empty define('MINIMUM_EMBED_WIDTH', $this->getConf('MINIMUM_EMBED_WIDTH')); define('MINIMUM_EMBED_HEIGHT', $this->getConf('MINIMUM_EMBED_WIDTH')); if(!($cacheHelper = $this->loadHelper('externalembed_cacheInterface'))) { throw new InvalidEmbed('Could not load cache interface helper'); } //validate config variables if(empty(YT_API_KEY)) { throw new InvalidEmbed('Empty API Key, set this in the configuration manager in the admin panel'); } if(empty(THUMBNAIL_CACHE_TIME)) { throw new InvalidEmbed('Empty cache time for thumbnails, set this in the configuration manager in the admin panel'); } if(empty(PLAYLIST_CACHE_TIME)) { throw new InvalidEmbed('Empty cache time for playlists, set this in the configuration manager in the admin panel'); } if(empty(DOMAIN_WHITELIST)) { throw new InvalidEmbed('Empty domain whitelist, set this in the configuration manager in the admin panel'); } $parameters = $this->getParameters($match); $embed_type = $this->getEmbedType($parameters); $parameters['type'] = $embed_type; //gets the embed type and checks if the domain is in the whitelist //MAIN PROGRAM: switch(true) { case ($embed_type === "youtube_video"): $validated_parameters = $this->parseYouTubeVideoString($parameters); $yt_request = $this->getVideoRequest($validated_parameters); $validated_parameters['thumbnail'] = $this->cacheYouTubeThumbnail($cacheHelper, $validated_parameters['video_id']); $html = $this->renderJSON($yt_request, $validated_parameters); return array('embed_html' => $html, 'video_ID' => $validated_parameters['video_id']); //return html and metadata case ($embed_type === "youtube_playlist"): $validated_parameters = $this->parseYouTubePlaylistString($parameters); $playlist_cache = $this->cachePlaylist($cacheHelper, $validated_parameters); $cached_video_id = $this->getLatestVideo($playlist_cache); $validated_parameters['video_id'] = $cached_video_id; //adds the video ID to the metadata later $validated_parameters['thumbnail'] = $this->cacheYouTubeThumbnail($cacheHelper, $validated_parameters['video_id']); $yt_request = $this->getVideoRequest($validated_parameters); $html = $this->renderJSON($yt_request, $validated_parameters); return array('embed_html' => $html, 'video_ID' => $validated_parameters['video_id'], 'playlist_ID' => $validated_parameters['playlist_id']); case ($embed_type === 'fusion'): $validated_parameters = $this->parseFusionString($parameters); $fusion_request = $this->getFusionRequest($validated_parameters); $html = $this->renderJSON($fusion_request, $validated_parameters); return array('embed_html' => $html); case ($embed_type === 'other'): $validated_parameters = $this->parseOtherEmbedString($parameters); $html = $this->renderJSON($validated_parameters['url'], $validated_parameters); return array('embed_html' => $html); default: throw new InvalidEmbed("Unknown Embed Type"); //todo: allow fusion embed links } } catch(InvalidEmbed $e) { $html = "
External Embed Error: " . $e->getMessage() . "
"; return array('embed_html' => $html); } } } return array(); } /** * Render xhtml output or metadata * * @param string $mode Renderer mode (supported modes: xhtml) * @param Doku_Renderer $renderer The renderer * @param array $data The data from the handler() function * * @return bool If rendering was successful. * @noinspection PhpParameterNameChangedDuringInheritanceInspection */ public function render($mode, Doku_Renderer $renderer, $data): bool { if($data === false) return false; if($mode == 'xhtml') { if(!empty($data['embed_html'])) { $renderer->doc .= $data['embed_html']; return true; } else { return false; } } elseif($mode == 'metadata') { if(!empty($data['video_ID'])) { /** @var Doku_Renderer_metadata $renderer */ // erase tags on persistent metadata no more used if(isset($renderer->persistent['plugin']['externalembed']['video_ids'])) { //unset($renderer->meta['plugin']['externalembed']['video_ids']); unset($renderer->persistent['plugin']['externalembed']['video_ids']); $renderer->meta['plugin']['externalembed']['video_ids'] = array(); //$renderer->persistent['plugin']['externalembed']['video_ids'] = array(); } // merge with previous tags and make the values unique if(!isset($renderer->meta['plugin']['externalembed']['video_ids'])) { $renderer->meta['plugin']['externalembed']['video_ids'] = array(); } //$renderer->persistent['plugin']['externalembed']['video_ids'] = array(); $renderer->meta['plugin']['externalembed']['video_ids'] = array_unique(array_merge($renderer->meta['plugin']['externalembed']['video_ids'], array($data['video_ID']))); //$renderer->persistent['plugin']['externalembed']['video_ids'] = array_unique(array_merge($renderer->persistent['plugin']['externalembed']['video_ids'], array($data['video_ID']))); if(!empty($data['playlist_ID'])) { if(isset($renderer->persistent['plugin']['externalembed']['playlist_ids'])) { unset($renderer->persistent['plugin']['externalembed']['playlist_ids']); $renderer->meta['plugin']['externalembed']['playlist_ids'] = array(); $renderer->persistent['plugin']['externalembed']['playlist_ids'] = array(); } if(!isset($renderer->meta['plugin']['externalembed']['playlist_ids'])) { $renderer->meta['plugin']['externalembed']['playlist_ids'] = array(); $renderer->persistent['plugin']['externalembed']['playlist_ids'] = array(); } $renderer->meta['plugin']['externalembed']['playlist_ids'] = array_unique(array_merge($renderer->meta['plugin']['externalembed']['playlist_ids'], array($data['playlist_ID']))); $renderer->persistent['plugin']['externalembed']['playlist_ids'] = array_unique(array_merge($renderer->persistent['plugin']['externalembed']['playlist_ids'], array($data['playlist_ID']))); } return true; } return false; } return false; } /** * Method that generates an HTML iframe for embedded content * Substitutes default privacy disclaimer if none is found the disclaimers array * * @param $request string the source url * @param $parameters array iframe attributes and url data * @return string the html to embed * @throws InvalidEmbed */ private function renderJSON(string $request, array $parameters): string { $parameters['disclaimer'] = DEFAULT_PRIVACY_DISCLAIMER; $parameters['request'] = $request; $type = $parameters['type']; if($type !== 'other') { $parameters['size'] = $this->getEmbedSize($parameters); } else { $parameters['size'] = ''; } if($parameters['embed-position'] == "centre") { $position = "mediacenter"; } else if($parameters['embed-position'] == 'right') { $position = "mediaright"; } else if($parameters['embed-position'] == "left") { $position = "medialeft"; } else { $position = ''; } //remove unnecessary parameters that don't need to be sent unset( $parameters['url'], $parameters['autoplay'], $parameters['loop'], $parameters['mute'], $parameters['controls'], $parameters['embed-position'] ); if(key_exists($parameters['domain'], DISCLAIMERS)) { //if there is a unique disclaimer for the domain, replace the default value with custom value if(!empty(DISCLAIMERS[$parameters['domain']])) { $parameters['disclaimer'] = DISCLAIMERS[$parameters['domain']]; } } $dataJSON = json_encode(array_map("utf8_encode", $parameters)); return ''; } /** * Selects the class to add to the embed so that its size is correct * @param $parameters * @return string * @throws InvalidEmbed */ private function getEmbedSize(&$parameters): string { switch($parameters['height']) { case '360': $parameters['width'] = '640'; return 'externalembed_height_360'; case '480': $parameters['width'] = '854'; return 'externalembed_height_480'; case '720': $parameters['width'] = '1280'; return 'externalembed_height_720'; case '1080': $parameters['width'] = '1920'; return 'externalembed_height_1080'; default: throw new InvalidEmbed('Unknown width value for size class'); } } /** * Check to see if domain in the url is in the domain whitelist. * * Check url to determine the type of embed * If the url is a YouTube playlist, the embed will show the latest video in the playlist * If the url is a YouTube video, the embed will only show the video * Else the type is 'other' as long as the domain is on the whitelist * * @param $parameters * @return string either: 'playlist' 'YT_video' or 'other' * @throws InvalidEmbed */ private function getEmbedType(&$parameters): string { if(key_exists('url', $parameters) === false) { throw new InvalidEmbed('Missing url parameter'); } $parameters['domain'] = $this->validateDomain($parameters['url']); //validate and return the domain of the url $embed_type = 'other'; if($parameters['domain'] === 'youtube.com' || $parameters['domain'] === 'youtu.be') { //determine if the url is a video or a playlist https://youtu.be/clD_8BItvh4 if(strpos($parameters['url'], 'playlist?list=') !== false) { return 'youtube_playlist'; } else if((strpos($parameters['url'], '/watch') || strpos($parameters['url'], 'youtu.be/')) !== false) { return 'youtube_video'; } else { throw new InvalidEmbed("Unknown youtube url"); } } if($parameters['domain'] === 'inventopia.autodesk360.com') { return 'fusion'; } return $embed_type; } /** * Method that checks the domain entered by the user against the accepted whitelist of domains sets in the configuration manager * * @param $url * @return string The valid domain * @throws InvalidEmbed If the domain is not in the whitelist */ private function validateDomain($url): string { $domain = ltrim(parse_url('http://' . str_replace(array('https://', 'http://'), '', $url), PHP_URL_HOST), 'www.'); if(array_search($domain, DOMAIN_WHITELIST) === false) { throw new InvalidEmbed( "Could not embed content from domain: " . htmlspecialchars($domain) . "