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 the Zoom plugin for 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
/**
18
 * Internal library of functions for module zoom
19
 *
20
 * All the zoom specific functions, needed to implement the module
21
 * logic, should go here. Never include this file from your lib.php!
22
 *
23
 * @package    mod_zoom
24
 * @copyright  2015 UC Regents
25
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26
 */
27
 
28
defined('MOODLE_INTERNAL') || die();
29
 
30
global $CFG;
31
require_once($CFG->dirroot . '/mod/zoom/lib.php');
32
require_once($CFG->dirroot . '/mod/zoom/classes/webservice_exception.php');
33
require_once($CFG->dirroot . '/mod/zoom/classes/api_limit_exception.php');
34
require_once($CFG->dirroot . '/mod/zoom/classes/bad_request_exception.php');
35
require_once($CFG->dirroot . '/mod/zoom/classes/not_found_exception.php');
36
require_once($CFG->dirroot . '/mod/zoom/classes/retry_failed_exception.php');
37
require_once($CFG->dirroot . '/mod/zoom/classes/webservice.php');
38
 
39
// Constants.
40
// Audio options.
41
define('ZOOM_AUDIO_TELEPHONY', 'telephony');
42
define('ZOOM_AUDIO_VOIP', 'voip');
43
define('ZOOM_AUDIO_BOTH', 'both');
44
// Meeting types.
45
define('ZOOM_INSTANT_MEETING', 1);
46
define('ZOOM_SCHEDULED_MEETING', 2);
47
define('ZOOM_RECURRING_MEETING', 3);
48
define('ZOOM_SCHEDULED_WEBINAR', 5);
49
define('ZOOM_RECURRING_WEBINAR', 6);
50
define('ZOOM_RECURRING_FIXED_MEETING', 8);
51
define('ZOOM_RECURRING_FIXED_WEBINAR', 9);
52
// Meeting status.
53
define('ZOOM_MEETING_EXPIRED', 0);
54
define('ZOOM_MEETING_EXISTS', 1);
55
 
56
// Number of meetings per page from zoom's get user report.
57
define('ZOOM_DEFAULT_RECORDS_PER_CALL', 30);
58
define('ZOOM_MAX_RECORDS_PER_CALL', 300);
59
// User types. Numerical values from Zoom API.
60
define('ZOOM_USER_TYPE_BASIC', 1);
61
define('ZOOM_USER_TYPE_PRO', 2);
62
define('ZOOM_USER_TYPE_CORP', 3);
63
define('ZOOM_MEETING_NOT_FOUND_ERROR_CODE', 3001);
64
define('ZOOM_USER_NOT_FOUND_ERROR_CODE', 1001);
65
define('ZOOM_INVALID_USER_ERROR_CODE', 1120);
66
// Webinar options.
67
define('ZOOM_WEBINAR_DISABLE', 0);
68
define('ZOOM_WEBINAR_SHOWONLYIFLICENSE', 1);
69
define('ZOOM_WEBINAR_ALWAYSSHOW', 2);
70
// Encryption type options.
71
define('ZOOM_ENCRYPTION_DISABLE', 0);
72
define('ZOOM_ENCRYPTION_SHOWONLYIFPOSSIBLE', 1);
73
define('ZOOM_ENCRYPTION_ALWAYSSHOW', 2);
74
// Encryption types. String values for Zoom API.
75
define('ZOOM_ENCRYPTION_TYPE_ENHANCED', 'enhanced_encryption');
76
define('ZOOM_ENCRYPTION_TYPE_E2EE', 'e2ee');
77
// Alternative hosts options.
78
define('ZOOM_ALTERNATIVEHOSTS_DISABLE', 0);
79
define('ZOOM_ALTERNATIVEHOSTS_INPUTFIELD', 1);
80
define('ZOOM_ALTERNATIVEHOSTS_PICKER', 2);
81
// Scheduling privilege options.
82
define('ZOOM_SCHEDULINGPRIVILEGE_DISABLE', 0);
83
define('ZOOM_SCHEDULINGPRIVILEGE_ENABLE', 1);
84
// All meetings options.
85
define('ZOOM_ALLMEETINGS_DISABLE', 0);
86
define('ZOOM_ALLMEETINGS_ENABLE', 1);
87
// Download iCal options.
88
define('ZOOM_DOWNLOADICAL_DISABLE', 0);
89
define('ZOOM_DOWNLOADICAL_ENABLE', 1);
90
// Capacity warning options.
91
define('ZOOM_CAPACITYWARNING_DISABLE', 0);
92
define('ZOOM_CAPACITYWARNING_ENABLE', 1);
93
// Recurrence type options.
94
define('ZOOM_RECURRINGTYPE_NOTIME', 0);
95
define('ZOOM_RECURRINGTYPE_DAILY', 1);
96
define('ZOOM_RECURRINGTYPE_WEEKLY', 2);
97
define('ZOOM_RECURRINGTYPE_MONTHLY', 3);
98
// Recurring monthly repeat options.
99
define('ZOOM_MONTHLY_REPEAT_OPTION_DAY', 1);
100
define('ZOOM_MONTHLY_REPEAT_OPTION_WEEK', 2);
101
// Recurring end date options.
102
define('ZOOM_END_DATE_OPTION_BY', 1);
103
define('ZOOM_END_DATE_OPTION_AFTER', 2);
104
// API endpoint options.
105
define('ZOOM_API_ENDPOINT_EU', 'eu');
106
define('ZOOM_API_ENDPOINT_GLOBAL', 'global');
107
define('ZOOM_API_URL_EU', 'https://eu01api-www4local.zoom.us/v2/');
108
define('ZOOM_API_URL_GLOBAL', 'https://api.zoom.us/v2/');
109
// Auto-recording options.
110
define('ZOOM_AUTORECORDING_NONE', 'none');
111
define('ZOOM_AUTORECORDING_USERDEFAULT', 'userdefault');
112
define('ZOOM_AUTORECORDING_LOCAL', 'local');
113
define('ZOOM_AUTORECORDING_CLOUD', 'cloud');
114
// Registration options.
115
define('ZOOM_REGISTRATION_AUTOMATIC', 0);
116
define('ZOOM_REGISTRATION_MANUAL', 1);
117
define('ZOOM_REGISTRATION_OFF', 2);
118
 
119
/**
120
 * Terminate the current script with a fatal error.
121
 *
122
 * Adapted from core_renderer's fatal_error() method. Needed because throwing errors with HTML links in them will convert links
123
 * to text using htmlentities. See MDL-66161 - Reflected XSS possible from some fatal error messages.
124
 *
125
 * So need custom error handler for fatal Zoom errors that have links to help people.
126
 *
127
 * @param string $errorcode The name of the string from error.php to print
128
 * @param string $module name of module
129
 * @param string $continuelink The url where the user will be prompted to continue.
130
 *                             If no url is provided the user will be directed to
131
 *                             the site index page.
132
 * @param mixed $a Extra words and phrases that might be required in the error string
133
 */
