1<?php
2/** @noinspection PhpUnused */
3/**
4 * DokuWiki Plugin externalembed (Helper Component)
5 *
6 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
7 * @author  Cameron Ward <cameronward007@gmail.com>
8 */
9
10if(!defined('DOKU_INC')) die();
11
12class helper_plugin_externalembed_cacheInterface extends DokuWiki_Plugin {
13
14    /**
15     * Get the data stored in the cache file e.g. thumbnail encoded data
16     *
17     * @param $cache_id string the id of the cache
18     * @return mixed
19     */
20    public function getExistingCache(string $cache_id) {
21        $cache = new cache_externalembed($cache_id);
22        return json_decode($cache->retrieveCache(), true);
23    }
24
25    /**
26     * Updates the E tag of the cache file to be the current time
27     * @param $cache_id string the id of the cache
28     */
29    public function updateETag(string $cache_id) {
30        $cache = new cache_externalembed($cache_id);
31        $cache->storeETag(md5(time()));
32    }
33
34    /**
35     * Return true if the cache is still fresh, otherwise return false
36     * @param      $cache_id string the cache id
37     * @param      $time     // the expiry time of the cache
38     * @return bool
39     */
40    public function checkCacheFreshness(string $cache_id, $time): bool {
41        $cache = new cache_externalembed($cache_id);
42
43        if($cache->checkETag($time)) return true;
44
45        return false;
46    }
47
48    /**
49     * Public function to get a thumbnail from a YouTube video
50     * Return the thumbnail data to be cached or checked with the existing cache.
51     * @param $video_id
52     * @return array the url of the thumbnail with the encoded thumbnail data
53     */
54    public function getYouTubeThumbnail($video_id): array {
55        $img_url   = 'https://img.youtube.com/vi/' . $video_id . '/maxresdefault.jpg';
56        $thumbnail = base64_encode(file_get_contents($img_url)); //encode the thumbnail to be sent to the browser later
57        return array('url' => $img_url, 'thumbnail' => $thumbnail); //return thumbnail data to be cached or checked with existing cache
58    }
59
60    /**
61     * Generate a new cache object and store the new data
62     * @param $video_id   string the id of the cache
63     * @param $cache_data mixed the data to store in the cache
64     * @return cache_externalembed the cache object
65     */
66    public function cacheYouTubeThumbnail(string $video_id, $cache_data): cache_externalembed {
67        $timestamp = md5(time());
68        return $this->newCache($video_id, $cache_data, $timestamp); //create cache file and return object
69    }
70
71    /**
72     * Public function generates new cache object
73     * Stores the data within a json encoded cache file
74     * @param      $cache_id  string the unique identifier for the cache
75     * @param null $data      The data to be stored in the cache
76     * @param null $timestamp When the cache was created
77     * @return cache_externalembed the cache object
78     */
79    public function newCache(string $cache_id, $data = null, $timestamp = null): cache_externalembed {
80        $cache = new cache_externalembed($cache_id);
81        $cache->storeCache(json_encode($data));
82        $cache->storeETag($timestamp);
83        return $cache;
84    }
85
86    /**
87     * Generate a new cache object and store the new data
88     * @param $playlist_id string the id of the cache
89     * @param $video_ids   mixed the data stored in the cache
90     * @return cache_externalembed the new cache object
91     */
92    public function cachePlaylist(string $playlist_id, $video_ids): cache_externalembed {
93        $timestamp = md5(time());
94        return $this->newCache($playlist_id, $video_ids, $timestamp);
95        //store the latest video from the playlist and return the cache object
96    }
97
98    /**
99     * Gets the current video ID's associated with a YouTube Playlist
100     * @param $playlist_id string the YouTube Playlist ID
101     * @return array The array of video ID's associated with the playlist
102     * @throws InvalidEmbed
103     */
104    public function getPlaylist(string $playlist_id): array {
105        $video_ids = array();
106        $response  = array();
107
108        while(key_exists('nextPageToken', $response) || empty($response)) { //keep sending requests until we have seen all the videos in a playlist
109            $response = $this->sendPlaylistRequest($playlist_id, '&pageToken=' . $response['nextPageToken']);
110            foreach($response['items'] as $video) {
111                array_push($video_ids, $video['contentDetails']['videoId']); //add the video_ids to the array
112            }
113        }
114        return $video_ids;
115    }
116
117    /**
118     * Method for getting the videos in a playlist using the YouTube Data API v3
119     *
120     * @param        $playlist_id     string the YouTube Playlist ID
121     * @param string $next_page_token token that the API needs to get the next set of results
122     * @return mixed The List of video ID's on the current page associated with the playlist ID
123     * @throws InvalidEmbed
124     */
125    private function sendPlaylistRequest(string $playlist_id, string $next_page_token = '') {
126        $url  = 'https://youtube.googleapis.com/youtube/v3/playlistItems?part=contentDetails&maxResults=50' . $next_page_token . '&playlistId=' . $playlist_id . '&key=AIzaSyCJFeNmYo-K7tzh9FfHeo8MACrPkJ8zi_Y';
127        $curl = curl_init($url);
128        curl_setopt($curl, CURLOPT_URL, $url);
129        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
130
131        $headers = array(
132            'Accept: application/json'
133        );
134
135        curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
136
137        //TODO: remove once in production:
138        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
139        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);//
140
141        $api_response = json_decode(curl_exec($curl), true); //decode JSON to associative array
142
143        if(curl_getinfo($curl, CURLINFO_HTTP_CODE) != 200) {
144            if(key_exists("error", $api_response)) {
145                $message = $api_response['error']['message'];
146            } else {
147                $message = "Unknown API api_response error";
148            }
149            throw new InvalidEmbed($message);
150        }
151        curl_close($curl);
152        return $api_response;
153    }
154
155    /**
156     * Public function to remove the video_id cache file from the depends array and the page metadata
157     * @param $video_id   string the video ID that is no longer needed on the page (new video from playlist)
158     * @param $page_cache mixed the page cache used in the PARSER CACHE USE event
159     */
160    public function removeOldVideo(string $video_id, $page_cache) {
161        $cache = new cache_externalembed($video_id);
162        if(($key = array_search($cache->cache, $page_cache->depends['files'])) !== false) {
163            unset($page_cache->depends['files'][$key]);//if file is in the array, remove it
164
165        }
166        $metadata = p_read_metadata($page_cache->page);//get complete metadata
167        //remove from current metadata:
168        if(($key = array_search($video_id, $metadata['current']['plugin']['externalembed']['video_ids'])) !== false) {
169            unset($metadata['current']['plugin']['externalembed']['video_ids'][$key]);//remove from metadata
170        }
171        //remove from persistent metadata:
172        if(($key = array_search($video_id, $metadata['persistent']['plugin']['externalembed']['video_ids'])) !== false) {
173            unset($metadata['persistent']['plugin']['externalembed']['video_ids'][$key]);//remove from metadata
174        }
175        p_save_metadata($page_cache->page, $metadata); //save updated metadata with removed video_id
176
177        //remove from depends array:
178        if(($key = array_search($this->getCacheFile($video_id), $page_cache->depends['files'])) !== false) {
179            unset($page_cache->depends['files'][$key]);
180        }
181    }
182
183    /**
184     * Get the file path of the cache file associated with the ID
185     * @param $cache_id string The id of the cache file
186     * @return string The file path for the cache file
187     */
188    public function getCacheFile(string $cache_id): string {
189        $cache = new cache_externalembed($cache_id);
190        return $cache->cache;
191    }
192}
193
194/**
195 * Class that handles cache files, file locking and cache expiry
196 */
197class cache_externalembed extends \dokuwiki\Cache\Cache {
198    public $e_tag = '';
199    var $_etag_time;
200
201    public function __construct($embed_id) {
202        parent::__construct($embed_id, '.externalembed');
203        $this->e_tag = substr($this->cache, 0, -15) . '.etag';
204    }
205
206    public function getETag($clean = true) {
207        return io_readFile($this->e_tag, $clean);
208    }
209
210    public function storeETag($e_tag_value): bool {
211        if($this->_nocache) return false;
212
213        return io_saveFile($this->e_tag, $e_tag_value);
214    }
215
216    public function getCacheData() {
217        return json_decode($this->retrieveCache(), true);
218
219    }
220
221    /**
222     * Public function that returns true if the cache (Etag) is still fresh
223     * Otherwise false
224     * @param $expireTime
225     * @return bool
226     */
227    public function checkETag($expireTime): bool {
228        if($expireTime < 0) return true;
229        if($expireTime == 0) return false;
230        if(!($this->_etag_time = @filemtime($this->e_tag))) return false; //check if cache is still there
231        if((time() - $this->_etag_time) > $expireTime) return false; //Cache has expired
232        return true;
233    }
234}
235