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