134
function zoom_fatal_error($errorcode, $module = '', $continuelink = '', $a = null) {
135
    global $CFG, $COURSE, $OUTPUT, $PAGE;
136
 
137
    $output = '';
138
    $obbuffer = '';
139
 
140
    // Assumes that function is run before output is generated.
141
    if ($OUTPUT->has_started()) {
142
        // If not then have to default to standard error.
143
        throw new moodle_exception($errorcode, $module, $continuelink, $a);
144
    }
145
 
146
    $PAGE->set_heading($COURSE->fullname);
147
    $output .= $OUTPUT->header();
148
 
149
    // Output message without messing with HTML content of error.
150
    $message = '<p class="errormessage">' . get_string($errorcode, $module, $a) . '</p>';
151
 
152
    $output .= $OUTPUT->box($message, 'errorbox alert alert-danger', null, ['data-rel' => 'fatalerror']);
153
 
154
    if ($CFG->debugdeveloper) {
155
        if (!empty($debuginfo)) {
156
            $debuginfo = s($debuginfo); // Removes all nasty JS.
157
            $debuginfo = str_replace("\n", '<br />', $debuginfo); // Keep newlines.
158
            $output .= $OUTPUT->notification('<strong>Debug info:</strong> ' . $debuginfo, 'notifytiny');
159
        }
160
 
161
        if (!empty($backtrace)) {
162
            $output .= $OUTPUT->notification('<strong>Stack trace:</strong> ' . format_backtrace($backtrace), 'notifytiny');
163
        }
164
 
165
        if ($obbuffer !== '') {
166
            $output .= $OUTPUT->notification('<strong>Output buffer:</strong> ' . s($obbuffer), 'notifytiny');
167
        }
168
    }
169
 
170
    if (!empty($continuelink)) {
171
        $output .= $OUTPUT->continue_button($continuelink);
172
    }
173
 
174
    $output .= $OUTPUT->footer();
175
 
176
    // Padding to encourage IE to display our error page, rather than its own.
177
    $output .= str_repeat(' ', 512);
178
 
179
    echo $output;
180
 
181
    exit(1); // General error code.
182
}
183
 
184
/**
185
 * Get course/cm/zoom objects from url parameters, and check for login/permissions.
186
 *
187
 * @return array Array of ($course, $cm, $zoom)
188
 */
189
function zoom_get_instance_setup() {
190
    global $DB;
191
 
192
    $id = optional_param('id', 0, PARAM_INT); // Course_module ID.
193
    $n = optional_param('n', 0, PARAM_INT);  // Zoom instance ID.
194
 
195
    if ($id) {
196
        $cm = get_coursemodule_from_id('zoom', $id, 0, false, MUST_EXIST);
197
        $course = $DB->get_record('course', ['id' => $cm->course], '*', MUST_EXIST);
198
        $zoom = $DB->get_record('zoom', ['id' => $cm->instance], '*', MUST_EXIST);
199
    } else if ($n) {
200
        $zoom = $DB->get_record('zoom', ['id' => $n], '*', MUST_EXIST);
201
        $course = $DB->get_record('course', ['id' => $zoom->course], '*', MUST_EXIST);
202
        $cm = get_coursemodule_from_instance('zoom', $zoom->id, $course->id, false, MUST_EXIST);
203
    } else {
204
        throw new moodle_exception('zoomerr_id_missing', 'mod_zoom');
205
    }
206
 
207
    require_login($course, true, $cm);
208
 
209
    $context = context_module::instance($cm->id);
210
    require_capability('mod/zoom:view', $context);
211
 
212
    return [$course, $cm, $zoom];
213
}
214
 
215
/**
216
 * Retrieves information for a meeting.
217
 *
218
 * @param int $zoomid
219
 * @return array information about the meeting
220
 */
221
function zoom_get_sessions_for_display($zoomid) {
222
    global $DB, $CFG;
223
 
224
    require_once($CFG->libdir . '/moodlelib.php');
225
 
226
    $sessions = [];
227
    $format = get_string('strftimedatetimeshort', 'langconfig');
228
 
229
    // Sort sessions in start_time ascending order.
230
    $instances = $DB->get_records('zoom_meeting_details', ['zoomid' => $zoomid], 'start_time');
231
 
232
    foreach ($instances as $instance) {
233
        // The meeting uuid, not the participant's uuid.
234
        $uuid = $instance->uuid;
235
        $participantlist = zoom_get_participants_report($instance->id);
236
        $sessions[$uuid]['participants'] = $participantlist;
237
 
238
        $uniquevalues = [];
239
        $uniqueparticipantcount = 0;
240
        foreach ($participantlist as $participant) {
241
            $unique = true;
242
            if ($participant->uuid != null) {
243
                if (array_key_exists($participant->uuid, $uniquevalues)) {
244
                    $unique = false;
245
                } else {
246
                    $uniquevalues[$participant->uuid] = true;
247
                }
248
            }
249
 
250
            if ($participant->userid != null) {
251
                if (!$unique || !array_key_exists($participant->userid, $uniquevalues)) {
252
                    $uniquevalues[$participant->userid] = true;
253
                } else {
254
                    $unique = false;
255
                }
256
            }
257
 
258
            if ($participant->user_email != null) {
259
                if (!$unique || !array_key_exists($participant->user_email, $uniquevalues)) {
260
                    $uniquevalues[$participant->user_email] = true;
261
                } else {
262
                    $unique = false;
263
                }
264
            }
265
 
266
            $uniqueparticipantcount += $unique ? 1 : 0;
267
        }
268
 
269
        $sessions[$uuid]['count'] = $uniqueparticipantcount;
270
        $sessions[$uuid]['topic'] = $instance->topic;
271
        $sessions[$uuid]['duration'] = $instance->duration;
272
        $sessions[$uuid]['starttime'] = userdate($instance->start_time, $format);
273
        $sessions[$uuid]['endtime'] = userdate($instance->start_time + $instance->duration * 60, $format);
274
    }
275
 
276
    return $sessions;
277
}
278
 
279
/**
280
 * Get the next occurrence of a meeting.
281
 *
282
 * @param stdClass $zoom
283
 * @return int The timestamp of the next occurrence of a recurring meeting or
284
 *             0 if this is a recurring meeting without fixed time or
285
 *             the timestamp of the meeting start date if this isn't a recurring meeting.
286
 */
