Skip to content

Commit

Permalink
Issue #193: Hide/show parts of zoom meeting invitation based on capab…
Browse files Browse the repository at this point in the history
…ilities (#235)

Issue #193: Hide/show parts of zoom meeting invitation based on capabilities

* Add two new caps: zoom/viewjoinurl, zoom/viewdialin
* Add settings to define regex to pull out key information
* Add toggle to turn this feature on/off
* Remove sections of message user does not have the capability to view
* Add setting to toggle using regex pattern to remove the generic invite sentence.
  • Loading branch information
andrewmadden authored Apr 15, 2021
1 parent 74f97a3 commit f881c49
Show file tree
Hide file tree
Showing 9 changed files with 569 additions and 12 deletions.
210 changes: 210 additions & 0 deletions classes/invitation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
<?php
// This file is part of 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/>.

/**
* Represents a Zoom invitation.
*
* @package mod_zoom
* @author Andrew Madden <[email protected]>
* @copyright 2021 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

namespace mod_zoom;

defined('MOODLE_INTERNAL') || die();

class invitation {

const PREFIX = 'invitation_';

/** @var string|null $invitation The unaltered zoom invitation text. */
private $invitation;

/** @var array $configregex Array of regex patterns defined in plugin settings. */
private $configregex;

/**
* invitation constructor.
*
* @param string|null $invitation Zoom invitation returned from
* https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetinginvitation.
*/
public function __construct($invitation) {
$this->invitation = $invitation;
}

/**
* Get the display string to show on the module page.
*
* @param int $coursemoduleid Course module where the user will view the invitation.
* @param int|null $userid Optionally supply the intended user to view the string. Defaults to global $USER.
* @return string|null
*/
public function get_display_string(int $coursemoduleid, int $userid = null) {
if (empty($this->invitation)) {
return null;
}
// If regex patterns are disabled, return the raw zoom meeting invitation.
if (!get_config('zoom', 'invitationregexenabled')) {
return $this->invitation;
}
$displaystring = $this->invitation;
try {
// If setting enabled, strip the invite message.
if (get_config('zoom', 'invitationremoveinvite')) {
$displaystring = $this->remove_element($displaystring, 'invite');
}
// Check user capabilities, and remove parts of the invitation they don't have permission to view.
if (!has_capability('mod/zoom:viewjoinurl', \context_module::instance($coursemoduleid), $userid)) {
$displaystring = $this->remove_element($displaystring, 'joinurl');
}
if (!has_capability('mod/zoom:viewdialin', \context_module::instance($coursemoduleid), $userid)) {
$displaystring = $this->remove_element($displaystring, 'onetapmobile');
$displaystring = $this->remove_element($displaystring, 'dialin');
$displaystring = $this->remove_element($displaystring, 'sip');
$displaystring = $this->remove_element($displaystring, 'h323');
} else {
// Fix the formatting of the onetapmobile section if it exists.
$displaystring = $this->add_paragraph_break_above_element($displaystring, 'onetapmobile');
}
} catch (\moodle_exception $e) {
// If the regex parsing fails, log a debugging message and return the whole invitation.
debugging($e->getMessage(), DEBUG_DEVELOPER);
return $this->invitation;
}
$displaystring = trim($this->clean_paragraphs($displaystring));
return $displaystring;
}

/**
* Remove instances of a zoom invitation element using a regex pattern.
*
* @param string $invitation
* @param string $element
* @return string
*
* @throws \coding_exception
* @throws \dml_exception
* @throws \moodle_exception
*/
private function remove_element(string $invitation, string $element): string {
global $PAGE;
$configregex = $this->get_config_invitation_regex();
if (!array_key_exists($element, $configregex)) {
throw new \coding_exception('Cannot remove element: ' . $element
. '. See mod/zoom/classes/invitation.php:get_default_invitation_regex for valid elements.');
}
$count = 0;
$invitation = @preg_replace($configregex[$element], "", $invitation, -1, $count);
// If invitation is null, an error occurred in preg_replace.
if ($invitation === null) {
throw new \moodle_exception('invitationmodificationfailed', 'mod_zoom', $PAGE->url,
['element' => $element, 'pattern' => $configregex[$element]]);
}
// Add debugging message to assist site administrator in testing regex patterns if no match is found.
if (empty($count)) {
debugging(get_string('invitationmatchnotfound', 'mod_zoom',
['element' => $element, 'pattern' => $configregex[$element]]),
DEBUG_DEVELOPER);
}
return $invitation;
}

/**
* Add a paragraph break above an element defined by a regex pattern in a zoom invitation.
*
* @param string $invitation
* @param string $element
* @return string
*
* @throws \coding_exception
* @throws \dml_exception
*/
private function add_paragraph_break_above_element(string $invitation, string $element): string {
$matches = [];
$configregex = $this->get_config_invitation_regex();
// If no pattern found for element, return the invitation string unaltered.
if (empty($configregex[$element])) {
return $invitation;
}
$result = preg_match($configregex[$element], $invitation, $matches, PREG_OFFSET_CAPTURE);
// If error occurred in preg_match, show debugging message to help site administrator.
if ($result === false) {
debugging(get_string('invitationmodificationfailed', 'mod_zoom',
['element' => $element, 'pattern' => $configregex[$element]]),
DEBUG_DEVELOPER);
}
// No match found, so return invitation string unaltered.
if (empty($matches)) {
return $invitation;
}
// Get the position of the element in the full invitation string.
$pos = $matches[0][1];
// Inject a paragraph break above element. Use $this->clean_paragraphs() to fix uneven breaks between paragraphs.
return substr_replace($invitation, "\r\n\r\n", $pos, 0);
}

/**
* Ensure that paragraphs in string have correct spacing.
*
* @param string $invitation
* @return string
*/
private function clean_paragraphs(string $invitation): string {
// Replace partial paragraph breaks with exactly two line breaks.
$invitation = preg_replace("/\r\n\n/m", "\r\n\r\n", $invitation);
// Replace breaks of more than two line breaks with exactly two.
$invitation = preg_replace("/\r\n\r\n[\r\n]+/m", "\r\n\r\n", $invitation);
return $invitation;
}

/**
* Get regex patterns from site config to find the different zoom invitation elements.
*
* @return array
* @throws \dml_exception
*/
private function get_config_invitation_regex(): array {
if ($this->configregex !== null) {
return $this->configregex;
}
$config = get_config('zoom');
$this->configregex = [];
// Get the regex defined in the plugin settings for each element.
foreach (self::get_default_invitation_regex() as $element => $pattern) {
$settingname = self::PREFIX . $element;
$this->configregex[$element] = $config->$settingname;
}
return $this->configregex;
}

/**
* Get default regex patterns to find the different zoom invitation elements.
*
* @return string[]
*/
public static function get_default_invitation_regex(): array {
return [
'invite' => '/^.+is inviting you to a scheduled zoom meeting.+$/mi',
'joinurl' => '/^join zoom meeting.*(\n.*)+?(\nmeeting id.+\npasscode.+)$/mi',
'onetapmobile' => '/^one tap mobile.*(\n\s*\+.+)+$/mi',
'dialin' => '/^dial by your location.*(\n\s*\+.+)+(\n.*)+find your local number.+$/mi',
'sip' => '/^join by sip.*\n.+$/mi',
'h323' => '/^join by h\.323.*(\n.*)+?(\nmeeting id.+\npasscode.+)$/mi'
];
}
}
9 changes: 4 additions & 5 deletions classes/webservice.php
Original file line number Diff line number Diff line change
Expand Up @@ -628,23 +628,22 @@ public function get_meeting_webinar_info($id, $webinar) {
* Get the meeting invite note that was sent for a specific meeting from Zoom.
*
* @param stdClass $zoom The zoom meeting
* @return string The meeting's invite note.
* @return \mod_zoom\invitation The meeting's invitation.
* @link https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetinginvitation
*/
public function get_meeting_invitation($zoom) {
// Webinar does not have meeting invite info.
if ($zoom->webinar) {
return null;
return new \mod_zoom\invitation(null);
}
$url = 'meetings/' . $zoom->meeting_id . '/invitation';
$response = null;
try {
$response = $this->_make_call($url);
} catch (moodle_exception $error) {
debugging($error->getMessage());
return null;
return new \mod_zoom\invitation(null);
}
return $response->invitation;
return new \mod_zoom\invitation($response->invitation);
}

/**
Expand Down
23 changes: 23 additions & 0 deletions db/access.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,27 @@
'editingteacher' => CAP_ALLOW
)
),

'mod/zoom:viewjoinurl' => array(
'riskbitmask' => RISK_PERSONAL,
'captype' => 'read',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => array(
'teacher' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
'manager' => CAP_ALLOW,
)
),

'mod/zoom:viewdialin' => array(
'riskbitmask' => RISK_PERSONAL,
'captype' => 'read',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => array(
'student' => CAP_ALLOW,
'teacher' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
'manager' => CAP_ALLOW,
)
),
);
2 changes: 1 addition & 1 deletion exportical.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@

// Get the meeting invite note to add to the description property.
$service = new mod_zoom_webservice();
$meetinginvite = $service->get_meeting_invitation($zoom);
$meetinginvite = $service->get_meeting_invitation($zoom)->get_display_string($cm->id);

// Compute and add description property to event.
$convertedtext = html_to_text($zoom->intro);
Expand Down
23 changes: 23 additions & 0 deletions lang/en/zoom.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,27 @@
$string['indicator:socialbreadth_help'] = 'This indicator is based on the social breadth reached by the student in a Zoom activity.';
$string['invalidscheduleuser'] = 'You cannot schedule for the specified user.';
$string['invalid_status'] = 'Status invalid, check the database.';
$string['invitationmatchnotfound'] = 'No match found in zoom invitation for element: "{$a->element}" with pattern: "{$a->pattern}".';
$string['invitationmodificationfailed'] = 'Error in regex for zoom invitation element: "{$a->element}" with pattern: "{$a->pattern}".';
$string['invitationregex'] = 'Zoom invitation regex and capabilities';
$string['invitationregex_help'] = 'Define the regex patterns to isolate each part of a zoom invitation so the information can be controlled by capabilities.';
$string['invitationregex_nohideif'] = 'Please note: The regex patterns will only be used if the \'{$a}\' setting is enabled.';
$string['invitationregexenabled'] = 'Enable zoom invitation regex and capabilities.';
$string['invitationregexenabled_help'] = 'When enabled, the zoom invitation shown in the activity will be broken up into elements using the following regex and capabilities will be used to decide which parts to display to each user. See zoom/viewjoinurl and zoom/viewdialin capabilities.';
$string['invitationremoveinvite'] = 'Remove zoom invitation invite message';
$string['invitationremoveinvite_help'] = 'If enabled, the introduction sentence in the zoom meeting email message will be stripped using the invitation_invite regex pattern.';
$string['invitation_dialin'] = 'Dial in pattern';
$string['invitation_dialin_help'] = 'The regex pattern to find the Zoom meeting dial in numbers.';
$string['invitation_h323'] = 'H.323 message pattern';
$string['invitation_h323_help'] = 'The regex pattern to find the Zoom meeting H.323 information.';
$string['invitation_invite'] = 'Invite message pattern';
$string['invitation_invite_help'] = 'The regex pattern to find the Zoom meeting introduction message.';
$string['invitation_joinurl'] = 'Join URL pattern';
$string['invitation_joinurl_help'] = 'The regex pattern to find the Zoom meeting join url.';
$string['invitation_onetapmobile'] = 'One tap mobile pattern';
$string['invitation_onetapmobile_help'] = 'The regex pattern to find the Zoom meeting one tap mobile details.';
$string['invitation_sip'] = 'SIP pattern';
$string['invitation_sip_help'] = 'The regex pattern to find the Zoom meeting SIP information.';
$string['join'] = 'Join';
$string['joinbeforehost'] = 'Join meeting before host';
$string['joinbeforehostenable'] = 'Allow participants to join anytime';
Expand Down Expand Up @@ -268,3 +289,5 @@
$string['zoom:eligiblealternativehost'] = 'Selectable as alternative host within Zoom meetings';
$string['zoom:refreshsessions'] = 'Refresh Zoom meeting reports';
$string['zoom:view'] = 'View Zoom meetings';
$string['zoom:viewdialin'] = 'View Zoom dial-in information';
$string['zoom:viewjoinurl'] = 'View Zoom join url';
42 changes: 41 additions & 1 deletion settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
require_once($CFG->dirroot.'/mod/zoom/locallib.php');
require_once($CFG->dirroot.'/mod/zoom/classes/webservice.php');

$moodlehashideif = version_compare(normalize_version($CFG->release), '3.7.0', '>=');

$settings = new admin_settingpage('modsettingzoom', get_string('pluginname', 'mod_zoom'));

// Test whether connection works and display result to user.
Expand Down Expand Up @@ -109,7 +111,7 @@
15, $jointimeselect);
$settings->add($firstabletojoin);

if (version_compare(normalize_version($CFG->release), '3.7.0', '>=')) {
if ($moodlehashideif) {
$displayleadtime = new admin_setting_configcheckbox('zoom/displayleadtime',
get_string('displayleadtime', 'mod_zoom'),
get_string('displayleadtime_desc', 'mod_zoom'), 0, 1, 0);
Expand Down Expand Up @@ -275,4 +277,42 @@
get_string('option_mute_upon_entry_help', 'mod_zoom'),
1, 1, 0);
$settings->add($defaultmuteuponentryoption);

$invitationregexhelp = get_string('invitationregex_help', 'mod_zoom');
if (!$moodlehashideif) {
$invitationregexhelp .= "\n\n" . get_string('invitationregex_nohideif', 'mod_zoom',
get_string('invitationregexenabled', 'mod_zoom'));
}
$settings->add(new admin_setting_heading('zoom/invitationregex',
get_string('invitationregex', 'mod_zoom'), $invitationregexhelp));

$settings->add(new admin_setting_configcheckbox('zoom/invitationregexenabled',
get_string('invitationregexenabled', 'mod_zoom'),
get_string('invitationregexenabled_help', 'mod_zoom'),
0, 1, 0));

$settings->add(new admin_setting_configcheckbox('zoom/invitationremoveinvite',
get_string('invitationremoveinvite', 'mod_zoom'),
get_string('invitationremoveinvite_help', 'mod_zoom'),
0, 1, 0));
if ($moodlehashideif) {
$settings->hide_if('zoom/invitationremoveinvite', 'zoom/invitationregexenabled', 'eq', 0);
}

// Allow admin to modify regex for invitation parts if zoom api changes.
foreach (\mod_zoom\invitation::get_default_invitation_regex() as $element => $pattern) {
$name = 'zoom/' . \mod_zoom\invitation::PREFIX . $element;
$visiblename = get_string(\mod_zoom\invitation::PREFIX . $element, 'mod_zoom');
$description = get_string(\mod_zoom\invitation::PREFIX . $element . '_help', 'mod_zoom');
$settings->add(new admin_setting_configtext($name, $visiblename, $description, $pattern));
if ($moodlehashideif) {
$settings->hide_if('zoom/' . \mod_zoom\invitation::PREFIX . $element,
'zoom/invitationregexenabled', 'eq', 0);
}
}

// Extra hideif for invite element.
if ($moodlehashideif) {
$settings->hide_if('zoom/invitation_invite', 'zoom/invitationremoveinvite', 'eq', 0);
}
}
Loading

0 comments on commit f881c49

Please sign in to comment.