Proyectos de Subversion Moodle

Rev

| 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
    /**
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
}