287
function zoom_get_next_occurrence($zoom) {
288
    global $DB;
289
 
290
    // Prepare an ad-hoc request cache as this function could be called multiple times throughout a request
291
    // and we want to avoid to make duplicate DB calls.
292
    $cacheoptions = [
293
        'simplekeys' => true,
294
        'simpledata' => true,
295
    ];
296
    $cache = cache::make_from_params(cache_store::MODE_REQUEST, 'zoom', 'nextoccurrence', [], $cacheoptions);
297
 
298
    // If the next occurrence wasn't already cached, fill the cache.
299
    $cachednextoccurrence = $cache->get($zoom->id);
300
    if ($cachednextoccurrence === false) {
301
        // If this isn't a recurring meeting.
302
        if (!$zoom->recurring) {
303
            // Use the meeting start time.
304
            $cachednextoccurrence = $zoom->start_time;
305
 
306
            // Or if this is a recurring meeting without fixed time.
307
        } else if ($zoom->recurrence_type == ZOOM_RECURRINGTYPE_NOTIME) {
308
            // Use 0 as there isn't anything better to return.
309
            $cachednextoccurrence = 0;
310
 
311
            // Otherwise we have a recurring meeting with a recurrence schedule.
312
        } else {
313
            // Get the calendar event of the next occurrence.
314
            $selectclause = "modulename = :modulename AND instance = :instance AND (timestart + timeduration) >= :now";
315
            $selectparams = ['modulename' => 'zoom', 'instance' => $zoom->id, 'now' => time()];
316
            $nextoccurrence = $DB->get_records_select('event', $selectclause, $selectparams, 'timestart ASC', 'timestart', 0, 1);
317
 
318
            // If we haven't got a single event.
319
            if (empty($nextoccurrence)) {
320
                // Use 0 as there isn't anything better to return.
321
                $cachednextoccurrence = 0;
322
            } else {
323
                // Use the timestamp of the event.
324
                $nextoccurenceobject = reset($nextoccurrence);
325
                $cachednextoccurrence = $nextoccurenceobject->timestart;
326
            }
327
        }
328
 
329
        // Store the next occurrence into the cache.
330
        $cache->set($zoom->id, $cachednextoccurrence);
331
    }
332
 
333
    // Return the next occurrence.
334
    return $cachednextoccurrence;
335
}
336
 
337
/**
338
 * Determine if a zoom meeting is in progress, is available, and/or is finished.
339
 *
340
 * @param stdClass $zoom
341
 * @return array Array of booleans: [in progress, available, finished].
342
 */
343
function zoom_get_state($zoom) {
344
    // Get plugin config.
345
    $config = get_config('zoom');
346
 
347
    // Get the current time as calculation basis.
348
    $now = time();
349
 
350
    // If this is a recurring meeting with a recurrence schedule.
351
    if ($zoom->recurring && $zoom->recurrence_type != ZOOM_RECURRINGTYPE_NOTIME) {
352
        // Get the next occurrence start time.
353
        $starttime = zoom_get_next_occurrence($zoom);
354
    } else {
355
        // Get the meeting start time.
356
        $starttime = $zoom->start_time;
357
    }
358
 
359
    // Calculate the time when the recurring meeting becomes available next,
360
    // based on the next occurrence start time and the general meeting lead time.
361
    $firstavailable = $starttime - ($config->firstabletojoin * 60);
362
 
363
    // Calculate the time when the meeting ends to be available,
364
    // based on the next occurrence start time and the meeting duration.
365
    $lastavailable = $starttime + $zoom->duration;
366
 
367
    // Determine if the meeting is in progress.
368
    $inprogress = ($firstavailable <= $now && $now <= $lastavailable);
369
 
370
    // Determine if its a recurring meeting with no fixed time.
371
    $isrecurringnotime = $zoom->recurring && $zoom->recurrence_type == ZOOM_RECURRINGTYPE_NOTIME;
372
 
373
    // Determine if the meeting is available,
374
    // based on the fact if it is recurring or in progress.
375
    $available = $isrecurringnotime || $inprogress;
376
 
377
    // Determine if the meeting is finished,
378
    // based on the fact if it is recurring or the meeting end time is still in the future.
379
    $finished = !$isrecurringnotime && $now > $lastavailable;
380
 
381
    // Return the requested information.
382
    return [$inprogress, $available, $finished];
383
}
384
 
385
/**
386
 * Get the Zoom id of the currently logged-in user.
387
 *
388
 * @param bool $required If true, will error if the user doesn't have a Zoom account.
389
 * @return string
390
 */
391
function zoom_get_user_id($required = true) {
392
    global $USER;
393
 
394
    $cache = cache::make('mod_zoom', 'zoomid');
395
    if (!($zoomuserid = $cache->get($USER->id))) {
396
        $zoomuserid = false;
397
        try {
398
            $zoomuser = zoom_get_user(zoom_get_api_identifier($USER));
399
            if ($zoomuser !== false && isset($zoomuser->id) && ($zoomuser->id !== false)) {
400
                $zoomuserid = $zoomuser->id;
401
                $cache->set($USER->id, $zoomuserid);
402
            }
403
        } catch (moodle_exception $error) {
404
            if ($required) {
405
                throw $error;
406
            }
407
        }
408
    }
409
 
410
    return $zoomuserid;
411
}
412
 
413
/**
414
 * Get the Zoom meeting security settings, including meeting password requirements of the user's master account.
415
 *
416
 * @param string|int $identifier The user's email or the user's ID per Zoom API.
417
 * @return stdClass
418
 */
419
function zoom_get_meeting_security_settings($identifier) {
420
    $cache = cache::make('mod_zoom', 'zoommeetingsecurity');
421
    $zoommeetingsecurity = $cache->get($identifier);
422
    if (empty($zoommeetingsecurity)) {
423
        $zoommeetingsecurity = zoom_webservice()->get_account_meeting_security_settings($identifier);
424
        $cache->set($identifier, $zoommeetingsecurity);
425
    }
426
 
427
    return $zoommeetingsecurity;
428
}
429
 
430
/**
431
 * Check if the error indicates that a meeting is gone.
432
 *
433
 * @param moodle_exception $error
434
 * @return bool
435
 */
436
function zoom_is_meeting_gone_error($error) {
437
    // If the meeting's owner/user cannot be found, we consider the meeting to be gone.
438
    return ($error->zoomerrorcode === ZOOM_MEETING_NOT_FOUND_ERROR_CODE) || zoom_is_user_not_found_error($error);
439
}
440
 
441
/**
442
 * Check if the error indicates that a user is not found or does not belong to the current account.
443
 *
444
 * @param moodle_exception $error
445
 * @return bool
446
 */
447
function zoom_is_user_not_found_error($error) {
448
    return ($error->zoomerrorcode === ZOOM_USER_NOT_FOUND_ERROR_CODE) || ($error->zoomerrorcode === ZOOM_INVALID_USER_ERROR_CODE);
449
}
450
 
451
/**
452
 * Return the string parameter for zoomerr_meetingnotfound.
453
 *
454
 * @param string $cmid
455
 * @return stdClass
456
 */
457
function zoom_meetingnotfound_param($cmid) {
458
    // Provide links to recreate and delete.
459
    $recreate = new moodle_url('/mod/zoom/recreate.php', ['id' => $cmid, 'sesskey' => sesskey()]);
460
    $delete = new moodle_url('/course/mod.php', ['delete' => $cmid, 'sesskey' => sesskey()]);
461
 
462
    // Convert links to strings and pass as error parameter.
463
    $param = new stdClass();
464
    $param->recreate = $recreate->out();
465
    $param->delete = $delete->out();
466
 
467
    return $param;
468
}
469
 
