Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
    /**
1441 ariadna 197
     * Helper function to retrieve recordings that failed to be fetched from a BigBlueButton server.
198
     *
199
     * @param array $keyids list of recordingids
200
     * @return array array of recording recordingids not fetched from server
201
     *  and sorted by {@see recording_proxy::sort_recordings}
202
     */
203
    public static function fetch_missing_recordings(array $keyids = []): array {
204
        $unfetchedids = [];
205
        $pagesize = 25;
206
        // If $ids is empty return array() to prevent a getRecordings with meetingID and recordID set to ''.
207
        if (empty($keyids)) {
208
            return $unfetchedids;
209
        }
210
        while ($ids = array_splice($keyids, 0, $pagesize)) {
211
            // We make getRecordings API call to check recordings are successfully retrieved.
212
            $xml = self::fetch_endpoint_xml('getRecordings', ['recordID' => implode(',', $ids), 'state' => 'any']);
213
            if (!$xml || $xml->returncode != 'SUCCESS' || !isset($xml->recordings)) {
214
                $unfetchedids = array_merge($unfetchedids, $ids);
215
                continue; // We will keep record of all unfetched ids.
216
            }
217
        }
218
        return $unfetchedids;
219
    }
220
 
221
    /**
1 efrain 222
     * Helper function to fetch recordings from a BigBlueButton server.
223
     *
224
     * @param array $keyids list of meetingids
225
     * @return array (associative) with recordings indexed by recordID, each recording is a non sequential array
226
     *  and sorted by {@see recording_proxy::sort_recordings}
227
     */
228
    public static function fetch_recording_by_meeting_id(array $keyids = []): array {
229
        $recordings = [];
230
 
231
        // If $ids is empty return array() to prevent a getRecordings with meetingID and recordID set to ''.
232
        if (empty($keyids)) {
233
            return $recordings;
234
        }
235
        $recordings = self::do_fetch_recordings($keyids, 'meetingID');
236
        return $recordings;
237
    }
238
 
239
    /**
240
     * Helper function to fetch recordings from a BigBlueButton server.
241
     *
242
     * @param array $keyids list of meetingids or recordingids
243
     * @param string $key the param name used for the BBB request (<recordID>|meetingID)
244
     * @return array (associative) with recordings indexed by recordID, each recording is a non sequential array.
245
     *  and sorted {@see recording_proxy::sort_recordings}
246
     */
247
    private static function do_fetch_recordings(array $keyids = [], string $key = 'recordID'): array {
248
        $recordings = [];
249
        $pagesize = 25;
250
        while ($ids = array_splice($keyids, 0, $pagesize)) {
251
            $fetchrecordings = self::fetch_recordings_page($ids, $key);
252
            $recordings += $fetchrecordings;
253
        }
254
        // Sort recordings.
255
        return self::sort_recordings($recordings);
256
    }
257
    /**
258
     * Helper function to fetch a page of recordings from the remote server.
259
     *
260
     * @param array $ids
261
     * @param string $key
262
     * @return array
263
     */
264
    private static function fetch_recordings_page(array $ids, $key = 'recordID'): array {
265
        // The getRecordings call is executed using a method GET (supported by all versions of BBB).
266
        $xml = self::fetch_endpoint_xml('getRecordings', [$key => implode(',', $ids), 'state' => 'any']);
267
 
268
        if (!$xml) {
269
            return [];
270
        }
271
 
272
        if ($xml->returncode != 'SUCCESS') {
273
            return [];
274
        }
275
 
276
        if (!isset($xml->recordings)) {
277
            return [];
278
        }
279
 
280
        $recordings = [];
281
        // If there were recordings already created.
282
        foreach ($xml->recordings->recording as $recordingxml) {
283
            $recording = self::parse_recording($recordingxml);
284
            $recordings[$recording['recordID']] = $recording;
285
            // Check if there are any child.
286
            if (isset($recordingxml->breakoutRooms->breakoutRoom)) {
287
                $breakoutrooms = [];
288
                foreach ($recordingxml->breakoutRooms->breakoutRoom as $breakoutroom) {
289
                    $breakoutrooms[] = trim((string) $breakoutroom);
290
                }
291
                if ($breakoutrooms) {
292
                    $xml = self::fetch_endpoint_xml('getRecordings', ['recordID' => implode(',', $breakoutrooms)]);
293
                    if ($xml && $xml->returncode == 'SUCCESS' && isset($xml->recordings)) {
294
                        // If there were already created meetings.
295
                        foreach ($xml->recordings->recording as $subrecordingxml) {
296
                            $recording = self::parse_recording($subrecordingxml);
297
                            $recordings[$recording['recordID']] = $recording;
298
                        }
299
                    }
300
                }
301
            }
302
        }
303
 
304
        return $recordings;
305
    }
