1<?php /** @noinspection PhpPossiblePolymorphicInvocationInspection */ 2/** @noinspection PhpUnused */ 3/** @noinspection DuplicatedCode */ 4/** 5 * DokuWiki Plugin externalembed (Syntax Component) 6 * 7 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 8 * @author Cameron <cameronward007@gmail.com> 9 */ 10 11// must be run within Dokuwiki 12if(!defined('DOKU_INC')) { 13 die(); 14} 15 16/** 17 * Exception Class 18 * 19 * Class InvalidYouTubeEmbed 20 */ 21class InvalidEmbed extends Exception { 22 public function errorMessage(): string { 23 return $this->getMessage(); 24 } 25} 26 27class syntax_plugin_externalembed extends DokuWiki_Syntax_Plugin { 28 /** 29 * @return string Syntax mode type 30 */ 31 public function getType(): string { 32 return 'substition'; 33 } 34 35 /** 36 * @return string Paragraph type 37 */ 38 public function getPType(): string { 39 return 'block'; 40 } 41 42 /** 43 * @return int Sort order - Low numbers go before high numbers 44 */ 45 public function getSort(): int { 46 return 2; 47 } 48 49 /** 50 * Connect lookup pattern to lexer. 51 * 52 * @param string $mode Parser mode 53 */ 54 public function connectTo($mode) { 55 $this->Lexer->addEntryPattern('{{external_embed>', $mode, 'plugin_externalembed'); 56 } 57 58 public function postConnect() { 59 $this->Lexer->addExitPattern('}}', 'plugin_externalembed'); 60 } 61 62 /** 63 * Handle matches of the externalembed syntax 64 * 65 * @param string $match The match of the syntax 66 * @param int $state The state of the handler 67 * @param int $pos The position in the document 68 * @param Doku_Handler $handler The handler 69 * 70 * @return array Data for the renderer 71 * @noinspection PhpMissingParamTypeInspection 72 */ 73 function handle($match, $state, $pos, $handler): array { 74 switch($state) { 75 case DOKU_LEXER_EXIT: 76 case DOKU_LEXER_ENTER : 77 /** @var array $data */ 78 return array(); 79 80 case DOKU_LEXER_SPECIAL: 81 case DOKU_LEXER_MATCHED : 82 break; 83 84 case DOKU_LEXER_UNMATCHED : 85 if(!empty($match)) { 86 try { 87 //get and define config variables 88 define('YT_API_KEY', $this->getConf('YT_API_KEY')); 89 define('THUMBNAIL_CACHE_TIME', $this->getConf('THUMBNAIL_CACHE_TIME') * 60 * 60); 90 define('PLAYLIST_CACHE_TIME', $this->getConf('PLAYLIST_CACHE_TIME')); 91 define('DEFAULT_PRIVACY_DISCLAIMER', $this->getConf('DEFAULT_PRIVACY_DISCLAIMER')); // cam be empty 92 $disclaimers = array(); 93 define('DOMAIN_WHITELIST', $this->getDomains($this->getConf('DOMAIN_WHITELIST'), $disclaimers)); 94 define('DISCLAIMERS', $disclaimers); //can be empty 95 define('MINIMUM_EMBED_WIDTH', $this->getConf('MINIMUM_EMBED_WIDTH')); 96 define('MINIMUM_EMBED_HEIGHT', $this->getConf('MINIMUM_EMBED_WIDTH')); 97 98 if(!($cacheHelper = $this->loadHelper('externalembed_cacheInterface'))) { 99 throw new InvalidEmbed('Could not load cache interface helper'); 100 } 101 102 //validate config variables 103 if(empty(YT_API_KEY)) { 104 throw new InvalidEmbed('Empty API Key, set this in the configuration manager in the admin panel'); 105 } 106 if(empty(THUMBNAIL_CACHE_TIME)) { 107 throw new InvalidEmbed('Empty cache time for thumbnails, set this in the configuration manager in the admin panel'); 108 } 109 if(empty(PLAYLIST_CACHE_TIME)) { 110 throw new InvalidEmbed('Empty cache time for playlists, set this in the configuration manager in the admin panel'); 111 } 112 if(empty(DOMAIN_WHITELIST)) { 113 throw new InvalidEmbed('Empty domain whitelist, set this in the configuration manager in the admin panel'); 114 } 115 116 $parameters = $this->getParameters($match); 117 $embed_type = $this->getEmbedType($parameters); 118 $parameters['type'] = $embed_type; 119 //gets the embed type and checks if the domain is in the whitelist 120 121 //MAIN PROGRAM: 122 switch(true) { 123 case ($embed_type === "youtube_video"): 124 $validated_parameters = $this->parseYouTubeVideoString($parameters); 125 $yt_request = $this->getVideoRequest($validated_parameters); 126 $validated_parameters['thumbnail'] = $this->cacheYouTubeThumbnail($cacheHelper, $validated_parameters['video_id']); 127 $html = $this->renderJSON($yt_request, $validated_parameters); 128 return array('embed_html' => $html, 'video_ID' => $validated_parameters['video_id']); //return html and metadata 129 case ($embed_type === "youtube_playlist"): 130 $validated_parameters = $this->parseYouTubePlaylistString($parameters); 131 $playlist_cache = $this->cachePlaylist($cacheHelper, $validated_parameters); 132 $cached_video_id = $this->getLatestVideo($playlist_cache); 133 $validated_parameters['video_id'] = $cached_video_id; //adds the video ID to the metadata later 134 $validated_parameters['thumbnail'] = $this->cacheYouTubeThumbnail($cacheHelper, $validated_parameters['video_id']); 135 $yt_request = $this->getVideoRequest($validated_parameters); 136 $html = $this->renderJSON($yt_request, $validated_parameters); 137 return array('embed_html' => $html, 'video_ID' => $validated_parameters['video_id'], 'playlist_ID' => $validated_parameters['playlist_id']); 138 case ($embed_type === 'fusion'): 139 $validated_parameters = $this->parseFusionString($parameters); 140 $fusion_request = $this->getFusionRequest($validated_parameters); 141 $html = $this->renderJSON($fusion_request, $validated_parameters); 142 return array('embed_html' => $html); 143 case ($embed_type === 'other'): 144 $validated_parameters = $this->parseOtherEmbedString($parameters); 145 $html = $this->renderJSON($validated_parameters['url'], $validated_parameters); 146 return array('embed_html' => $html); 147 default: 148 throw new InvalidEmbed("Unknown Embed Type"); 149 150 //todo: allow fusion embed links 151 } 152 } catch(InvalidEmbed $e) { 153 $html = "<p style='color: red; font-weight: bold;'>External Embed Error: " . $e->getMessage() . "</p>"; 154 return array('embed_html' => $html); 155 } 156 } 157 } 158 return array(); 159 } 160 161 /** 162 * Render xhtml output or metadata 163 * 164 * @param string $mode Renderer mode (supported modes: xhtml) 165 * @param Doku_Renderer $renderer The renderer 166 * @param array $data The data from the handler() function 167 * 168 * @return bool If rendering was successful. 169 * @noinspection PhpParameterNameChangedDuringInheritanceInspection 170 */ 171 public function render($mode, Doku_Renderer $renderer, $data): bool { 172 if($data === false) return false; 173 174 if($mode == 'xhtml') { 175 if(!empty($data['embed_html'])) { 176 $renderer->doc .= $data['embed_html']; 177 return true; 178 } else { 179 return false; 180 } 181 } elseif($mode == 'metadata') { 182 if(!empty($data['video_ID'])) { 183 /** @var Doku_Renderer_metadata $renderer */ 184 // erase tags on persistent metadata no more used 185 if(isset($renderer->persistent['plugin']['externalembed']['video_ids'])) { 186 //unset($renderer->meta['plugin']['externalembed']['video_ids']); 187 unset($renderer->persistent['plugin']['externalembed']['video_ids']); 188 $renderer->meta['plugin']['externalembed']['video_ids'] = array(); 189 //$renderer->persistent['plugin']['externalembed']['video_ids'] = array(); 190 } 191 192 // merge with previous tags and make the values unique 193 if(!isset($renderer->meta['plugin']['externalembed']['video_ids'])) { 194 $renderer->meta['plugin']['externalembed']['video_ids'] = array(); 195 } 196 //$renderer->persistent['plugin']['externalembed']['video_ids'] = array(); 197 198 $renderer->meta['plugin']['externalembed']['video_ids'] = array_unique(array_merge($renderer->meta['plugin']['externalembed']['video_ids'], array($data['video_ID']))); 199 //$renderer->persistent['plugin']['externalembed']['video_ids'] = array_unique(array_merge($renderer->persistent['plugin']['externalembed']['video_ids'], array($data['video_ID']))); 200 201 if(!empty($data['playlist_ID'])) { 202 if(isset($renderer->persistent['plugin']['externalembed']['playlist_ids'])) { 203 unset($renderer->persistent['plugin']['externalembed']['playlist_ids']); 204 $renderer->meta['plugin']['externalembed']['playlist_ids'] = array(); 205 $renderer->persistent['plugin']['externalembed']['playlist_ids'] = array(); 206 } 207 208 if(!isset($renderer->meta['plugin']['externalembed']['playlist_ids'])) { 209 $renderer->meta['plugin']['externalembed']['playlist_ids'] = array(); 210 $renderer->persistent['plugin']['externalembed']['playlist_ids'] = array(); 211 } 212 $renderer->meta['plugin']['externalembed']['playlist_ids'] = array_unique(array_merge($renderer->meta['plugin']['externalembed']['playlist_ids'], array($data['playlist_ID']))); 213 $renderer->persistent['plugin']['externalembed']['playlist_ids'] = array_unique(array_merge($renderer->persistent['plugin']['externalembed']['playlist_ids'], array($data['playlist_ID']))); 214 215 } 216 return true; 217 } 218 return false; 219 } 220 return false; 221 } 222 223 /** 224 * Method that generates an HTML iframe for embedded content 225 * Substitutes default privacy disclaimer if none is found the disclaimers array 226 * 227 * @param $request string the source url 228 * @param $parameters array iframe attributes and url data 229 * @return string the html to embed 230 * @throws InvalidEmbed 231 */ 232 private function renderJSON(string $request, array $parameters): string { 233 $parameters['disclaimer'] = DEFAULT_PRIVACY_DISCLAIMER; 234 $parameters['request'] = $request; 235 $type = $parameters['type']; 236 if($type !== 'other') { 237 $parameters['size'] = $this->getEmbedSize($parameters); 238 } else { 239 $parameters['size'] = ''; 240 } 241 242 if($parameters['embed-position'] == "centre") { 243 $position = "mediacenter"; 244 } else if($parameters['embed-position'] == 'right') { 245 $position = "mediaright"; 246 } else if($parameters['embed-position'] == "left") { 247 $position = "medialeft"; 248 } else { 249 $position = ''; 250 } 251 252 //remove unnecessary parameters that don't need to be sent 253 unset( 254 $parameters['url'], 255 $parameters['autoplay'], 256 $parameters['loop'], 257 $parameters['mute'], 258 $parameters['controls'], 259 $parameters['embed-position'] 260 ); 261 262 if(key_exists($parameters['domain'], DISCLAIMERS)) { //if there is a unique disclaimer for the domain, replace the default value with custom value 263 if(!empty(DISCLAIMERS[$parameters['domain']])) { 264 $parameters['disclaimer'] = DISCLAIMERS[$parameters['domain']]; 265 } 266 } 267 $dataJSON = json_encode(array_map("utf8_encode", $parameters)); 268 return '<div class="' . $position . ' externalembed_embed externalembed_TOS ' . $parameters['size'] . '" data-json=\'' . $dataJSON . '\'></div>'; 269 } 270 271 /** 272 * Selects the class to add to the embed so that its size is correct 273 * @param $parameters 274 * @return string 275 * @throws InvalidEmbed 276 */ 277 private function getEmbedSize(&$parameters): string { 278 switch($parameters['height']) { 279 case '360': 280 $parameters['width'] = '640'; 281 return 'externalembed_height_360'; 282 case '480': 283 $parameters['width'] = '854'; 284 return 'externalembed_height_480'; 285 case '720': 286 $parameters['width'] = '1280'; 287 return 'externalembed_height_720'; 288 case '1080': 289 $parameters['width'] = '1920'; 290 return 'externalembed_height_1080'; 291 default: 292 throw new InvalidEmbed('Unknown width value for size class'); 293 } 294 } 295 296 /** 297 * Check to see if domain in the url is in the domain whitelist. 298 * 299 * Check url to determine the type of embed 300 * If the url is a YouTube playlist, the embed will show the latest video in the playlist 301 * If the url is a YouTube video, the embed will only show the video 302 * Else the type is 'other' as long as the domain is on the whitelist 303 * 304 * @param $parameters 305 * @return string either: 'playlist' 'YT_video' or 'other' 306 * @throws InvalidEmbed 307 */ 308 private function getEmbedType(&$parameters): string { 309 if(key_exists('url', $parameters) === false) { 310 throw new InvalidEmbed('Missing url parameter'); 311 } 312 $parameters['domain'] = $this->validateDomain($parameters['url']); //validate and return the domain of the url 313 314 $embed_type = 'other'; 315 316 if($parameters['domain'] === 'youtube.com' || $parameters['domain'] === 'youtu.be') { 317 //determine if the url is a video or a playlist https://youtu.be/clD_8BItvh4 318 if(strpos($parameters['url'], 'playlist?list=') !== false) { 319 return 'youtube_playlist'; 320 } else if((strpos($parameters['url'], '/watch') || strpos($parameters['url'], 'youtu.be/')) !== false) { 321 return 'youtube_video'; 322 } else { 323 throw new InvalidEmbed("Unknown youtube url"); 324 } 325 } 326 if($parameters['domain'] === 'inventopia.autodesk360.com') { 327 return 'fusion'; 328 } 329 330 return $embed_type; 331 } 332 333 /** 334 * Method that checks the domain entered by the user against the accepted whitelist of domains sets in the configuration manager 335 * 336 * @param $url 337 * @return string The valid domain 338 * @throws InvalidEmbed If the domain is not in the whitelist 339 */ 340 private function validateDomain($url): string { 341 $domain = ltrim(parse_url('http://' . str_replace(array('https://', 'http://'), '', $url), PHP_URL_HOST), 'www.'); 342 343 if(array_search($domain, DOMAIN_WHITELIST) === false) { 344 throw new InvalidEmbed( 345 "Could not embed content from domain: " . htmlspecialchars($domain) . " 346 <br>Contact your administrator to add it to their whitelist. 347 <br>Accepted Domains: " . implode(" | ", DOMAIN_WHITELIST) 348 ); 349 } 350 return $domain; 351 } 352 353 /** 354 * Method for extracting the accepted domains from the config string 355 * Split each data entry by line 356 * Then split each line by commas to extract the disclaimer for each accepted domain. 357 * If there is no disclaimer for a domain, store this as an empty string "" in the disclaimers array 358 * 359 * @param $whitelist_string string string entered from config file 360 * @param $disclaimers array array that stores the disclaimers for each accepted domain 361 * @return array 362 */ 363 private function getDomains(string $whitelist_string, array &$disclaimers): array { 364 $domains = array(); 365 $items = explode("\n", $whitelist_string); 366 foreach($items as $domain_disclaimer) { 367 $data = explode(',', $domain_disclaimer); 368 array_push($domains, trim($data[0])); 369 $disclaimers[trim($data[0])] = trim($data[1]); 370 } 371 return $domains; 372 } 373 374 /** 375 * Method that parses the users string query for a video from the wiki editor 376 * 377 * @param $parameters 378 * @return array //an array of parameter: value, associations 379 * @throws InvalidEmbed 380 */ 381 private function parseYouTubeVideoString($parameters): array { 382 $video_parameter_types = array("type" => true, 'url' => true, 'domain' => true, 'video_id' => true, 'height' => '720', 'autoplay' => 'false', 'mute' => 'false', 'loop' => 'false', 'controls' => 'true', "embed-position" => "block"); 383 $video_parameter_values = array('autoplay' => ['', 'true', 'false'], 'mute' => ['', 'true', 'false'], 'loop' => ['', 'true', 'false'], 'controls' => ['', 'true', 'false'], 'height' => ['360', '480', '720', '1080'], "embed-position" => ['', 'left', 'centre', 'right', 'block']); 384 $regex = '/^((?:https?:)?\/\/)?((?:www|m)\.)?(youtube\.com|youtu.be)(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/'; 385 386 if(preg_match($regex, $parameters['url'], $match)) { 387 $parameters['video_id'] = $match[5]; 388 } else { 389 throw new InvalidEmbed('Invalid YouTube URL'); 390 } 391 392 return $this->checkParameters($parameters, $video_parameter_types, $video_parameter_values); 393 } 394 395 /** 396 * Method that parses the users string query for a playlist from the wiki editor 397 * 398 * @param $parameters 399 * @return array //an array of parameter: value, associations 400 * @throws InvalidEmbed 401 */ 402 private function parseYouTubePlaylistString($parameters): array { 403 $playlist_parameter_types = array("type" => true, 'url' => true, 'domain' => true, 'playlist_id' => true, 'height' => '720', 'autoplay' => 'false', 'mute' => 'false', 'loop' => 'false', 'controls' => 'true', "embed-position" => "block"); 404 $playlist_parameter_values = array('autoplay' => ['', 'true', 'false'], 'mute' => ['', 'true', 'false'], 'loop' => ['', 'true', 'false'], 'controls' => ['', 'true', 'false'], 'height' => ['360', '480', '720', '1080'], "embed-position" => ['', 'left', 'centre', 'right', 'block']); 405 $regex = '/^.*(youtu.be\/|list=)([^#&?]*).*/'; 406 407 if(preg_match($regex, $parameters['url'], $matches)) { 408 $parameters['playlist_id'] = $matches[2]; //set the playlist id 409 } 410 return $this->checkParameters($parameters, $playlist_parameter_types, $playlist_parameter_values); 411 } 412 413 /** 414 * Method that parses the users string query for a fusion embed 415 * 416 * @param $parameters 417 * @return array an array of validated parameters 418 * @throws InvalidEmbed 419 */ 420 private function parseFusionString($parameters): array { 421 $fusion_parameter_types = array('type' => true, 'url' => true, 'domain' => true, 'width' => '1280', 'height' => '720', 'allowFullScreen' => 'true', "embed-position" => "block"); 422 $fusion_parameter_values = array('allowFullScreen' => ['true', 'false'], "embed-position" => ['', 'left', 'centre', 'right', 'block']); 423 424 return $this->checkParameters($parameters, $fusion_parameter_types, $fusion_parameter_values); 425 } 426 427 /** 428 * Method that parses the users string query for an embed type classed as "other" 429 * 430 * @param $parameters 431 * @return array an array of validated parameters 432 * @throws InvalidEmbed 433 */ 434 private function parseOtherEmbedString($parameters): array { 435 $other_parameter_types = array("type" => true, 'url' => true, 'domain' => true, 'width' => '1280', 'height' => '720', "embed-position" => "block"); 436 $other_parameter_values = array("embed-position" => ['', 'left', 'centre', 'right', 'block']); 437 438 return $this->checkParameters($parameters, $other_parameter_types, $other_parameter_values); 439 } 440 441 /** 442 * Splits the query string into an associative array of Type => Value pairs 443 * 444 * @param $user_string string The user's embed query 445 * @return array 446 */ 447 private function getParameters(string $user_string): array { 448 $query = array(); 449 $string_array = explode(' | ', $user_string); 450 foreach($string_array as $item) { 451 $parameter = explode(": ", $item); //creates key value pairs for parameters e.g. [type] = "image" 452 $query[strtolower($parameter[0])] = str_replace('"', '', $parameter[1]); //removes quotes 453 } 454 if(array_key_exists("fields", $query)) { // separate field names into an array if it exists 455 $fields = array_map("trim", explode(",", $query['fields'])); 456 $query['fields'] = $fields; 457 } 458 return $query; 459 } 460 461 /** 462 * Checks query parameters to make sure: 463 * Required parameters are present 464 * Missing parameters are substituted with default params 465 * Parameter values match expected values 466 * 467 * @param $query_array array 468 * @param $required_parameters array 469 * @param $parameter_values array 470 * @return array // query array with added default parameters 471 * @throws InvalidEmbed 472 */ 473 private function checkParameters(array &$query_array, array $required_parameters, array $parameter_values): array { 474 foreach($required_parameters as $key => $value) { 475 if(!array_key_exists($key, $query_array)) { // if parameter is missing: 476 if($value === true) { // check if parameter is required 477 throw new InvalidEmbed("Missing Parameter: " . $key); 478 } 479 $query_array[$key] = $value; // substitute default 480 } 481 482 if(($query_array[$key] == null || $query_array[$key] === "") && $value === true) { //if parameter is required but value is not present 483 throw new InvalidEmbed("Missing Parameter Value for: '" . $key . "'."); 484 } 485 486 if(array_key_exists($key, $parameter_values)) { //check accepted parameter_values array 487 if(!in_array($query_array[$key], $parameter_values[$key])) { //if parameter value is not accepted: 488 $message = "Invalid Parameter Value: '" . htmlspecialchars($query_array[$key]) . "' for Key: '" . $key . "'. 489 <br>Possible values: " . implode(" | ", $parameter_values[$key]); 490 if(in_array("", $parameter_values[$key])) { 491 $message .= " or ''"; 492 } 493 throw new InvalidEmbed($message); 494 } 495 } 496 } 497 //if(intval($query_array['width']) < MINIMUM_EMBED_WIDTH) $query_array['width'] = MINIMUM_EMBED_WIDTH; 498 if(intval($query_array['height']) < MINIMUM_EMBED_WIDTH) $query_array['height'] = MINIMUM_EMBED_HEIGHT; 499 500 foreach($query_array as $key => $value) { 501 if(!array_key_exists($key, $required_parameters)) { 502 throw new InvalidEmbed("Invalid parameter: " . htmlspecialchars($key) . '. For url: ' . htmlspecialchars($query_array['url'])); 503 } 504 } 505 506 return $query_array; 507 } 508 509 /** 510 * Method that generates the src attribute for the iframe element 511 * 512 * @param $parameters 513 * @return string 514 */ 515 private function getVideoRequest($parameters): string { 516 if($parameters['autoplay'] === 'true') { 517 $autoplay = '1'; 518 } else { 519 $autoplay = '0'; 520 } 521 522 if($parameters['mute'] === 'true') { 523 $mute = '1'; 524 } else { 525 $mute = '0'; 526 } 527 528 if($parameters['loop'] === 'true') { 529 $loop = '1'; 530 } else { 531 $loop = '0'; 532 } 533 534 if($parameters['controls'] === 'true') { 535 $controls = '1'; 536 } else { 537 $controls = '0'; 538 } 539 return 'https://www.youtube.com/embed/' . $parameters['video_id'] . '?' . 'autoplay=' . $autoplay . '&mute=' . $mute . '&loop=' . $loop . '&controls=' . $controls; 540 } 541 542 /** 543 * Method that turns a normal fusion url into an embed url 544 * Also sets the required iframe parameters for enabling fullscreen 545 * @param $parameters 546 * @return string 547 */ 548 private function getFusionRequest(&$parameters): string { 549 if($parameters['allowFullScreen'] === 'true') { 550 $parameters['allowfullscreen'] = 'true'; 551 $parameters['webkitallowfullscreen'] = 'true'; 552 $parameters['mozallowfullscreen'] = 'true'; 553 } 554 unset($parameters['allowFullScreen']); 555 return $parameters['url'] . '?mode=embed'; 556 } 557 558 /** 559 * @param $video_cache 560 * @return string //returns the last video in the cache (the latest one) 561 */ 562 private function getLatestVideo($video_cache): string { 563 $cache_data = json_decode($video_cache->retrieveCache()); 564 return end($cache_data); 565 } 566 567 /** 568 * Method for getting YouTube thumbnail and storing it as a base64 string in a cache file 569 * 570 * @param $cache_helper object The ExternalEmbedInterface 571 * @param $video_id string the YouTube video ID 572 * @return string YouTube Thumbnail as a base 64 string 573 */ 574 private function cacheYouTubeThumbnail(object $cache_helper, string $video_id): string { 575 $thumbnail = $cache_helper->getYouTubeThumbnail($video_id); 576 $cache_helper->cacheYouTubeThumbnail($video_id, $thumbnail); //use the helper interface to create the cache 577 return $thumbnail['thumbnail']; //return the thumbnail encoded data 578 } 579 580 /** 581 * Generates a cache json file using the playlist ID. 582 * The cache file stores all the video ids from the playlist 583 * 584 * @param $cacheHelper 585 * @param $parameters 586 * @return mixed 587 */ 588 private function cachePlaylist($cacheHelper, $parameters) { 589 $playlist_data = $cacheHelper->getPlaylist($parameters['playlist_id']); 590 return $cacheHelper->cachePlaylist($parameters['playlist_id'], $playlist_data); 591 } 592} 593