470
/**
471
 * Get the data of each user for the participants report.
472
 * @param string $detailsid The meeting ID that you want to get the participants report for.
473
 * @return array The user data as an array of records (array of arrays).
474
 */
475
function zoom_get_participants_report($detailsid) {
476
    global $DB;
477
    $sql = 'SELECT zmp.id,
478
                   zmp.name,
479
                   zmp.userid,
480
                   zmp.user_email,
481
                   zmp.join_time,
482
                   zmp.leave_time,
483
                   zmp.duration,
484
                   zmp.uuid
485
              FROM {zoom_meeting_participants} zmp
486
             WHERE zmp.detailsid = :detailsid
487
    ';
488
    $params = [
489
        'detailsid' => $detailsid,
490
    ];
491
    $participants = $DB->get_records_sql($sql, $params);
492
    return $participants;
493
}
494
 
495
/**
496
 * Creates a default passcode from the user's Zoom meeting security settings.
497
 *
498
 * @param stdClass $meetingpasswordrequirement
499
 * @return string passcode
500
 */
501
function zoom_create_default_passcode($meetingpasswordrequirement) {
502
    $length = max($meetingpasswordrequirement->length, 6);
503
    $random = rand(0, pow(10, $length) - 1);
504
    $passcode = str_pad(strval($random), $length, '0', STR_PAD_LEFT);
505
 
506
    // Get a random set of indexes to replace with non-numberic values.
507
    $indexes = range(0, $length - 1);
508
    shuffle($indexes);
509
 
510
    if ($meetingpasswordrequirement->have_letter || $meetingpasswordrequirement->have_upper_and_lower_characters) {
511
        // Random letter from A-Z.
512
        $passcode[$indexes[0]] = chr(rand(65, 90));
513
        // Random letter from a-z.
514
        $passcode[$indexes[1]] = chr(rand(97, 122));
515
    }
516
 
517
    if ($meetingpasswordrequirement->have_special_character) {
518
        $specialchar = '@_*-';
519
        $passcode[$indexes[2]] = substr(str_shuffle($specialchar), 0, 1);
520
    }
521
 
522
    return $passcode;
523
}
524
 
525
/**
526
 * Creates a description string from the user's Zoom meeting security settings.
527
 *
528
 * @param stdClass $meetingpasswordrequirement
529
 * @return string description of password requirements
530
 */
531
function zoom_create_passcode_description($meetingpasswordrequirement) {
532
    $description = '';
533
    if ($meetingpasswordrequirement->only_allow_numeric) {
534
        $description .= get_string('password_only_numeric', 'mod_zoom') . ' ';
535
    } else {
536
        if ($meetingpasswordrequirement->have_letter && !$meetingpasswordrequirement->have_upper_and_lower_characters) {
537
            $description .= get_string('password_letter', 'mod_zoom') . ' ';
538
        } else if ($meetingpasswordrequirement->have_upper_and_lower_characters) {
539
            $description .= get_string('password_lower_upper', 'mod_zoom') . ' ';
540
        }
541
 
542
        if ($meetingpasswordrequirement->have_number) {
543
            $description .= get_string('password_number', 'mod_zoom') . ' ';
544
        }
545
 
546
        if ($meetingpasswordrequirement->have_special_character) {
547
            $description .= get_string('password_special', 'mod_zoom') . ' ';
548
        } else {
549
            $description .= get_string('password_allowed_char', 'mod_zoom') . ' ';
550
        }
551
    }
552
 
553
    if ($meetingpasswordrequirement->length) {
554
        $description .= get_string('password_length', 'mod_zoom', $meetingpasswordrequirement->length) . ' ';
555
    }
556
 
557
    if ($meetingpasswordrequirement->consecutive_characters_length > 0) {
558
        $description .= get_string(
559
            'password_consecutive',
560
            'mod_zoom',
561
            $meetingpasswordrequirement->consecutive_characters_length - 1
562
        ) . ' ';
563
    }
564
 
565
    $description .= get_string('password_max_length', 'mod_zoom');
566
    return $description;
567
}
568
 
569
/**
570
 * Creates an array of users who can be selected as alternative host in a given context.
571
 *
572
 * @param context $context The context to be used.
573
 *
574
 * @return array Array of users (mail => fullname).
575
 */
576
function zoom_get_selectable_alternative_hosts_list(context $context) {
577
    // Get selectable alternative host users based on the capability.
578
    $users = get_enrolled_users($context, 'mod/zoom:eligiblealternativehost', 0, 'u.*', 'lastname');
579
 
580
    // Create array of users.
581
    $selectablealternativehosts = [];
582
 
583
    // Iterate over selectable alternative host users.
584
    foreach ($users as $u) {
585
        // Note: Basically, if this is the user's own data row, the data row should be skipped.
586
        // But this would then not cover the case when a user is scheduling the meeting _for_ another user
587
        // and wants to be an alternative host himself.
588
        // As this would have to be handled at runtime in the browser, we just offer all users with the
589
        // capability as selectable and leave this aspect as possible improvement for the future.
590
        // At least, Zoom does not care if the user who is the host adds himself as alternative host as well.
591
 
592
        // Verify that the user really has a Zoom account.
593
        // Furthermore, verify that the user's status is active. Adding a pending or inactive user as alternative host will result
594
        // in a Zoom API error otherwise.
595
        $zoomuser = zoom_get_user($u->email);
596
        if ($zoomuser !== false && $zoomuser->status === 'active') {
597
            // Add user to array of users.
598
            $selectablealternativehosts[$u->email] = fullname($u);
599
        }
600
    }
601
 
602
    return $selectablealternativehosts;
603
}
604
 
605
/**
606
 * Creates a string of roles who can be selected as alternative host in a given context.
607
 *
608
 * @param context $context The context to be used.
609
 *
610
 * @return string The string of roles.
611
 */
612
function zoom_get_selectable_alternative_hosts_rolestring(context $context) {
613
    // Get selectable alternative host users based on the capability.
614
    $roles = get_role_names_with_caps_in_context($context, ['mod/zoom:eligiblealternativehost']);
615
 
616
    // Compose string.
617
    $rolestring = implode(', ', $roles);
618
 
619
    return $rolestring;
620
}
621
 
622
/**
623
 * Get existing Moodle users from a given set of alternative hosts.
624
 *
625
 * @param array $alternativehosts The array of alternative hosts email addresses.
626
 *
627
 * @return array The array of existing Moodle user objects.
628
 */
629
function zoom_get_users_from_alternativehosts(array $alternativehosts) {
630
    global $DB;
631
 
632
    // Get the existing Moodle user objects from the DB.
633
    [$insql, $inparams] = $DB->get_in_or_equal($alternativehosts);
634
    $sql = 'SELECT *
635
            FROM {user}
636
            WHERE email ' . $insql . '
637
            ORDER BY lastname ASC';
638
    $alternativehostusers = $DB->get_records_sql($sql, $inparams);
639
 
640
    return $alternativehostusers;
641
}
642
 
