AutorÃa | Ultima modificación | Ver Log |
<?php
// This file is part of the Zoom plugin for Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Internal library of functions for module zoom
*
* All the zoom specific functions, needed to implement the module
* logic, should go here. Never include this file from your lib.php!
*
* @package mod_zoom
* @copyright 2015 UC Regents
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/zoom/lib.php');
require_once($CFG->dirroot . '/mod/zoom/classes/webservice_exception.php');
require_once($CFG->dirroot . '/mod/zoom/classes/api_limit_exception.php');
require_once($CFG->dirroot . '/mod/zoom/classes/bad_request_exception.php');
require_once($CFG->dirroot . '/mod/zoom/classes/not_found_exception.php');
require_once($CFG->dirroot . '/mod/zoom/classes/retry_failed_exception.php');
require_once($CFG->dirroot . '/mod/zoom/classes/webservice.php');
// Constants.
// Audio options.
define('ZOOM_AUDIO_TELEPHONY', 'telephony');
define('ZOOM_AUDIO_VOIP', 'voip');
define('ZOOM_AUDIO_BOTH', 'both');
// Meeting types.
define('ZOOM_INSTANT_MEETING', 1);
define('ZOOM_SCHEDULED_MEETING', 2);
define('ZOOM_RECURRING_MEETING', 3);
define('ZOOM_SCHEDULED_WEBINAR', 5);
define('ZOOM_RECURRING_WEBINAR', 6);
define('ZOOM_RECURRING_FIXED_MEETING', 8);
define('ZOOM_RECURRING_FIXED_WEBINAR', 9);
// Meeting status.
define('ZOOM_MEETING_EXPIRED', 0);
define('ZOOM_MEETING_EXISTS', 1);
// Number of meetings per page from zoom's get user report.
define('ZOOM_DEFAULT_RECORDS_PER_CALL', 30);
define('ZOOM_MAX_RECORDS_PER_CALL', 300);
// User types. Numerical values from Zoom API.
define('ZOOM_USER_TYPE_BASIC', 1);
define('ZOOM_USER_TYPE_PRO', 2);
define('ZOOM_USER_TYPE_CORP', 3);
define('ZOOM_MEETING_NOT_FOUND_ERROR_CODE', 3001);
define('ZOOM_USER_NOT_FOUND_ERROR_CODE', 1001);
define('ZOOM_INVALID_USER_ERROR_CODE', 1120);
// Webinar options.
define('ZOOM_WEBINAR_DISABLE', 0);
define('ZOOM_WEBINAR_SHOWONLYIFLICENSE', 1);
define('ZOOM_WEBINAR_ALWAYSSHOW', 2);
// Encryption type options.
define('ZOOM_ENCRYPTION_DISABLE', 0);
define('ZOOM_ENCRYPTION_SHOWONLYIFPOSSIBLE', 1);
define('ZOOM_ENCRYPTION_ALWAYSSHOW', 2);
// Encryption types. String values for Zoom API.
define('ZOOM_ENCRYPTION_TYPE_ENHANCED', 'enhanced_encryption');
define('ZOOM_ENCRYPTION_TYPE_E2EE', 'e2ee');
// Alternative hosts options.
define('ZOOM_ALTERNATIVEHOSTS_DISABLE', 0);
define('ZOOM_ALTERNATIVEHOSTS_INPUTFIELD', 1);
define('ZOOM_ALTERNATIVEHOSTS_PICKER', 2);
// Scheduling privilege options.
define('ZOOM_SCHEDULINGPRIVILEGE_DISABLE', 0);
define('ZOOM_SCHEDULINGPRIVILEGE_ENABLE', 1);
// All meetings options.
define('ZOOM_ALLMEETINGS_DISABLE', 0);
define('ZOOM_ALLMEETINGS_ENABLE', 1);
// Download iCal options.
define('ZOOM_DOWNLOADICAL_DISABLE', 0);
define('ZOOM_DOWNLOADICAL_ENABLE', 1);
// Capacity warning options.
define('ZOOM_CAPACITYWARNING_DISABLE', 0);
define('ZOOM_CAPACITYWARNING_ENABLE', 1);
// Recurrence type options.
define('ZOOM_RECURRINGTYPE_NOTIME', 0);
define('ZOOM_RECURRINGTYPE_DAILY', 1);
define('ZOOM_RECURRINGTYPE_WEEKLY', 2);
define('ZOOM_RECURRINGTYPE_MONTHLY', 3);
// Recurring monthly repeat options.
define('ZOOM_MONTHLY_REPEAT_OPTION_DAY', 1);
define('ZOOM_MONTHLY_REPEAT_OPTION_WEEK', 2);
// Recurring end date options.
define('ZOOM_END_DATE_OPTION_BY', 1);
define('ZOOM_END_DATE_OPTION_AFTER', 2);
// API endpoint options.
define('ZOOM_API_ENDPOINT_EU', 'eu');
define('ZOOM_API_ENDPOINT_GLOBAL', 'global');
define('ZOOM_API_URL_EU', 'https://eu01api-www4local.zoom.us/v2/');
define('ZOOM_API_URL_GLOBAL', 'https://api.zoom.us/v2/');
// Auto-recording options.
define('ZOOM_AUTORECORDING_NONE', 'none');
define('ZOOM_AUTORECORDING_USERDEFAULT', 'userdefault');
define('ZOOM_AUTORECORDING_LOCAL', 'local');
define('ZOOM_AUTORECORDING_CLOUD', 'cloud');
// Registration options.
define('ZOOM_REGISTRATION_AUTOMATIC', 0);
define('ZOOM_REGISTRATION_MANUAL', 1);
define('ZOOM_REGISTRATION_OFF', 2);
/**
* Terminate the current script with a fatal error.
*
* Adapted from core_renderer's fatal_error() method. Needed because throwing errors with HTML links in them will convert links
* to text using htmlentities. See MDL-66161 - Reflected XSS possible from some fatal error messages.
*
* So need custom error handler for fatal Zoom errors that have links to help people.
*
* @param string $errorcode The name of the string from error.php to print
* @param string $module name of module
* @param string $continuelink The url where the user will be prompted to continue.
* If no url is provided the user will be directed to
* the site index page.
* @param mixed $a Extra words and phrases that might be required in the error string
*/
function zoom_fatal_error($errorcode, $module = '', $continuelink = '', $a = null) {
global $CFG, $COURSE, $OUTPUT, $PAGE;
$output = '';
$obbuffer = '';
// Assumes that function is run before output is generated.
if ($OUTPUT->has_started()) {
// If not then have to default to standard error.
throw new moodle_exception($errorcode, $module, $continuelink, $a);
}
$PAGE->set_heading($COURSE->fullname);
$output .= $OUTPUT->header();
// Output message without messing with HTML content of error.
$message = '<p class="errormessage">' . get_string($errorcode, $module, $a) . '</p>';
$output .= $OUTPUT->box($message, 'errorbox alert alert-danger', null, ['data-rel' => 'fatalerror']);
if ($CFG->debugdeveloper) {
if (!empty($debuginfo)) {
$debuginfo = s($debuginfo); // Removes all nasty JS.
$debuginfo = str_replace("\n", '<br />', $debuginfo); // Keep newlines.
$output .= $OUTPUT->notification('<strong>Debug info:</strong> ' . $debuginfo, 'notifytiny');
}
if (!empty($backtrace)) {
$output .= $OUTPUT->notification('<strong>Stack trace:</strong> ' . format_backtrace($backtrace), 'notifytiny');
}
if ($obbuffer !== '') {
$output .= $OUTPUT->notification('<strong>Output buffer:</strong> ' . s($obbuffer), 'notifytiny');
}
}
if (!empty($continuelink)) {
$output .= $OUTPUT->continue_button($continuelink);
}
$output .= $OUTPUT->footer();
// Padding to encourage IE to display our error page, rather than its own.
$output .= str_repeat(' ', 512);
echo $output;
exit(1); // General error code.
}
/**
* Get course/cm/zoom objects from url parameters, and check for login/permissions.
*
* @return array Array of ($course, $cm, $zoom)
*/
function zoom_get_instance_setup() {
global $DB;
$id = optional_param('id', 0, PARAM_INT); // Course_module ID.
$n = optional_param('n', 0, PARAM_INT); // Zoom instance ID.
if ($id) {
$cm = get_coursemodule_from_id('zoom', $id, 0, false, MUST_EXIST);
$course = $DB->get_record('course', ['id' => $cm->course], '*', MUST_EXIST);
$zoom = $DB->get_record('zoom', ['id' => $cm->instance], '*', MUST_EXIST);
} else if ($n) {
$zoom = $DB->get_record('zoom', ['id' => $n], '*', MUST_EXIST);
$course = $DB->get_record('course', ['id' => $zoom->course], '*', MUST_EXIST);
$cm = get_coursemodule_from_instance('zoom', $zoom->id, $course->id, false, MUST_EXIST);
} else {
throw new moodle_exception('zoomerr_id_missing', 'mod_zoom');
}
require_login($course, true, $cm);
$context = context_module::instance($cm->id);
require_capability('mod/zoom:view', $context);
return [$course, $cm, $zoom];
}
/**
* Retrieves information for a meeting.
*
* @param int $zoomid
* @return array information about the meeting
*/
function zoom_get_sessions_for_display($zoomid) {
global $DB, $CFG;
require_once($CFG->libdir . '/moodlelib.php');
$sessions = [];
$format = get_string('strftimedatetimeshort', 'langconfig');
// Sort sessions in start_time ascending order.
$instances = $DB->get_records('zoom_meeting_details', ['zoomid' => $zoomid], 'start_time');
foreach ($instances as $instance) {
// The meeting uuid, not the participant's uuid.
$uuid = $instance->uuid;
$participantlist = zoom_get_participants_report($instance->id);
$sessions[$uuid]['participants'] = $participantlist;
$uniquevalues = [];
$uniqueparticipantcount = 0;
foreach ($participantlist as $participant) {
$unique = true;
if ($participant->uuid != null) {
if (array_key_exists($participant->uuid, $uniquevalues)) {
$unique = false;
} else {
$uniquevalues[$participant->uuid] = true;
}
}
if ($participant->userid != null) {
if (!$unique || !array_key_exists($participant->userid, $uniquevalues)) {
$uniquevalues[$participant->userid] = true;
} else {
$unique = false;
}
}
if ($participant->user_email != null) {
if (!$unique || !array_key_exists($participant->user_email, $uniquevalues)) {
$uniquevalues[$participant->user_email] = true;
} else {
$unique = false;
}
}
$uniqueparticipantcount += $unique ? 1 : 0;
}
$sessions[$uuid]['count'] = $uniqueparticipantcount;
$sessions[$uuid]['topic'] = $instance->topic;
$sessions[$uuid]['duration'] = $instance->duration;
$sessions[$uuid]['starttime'] = userdate($instance->start_time, $format);
$sessions[$uuid]['endtime'] = userdate($instance->start_time + $instance->duration * 60, $format);
}
return $sessions;
}
/**
* Get the next occurrence of a meeting.
*
* @param stdClass $zoom
* @return int The timestamp of the next occurrence of a recurring meeting or
* 0 if this is a recurring meeting without fixed time or
* the timestamp of the meeting start date if this isn't a recurring meeting.
*/
function zoom_get_next_occurrence($zoom) {
global $DB;
// Prepare an ad-hoc request cache as this function could be called multiple times throughout a request
// and we want to avoid to make duplicate DB calls.
$cacheoptions = [
'simplekeys' => true,
'simpledata' => true,
];
$cache = cache::make_from_params(cache_store::MODE_REQUEST, 'zoom', 'nextoccurrence', [], $cacheoptions);
// If the next occurrence wasn't already cached, fill the cache.
$cachednextoccurrence = $cache->get($zoom->id);
if ($cachednextoccurrence === false) {
// If this isn't a recurring meeting.
if (!$zoom->recurring) {
// Use the meeting start time.
$cachednextoccurrence = $zoom->start_time;
// Or if this is a recurring meeting without fixed time.
} else if ($zoom->recurrence_type == ZOOM_RECURRINGTYPE_NOTIME) {
// Use 0 as there isn't anything better to return.
$cachednextoccurrence = 0;
// Otherwise we have a recurring meeting with a recurrence schedule.
} else {
// Get the calendar event of the next occurrence.
$selectclause = "modulename = :modulename AND instance = :instance AND (timestart + timeduration) >= :now";
$selectparams = ['modulename' => 'zoom', 'instance' => $zoom->id, 'now' => time()];
$nextoccurrence = $DB->get_records_select('event', $selectclause, $selectparams, 'timestart ASC', 'timestart', 0, 1);
// If we haven't got a single event.
if (empty($nextoccurrence)) {
// Use 0 as there isn't anything better to return.
$cachednextoccurrence = 0;
} else {
// Use the timestamp of the event.
$nextoccurenceobject = reset($nextoccurrence);
$cachednextoccurrence = $nextoccurenceobject->timestart;
}
}
// Store the next occurrence into the cache.
$cache->set($zoom->id, $cachednextoccurrence);
}
// Return the next occurrence.
return $cachednextoccurrence;
}
/**
* Determine if a zoom meeting is in progress, is available, and/or is finished.
*
* @param stdClass $zoom
* @return array Array of booleans: [in progress, available, finished].
*/
function zoom_get_state($zoom) {
// Get plugin config.
$config = get_config('zoom');
// Get the current time as calculation basis.
$now = time();
// If this is a recurring meeting with a recurrence schedule.
if ($zoom->recurring && $zoom->recurrence_type != ZOOM_RECURRINGTYPE_NOTIME) {
// Get the next occurrence start time.
$starttime = zoom_get_next_occurrence($zoom);
} else {
// Get the meeting start time.
$starttime = $zoom->start_time;
}
// Calculate the time when the recurring meeting becomes available next,
// based on the next occurrence start time and the general meeting lead time.
$firstavailable = $starttime - ($config->firstabletojoin * 60);
// Calculate the time when the meeting ends to be available,
// based on the next occurrence start time and the meeting duration.
$lastavailable = $starttime + $zoom->duration;
// Determine if the meeting is in progress.
$inprogress = ($firstavailable <= $now && $now <= $lastavailable);
// Determine if its a recurring meeting with no fixed time.
$isrecurringnotime = $zoom->recurring && $zoom->recurrence_type == ZOOM_RECURRINGTYPE_NOTIME;
// Determine if the meeting is available,
// based on the fact if it is recurring or in progress.
$available = $isrecurringnotime || $inprogress;
// Determine if the meeting is finished,
// based on the fact if it is recurring or the meeting end time is still in the future.
$finished = !$isrecurringnotime && $now > $lastavailable;
// Return the requested information.
return [$inprogress, $available, $finished];
}
/**
* Get the Zoom id of the currently logged-in user.
*
* @param bool $required If true, will error if the user doesn't have a Zoom account.
* @return string
*/
function zoom_get_user_id($required = true) {
global $USER;
$cache = cache::make('mod_zoom', 'zoomid');
if (!($zoomuserid = $cache->get($USER->id))) {
$zoomuserid = false;
try {
$zoomuser = zoom_get_user(zoom_get_api_identifier($USER));
if ($zoomuser !== false && isset($zoomuser->id) && ($zoomuser->id !== false)) {
$zoomuserid = $zoomuser->id;
$cache->set($USER->id, $zoomuserid);
}
} catch (moodle_exception $error) {
if ($required) {
throw $error;
}
}
}
return $zoomuserid;
}
/**
* Get the Zoom meeting security settings, including meeting password requirements of the user's master account.
*
* @param string|int $identifier The user's email or the user's ID per Zoom API.
* @return stdClass
*/
function zoom_get_meeting_security_settings($identifier) {
$cache = cache::make('mod_zoom', 'zoommeetingsecurity');
$zoommeetingsecurity = $cache->get($identifier);
if (empty($zoommeetingsecurity)) {
$zoommeetingsecurity = zoom_webservice()->get_account_meeting_security_settings($identifier);
$cache->set($identifier, $zoommeetingsecurity);
}
return $zoommeetingsecurity;
}
/**
* Check if the error indicates that a meeting is gone.
*
* @param moodle_exception $error
* @return bool
*/
function zoom_is_meeting_gone_error($error) {
// If the meeting's owner/user cannot be found, we consider the meeting to be gone.
return ($error->zoomerrorcode === ZOOM_MEETING_NOT_FOUND_ERROR_CODE) || zoom_is_user_not_found_error($error);
}
/**
* Check if the error indicates that a user is not found or does not belong to the current account.
*
* @param moodle_exception $error
* @return bool
*/
function zoom_is_user_not_found_error($error) {
return ($error->zoomerrorcode === ZOOM_USER_NOT_FOUND_ERROR_CODE) || ($error->zoomerrorcode === ZOOM_INVALID_USER_ERROR_CODE);
}
/**
* Return the string parameter for zoomerr_meetingnotfound.
*
* @param string $cmid
* @return stdClass
*/
function zoom_meetingnotfound_param($cmid) {
// Provide links to recreate and delete.
$recreate = new moodle_url('/mod/zoom/recreate.php', ['id' => $cmid, 'sesskey' => sesskey()]);
$delete = new moodle_url('/course/mod.php', ['delete' => $cmid, 'sesskey' => sesskey()]);
// Convert links to strings and pass as error parameter.
$param = new stdClass();
$param->recreate = $recreate->out();
$param->delete = $delete->out();
return $param;
}
/**
* Get the data of each user for the participants report.
* @param string $detailsid The meeting ID that you want to get the participants report for.
* @return array The user data as an array of records (array of arrays).
*/
function zoom_get_participants_report($detailsid) {
global $DB;
$sql = 'SELECT zmp.id,
zmp.name,
zmp.userid,
zmp.user_email,
zmp.join_time,
zmp.leave_time,
zmp.duration,
zmp.uuid
FROM {zoom_meeting_participants} zmp
WHERE zmp.detailsid = :detailsid
';
$params = [
'detailsid' => $detailsid,
];
$participants = $DB->get_records_sql($sql, $params);
return $participants;
}
/**
* Creates a default passcode from the user's Zoom meeting security settings.
*
* @param stdClass $meetingpasswordrequirement
* @return string passcode
*/
function zoom_create_default_passcode($meetingpasswordrequirement) {
$length = max($meetingpasswordrequirement->length, 6);
$random = rand(0, pow(10, $length) - 1);
$passcode = str_pad(strval($random), $length, '0', STR_PAD_LEFT);
// Get a random set of indexes to replace with non-numberic values.
$indexes = range(0, $length - 1);
shuffle($indexes);
if ($meetingpasswordrequirement->have_letter || $meetingpasswordrequirement->have_upper_and_lower_characters) {
// Random letter from A-Z.
$passcode[$indexes[0]] = chr(rand(65, 90));
// Random letter from a-z.
$passcode[$indexes[1]] = chr(rand(97, 122));
}
if ($meetingpasswordrequirement->have_special_character) {
$specialchar = '@_*-';
$passcode[$indexes[2]] = substr(str_shuffle($specialchar), 0, 1);
}
return $passcode;
}
/**
* Creates a description string from the user's Zoom meeting security settings.
*
* @param stdClass $meetingpasswordrequirement
* @return string description of password requirements
*/
function zoom_create_passcode_description($meetingpasswordrequirement) {
$description = '';
if ($meetingpasswordrequirement->only_allow_numeric) {
$description .= get_string('password_only_numeric', 'mod_zoom') . ' ';
} else {
if ($meetingpasswordrequirement->have_letter && !$meetingpasswordrequirement->have_upper_and_lower_characters) {
$description .= get_string('password_letter', 'mod_zoom') . ' ';
} else if ($meetingpasswordrequirement->have_upper_and_lower_characters) {
$description .= get_string('password_lower_upper', 'mod_zoom') . ' ';
}
if ($meetingpasswordrequirement->have_number) {
$description .= get_string('password_number', 'mod_zoom') . ' ';
}
if ($meetingpasswordrequirement->have_special_character) {
$description .= get_string('password_special', 'mod_zoom') . ' ';
} else {
$description .= get_string('password_allowed_char', 'mod_zoom') . ' ';
}
}
if ($meetingpasswordrequirement->length) {
$description .= get_string('password_length', 'mod_zoom', $meetingpasswordrequirement->length) . ' ';
}
if ($meetingpasswordrequirement->consecutive_characters_length > 0) {
$description .= get_string(
'password_consecutive',
'mod_zoom',
$meetingpasswordrequirement->consecutive_characters_length - 1
) . ' ';
}
$description .= get_string('password_max_length', 'mod_zoom');
return $description;
}
/**
* Creates an array of users who can be selected as alternative host in a given context.
*
* @param context $context The context to be used.
*
* @return array Array of users (mail => fullname).
*/
function zoom_get_selectable_alternative_hosts_list(context $context) {
// Get selectable alternative host users based on the capability.
$users = get_enrolled_users($context, 'mod/zoom:eligiblealternativehost', 0, 'u.*', 'lastname');
// Create array of users.
$selectablealternativehosts = [];
// Iterate over selectable alternative host users.
foreach ($users as $u) {
// Note: Basically, if this is the user's own data row, the data row should be skipped.
// But this would then not cover the case when a user is scheduling the meeting _for_ another user
// and wants to be an alternative host himself.
// As this would have to be handled at runtime in the browser, we just offer all users with the
// capability as selectable and leave this aspect as possible improvement for the future.
// At least, Zoom does not care if the user who is the host adds himself as alternative host as well.
// Verify that the user really has a Zoom account.
// Furthermore, verify that the user's status is active. Adding a pending or inactive user as alternative host will result
// in a Zoom API error otherwise.
$zoomuser = zoom_get_user($u->email);
if ($zoomuser !== false && $zoomuser->status === 'active') {
// Add user to array of users.
$selectablealternativehosts[$u->email] = fullname($u);
}
}
return $selectablealternativehosts;
}
/**
* Creates a string of roles who can be selected as alternative host in a given context.
*
* @param context $context The context to be used.
*
* @return string The string of roles.
*/
function zoom_get_selectable_alternative_hosts_rolestring(context $context) {
// Get selectable alternative host users based on the capability.
$roles = get_role_names_with_caps_in_context($context, ['mod/zoom:eligiblealternativehost']);
// Compose string.
$rolestring = implode(', ', $roles);
return $rolestring;
}
/**
* Get existing Moodle users from a given set of alternative hosts.
*
* @param array $alternativehosts The array of alternative hosts email addresses.
*
* @return array The array of existing Moodle user objects.
*/
function zoom_get_users_from_alternativehosts(array $alternativehosts) {
global $DB;
// Get the existing Moodle user objects from the DB.
[$insql, $inparams] = $DB->get_in_or_equal($alternativehosts);
$sql = 'SELECT *
FROM {user}
WHERE email ' . $insql . '
ORDER BY lastname ASC';
$alternativehostusers = $DB->get_records_sql($sql, $inparams);
return $alternativehostusers;
}
/**
* Get non-Moodle users from a given set of alternative hosts.
*
* @param array $alternativehosts The array of alternative hosts email addresses.
*
* @return array The array of non-Moodle user mail addresses.
*/
function zoom_get_nonusers_from_alternativehosts(array $alternativehosts) {
global $DB;
// Get the non-Moodle user mail addresses by checking which one does not exist in the DB.
$alternativehostnonusers = [];
[$insql, $inparams] = $DB->get_in_or_equal($alternativehosts);
$sql = 'SELECT email
FROM {user}
WHERE email ' . $insql . '
ORDER BY email ASC';
$alternativehostusersmails = $DB->get_records_sql($sql, $inparams);
foreach ($alternativehosts as $ah) {
if (!array_key_exists($ah, $alternativehostusersmails)) {
$alternativehostnonusers[] = $ah;
}
}
return $alternativehostnonusers;
}
/**
* Get the unavailability note based on the Zoom plugin configuration.
*
* @param object $zoom The Zoom meeting object.
* @param bool|null $finished The function needs to know if the meeting is already finished.
* You can provide this information, if already available, to the function.
* Otherwise it will determine it with a small overhead.
*
* @return string The unavailability note.
*/
function zoom_get_unavailability_note($zoom, $finished = null) {
// Get config.
$config = get_config('zoom');
// Get the plain unavailable string.
$strunavailable = get_string('unavailable', 'mod_zoom');
// If this is a recurring meeting without fixed time, just use the plain unavailable string.
if ($zoom->recurring && $zoom->recurrence_type == ZOOM_RECURRINGTYPE_NOTIME) {
$unavailabilitynote = $strunavailable;
// Otherwise we add some more information to the unavailable string.
} else {
// If we don't have the finished information yet, get it with a small overhead.
if ($finished === null) {
[$inprogress, $available, $finished] = zoom_get_state($zoom);
}
// If this meeting is still pending.
if ($finished !== true) {
// If the admin wants to show the leadtime.
if (!empty($config->displayleadtime) && $config->firstabletojoin > 0) {
$unavailabilitynote = $strunavailable . '<br />' .
get_string('unavailablefirstjoin', 'mod_zoom', ['mins' => ($config->firstabletojoin)]);
// Otherwise.
} else {
$unavailabilitynote = $strunavailable . '<br />' . get_string('unavailablenotstartedyet', 'mod_zoom');
}
// Otherwise, the meeting has finished.
} else {
$unavailabilitynote = $strunavailable . '<br />' . get_string('unavailablefinished', 'mod_zoom');
}
}
return $unavailabilitynote;
}
/**
* Gets the meeting capacity of a given Zoom user.
* Please note: This function does not check if the Zoom user really exists, this has to be checked before calling this function.
*
* @param string $zoomhostid The Zoom ID of the host.
* @param bool $iswebinar The meeting is a webinar.
*
* @return int|bool The meeting capacity of the Zoom user or false if the user does not have any meeting capacity at all.
*/
function zoom_get_meeting_capacity(string $zoomhostid, bool $iswebinar = false) {
// Get the 'feature' section of the user's Zoom settings.
$userfeatures = zoom_get_user_settings($zoomhostid)->feature;
$meetingcapacity = false;
// If this is a webinar.
if ($iswebinar === true) {
// Get the appropriate capacity value.
if (!empty($userfeatures->webinar_capacity)) {
$meetingcapacity = $userfeatures->webinar_capacity;
} else if (!empty($userfeatures->zoom_events_capacity)) {
$meetingcapacity = $userfeatures->zoom_events_capacity;
}
} else {
// If this is a meeting, get the 'meeting_capacity' value.
if (!empty($userfeatures->meeting_capacity)) {
$meetingcapacity = $userfeatures->meeting_capacity;
// Check if the user has a 'large_meeting' license that has a higher capacity value.
if (!empty($userfeatures->large_meeting_capacity) && $userfeatures->large_meeting_capacity > $meetingcapacity) {
$meetingcapacity = $userfeatures->large_meeting_capacity;
}
}
}
return $meetingcapacity;
}
/**
* Gets the number of eligible meeting participants in a given context.
* Please note: This function only covers users who are enrolled into the given context.
* It does _not_ include users who have the necessary capability on a higher context without being enrolled.
*
* @param context $context The context which we want to check.
*
* @return int The number of eligible meeting participants.
*/
function zoom_get_eligible_meeting_participants(context $context) {
global $DB;
// Compose SQL query.
$sqlsnippets = get_enrolled_with_capabilities_join($context, '', 'mod/zoom:view', 0, true);
$sql = 'SELECT count(DISTINCT u.id)
FROM {user} u ' . $sqlsnippets->joins . ' WHERE ' . $sqlsnippets->wheres;
// Run query and count records.
$eligibleparticipantcount = $DB->count_records_sql($sql, $sqlsnippets->params);
return $eligibleparticipantcount;
}
/**
* Get array of alternative hosts from a string.
*
* @param string $alternativehoststring Comma (or semicolon) separated list of alternative hosts.
* @return string[] $alternativehostarray Array of alternative hosts.
*/
function zoom_get_alternative_host_array_from_string($alternativehoststring) {
if (empty($alternativehoststring)) {
return [];
}
// The Zoom API has historically returned either semicolons or commas, so we need to support both.
$alternativehoststring = str_replace(';', ',', $alternativehoststring);
$alternativehostarray = array_filter(explode(',', $alternativehoststring));
return $alternativehostarray;
}
/**
* Get all custom user profile fields of type text
*
* @return array list of user profile fields
*/
function zoom_get_user_profile_fields() {
global $DB;
$userfields = [];
$records = $DB->get_records('user_info_field', ['datatype' => 'text']);
foreach ($records as $record) {
$userfields[$record->shortname] = $record->name;
}
return $userfields;
}
/**
* Get all valid options for API Identifier field
*
* @return array list of all valid options
*/
function zoom_get_api_identifier_fields() {
$options = [
'email' => get_string('email'),
'username' => get_string('username'),
'idnumber' => get_string('idnumber'),
];
$userfields = zoom_get_user_profile_fields();
if (!empty($userfields)) {
$options += $userfields;
}
return $options;
}
/**
* Get the zoom api identifier
*
* @param object $user The user object
*
* @return string the value of the identifier
*/
function zoom_get_api_identifier($user) {
// Get the value from the config first.
$field = get_config('zoom', 'apiidentifier');
$identifier = '';
if (isset($user->$field)) {
// If one of the standard user fields.
$identifier = $user->$field;
} else if (isset($user->profile[$field])) {
// If one of the custom user fields.
$identifier = $user->profile[$field];
}
if (empty($identifier)) {
// Fallback to email if the field is not set.
$identifier = $user->email;
}
return $identifier;
}
/**
* Creates an iCalendar_event for a Zoom meeting.
*
* @param stdClass $event The meeting object.
* @param string $description The event description.
*
* @return iCalendar_event
*/
function zoom_helper_icalendar_event($event, $description) {
global $CFG;
// Match Moodle's uid format for iCal events.
$hostaddress = str_replace('http://', '', $CFG->wwwroot);
$hostaddress = str_replace('https://', '', $hostaddress);
$uid = $event->id . '@' . $hostaddress;
$icalevent = new iCalendar_event();
$icalevent->add_property('uid', $uid); // A unique identifier.
$icalevent->add_property('summary', $event->name); // Title.
$icalevent->add_property('dtstamp', Bennu::timestamp_to_datetime()); // Time of creation.
$icalevent->add_property('last-modified', Bennu::timestamp_to_datetime($event->timemodified));
$icalevent->add_property('dtstart', Bennu::timestamp_to_datetime($event->timestart)); // Start time.
$icalevent->add_property('dtend', Bennu::timestamp_to_datetime($event->timestart + $event->timeduration)); // End time.
$icalevent->add_property('description', $description);
return $icalevent;
}
/**
* Get the configured Zoom API URL.
*
* @return string The API URL.
*/
function zoom_get_api_url() {
// Get the API endpoint setting.
$apiendpoint = get_config('zoom', 'apiendpoint');
// Pick the corresponding API URL.
switch ($apiendpoint) {
case ZOOM_API_ENDPOINT_EU:
$apiurl = ZOOM_API_URL_EU;
break;
case ZOOM_API_ENDPOINT_GLOBAL:
default:
$apiurl = ZOOM_API_URL_GLOBAL;
break;
}
// Return API URL.
return $apiurl;
}
/**
* Loads the zoom meeting and passes back a meeting URL
* after processing events, view completion, grades, and license updates.
*
* @param int $id course module id
* @param object $context moodle context object
* @param bool $usestarturl
* @return array $returns contains url object 'nexturl' or string 'error'
*/
function zoom_load_meeting($id, $context, $usestarturl = true) {
global $CFG, $DB, $USER;
require_once($CFG->libdir . '/gradelib.php');
$cm = get_coursemodule_from_id('zoom', $id, 0, false, MUST_EXIST);
$course = get_course($cm->course);
$zoom = $DB->get_record('zoom', ['id' => $cm->instance], '*', MUST_EXIST);
require_login($course, true, $cm);
require_capability('mod/zoom:view', $context);
$returns = ['nexturl' => null, 'error' => null];
[$inprogress, $available, $finished] = zoom_get_state($zoom);
$userisregistered = false;
$userisregistering = false;
if ($zoom->registration != ZOOM_REGISTRATION_OFF) {
// Check if user already registered.
$registrantjoinurl = zoom_get_registrant_join_url($USER->email, $zoom->meeting_id, $zoom->webinar);
$userisregistered = !empty($registrantjoinurl);
// Allow unregistered users to register.
if (!$userisregistered) {
$userisregistering = true;
}
}
// If the meeting is not yet available, deny access.
if (!$available && !$userisregistering) {
// Get unavailability note.
$returns['error'] = zoom_get_unavailability_note($zoom, $finished);
return $returns;
}
$userisrealhost = (zoom_get_user_id(false) === $zoom->host_id);
$alternativehosts = zoom_get_alternative_host_array_from_string($zoom->alternative_hosts);
$userishost = ($userisrealhost || in_array(zoom_get_api_identifier($USER), $alternativehosts, true));
// Check if we should use the start meeting url.
if ($userisrealhost && $usestarturl) {
// Important: Only the real host can use this URL, because it joins the meeting as the host user.
$starturl = zoom_get_start_url($zoom->meeting_id, $zoom->webinar, $zoom->join_url);
$returns['nexturl'] = new moodle_url($starturl);
} else {
$url = $zoom->join_url;
if ($userisregistered) {
$url = $registrantjoinurl;
}
$unamesetting = get_config('zoom', 'unamedisplay');
switch ($unamesetting) {
case 'fullname':
default:
$unamedisplay = fullname($USER);
break;
case 'firstname':
$unamedisplay = $USER->firstname;
break;
case 'idfullname':
$unamedisplay = '(' . $USER->id . ') ' . fullname($USER);
break;
case 'id':
$unamedisplay = '(' . $USER->id . ')';
break;
}
// Try to send the user email (not guaranteed).
$returns['nexturl'] = new moodle_url($url, ['uname' => $unamedisplay, 'uemail' => $USER->email]);
}
// If the user is pre-registering, skip grading/completion.
if (!$available && $userisregistering) {
return $returns;
}
// Record user's clicking join.
\mod_zoom\event\join_meeting_button_clicked::create([
'context' => $context,
'objectid' => $zoom->id,
'other' => [
'cmid' => $id,
'meetingid' => (int) $zoom->meeting_id,
'userishost' => $userishost,
],
])->trigger();
// Track completion viewed.
$completion = new completion_info($course);
$completion->set_module_viewed($cm);
// Check the grading method settings.
if (!empty($zoom->grading_method)) {
$gradingmethod = $zoom->grading_method;
} else if ($defaultgrading = get_config('gradingmethod', 'zoom')) {
$gradingmethod = $defaultgrading;
} else {
$gradingmethod = 'entry';
}
if ($gradingmethod === 'entry') {
// Check whether user has a grade. If not, then assign full credit to them.
$gradelist = grade_get_grades($course->id, 'mod', 'zoom', $cm->instance, $USER->id);
// Assign full credits for user who has no grade yet, if this meeting is gradable (i.e. the grade type is not "None").
if (!empty($gradelist->items) && empty($gradelist->items[0]->grades[$USER->id]->grade)) {
$grademax = $gradelist->items[0]->grademax;
$grades = [
'rawgrade' => $grademax,
'userid' => $USER->id,
'usermodified' => $USER->id,
'dategraded' => '',
'feedbackformat' => '',
'feedback' => '',
];
zoom_grade_item_update($zoom, $grades);
}
} // Otherwise, the get_meetings_report task calculates the grades according to duration.
// Upgrade host upon joining meeting, if host is not Licensed.
if ($userishost) {
$config = get_config('zoom');
if (!empty($config->recycleonjoin)) {
zoom_webservice()->provide_license($zoom->host_id);
}
}
return $returns;
}
/**
* Fetches a fresh URL that can be used to start the Zoom meeting.
*
* @param string $meetingid Zoom meeting ID.
* @param bool $iswebinar If the session is a webinar.
* @param string $fallbackurl URL to use if the webservice call fails.
* @return string Best available URL for starting the meeting.
*/
function zoom_get_start_url($meetingid, $iswebinar, $fallbackurl) {
try {
$response = zoom_webservice()->get_meeting_webinar_info($meetingid, $iswebinar);
return $response->start_url ?? $response->join_url;
} catch (moodle_exception $e) {
// If an exception was thrown, gracefully use the fallback URL.
return $fallbackurl;
}
}
/**
* Get the configured Zoom tracking fields.
*
* @return array tracking fields, keys as lower case
*/
function zoom_list_tracking_fields() {
$trackingfields = [];
// Get the tracking fields configured on the account.
$response = zoom_webservice()->list_tracking_fields();
if (isset($response->tracking_fields)) {
foreach ($response->tracking_fields as $trackingfield) {
$field = str_replace(' ', '_', strtolower($trackingfield->field));
$trackingfields[$field] = (array) $trackingfield;
}
}
return $trackingfields;
}
/**
* Trim and lower case tracking fields.
*
* @return array tracking fields trimmed, keys as lower case
*/
function zoom_clean_tracking_fields() {
$config = get_config('zoom');
$defaulttrackingfields = explode(',', $config->defaulttrackingfields);
$trackingfields = [];
foreach ($defaulttrackingfields as $key => $defaulttrackingfield) {
$trimmed = trim($defaulttrackingfield);
if (!empty($trimmed)) {
$key = str_replace(' ', '_', strtolower($trimmed));
$trackingfields[$key] = $trimmed;
}
}
return $trackingfields;
}
/**
* Synchronize tracking field data for a meeting.
*
* @param int $zoomid Zoom meeting ID
* @param array $trackingfields Tracking fields configured in Zoom.
*/
function zoom_sync_meeting_tracking_fields($zoomid, $trackingfields) {
global $DB;
$tfvalues = [];
foreach ($trackingfields as $trackingfield) {
$field = str_replace(' ', '_', strtolower($trackingfield->field));
$tfvalues[$field] = $trackingfield->value;
}
$tfrows = $DB->get_records('zoom_meeting_tracking_fields', ['meeting_id' => $zoomid]);
$tfobjects = [];
foreach ($tfrows as $tfrow) {
$tfobjects[$tfrow->tracking_field] = $tfrow;
}
$defaulttrackingfields = zoom_clean_tracking_fields();
foreach ($defaulttrackingfields as $key => $defaulttrackingfield) {
$value = $tfvalues[$key] ?? '';
if (isset($tfobjects[$key])) {
$tfobject = $tfobjects[$key];
if ($value === '') {
$DB->delete_records('zoom_meeting_tracking_fields', ['meeting_id' => $zoomid, 'tracking_field' => $key]);
} else if ($tfobject->value !== $value) {
$tfobject->value = $value;
$DB->update_record('zoom_meeting_tracking_fields', $tfobject);
}
} else if ($value !== '') {
$tfobject = new stdClass();
$tfobject->meeting_id = $zoomid;
$tfobject->tracking_field = $key;
$tfobject->value = $value;
$DB->insert_record('zoom_meeting_tracking_fields', $tfobject);
}
}
}
/**
* Get all meeting records
*
* @return array All zoom meetings stored in the database.
*/
function zoom_get_all_meeting_records() {
global $DB;
$meetings = [];
// Only get meetings that exist on zoom.
$records = $DB->get_records('zoom', ['exists_on_zoom' => ZOOM_MEETING_EXISTS]);
foreach ($records as $record) {
$meetings[] = $record;
}
return $meetings;
}
/**
* Get all recordings for a particular meeting.
*
* @param int $zoomid Optional. The id of the zoom meeting.
*
* @return array All the recordings for the zoom meeting.
*/
function zoom_get_meeting_recordings($zoomid = null) {
global $DB;
$params = [];
if ($zoomid !== null) {
$params['zoomid'] = $zoomid;
}
$records = $DB->get_records('zoom_meeting_recordings', $params);
$recordings = [];
foreach ($records as $recording) {
$recordings[$recording->zoomrecordingid] = $recording;
}
return $recordings;
}
/**
* Get all meeting recordings grouped together.
*
* @param int $zoomid Optional. The id of the zoom meeting.
*
* @return array All recordings for the zoom meeting grouped together.
*/
function zoom_get_meeting_recordings_grouped($zoomid = null) {
global $DB;
$params = [];
if ($zoomid !== null) {
$params['zoomid'] = $zoomid;
}
$records = $DB->get_records('zoom_meeting_recordings', $params, 'recordingstart ASC');
$recordings = [];
foreach ($records as $recording) {
$recordings[$recording->meetinguuid][$recording->zoomrecordingid] = $recording;
}
return $recordings;
}
/**
* Singleton for Zoom webservice class.
*
* @return \mod_zoom\webservice
*/
function zoom_webservice() {
static $service;
if (empty($service)) {
$service = new \mod_zoom\webservice();
}
return $service;
}
/**
* Helper to get a Zoom user, efficiently.
*
* @param string|int $identifier The user's email or the user's ID per Zoom API.
* @return stdClass|false If user is found, returns a Zoom user object. Otherwise, returns false.
*/
function zoom_get_user($identifier) {
static $users = [];
if (!isset($users[$identifier])) {
$users[$identifier] = zoom_webservice()->get_user($identifier);
}
return $users[$identifier];
}
/**
* Helper to get Zoom user settings, efficiently.
*
* @param string|int $identifier The user's email or the user's ID per Zoom API.
* @return stdClass|false If user is found, returns a Zoom user object. Otherwise, returns false.
*/
function zoom_get_user_settings($identifier) {
static $settings = [];
if (!isset($settings[$identifier])) {
$settings[$identifier] = zoom_webservice()->get_user_settings($identifier);
}
return $settings[$identifier];
}
/**
* Get the zoom meeting registrants.
*
* @param string $meetingid Zoom meeting ID.
* @param bool $iswebinar If the session is a webinar.
* @return stdClass Returns a Zoom object containing the registrants (if found).
*/
function zoom_get_meeting_registrants($meetingid, $iswebinar) {
$response = zoom_webservice()->get_meeting_registrants($meetingid, $iswebinar);
return $response;
}
/**
* Checks if a user has registered for a meeting/webinar based on their email address.
*
* @param string $useremail The email address of a user used to determine if they registered or not.
* @param string $meetingid Zoom meeting ID.
* @param bool $iswebinar If the session is a webinar.
* @return bool Returns whether or not the user has registered for the zoom meeting/webinar based on their email address.
*/
function zoom_is_user_registered_for_meeting($useremail, $meetingid, $iswebinar) {
$registrantjoinurl = zoom_get_registrant_join_url($useremail, $meetingid, $iswebinar);
return !empty($registrantjoinurl);
}
/**
* Get the join url for a user for the specified meeting/webinar.
*
* @param string $useremail The email address of a user used to determine if they registered or not.
* @param string $meetingid Zoom meeting ID.
* @param bool $iswebinar If the session is a webinar.
* @return string|false Returns the join url for the user (based on email address) for the specified meeting (if found).
*/
function zoom_get_registrant_join_url($useremail, $meetingid, $iswebinar) {
$response = zoom_get_meeting_registrants($meetingid, $iswebinar);
if (isset($response->registrants)) {
foreach ($response->registrants as $registrant) {
if (strcasecmp($useremail, $registrant->email) == 0) {
return $registrant->join_url;
}
}
}
return false;
}
/**
* Get the display name for a Zoom user.
* This is wrapped in a function to avoid unnecessary API calls.
*
* @param string $zoomuserid Zoom user ID.
* @return ?string
*/
function zoom_get_user_display_name($zoomuserid) {
try {
$hostuser = zoom_get_user($zoomuserid);
// Compose Moodle user object for host.
$hostmoodleuser = new stdClass();
$hostmoodleuser->firstname = $hostuser->first_name;
$hostmoodleuser->lastname = $hostuser->last_name;
$hostmoodleuser->alternatename = '';
$hostmoodleuser->firstnamephonetic = '';
$hostmoodleuser->lastnamephonetic = '';
$hostmoodleuser->middlename = '';
return fullname($hostmoodleuser);
} catch (moodle_exception $error) {
return null;
}
}