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