643
/**
644
 * Get non-Moodle users from a given set of alternative hosts.
645
 *
646
 * @param array $alternativehosts The array of alternative hosts email addresses.
647
 *
648
 * @return array The array of non-Moodle user mail addresses.
649
 */
650
function zoom_get_nonusers_from_alternativehosts(array $alternativehosts) {
651
    global $DB;
652
 
653
    // Get the non-Moodle user mail addresses by checking which one does not exist in the DB.
654
    $alternativehostnonusers = [];
655
    [$insql, $inparams] = $DB->get_in_or_equal($alternativehosts);
656
    $sql = 'SELECT email
657
            FROM {user}
658
            WHERE email ' . $insql . '
659
            ORDER BY email ASC';
660
    $alternativehostusersmails = $DB->get_records_sql($sql, $inparams);
661
    foreach ($alternativehosts as $ah) {
662
        if (!array_key_exists($ah, $alternativehostusersmails)) {
663
            $alternativehostnonusers[] = $ah;
664
        }
665
    }
666
 
667
    return $alternativehostnonusers;
668
}
669
 
670
/**
671
 * Get the unavailability note based on the Zoom plugin configuration.
672
 *
673
 * @param object $zoom The Zoom meeting object.
674
 * @param bool|null $finished The function needs to know if the meeting is already finished.
675
 *                       You can provide this information, if already available, to the function.
676
 *                       Otherwise it will determine it with a small overhead.
677
 *
678
 * @return string The unavailability note.
679
 */
680
function zoom_get_unavailability_note($zoom, $finished = null) {
681
    // Get config.
682
    $config = get_config('zoom');
683
 
684
    // Get the plain unavailable string.
685
    $strunavailable = get_string('unavailable', 'mod_zoom');
686
 
687
    // If this is a recurring meeting without fixed time, just use the plain unavailable string.
688
    if ($zoom->recurring && $zoom->recurrence_type == ZOOM_RECURRINGTYPE_NOTIME) {
689
        $unavailabilitynote = $strunavailable;
690
 
691
        // Otherwise we add some more information to the unavailable string.
692
    } else {
693
        // If we don't have the finished information yet, get it with a small overhead.
694
        if ($finished === null) {
695
            [$inprogress, $available, $finished] = zoom_get_state($zoom);
696
        }
697
 
698
        // If this meeting is still pending.
699
        if ($finished !== true) {
700
            // If the admin wants to show the leadtime.
701
            if (!empty($config->displayleadtime) && $config->firstabletojoin > 0) {
702
                $unavailabilitynote = $strunavailable . '<br />' .
703
                        get_string('unavailablefirstjoin', 'mod_zoom', ['mins' => ($config->firstabletojoin)]);
704
 
705
                // Otherwise.
706
            } else {
707
                $unavailabilitynote = $strunavailable . '<br />' . get_string('unavailablenotstartedyet', 'mod_zoom');
708
            }
709
 
710
            // Otherwise, the meeting has finished.
711
        } else {
712
            $unavailabilitynote = $strunavailable . '<br />' . get_string('unavailablefinished', 'mod_zoom');
713
        }
714
    }
715
 
716
    return $unavailabilitynote;
717
}
718
 
719
/**
720
 * Gets the meeting capacity of a given Zoom user.
721
 * Please note: This function does not check if the Zoom user really exists, this has to be checked before calling this function.
722
 *
723
 * @param string $zoomhostid The Zoom ID of the host.
724
 * @param bool $iswebinar The meeting is a webinar.
725
 *
726
 * @return int|bool The meeting capacity of the Zoom user or false if the user does not have any meeting capacity at all.
727
 */
728
function zoom_get_meeting_capacity(string $zoomhostid, bool $iswebinar = false) {
729
    // Get the 'feature' section of the user's Zoom settings.
730
    $userfeatures = zoom_get_user_settings($zoomhostid)->feature;
731
 
732
    $meetingcapacity = false;
733
 
734
    // If this is a webinar.
735
    if ($iswebinar === true) {
736
        // Get the appropriate capacity value.
737
        if (!empty($userfeatures->webinar_capacity)) {
738
            $meetingcapacity = $userfeatures->webinar_capacity;
739
        } else if (!empty($userfeatures->zoom_events_capacity)) {
740
            $meetingcapacity = $userfeatures->zoom_events_capacity;
741
        }
742
    } else {
743
        // If this is a meeting, get the 'meeting_capacity' value.
744
        if (!empty($userfeatures->meeting_capacity)) {
745
            $meetingcapacity = $userfeatures->meeting_capacity;
746
 
747
            // Check if the user has a 'large_meeting' license that has a higher capacity value.
748
            if (!empty($userfeatures->large_meeting_capacity) && $userfeatures->large_meeting_capacity > $meetingcapacity) {
749
                $meetingcapacity = $userfeatures->large_meeting_capacity;
750
            }
751
        }
752
    }
753
 
754
    return $meetingcapacity;
755
}
756
 
757
/**
758
 * Gets the number of eligible meeting participants in a given context.
759
 * Please note: This function only covers users who are enrolled into the given context.
760
 * It does _not_ include users who have the necessary capability on a higher context without being enrolled.
761
 *
762
 * @param context $context The context which we want to check.
763
 *
764
 * @return int The number of eligible meeting participants.
765
 */
766
function zoom_get_eligible_meeting_participants(context $context) {
767
    global $DB;
768
 
769
    // Compose SQL query.
770
    $sqlsnippets = get_enrolled_with_capabilities_join($context, '', 'mod/zoom:view', 0, true);
771
    $sql = 'SELECT count(DISTINCT u.id)
772
            FROM {user} u ' . $sqlsnippets->joins . ' WHERE ' . $sqlsnippets->wheres;
773
 
774
    // Run query and count records.
775
    $eligibleparticipantcount = $DB->count_records_sql($sql, $sqlsnippets->params);
776
 
777
    return $eligibleparticipantcount;
778
}
779
 
780
/**
781
 * Get array of alternative hosts from a string.
782
 *
783
 * @param string $alternativehoststring Comma (or semicolon) separated list of alternative hosts.
784
 * @return string[] $alternativehostarray Array of alternative hosts.
785
 */
786
function zoom_get_alternative_host_array_from_string($alternativehoststring) {
787
    if (empty($alternativehoststring)) {
788
        return [];
789
    }
790
 
791
    // The Zoom API has historically returned either semicolons or commas, so we need to support both.
792
    $alternativehoststring = str_replace(';', ',', $alternativehoststring);
793
    $alternativehostarray = array_filter(explode(',', $alternativehoststring));
794
    return $alternativehostarray;
795
}
796
 
797
/**
798
 * Get all custom user profile fields of type text
799
 *
800
 * @return array list of user profile fields
801
 */
