Your IP : 3.144.37.240


Current Path : /home/ncdcgo/ele.ncdc.go.ug/mod/zoom/
Upload File :
Current File : /home/ncdcgo/ele.ncdc.go.ug/mod/zoom/locallib.php

<?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.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');

/**
 * Entry not found on Zoom.
 */
class zoom_not_found_exception extends moodle_exception {
    /**
     * Web service response.
     * @var string
     */
    public $response = null;

    /**
     * Constructor
     * @param string $response      Web service response message
     * @param int $errorcode     Web service response error code
     */
    public function __construct($response, $errorcode) {
        $this->response = $response;
        $this->zoomerrorcode = $errorcode;
        parent::__construct('errorwebservice_notfound', 'zoom');
    }
}

/**
 * Bad request received by Zoom.
 */
class zoom_bad_request_exception extends moodle_exception {
    /**
     * Web service response.
     * @var string
     */
    public $response = null;

    /**
     * Constructor
     * @param string $response      Web service response message
     * @param int $errorcode     Web service response error code
     */
    public function __construct($response, $errorcode) {
        $this->response = $response;
        $this->zoomerrorcode = $errorcode;
        parent::__construct('errorwebservice_badrequest', 'zoom', '', $response);
    }
}

/**
 * Couldn't succeed within the allowed number of retries.
 */
class zoom_api_retry_failed_exception extends moodle_exception {
    /**
     * Web service response.
     * @var string
     */
    public $response = null;

    /**
     * Constructor
     * @param string $response      Web service response
     * @param int $errorcode     Web service response error code
     */
    public function __construct($response, $errorcode) {
        $this->response = $response;
        $this->zoomerrorcode = $errorcode;
        $a = new stdClass();
        $a->response = $response;
        $a->maxretries = mod_zoom_webservice::MAX_RETRIES;
        parent::__construct('zoomerr_maxretries', 'zoom', '', $a);
    }
}

/**
 * Exceeded daily API limit.
 */
class zoom_api_limit_exception extends moodle_exception {
    /**
     * Web service response.
     * @var string
     */
    public $response = null;

    /**
     * Unix timestamp of next time to API can be called.
     * @var int
     */
    public $retryafter = null;

    /**
     * Constructor
     * @param string $response  Web service response
     * @param int $errorcode    Web service response error code
     * @param int $retryafter   Unix timestamp of next time to API can be called.
     */
    public function __construct($response, $errorcode, $retryafter) {
        $this->response = $response;
        $this->zoomerrorcode = $errorcode;
        $this->retryafter = $retryafter;
        $a = new stdClass();
        $a->response = $response;
        parent::__construct('zoomerr_apilimit', 'zoom', '',
                userdate($retryafter, get_string('strftimedaydatetime', 'core_langconfig')));
    }
}

/**
 * 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, array('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', array('id' => $cm->course), '*', MUST_EXIST);
        $zoom  = $DB->get_record('zoom', array('id' => $cm->instance), '*', MUST_EXIST);
    } else if ($n) {
        $zoom  = $DB->get_record('zoom', array('id' => $n), '*', MUST_EXIST);
        $course     = $DB->get_record('course', array('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 array($course, $cm, $zoom);
}

/**
 * Retrieves information for a meeting.
 *
 * @param int $zoomid
 * @return array information about the meeting
 */
function zoom_get_sessions_for_display($zoomid) {
    require_once(__DIR__.'/../../lib/moodlelib.php');
    global $DB;

    $sessions = array();
    $format = get_string('strftimedatetimeshort', 'langconfig');

    $instances = $DB->get_records('zoom_meeting_details', array('zoomid' => $zoomid));

    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 = array(
        'simplekeys' => true,
        'simpledata' => true,
    );
    $cache = cache::make_from_params(cache_store::MODE_REQUEST, 'zoom', 'nextoccurrence', array(), $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 = array('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 array($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.
 *
 * @return stdClass
 */
function zoom_get_meeting_security_settings() {
    $cache = cache::make('mod_zoom', 'zoommeetingsecurity');
    if (!($zoommeetingsecurity = $cache->get('meetingsecurity'))) {
        try {
            $zoommeetingsecurity = zoom_webservice()->get_account_meeting_security_settings();
        } catch (moodle_exception $error) {
            throw $error;
        }
        $cache->set('meetingsecurity', $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', array('id' => $cmid, 'sesskey' => sesskey()));
    $delete = new moodle_url('/course/mod.php', array('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 &&
        $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 = array();

    // 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, array('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.
    list($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 = array();
    list($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) {
            list($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', array('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 array();
    }

    // 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', array('id' => $cm->instance), '*', MUST_EXIST);

    require_login($course, true, $cm);

    require_capability('mod/zoom:view', $context);

    $returns = array('nexturl' => null, 'error' => null);

    list($inprogress, $available, $finished) = zoom_get_state($zoom);

    // If the meeting is not yet available, deny access.
    if ($available !== true) {
        // 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 {
        $returns['nexturl'] = new moodle_url($zoom->join_url, array('uname' => fullname($USER)));
    }

    // Record user's clicking join.
    \mod_zoom\event\join_meeting_button_clicked::create(array(
        'context' => $context,
        'objectid' => $zoom->id,
        'other' => array(
            'cmid' => $id,
            'meetingid' => (int) $zoom->meeting_id,
            'userishost' => $userishost,
        ),
    ))->trigger();

    // Track completion viewed.
    $completion = new completion_info($course);
    $completion->set_module_viewed($cm);

    // 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 = array(
            'rawgrade' => $grademax,
            'userid' => $USER->id,
            'usermodified' => $USER->id,
            'dategraded' => '',
            'feedbackformat' => '',
            'feedback' => '',
        );

        zoom_grade_item_update($zoom, $grades);
    }

    // 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 = array();

    // 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 = array();

    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 = array();
    foreach ($trackingfields as $trackingfield) {
        $field = str_replace(' ', '_', strtolower($trackingfield->field));
        $tfvalues[$field] = $trackingfield->value;
    }

    $tfrows = $DB->get_records('zoom_meeting_tracking_fields', array('meeting_id' => $zoomid));
    $tfobjects = array();
    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', array('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 The id of the zoom meeting.
 *
 * @return array All recordings for the zoom meeting grouped together.
 */
function zoom_get_meeting_recordings_grouped($zoomid) {
    global $DB;

    $records = $DB->get_records('zoom_meeting_recordings', ['zoomid' => $zoomid], 'recordingstart ASC');
    $recordings = [];
    foreach ($records as $recording) {
        $recordings[$recording->meetinguuid][] = $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 = array();

    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 = array();

    if (!isset($settings[$identifier])) {
        $settings[$identifier] = zoom_webservice()->get_user_settings($identifier);
    }

    return $settings[$identifier];
}