Differences Between: [Versions 402 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 namespace mod_bigbluebuttonbn\local\proxy; 18 19 use cache; 20 use cache_helper; 21 use SimpleXMLElement; 22 23 /** 24 * The recording proxy. 25 * 26 * This class acts as a proxy between Moodle and the BigBlueButton API server, 27 * and deals with all requests relating to recordings. 28 * 29 * @package mod_bigbluebuttonbn 30 * @copyright 2021 onwards, Blindside Networks Inc 31 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 32 * @author Laurent David (laurent [at] call-learning [dt] fr) 33 * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) 34 */ 35 class recording_proxy extends proxy_base { 36 37 /** 38 * Invalidate the MUC cache for the specified recording. 39 * 40 * @param string $recordid 41 */ 42 protected static function invalidate_cache_for_recording(string $recordid): void { 43 cache_helper::invalidate_by_event('mod_bigbluebuttonbn/recordingchanged', [$recordid]); 44 } 45 46 /** 47 * Perform deleteRecordings on BBB. 48 * 49 * @param string $recordid a recording id 50 * @return bool 51 */ 52 public static function delete_recording(string $recordid): bool { 53 $result = self::fetch_endpoint_xml('deleteRecordings', ['recordID' => $recordid]); 54 if (!$result || $result->returncode != 'SUCCESS') { 55 return false; 56 } 57 return true; 58 } 59 60 /** 61 * Perform publishRecordings on BBB. 62 * 63 * @param string $recordid 64 * @param string $publish 65 * @return bool 66 */ 67 public static function publish_recording(string $recordid, string $publish = 'true'): bool { 68 $result = self::fetch_endpoint_xml('publishRecordings', [ 69 'recordID' => $recordid, 70 'publish' => $publish, 71 ]); 72 73 self::invalidate_cache_for_recording($recordid); 74 75 if (!$result || $result->returncode != 'SUCCESS') { 76 return false; 77 } 78 79 return true; 80 } 81 82 /** 83 * Perform publishRecordings on BBB. 84 * 85 * @param string $recordid 86 * @param string $protected 87 * @return bool 88 */ 89 public static function protect_recording(string $recordid, string $protected = 'true'): bool { 90 global $CFG; 91 92 // Ignore action if recording_protect_editable is set to false. 93 if (empty($CFG->bigbluebuttonbn_recording_protect_editable)) { 94 return false; 95 } 96 97 $result = self::fetch_endpoint_xml('updateRecordings', [ 98 'recordID' => $recordid, 99 'protect' => $protected, 100 ]); 101 102 self::invalidate_cache_for_recording($recordid); 103 104 if (!$result || $result->returncode != 'SUCCESS') { 105 return false; 106 } 107 108 return true; 109 } 110 111 /** 112 * Perform updateRecordings on BBB. 113 * 114 * @param string $recordid a single record identifier 115 * @param array $params ['key'=>param_key, 'value'] 116 */ 117 public static function update_recording(string $recordid, array $params): bool { 118 $result = self::fetch_endpoint_xml('updateRecordings', array_merge([ 119 'recordID' => $recordid 120 ], $params)); 121 122 self::invalidate_cache_for_recording($recordid); 123 124 return $result ? $result->returncode == 'SUCCESS' : false; 125 } 126 127 /** 128 * Helper function to fetch a single recording from a BigBlueButton server. 129 * 130 * @param string $recordingid 131 * @return null|array 132 */ 133 public static function fetch_recording(string $recordingid): ?array { 134 $data = self::fetch_recordings([$recordingid]); 135 136 if (array_key_exists($recordingid, $data)) { 137 return $data[$recordingid]; 138 } 139 140 return null; 141 } 142 143 /** 144 * Check whether the current recording is a protected recording and purge the cache if necessary. 145 * 146 * @param string $recordingid 147 */ 148 public static function purge_protected_recording(string $recordingid): void { 149 $cache = cache::make('mod_bigbluebuttonbn', 'recordings'); 150 151 $recording = $cache->get($recordingid); 152 if (empty($recording)) { 153 // This value was not cached to begin with. 154 return; 155 } 156 157 $currentfetchcache = cache::make('mod_bigbluebuttonbn', 'currentfetch'); 158 if ($currentfetchcache->has($recordingid)) { 159 // This item was fetched in the current request. 160 return; 161 } 162 163 if (array_key_exists('protected', $recording) && $recording['protected'] === 'true') { 164 // This item is protected. Purge it from the cache. 165 $cache->delete($recordingid); 166 return; 167 } 168 } 169 170 /** 171 * Helper function to fetch recordings from a BigBlueButton server. 172 * 173 * We use a cache to store recording indexed by keyids/recordingID. 174 * @param array $keyids list of recordingids 175 * @return array (associative) with recordings indexed by recordID, each recording is a non sequential array 176 * and sorted by {@see recording_proxy::sort_recordings} 177 */ 178 public static function fetch_recordings(array $keyids = []): array { 179 $recordings = []; 180 181 // If $ids is empty return array() to prevent a getRecordings with meetingID and recordID set to ''. 182 if (empty($keyids)) { 183 return $recordings; 184 } 185 $cache = cache::make('mod_bigbluebuttonbn', 'recordings'); 186 $currentfetchcache = cache::make('mod_bigbluebuttonbn', 'currentfetch'); 187 $recordings = array_filter($cache->get_many($keyids)); 188 $missingkeys = array_diff(array_values($keyids), array_keys($recordings)); 189 190 $recordings += self::do_fetch_recordings($missingkeys); 191 $cache->set_many($recordings); 192 $currentfetchcache->set_many(array_flip(array_keys($recordings))); 193 return $recordings; 194 } 195 196 /** 197 * Helper function to fetch recordings from a BigBlueButton server. 198 * 199 * @param array $keyids list of meetingids 200 * @return array (associative) with recordings indexed by recordID, each recording is a non sequential array 201 * and sorted by {@see recording_proxy::sort_recordings} 202 */ 203 public static function fetch_recording_by_meeting_id(array $keyids = []): array { 204 $recordings = []; 205 206 // If $ids is empty return array() to prevent a getRecordings with meetingID and recordID set to ''. 207 if (empty($keyids)) { 208 return $recordings; 209 } 210 $recordings = self::do_fetch_recordings($keyids, 'meetingID'); 211 return $recordings; 212 } 213 214 /** 215 * Helper function to fetch recordings from a BigBlueButton server. 216 * 217 * @param array $keyids list of meetingids or recordingids 218 * @param string $key the param name used for the BBB request (<recordID>|meetingID) 219 * @return array (associative) with recordings indexed by recordID, each recording is a non sequential array. 220 * and sorted {@see recording_proxy::sort_recordings} 221 */ 222 private static function do_fetch_recordings(array $keyids = [], string $key = 'recordID'): array { 223 $recordings = []; 224 $pagesize = 25; 225 while ($ids = array_splice($keyids, 0, $pagesize)) { 226 $fetchrecordings = self::fetch_recordings_page($ids, $key); 227 $recordings += $fetchrecordings; 228 } 229 // Sort recordings. 230 return self::sort_recordings($recordings); 231 } 232 /** 233 * Helper function to fetch a page of recordings from the remote server. 234 * 235 * @param array $ids 236 * @param string $key 237 * @return array 238 */ 239 private static function fetch_recordings_page(array $ids, $key = 'recordID'): array { 240 // The getRecordings call is executed using a method GET (supported by all versions of BBB). 241 $xml = self::fetch_endpoint_xml('getRecordings', [$key => implode(',', $ids), 'state' => 'any']); 242 243 if (!$xml) { 244 return []; 245 } 246 247 if ($xml->returncode != 'SUCCESS') { 248 return []; 249 } 250 251 if (!isset($xml->recordings)) { 252 return []; 253 } 254 255 $recordings = []; 256 // If there were recordings already created. 257 foreach ($xml->recordings->recording as $recordingxml) { 258 $recording = self::parse_recording($recordingxml); 259 $recordings[$recording['recordID']] = $recording; 260 // Check if there are any child. 261 if (isset($recordingxml->breakoutRooms->breakoutRoom)) { 262 $breakoutrooms = []; 263 foreach ($recordingxml->breakoutRooms->breakoutRoom as $breakoutroom) { 264 $breakoutrooms[] = trim((string) $breakoutroom); 265 } 266 if ($breakoutrooms) { 267 $xml = self::fetch_endpoint_xml('getRecordings', ['recordID' => implode(',', $breakoutrooms)]); 268 if ($xml && $xml->returncode == 'SUCCESS' && isset($xml->recordings)) { 269 // If there were already created meetings. 270 foreach ($xml->recordings->recording as $subrecordingxml) { 271 $recording = self::parse_recording($subrecordingxml); 272 $recordings[$recording['recordID']] = $recording; 273 } 274 } 275 } 276 } 277 } 278 279 return $recordings; 280 } 281 282 /** 283 * Helper function to sort an array of recordings. It compares the startTime in two recording objects. 284 * 285 * @param array $recordings 286 * @return array 287 */ 288 public static function sort_recordings(array $recordings): array { 289 global $CFG; 290 291 uasort($recordings, function($a, $b) { 292 if ($a['startTime'] < $b['startTime']) { 293 return -1; 294 } 295 if ($a['startTime'] == $b['startTime']) { 296 return 0; 297 } 298 return 1; 299 }); 300 301 return $recordings; 302 } 303 304 /** 305 * Helper function to parse an xml recording object and produce an array in the format used by the plugin. 306 * 307 * @param SimpleXMLElement $recording 308 * 309 * @return array 310 */ 311 public static function parse_recording(SimpleXMLElement $recording): array { 312 // Add formats. 313 $playbackarray = []; 314 foreach ($recording->playback->format as $format) { 315 $playbackarray[(string) $format->type] = [ 316 'type' => (string) $format->type, 317 'url' => trim((string) $format->url), 'length' => (string) $format->length 318 ]; 319 // Add preview per format when existing. 320 if ($format->preview) { 321 $playbackarray[(string) $format->type]['preview'] = 322 self::parse_preview_images($format->preview); 323 } 324 } 325 // Add the metadata to the recordings array. 326 $metadataarray = 327 self::parse_recording_meta(get_object_vars($recording->metadata)); 328 $recordingarray = [ 329 'recordID' => (string) $recording->recordID, 330 'meetingID' => (string) $recording->meetingID, 331 'meetingName' => (string) $recording->name, 332 'published' => (string) $recording->published, 333 'state' => (string) $recording->state, 334 'startTime' => (string) $recording->startTime, 335 'endTime' => (string) $recording->endTime, 336 'playbacks' => $playbackarray 337 ]; 338 if (isset($recording->protected)) { 339 $recordingarray['protected'] = (string) $recording->protected; 340 } 341 return $recordingarray + $metadataarray; 342 } 343 344 /** 345 * Helper function to convert an xml recording metadata object to an array in the format used by the plugin. 346 * 347 * @param array $metadata 348 * 349 * @return array 350 */ 351 public static function parse_recording_meta(array $metadata): array { 352 $metadataarray = []; 353 foreach ($metadata as $key => $value) { 354 if (is_object($value)) { 355 $value = ''; 356 } 357 $metadataarray['meta_' . $key] = $value; 358 } 359 return $metadataarray; 360 } 361 362 /** 363 * Helper function to convert an xml recording preview images to an array in the format used by the plugin. 364 * 365 * @param SimpleXMLElement $preview 366 * 367 * @return array 368 */ 369 public static function parse_preview_images(SimpleXMLElement $preview): array { 370 $imagesarray = []; 371 foreach ($preview->images->image as $image) { 372 $imagearray = ['url' => trim((string) $image)]; 373 foreach ($image->attributes() as $attkey => $attvalue) { 374 $imagearray[$attkey] = (string) $attvalue; 375 } 376 array_push($imagesarray, $imagearray); 377 } 378 return $imagesarray; 379 } 380 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body