802
function zoom_get_user_profile_fields() {
803
    global $DB;
804
 
805
    $userfields = [];
806
    $records = $DB->get_records('user_info_field', ['datatype' => 'text']);
807
    foreach ($records as $record) {
808
        $userfields[$record->shortname] = $record->name;
809
    }
810
 
811
    return $userfields;
812
}
813
 
814
/**
815
 * Get all valid options for API Identifier field
816
 *
817
 * @return array list of all valid options
818
 */
819
function zoom_get_api_identifier_fields() {
820
    $options = [
821
        'email' => get_string('email'),
822
        'username' => get_string('username'),
823
        'idnumber' => get_string('idnumber'),
824
    ];
825
 
826
    $userfields = zoom_get_user_profile_fields();
827
    if (!empty($userfields)) {
828
        $options += $userfields;
829
    }
830
 
831
    return $options;
832
}
833
 
834
/**
835
 * Get the zoom api identifier
836
 *
837
 * @param object $user The user object
838
 *
839
 * @return string the value of the identifier
840
 */
841
function zoom_get_api_identifier($user) {
842
    // Get the value from the config first.
843
    $field = get_config('zoom', 'apiidentifier');
844
 
845
    $identifier = '';
846
    if (isset($user->$field)) {
847
        // If one of the standard user fields.
848
        $identifier = $user->$field;
849
    } else if (isset($user->profile[$field])) {
850
        // If one of the custom user fields.
851
        $identifier = $user->profile[$field];
852
    }
853
 
854
    if (empty($identifier)) {
855
        // Fallback to email if the field is not set.
856
        $identifier = $user->email;
857
    }
858
 
859
    return $identifier;
860
}
861
 
862
/**
863
 * Creates an iCalendar_event for a Zoom meeting.
864
 *
865
 * @param stdClass $event The meeting object.
866
 * @param string $description The event description.
867
 *
868
 * @return iCalendar_event
869
 */
870
function zoom_helper_icalendar_event($event, $description) {
871
    global $CFG;
872
 
873
    // Match Moodle's uid format for iCal events.
874
    $hostaddress = str_replace('http://', '', $CFG->wwwroot);
875
    $hostaddress = str_replace('https://', '', $hostaddress);
876
    $uid = $event->id . '@' . $hostaddress;
877
 
878
    $icalevent = new iCalendar_event();
879
    $icalevent->add_property('uid', $uid); // A unique identifier.
880
    $icalevent->add_property('summary', $event->name); // Title.
881
    $icalevent->add_property('dtstamp', Bennu::timestamp_to_datetime()); // Time of creation.
882
    $icalevent->add_property('last-modified', Bennu::timestamp_to_datetime($event->timemodified));
883
    $icalevent->add_property('dtstart', Bennu::timestamp_to_datetime($event->timestart)); // Start time.
884
    $icalevent->add_property('dtend', Bennu::timestamp_to_datetime($event->timestart + $event->timeduration)); // End time.
885
    $icalevent->add_property('description', $description);
886
    return $icalevent;
887
}
888
 
889
/**
890
 * Get the configured Zoom API URL.
891
 *
892
 * @return string The API URL.
893
 */
894
function zoom_get_api_url() {
895
    // Get the API endpoint setting.
896
    $apiendpoint = get_config('zoom', 'apiendpoint');
897
 
898
    // Pick the corresponding API URL.
899
    switch ($apiendpoint) {
900
        case ZOOM_API_ENDPOINT_EU:
901
            $apiurl = ZOOM_API_URL_EU;
902
            break;
903
 
904
        case ZOOM_API_ENDPOINT_GLOBAL:
905
        default:
906
            $apiurl = ZOOM_API_URL_GLOBAL;
907
            break;
908
    }
909
 
910
    // Return API URL.
911
    return $apiurl;
912
}
913
 
914
/**
915
 * Loads the zoom meeting and passes back a meeting URL
916
 * after processing events, view completion, grades, and license updates.
917
 *
918
 * @param int $id course module id
919
 * @param object $context moodle context object
920
 * @param bool $usestarturl
921
 * @return array $returns contains url object 'nexturl' or string 'error'
922
 */
923
function zoom_load_meeting($id, $context, $usestarturl = true) {
924
    global $CFG, $DB, $USER;
925
    require_once($CFG->libdir . '/gradelib.php');
926
 
927
    $cm = get_coursemodule_from_id('zoom', $id, 0, false, MUST_EXIST);
928
    $course = get_course($cm->course);
929
    $zoom = $DB->get_record('zoom', ['id' => $cm->instance], '*', MUST_EXIST);
930
 
931
    require_login($course, true, $cm);
932
 
933
    require_capability('mod/zoom:view', $context);
934
 
935
    $returns = ['nexturl' => null, 'error' => null];
936
 
937
    [$inprogress, $available, $finished] = zoom_get_state($zoom);
938
 
939
    $userisregistered = false;
940
    $userisregistering = false;
941
    if ($zoom->registration != ZOOM_REGISTRATION_OFF) {
942
        // Check if user already registered.
943
        $registrantjoinurl = zoom_get_registrant_join_url($USER->email, $zoom->meeting_id, $zoom->webinar);
944
        $userisregistered = !empty($registrantjoinurl);
945
 
946
        // Allow unregistered users to register.
947
        if (!$userisregistered) {
948
            $userisregistering = true;
949
        }
950
    }
951
 
952
    // If the meeting is not yet available, deny access.
953
    if (!$available && !$userisregistering) {
954
        // Get unavailability note.
955
        $returns['error'] = zoom_get_unavailability_note($zoom, $finished);
956
        return $returns;
957
    }
958
 
959
    $userisrealhost = (zoom_get_user_id(false) === $zoom->host_id);
960
    $alternativehosts = zoom_get_alternative_host_array_from_string($zoom->alternative_hosts);
961
    $userishost = ($userisrealhost || in_array(zoom_get_api_identifier($USER), $alternativehosts, true));
962
 
963
    // Check if we should use the start meeting url.
964
    if ($userisrealhost && $usestarturl) {
965
        // Important: Only the real host can use this URL, because it joins the meeting as the host user.
966
        $starturl = zoom_get_start_url($zoom->meeting_id, $zoom->webinar, $zoom->join_url);
967
        $returns['nexturl'] = new moodle_url($starturl);
968
    } else {
969
        $url = $zoom->join_url;
970
        if ($userisregistered) {
971
            $url = $registrantjoinurl;
972
        }
973
 
974
        $unamesetting = get_config('zoom', 'unamedisplay');
975
        switch ($unamesetting) {
976
            case 'fullname':
977
            default:
978
                $unamedisplay = fullname($USER);
979
                break;
980
 
981
            case 'firstname':
982
                $unamedisplay = $USER->firstname;
983
                break;
984
 
985
            case 'idfullname':
986
                $unamedisplay = '(' . $USER->id . ') ' . fullname($USER);
987
                break;
988
 
989
            case 'id':
990
                $unamedisplay = '(' . $USER->id . ')';
991
                break;
992
        }
993
 
994
        // Try to send the user email (not guaranteed).
995
        $returns['nexturl'] = new moodle_url($url, ['uname' => $unamedisplay, 'uemail' => $USER->email]);
996
    }
997
 
998
    // If the user is pre-registering, skip grading/completion.
999
    if (!$available && $userisregistering) {
1000
        return $returns;
1001
    }
1002
 
1003
    // Record user's clicking join.
1004
    \mod_zoom\event\join_meeting_button_clicked::create([
1005
        'context' => $context,
1006
        'objectid' => $zoom->id,
1007
        'other' => [
1008
            'cmid' => $id,
1009
            'meetingid' => (int) $zoom->meeting_id,
1010
            'userishost' => $userishost,
1011
        ],
1012
    ])->trigger();
1013
 
1014
    // Track completion viewed.
1015
    $completion = new completion_info($course);
1016
    $completion->set_module_viewed($cm);
1017
 
1018
    // Check the grading method settings.
1019
    if (!empty($zoom->grading_method)) {
1020
        $gradingmethod = $zoom->grading_method;
1021
    } else if ($defaultgrading = get_config('gradingmethod', 'zoom')) {
1022
        $gradingmethod = $defaultgrading;
1023
    } else {
1024
        $gradingmethod = 'entry';
1025
    }
1026
 
1027
    if ($gradingmethod === 'entry') {
1028
        // Check whether user has a grade. If not, then assign full credit to them.
1029
        $gradelist = grade_get_grades($course->id, 'mod', 'zoom', $cm->instance, $USER->id);
1030
 
1031
        // Assign full credits for user who has no grade yet, if this meeting is gradable (i.e. the grade type is not "None").
1032
        if (!empty($gradelist->items) && empty($gradelist->items[0]->grades[$USER->id]->grade)) {
1033
            $grademax = $gradelist->items[0]->grademax;
1034
            $grades = [
1035
                'rawgrade' => $grademax,
1036
                'userid' => $USER->id,
1037
                'usermodified' => $USER->id,
1038
                'dategraded' => '',
1039
                'feedbackformat' => '',
1040
                'feedback' => '',
1041
            ];
1042
 
1043
            zoom_grade_item_update($zoom, $grades);
1044
        }
1045
    } // Otherwise, the get_meetings_report task calculates the grades according to duration.
1046
 
1047
    // Upgrade host upon joining meeting, if host is not Licensed.
1048
    if ($userishost) {
1049
        $config = get_config('zoom');
1050
        if (!empty($config->recycleonjoin)) {
1051
            zoom_webservice()->provide_license($zoom->host_id);
1052
        }
1053
    }
1054
 
1055
    return $returns;
1056
}
1057
 