306
 
307
    /**
308
     *  Helper function to sort an array of recordings. It compares the startTime in two recording objects.
309
     *
310
     * @param array $recordings
311
     * @return array
312
     */
313
    public static function sort_recordings(array $recordings): array {
314
        global $CFG;
315
 
316
        uasort($recordings, function($a, $b) {
317
            if ($a['startTime'] < $b['startTime']) {
318
                return -1;
319
            }
320
            if ($a['startTime'] == $b['startTime']) {
321
                return 0;
322
            }
323
            return 1;
324
        });
325
 
326
        return $recordings;
327
    }
328
 
329
    /**
330
     * Helper function to parse an xml recording object and produce an array in the format used by the plugin.
331
     *
332
     * @param SimpleXMLElement $recording
333
     *
334
     * @return array
335
     */
336
    public static function parse_recording(SimpleXMLElement $recording): array {
337
        // Add formats.
338
        $playbackarray = [];
339
        foreach ($recording->playback->format as $format) {
340
            $playbackarray[(string) $format->type] = [
341
                'type' => (string) $format->type,
342
                'url' => trim((string) $format->url), 'length' => (string) $format->length
343
            ];
344
            // Add preview per format when existing.
345
            if ($format->preview) {
346
                $playbackarray[(string) $format->type]['preview'] =
347
                    self::parse_preview_images($format->preview);
348
            }
349
        }
350
        // Add the metadata to the recordings array.
351
        $metadataarray =
352
            self::parse_recording_meta(get_object_vars($recording->metadata));
353
        $recordingarray = [
354
            'recordID' => (string) $recording->recordID,
355
            'meetingID' => (string) $recording->meetingID,
356
            'meetingName' => (string) $recording->name,
357
            'published' => (string) $recording->published,
358
            'state' => (string) $recording->state,
359
            'startTime' => (string) $recording->startTime,
360
            'endTime' => (string) $recording->endTime,
361
            'playbacks' => $playbackarray
362
        ];
363
        if (isset($recording->protected)) {
364
            $recordingarray['protected'] = (string) $recording->protected;
365
        }
366
        return $recordingarray + $metadataarray;
367
    }
368
 
369
    /**
370
     * Helper function to convert an xml recording metadata object to an array in the format used by the plugin.
371
     *
372
     * @param array $metadata
373
     *
374
     * @return array
375
     */
376
    public static function parse_recording_meta(array $metadata): array {
377
        $metadataarray = [];
378
        foreach ($metadata as $key => $value) {
379
            if (is_object($value)) {
380
                $value = '';
381
            }
382
            $metadataarray['meta_' . $key] = $value;
383
        }
384
        return $metadataarray;
385
    }
386
 
387
    /**
388
     * Helper function to convert an xml recording preview images to an array in the format used by the plugin.
389
     *
390
     * @param SimpleXMLElement $preview
391
     *
392
     * @return array
393
     */
394
    public static function parse_preview_images(SimpleXMLElement $preview): array {
395
        $imagesarray = [];
396
        foreach ($preview->images->image as $image) {
397
            $imagearray = ['url' => trim((string) $image)];
398
            foreach ($image->attributes() as $attkey => $attvalue) {
399
                $imagearray[$attkey] = (string) $attvalue;
400
            }
401
            array_push($imagesarray, $imagearray);
402
        }
403
        return $imagesarray;
404
    }
405
}