1058
/**
1059
 * Fetches a fresh URL that can be used to start the Zoom meeting.
1060
 *
1061
 * @param string $meetingid Zoom meeting ID.
1062
 * @param bool $iswebinar If the session is a webinar.
1063
 * @param string $fallbackurl URL to use if the webservice call fails.
1064
 * @return string Best available URL for starting the meeting.
1065
 */
1066
function zoom_get_start_url($meetingid, $iswebinar, $fallbackurl) {
1067
    try {
1068
        $response = zoom_webservice()->get_meeting_webinar_info($meetingid, $iswebinar);
1069
        return $response->start_url ?? $response->join_url;
1070
    } catch (moodle_exception $e) {
1071
        // If an exception was thrown, gracefully use the fallback URL.
1072
        return $fallbackurl;
1073
    }
1074
}
1075
 
1076
/**
1077
 * Get the configured Zoom tracking fields.
1078
 *
1079
 * @return array tracking fields, keys as lower case
1080
 */
1081
function zoom_list_tracking_fields() {
1082
    $trackingfields = [];
1083
 
1084
    // Get the tracking fields configured on the account.
1085
    $response = zoom_webservice()->list_tracking_fields();
1086
    if (isset($response->tracking_fields)) {
1087
        foreach ($response->tracking_fields as $trackingfield) {
1088
            $field = str_replace(' ', '_', strtolower($trackingfield->field));
1089
            $trackingfields[$field] = (array) $trackingfield;
1090
        }
1091
    }
1092
 
1093
    return $trackingfields;
1094
}
1095
 
1096
/**
1097
 * Trim and lower case tracking fields.
1098
 *
1099
 * @return array tracking fields trimmed, keys as lower case
1100
 */
1101
function zoom_clean_tracking_fields() {
1102
    $config = get_config('zoom');
1103
    $defaulttrackingfields = explode(',', $config->defaulttrackingfields);
1104
    $trackingfields = [];
1105
 
1106
    foreach ($defaulttrackingfields as $key => $defaulttrackingfield) {
1107
        $trimmed = trim($defaulttrackingfield);
1108
        if (!empty($trimmed)) {
1109
            $key = str_replace(' ', '_', strtolower($trimmed));
1110
            $trackingfields[$key] = $trimmed;
1111
        }
1112
    }
1113
 
1114
    return $trackingfields;
1115
}
1116
 
1117
/**
1118
 * Synchronize tracking field data for a meeting.
1119
 *
1120
 * @param int $zoomid Zoom meeting ID
1121
 * @param array $trackingfields Tracking fields configured in Zoom.
1122
 */
1123
function zoom_sync_meeting_tracking_fields($zoomid, $trackingfields) {
1124
    global $DB;
1125
 
1126
    $tfvalues = [];
1127
    foreach ($trackingfields as $trackingfield) {
1128
        $field = str_replace(' ', '_', strtolower($trackingfield->field));
1129
        $tfvalues[$field] = $trackingfield->value;
1130
    }
1131
 
1132
    $tfrows = $DB->get_records('zoom_meeting_tracking_fields', ['meeting_id' => $zoomid]);
1133
    $tfobjects = [];
1134
    foreach ($tfrows as $tfrow) {
1135
        $tfobjects[$tfrow->tracking_field] = $tfrow;
1136
    }
1137
 
1138
    $defaulttrackingfields = zoom_clean_tracking_fields();
1139
    foreach ($defaulttrackingfields as $key => $defaulttrackingfield) {
1140
        $value = $tfvalues[$key] ?? '';
1141
        if (isset($tfobjects[$key])) {
1142
            $tfobject = $tfobjects[$key];
1143
            if ($value === '') {
1144
                $DB->delete_records('zoom_meeting_tracking_fields', ['meeting_id' => $zoomid, 'tracking_field' => $key]);
1145
            } else if ($tfobject->value !== $value) {
1146
                $tfobject->value = $value;
1147
                $DB->update_record('zoom_meeting_tracking_fields', $tfobject);
1148
            }
1149
        } else if ($value !== '') {
1150
            $tfobject = new stdClass();
1151
            $tfobject->meeting_id = $zoomid;
1152
            $tfobject->tracking_field = $key;
1153
            $tfobject->value = $value;
1154
            $DB->insert_record('zoom_meeting_tracking_fields', $tfobject);
1155
        }
1156
    }
1157
}
1158
 
1159
/**
1160
 * Get all meeting records
1161
 *
1162
 * @return array All zoom meetings stored in the database.
1163
 */
1164
function zoom_get_all_meeting_records() {
1165
    global $DB;
1166
 
1167
    $meetings = [];
1168
    // Only get meetings that exist on zoom.
1169
    $records = $DB->get_records('zoom', ['exists_on_zoom' => ZOOM_MEETING_EXISTS]);
1170
    foreach ($records as $record) {
1171
        $meetings[] = $record;
1172
    }
1173
 
1174
    return $meetings;
1175
}
1176
 
1177
/**
1178
 * Get all recordings for a particular meeting.
1179
 *
1180
 * @param int $zoomid Optional. The id of the zoom meeting.
1181
 *
1182
 * @return array All the recordings for the zoom meeting.
1183
 */
1184
function zoom_get_meeting_recordings($zoomid = null) {
1185
    global $DB;
1186
 
1187
    $params = [];
1188
    if ($zoomid !== null) {
1189
        $params['zoomid'] = $zoomid;
1190
    }
1191
 
1192
    $records = $DB->get_records('zoom_meeting_recordings', $params);
1193
    $recordings = [];
1194
    foreach ($records as $recording) {
1195
        $recordings[$recording->zoomrecordingid] = $recording;
1196
    }
1197
 
1198
    return $recordings;
1199
}
1200
 
1201
/**
1202
 * Get all meeting recordings grouped together.
1203
 *
1204
 * @param int $zoomid Optional. The id of the zoom meeting.
1205
 *
1206
 * @return array All recordings for the zoom meeting grouped together.
1207
 */
1208
function zoom_get_meeting_recordings_grouped($zoomid = null) {
1209
    global $DB;
1210
 
1211
    $params = [];
1212
    if ($zoomid !== null) {
1213
        $params['zoomid'] = $zoomid;
1214
    }
1215
 
1216
    $records = $DB->get_records('zoom_meeting_recordings', $params, 'recordingstart ASC');
1217
    $recordings = [];
1218
    foreach ($records as $recording) {
1219
        $recordings[$recording->meetinguuid][$recording->zoomrecordingid] = $recording;
1220
    }
1221
 
1222
    return $recordings;
1223
}
1224
 
1225
/**
1226
 * Singleton for Zoom webservice class.
1227
 *
1228
 * @return \mod_zoom\webservice
1229
 */
1230
function zoom_webservice() {
1231
    static $service;
1232
 
1233
    if (empty($service)) {
1234
        $service = new \mod_zoom\webservice();
1235
    }
1236
 
1237
    return $service;
1238
}
1239
 
1240
/**
1241
 * Helper to get a Zoom user, efficiently.
1242
 *
1243
 * @param string|int $identifier The user's email or the user's ID per Zoom API.
1244
 * @return stdClass|false If user is found, returns a Zoom user object. Otherwise, returns false.
1245
 */
1246
function zoom_get_user($identifier) {
1247
    static $users = [];
1248
 
1249
    if (!isset($users[$identifier])) {
1250
        $users[$identifier] = zoom_webservice()->get_user($identifier);
1251
    }
1252
 
1253
    return $users[$identifier];
1254
}
1255
 
1256
/**
1257
 * Helper to get Zoom user settings, efficiently.
1258
 *
1259
 * @param string|int $identifier The user's email or the user's ID per Zoom API.
1260
 * @return stdClass|false If user is found, returns a Zoom user object. Otherwise, returns false.
1261
 */
1262
function zoom_get_user_settings($identifier) {
1263
    static $settings = [];
1264
 
1265
    if (!isset($settings[$identifier])) {
1266
        $settings[$identifier] = zoom_webservice()->get_user_settings($identifier);
1267
    }
1268
 
1269
    return $settings[$identifier];
1270
}
1271
 
1272
/**
1273
 * Get the zoom meeting registrants.
1274
 *
1275
 * @param string $meetingid Zoom meeting ID.
1276
 * @param bool $iswebinar If the session is a webinar.
1277
 * @return stdClass Returns a Zoom object containing the registrants (if found).
1278
 */
1279
function zoom_get_meeting_registrants($meetingid, $iswebinar) {
1280
    $response = zoom_webservice()->get_meeting_registrants($meetingid, $iswebinar);
1281
    return $response;
1282
}
1283
 
1284
/**
1285
 * Checks if a user has registered for a meeting/webinar based on their email address.
1286
 *
1287
 * @param string $useremail The email address of a user used to determine if they registered or not.
1288
 * @param string $meetingid Zoom meeting ID.
1289
 * @param bool $iswebinar If the session is a webinar.
1290
 * @return bool Returns whether or not the user has registered for the zoom meeting/webinar based on their email address.
1291
 */
1292
function zoom_is_user_registered_for_meeting($useremail, $meetingid, $iswebinar) {
1293
    $registrantjoinurl = zoom_get_registrant_join_url($useremail, $meetingid, $iswebinar);
1294
    return !empty($registrantjoinurl);
1295
}
1296
 
1297
/**
1298
 * Get the join url for a user for the specified meeting/webinar.
1299
 *
1300
 * @param string $useremail The email address of a user used to determine if they registered or not.
1301
 * @param string $meetingid Zoom meeting ID.
1302
 * @param bool $iswebinar If the session is a webinar.
1303
 * @return string|false Returns the join url for the user (based on email address) for the specified meeting (if found).
1304
 */
1305
function zoom_get_registrant_join_url($useremail, $meetingid, $iswebinar) {
1306
    $response = zoom_get_meeting_registrants($meetingid, $iswebinar);
1307
    if (isset($response->registrants)) {
1308
        foreach ($response->registrants as $registrant) {
1309
            if (strcasecmp($useremail, $registrant->email) == 0) {
1310
                return $registrant->join_url;
1311
            }
1312
        }
1313
    }
1314
 
1315
    return false;
1316
}
1317
 
1318
/**
1319
 * Get the display name for a Zoom user.
1320
 * This is wrapped in a function to avoid unnecessary API calls.
1321
 *
1322
 * @param string $zoomuserid Zoom user ID.
1323
 * @return ?string
1324
 */
1325
function zoom_get_user_display_name($zoomuserid) {
1326
    try {
1327
        $hostuser = zoom_get_user($zoomuserid);
1328
 
1329
        // Compose Moodle user object for host.
1330
        $hostmoodleuser = new stdClass();
1331
        $hostmoodleuser->firstname = $hostuser->first_name;
1332
        $hostmoodleuser->lastname = $hostuser->last_name;
1333
        $hostmoodleuser->alternatename = '';
1334
        $hostmoodleuser->firstnamephonetic = '';
1335
        $hostmoodleuser->lastnamephonetic = '';
1336
        $hostmoodleuser->middlename = '';
1337
 
1338
        return fullname($hostmoodleuser);
1339
    } catch (moodle_exception $error) {
1340
        return null;
1341
    }
1342
}