public/plugin/zoom/lib/ZoomPlugin.php
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CourseBundle\Entity\CGroup;
use Chamilo\PluginBundle\Zoom\API\JWTClient;
use Chamilo\PluginBundle\Zoom\API\MeetingInfoGet;
use Chamilo\PluginBundle\Zoom\API\MeetingRegistrant;
use Chamilo\PluginBundle\Zoom\API\MeetingSettings;
use Chamilo\PluginBundle\Zoom\API\RecordingFile;
use Chamilo\PluginBundle\Zoom\API\RecordingList;
use Chamilo\PluginBundle\Zoom\Meeting;
use Chamilo\PluginBundle\Zoom\MeetingActivity;
use Chamilo\PluginBundle\Zoom\MeetingRepository;
use Chamilo\PluginBundle\Zoom\Recording;
use Chamilo\PluginBundle\Zoom\RecordingRepository;
use Chamilo\PluginBundle\Zoom\Registrant;
use Chamilo\PluginBundle\Zoom\RegistrantRepository;
use Chamilo\UserBundle\Entity\User;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\ORM\Tools\ToolsException;
use Chamilo\CoreBundle\Component\Utils\ActionIcon;
use Chamilo\CoreBundle\Component\Utils\ToolIcon;
/**
* Class ZoomPlugin. Integrates Zoom meetings in courses.
*/
class ZoomPlugin extends Plugin
{
const RECORDING_TYPE_CLOUD = 'cloud';
const RECORDING_TYPE_LOCAL = 'local';
const RECORDING_TYPE_NONE = 'none';
public $isCoursePlugin = true;
/**
* @var JWTClient
*/
private $jwtClient;
/**
* ZoomPlugin constructor.
* {@inheritdoc}
* Initializes the API JWT client and the entity repositories.
*/
public function __construct()
{
parent::__construct(
'0.3',
'Sébastien Ducoulombier, Julio Montoya',
[
'tool_enable' => 'boolean',
'apiKey' => 'text',
'apiSecret' => 'text',
'verificationToken' => 'text',
'enableParticipantRegistration' => 'boolean',
'enableCloudRecording' => [
'type' => 'select',
'options' => [
self::RECORDING_TYPE_CLOUD => 'Cloud',
self::RECORDING_TYPE_LOCAL => 'Local',
self::RECORDING_TYPE_NONE => get_lang('None'),
],
],
'enableGlobalConference' => 'boolean',
'globalConferenceAllowRoles' => [
'type' => 'select',
'options' => [
PLATFORM_ADMIN => get_lang('Administrator'),
COURSEMANAGER => get_lang('Teacher'),
STUDENT => get_lang('Student'),
STUDENT_BOSS => get_lang('StudentBoss'),
],
'attributes' => ['multiple' => 'multiple'],
],
]
);
$this->isAdminPlugin = true;
if ($this->isEnabled(true)) {
$this->jwtClient = new JWTClient($this->get('apiKey'), $this->get('apiSecret'));
}
}
/**
* Caches and returns an instance of this class.
*
* @return ZoomPlugin the instance to use
*/
public static function create()
{
static $instance = null;
return $instance ? $instance : $instance = new self();
}
/**
* @return bool
*/
public static function currentUserCanJoinGlobalMeeting()
{
$user = api_get_user_entity(api_get_user_id());
if (null === $user) {
return false;
}
return
'true' === api_get_plugin_setting('zoom', 'enableGlobalConference')
&& in_array(
(api_is_platform_admin() ? PLATFORM_ADMIN : $user->getStatus()),
(array) api_get_plugin_setting('zoom', 'globalConferenceAllowRoles')
);
}
/**
* @return array
*/
public function getProfileBlockItems()
{
$elements = $this->meetingsToWhichCurrentUserIsRegisteredComingSoon();
$addMeetingLink = false;
if (self::currentUserCanJoinGlobalMeeting()) {
$addMeetingLink = true;
}
if ($addMeetingLink) {
$elements[$this->get_lang('Meetings')] = api_get_path(WEB_PLUGIN_PATH).'zoom/meetings.php';
}
$items = [];
foreach ($elements as $title => $link) {
$items[] = [
'class' => 'video-conference',
'icon' => Display::getMdiIcon(ToolIcon::VIDEOCONFERENCE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('VideoConference')),
'link' => $link,
'title' => $title,
];
}
return $items;
}
/**
* @return array [ $title => $link ]
*/
public function meetingsToWhichCurrentUserIsRegisteredComingSoon()
{
$linkTemplate = api_get_path(WEB_PLUGIN_PATH).'zoom/join_meeting.php?meetingId=%s';
$user = api_get_user_entity(api_get_user_id());
$meetings = self::getRegistrantRepository()->meetingsComingSoonRegistrationsForUser($user);
$items = [];
foreach ($meetings as $registrant) {
$meeting = $registrant->getMeeting();
$items[sprintf(
$this->get_lang('DateMeetingTitle'),
$meeting->formattedStartTime,
$meeting->getMeetingInfoGet()->topic
)] = sprintf($linkTemplate, $meeting->getId());
}
return $items;
}
/**
* @return RegistrantRepository|EntityRepository
*/
public static function getRegistrantRepository()
{
return Database::getManager()->getRepository(Registrant::class);
}
/**
* Creates this plugin's related tables in the internal database.
* Installs course fields in all courses.
*
* @throws ToolsException
*/
public function install()
{
$schemaManager = Database::getManager()->getConnection()->getSchemaManager();
$tablesExists = $schemaManager->tablesExist(
[
'plugin_zoom_meeting',
'plugin_zoom_meeting_activity',
'plugin_zoom_recording',
'plugin_zoom_registrant',
]
);
if ($tablesExists) {
return;
}
(new SchemaTool(Database::getManager()))->createSchema(
[
Database::getManager()->getClassMetadata(Meeting::class),
Database::getManager()->getClassMetadata(MeetingActivity::class),
Database::getManager()->getClassMetadata(Recording::class),
Database::getManager()->getClassMetadata(Registrant::class),
]
);
$this->install_course_fields_in_all_courses();
}
/**
* Drops this plugins' related tables from the internal database.
* Uninstalls course fields in all courses().
*/
public function uninstall()
{
(new SchemaTool(Database::getManager()))->dropSchema(
[
Database::getManager()->getClassMetadata(Meeting::class),
Database::getManager()->getClassMetadata(MeetingActivity::class),
Database::getManager()->getClassMetadata(Recording::class),
Database::getManager()->getClassMetadata(Registrant::class),
]
);
$this->uninstall_course_fields_in_all_courses();
}
/**
* Generates the search form to include in the meeting list administration page.
* The form has DatePickers 'start' and 'end' and Checkbox 'reloadRecordingLists'.
*
* @return FormValidator the form
*/
public function getAdminSearchForm()
{
$form = new FormValidator('search');
$form->addHeader($this->get_lang('SearchMeeting'));
$form->addDatePicker('start', get_lang('StartDate'));
$form->addDatePicker('end', get_lang('EndDate'));
$form->addButtonSearch(get_lang('Search'));
$oneMonth = new DateInterval('P1M');
if ($form->validate()) {
try {
$start = new DateTime($form->getSubmitValue('start'));
} catch (Exception $exception) {
$start = new DateTime();
$start->sub($oneMonth);
}
try {
$end = new DateTime($form->getSubmitValue('end'));
} catch (Exception $exception) {
$end = new DateTime();
$end->add($oneMonth);
}
} else {
$start = new DateTime();
$start->sub($oneMonth);
$end = new DateTime();
$end->add($oneMonth);
}
try {
$form->setDefaults(
[
'start' => $start->format('Y-m-d'),
'end' => $end->format('Y-m-d'),
]
);
} catch (Exception $exception) {
error_log(join(':', [__FILE__, __LINE__, $exception]));
}
return $form;
}
/**
* Generates a meeting edit form and updates the meeting on validation.
*
* @param Meeting $meeting the meeting
*
* @throws Exception
*
* @return FormValidator
*/
public function getEditMeetingForm($meeting)
{
$meetingInfoGet = $meeting->getMeetingInfoGet();
$form = new FormValidator('edit', 'post', $_SERVER['REQUEST_URI']);
$form->addHeader($this->get_lang('UpdateMeeting'));
$form->addText('topic', $this->get_lang('Topic'));
if ($meeting->requiresDateAndDuration()) {
$startTimeDatePicker = $form->addDateTimePicker('startTime', get_lang('StartTime'));
$form->setRequired($startTimeDatePicker);
$durationNumeric = $form->addNumeric('duration', $this->get_lang('DurationInMinutes'));
$form->setRequired($durationNumeric);
}
$form->addTextarea('agenda', get_lang('Agenda'), ['maxlength' => 2000]);
//$form->addLabel(get_lang('Password'), $meeting->getMeetingInfoGet()->password);
// $form->addText('password', get_lang('Password'), false, ['maxlength' => '10']);
$form->addButtonUpdate(get_lang('Update'));
if ($form->validate()) {
if ($meeting->requiresDateAndDuration()) {
$meetingInfoGet->start_time = (new DateTime($form->getSubmitValue('startTime')))->format(
DateTimeInterface::ISO8601
);
$meetingInfoGet->timezone = date_default_timezone_get();
$meetingInfoGet->duration = (int) $form->getSubmitValue('duration');
}
$meetingInfoGet->topic = $form->getSubmitValue('topic');
$meetingInfoGet->agenda = $form->getSubmitValue('agenda');
try {
$meetingInfoGet->update();
$meeting->setMeetingInfoGet($meetingInfoGet);
Database::getManager()->persist($meeting);
Database::getManager()->flush();
Display::addFlash(
Display::return_message($this->get_lang('MeetingUpdated'), 'confirm')
);
} catch (Exception $exception) {
Display::addFlash(
Display::return_message($exception->getMessage(), 'error')
);
}
}
$defaults = [
'topic' => $meetingInfoGet->topic,
'agenda' => $meetingInfoGet->agenda,
];
if ($meeting->requiresDateAndDuration()) {
$defaults['startTime'] = $meeting->startDateTime->format('Y-m-d H:i');
$defaults['duration'] = $meetingInfoGet->duration;
}
$form->setDefaults($defaults);
return $form;
}
/**
* Generates a meeting delete form and deletes the meeting on validation.
*
* @param Meeting $meeting
* @param string $returnURL where to redirect to on successful deletion
*
* @throws Exception
*
* @return FormValidator
*/
public function getDeleteMeetingForm($meeting, $returnURL)
{
$id = $meeting->getMeetingId();
$form = new FormValidator('delete', 'post', api_get_self().'?meetingId='.$id);
$form->addButtonDelete($this->get_lang('DeleteMeeting'));
if ($form->validate()) {
$this->deleteMeeting($meeting, $returnURL);
}
return $form;
}
/**
* @param Meeting $meeting
* @param string $returnURL
*
* @return false
*/
public function deleteMeeting($meeting, $returnURL)
{
if (null === $meeting) {
return false;
}
$em = Database::getManager();
try {
// No need to delete a instant meeting.
if (\Chamilo\PluginBundle\Zoom\API\Meeting::TYPE_INSTANT != $meeting->getMeetingInfoGet()->type) {
$meeting->getMeetingInfoGet()->delete();
}
$em->remove($meeting);
$em->flush();
Display::addFlash(
Display::return_message($this->get_lang('MeetingDeleted'), 'confirm')
);
api_location($returnURL);
} catch (Exception $exception) {
$this->handleException($exception);
}
}
/**
* @param Exception $exception
*/
public function handleException($exception)
{
if ($exception instanceof Exception) {
$error = json_decode($exception->getMessage());
$message = $exception->getMessage();
if ($error->message) {
$message = $error->message;
}
Display::addFlash(
Display::return_message($message, 'error')
);
}
}
/**
* Generates a registrant list update form listing course and session users.
* Updates the list on validation.
*
* @param Meeting $meeting
*
* @throws Exception
*
* @return FormValidator
*/
public function getRegisterParticipantForm($meeting)
{
$form = new FormValidator('register', 'post', $_SERVER['REQUEST_URI']);
$userIdSelect = $form->addSelect('userIds', $this->get_lang('RegisteredUsers'));
$userIdSelect->setMultiple(true);
$form->addButtonSend($this->get_lang('UpdateRegisteredUserList'));
$users = $meeting->getRegistrableUsers();
foreach ($users as $user) {
$userIdSelect->addOption(
api_get_person_name($user->getFirstname(), $user->getLastname()),
$user->getId()
);
}
if ($form->validate()) {
$selectedUserIds = $form->getSubmitValue('userIds');
$selectedUsers = [];
if (!empty($selectedUserIds)) {
foreach ($users as $user) {
if (in_array($user->getId(), $selectedUserIds)) {
$selectedUsers[] = $user;
}
}
}
try {
$this->updateRegistrantList($meeting, $selectedUsers);
Display::addFlash(
Display::return_message($this->get_lang('RegisteredUserListWasUpdated'), 'confirm')
);
} catch (Exception $exception) {
Display::addFlash(
Display::return_message($exception->getMessage(), 'error')
);
}
}
$registeredUserIds = [];
foreach ($meeting->getRegistrants() as $registrant) {
$registeredUserIds[] = $registrant->getUser()->getId();
}
$userIdSelect->setSelected($registeredUserIds);
return $form;
}
/**
* Generates a meeting recording files management form.
* Takes action on validation.
*
* @param Meeting $meeting
*
* @throws Exception
*
* @return FormValidator
*/
public function getFileForm($meeting, $returnURL)
{
$form = new FormValidator('fileForm', 'post', $_SERVER['REQUEST_URI']);
if (!$meeting->getRecordings()->isEmpty()) {
$fileIdSelect = $form->addSelect('fileIds', get_lang('Files'));
$fileIdSelect->setMultiple(true);
$recordingList = $meeting->getRecordings();
foreach ($recordingList as &$recording) {
// $recording->instanceDetails = $plugin->getPastMeetingInstanceDetails($instance->uuid);
$options = [];
$recordings = $recording->getRecordingMeeting()->recording_files;
foreach ($recordings as $file) {
$options[] = [
'text' => sprintf(
'%s.%s (%s)',
$file->recording_type,
$file->file_type,
$file->file_size
),
'value' => $file->id,
];
}
$fileIdSelect->addOptGroup(
$options,
sprintf("%s (%s)", $recording->formattedStartTime, $recording->formattedDuration)
);
}
$actions = [];
if ($meeting->isCourseMeeting()) {
$actions['CreateLinkInCourse'] = $this->get_lang('CreateLinkInCourse');
$actions['CopyToCourse'] = $this->get_lang('CopyToCourse');
}
$actions['DeleteFile'] = $this->get_lang('DeleteFile');
$form->addRadio(
'action',
get_lang('Action'),
$actions
);
$form->addButtonUpdate($this->get_lang('DoIt'));
if ($form->validate()) {
$action = $form->getSubmitValue('action');
$idList = $form->getSubmitValue('fileIds');
foreach ($recordingList as $recording) {
$recordings = $recording->getRecordingMeeting()->recording_files;
foreach ($recordings as $file) {
if (in_array($file->id, $idList)) {
$name = sprintf(
$this->get_lang('XRecordingOfMeetingXFromXDurationXDotX'),
$file->recording_type,
$meeting->getId(),
$recording->formattedStartTime,
$recording->formattedDuration,
$file->file_type
);
if ('CreateLinkInCourse' === $action && $meeting->isCourseMeeting()) {
try {
$this->createLinkToFileInCourse($meeting, $file, $name);
Display::addFlash(
Display::return_message(
$this->get_lang('LinkToFileWasCreatedInCourse'),
'success'
)
);
} catch (Exception $exception) {
Display::addFlash(
Display::return_message($exception->getMessage(), 'error')
);
}
} elseif ('CopyToCourse' === $action && $meeting->isCourseMeeting()) {
try {
$this->copyFileToCourse($meeting, $file, $name);
Display::addFlash(
Display::return_message($this->get_lang('FileWasCopiedToCourse'), 'confirm')
);
} catch (Exception $exception) {
Display::addFlash(
Display::return_message($exception->getMessage(), 'error')
);
}
} elseif ('DeleteFile' === $action) {
try {
$name = $file->recording_type;
$file->delete();
Display::addFlash(
Display::return_message($this->get_lang('FileWasDeleted').': '.$name, 'confirm')
);
} catch (Exception $exception) {
Display::addFlash(
Display::return_message($exception->getMessage(), 'error')
);
}
}
}
}
}
api_location($returnURL);
}
}
return $form;
}
/**
* Adds to the meeting course documents a link to a meeting instance recording file.
*
* @param Meeting $meeting
* @param RecordingFile $file
* @param string $name
*
* @throws Exception
*/
public function createLinkToFileInCourse($meeting, $file, $name)
{
$course = $meeting->getCourse();
if (null === $course) {
throw new Exception('This meeting is not linked to a course');
}
$courseInfo = api_get_course_info_by_id($course->getId());
if (empty($courseInfo)) {
throw new Exception('This meeting is not linked to a valid course');
}
/*$path = '/zoom_meeting_recording_file_'.$file->id.'.'.$file->file_type;
$docId = DocumentManager::addCloudLink($courseInfo, $path, $file->play_url, $name);
if (!$docId) {
throw new Exception(get_lang(DocumentManager::cloudLinkExists($courseInfo, $path, $file->play_url) ? 'UrlAlreadyExists' : 'ErrorAddCloudLink'));
}*/
}
/**
* Copies a recording file to a meeting's course.
*
* @param Meeting $meeting
* @param RecordingFile $file
* @param string $name
*
* @throws Exception
*/
public function copyFileToCourse($meeting, $file, $name)
{
$course = $meeting->getCourse();
if (null === $course) {
throw new Exception('This meeting is not linked to a course');
}
$courseInfo = api_get_course_info_by_id($course->getId());
if (empty($courseInfo)) {
throw new Exception('This meeting is not linked to a valid course');
}
$tmpFile = tmpfile();
if (false === $tmpFile) {
throw new Exception('tmpfile() returned false');
}
$curl = curl_init($file->getFullDownloadURL($this->jwtClient->token));
if (false === $curl) {
throw new Exception('Could not init curl: '.curl_error($curl));
}
if (!curl_setopt_array(
$curl,
[
CURLOPT_FILE => $tmpFile,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 120,
]
)) {
throw new Exception("Could not set curl options: ".curl_error($curl));
}
if (false === curl_exec($curl)) {
throw new Exception("curl_exec failed: ".curl_error($curl));
}
$sessionId = 0;
$session = $meeting->getSession();
if (null !== $session) {
$sessionId = $session->getId();
}
$groupId = 0;
$group = $meeting->getGroup();
if (null !== $group) {
$groupId = $group->getIid();
}
$newPath = null;
/*$newPath = handle_uploaded_document(
$courseInfo,
[
'name' => $name,
'tmp_name' => stream_get_meta_data($tmpFile)['uri'],
'size' => filesize(stream_get_meta_data($tmpFile)['uri']),
'from_file' => true,
'move_file' => true,
'type' => $file->file_type,
],
api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document',
'/',
api_get_user_id(),
$groupId,
null,
0,
'overwrite',
true,
false,
null,
$sessionId,
true
);*/
fclose($tmpFile);
if (false === $newPath) {
throw new Exception('Could not handle uploaded document');
}
}
/**
* Generates a form to fast and easily create and start an instant meeting.
* On validation, create it then redirect to it and exit.
*
* @return FormValidator
*/
public function getCreateInstantMeetingForm(
User $user,
Course $course,
CGroup $group = null,
Session $session = null
) {
$extraUrl = '';
if (!empty($course)) {
$extraUrl = api_get_cidreq();
}
$form = new FormValidator('createInstantMeetingForm', 'post', api_get_self().'?'.$extraUrl, '_blank');
$form->addButton('startButton', $this->get_lang('StartInstantMeeting'), 'video-camera', 'primary');
if ($form->validate()) {
try {
$this->startInstantMeeting($this->get_lang('InstantMeeting'), $user, $course, $group, $session);
} catch (Exception $exception) {
Display::addFlash(
Display::return_message($exception->getMessage(), 'error')
);
}
}
return $form;
}
/**
* Generates a form to schedule a meeting.
* On validation, creates it and redirects to its page.
*
* @throws Exception
*
* @return FormValidator
*/
public function getScheduleMeetingForm(User $user, Course $course = null, CGroup $group = null, Session $session = null)
{
$extraUrl = '';
if (!empty($course)) {
$extraUrl = api_get_cidreq();
}
$form = new FormValidator('scheduleMeetingForm', 'post', api_get_self().'?'.$extraUrl);
$form->addHeader($this->get_lang('ScheduleAMeeting'));
$startTimeDatePicker = $form->addDateTimePicker('startTime', get_lang('StartTime'));
$form->setRequired($startTimeDatePicker);
$form->addText('topic', $this->get_lang('Topic'), true);
$form->addTextarea('agenda', get_lang('Agenda'), ['maxlength' => 2000]);
$durationNumeric = $form->addNumeric('duration', $this->get_lang('DurationInMinutes'));
$form->setRequired($durationNumeric);
if (null === $course && 'true' === $this->get('enableGlobalConference')) {
$options = [];
$options['everyone'] = $this->get_lang('ForEveryone');
$options['registered_users'] = $this->get_lang('SomeUsers');
if (!empty($options)) {
if (1 === count($options)) {
$form->addHidden('type', key($options));
} else {
$form->addSelect('type', $this->get_lang('ConferenceType'), $options);
}
}
} else {
// To course
$form->addHidden('type', 'course');
}
/*
// $passwordText = $form->addText('password', get_lang('Password'), false, ['maxlength' => '10']);
if (null !== $course) {
$registrationOptions = [
'RegisterAllCourseUsers' => $this->get_lang('RegisterAllCourseUsers'),
];
$groups = GroupManager::get_groups();
if (!empty($groups)) {
$registrationOptions['RegisterTheseGroupMembers'] = get_lang('RegisterTheseGroupMembers');
}
$registrationOptions['RegisterNoUser'] = $this->get_lang('RegisterNoUser');
$userRegistrationRadio = $form->addRadio(
'userRegistration',
$this->get_lang('UserRegistration'),
$registrationOptions
);
$groupOptions = [];
foreach ($groups as $group) {
$groupOptions[$group['id']] = $group['name'];
}
$groupIdsSelect = $form->addSelect(
'groupIds',
$this->get_lang('RegisterTheseGroupMembers'),
$groupOptions
);
$groupIdsSelect->setMultiple(true);
if (!empty($groups)) {
$jsCode = sprintf(
"getElementById('%s').parentNode.parentNode.parentNode.style.display = getElementById('%s').checked ? 'block' : 'none'",
$groupIdsSelect->getAttribute('id'),
$userRegistrationRadio->getelements()[1]->getAttribute('id')
);
$form->setAttribute('onchange', $jsCode);
}
}*/
$form->addButtonCreate(get_lang('Save'));
if ($form->validate()) {
$type = $form->getSubmitValue('type');
switch ($type) {
case 'everyone':
$user = null;
$group = null;
$course = null;
$session = null;
break;
case 'registered_users':
//$user = null;
$course = null;
$session = null;
break;
case 'course':
$user = null;
//$course = null;
//$session = null;
break;
}
try {
$newMeeting = $this->createScheduleMeeting(
$user,
$course,
$group,
$session,
new DateTime($form->getSubmitValue('startTime')),
$form->getSubmitValue('duration'),
$form->getSubmitValue('topic'),
$form->getSubmitValue('agenda'),
substr(uniqid('z', true), 0, 10)
);
Display::addFlash(
Display::return_message($this->get_lang('NewMeetingCreated'))
);
if ($newMeeting->isCourseMeeting()) {
if ('RegisterAllCourseUsers' === $form->getSubmitValue('userRegistration')) {
$this->registerAllCourseUsers($newMeeting);
Display::addFlash(
Display::return_message($this->get_lang('AllCourseUsersWereRegistered'))
);
} elseif ('RegisterTheseGroupMembers' === $form->getSubmitValue('userRegistration')) {
$userIds = [];
foreach ($form->getSubmitValue('groupIds') as $groupId) {
$userIds = array_unique(array_merge($userIds, GroupManager::get_users($groupId)));
}
$users = Database::getManager()->getRepository('ChamiloUserBundle:User')->findBy(
['id' => $userIds]
);
$this->registerUsers($newMeeting, $users);
Display::addFlash(
Display::return_message($this->get_lang('GroupUsersWereRegistered'))
);
}
}
api_location('meeting.php?meetingId='.$newMeeting->getMeetingId().'&'.$extraUrl);
} catch (Exception $exception) {
Display::addFlash(
Display::return_message($exception->getMessage(), 'error')
);
}
} else {
$form->setDefaults(
[
'duration' => 60,
'userRegistration' => 'RegisterAllCourseUsers',
]
);
}
return $form;
}
/**
* Return the current global meeting (create it if needed).
*
* @throws Exception
*
* @return string
*/
public function getGlobalMeeting()
{
foreach ($this->getMeetingRepository()->unfinishedGlobalMeetings() as $meeting) {
return $meeting;
}
return $this->createGlobalMeeting();
}
/**
* @return MeetingRepository|EntityRepository
*/
public static function getMeetingRepository()
{
return Database::getManager()->getRepository(Meeting::class);
}
/**
* Returns the URL to enter (start or join) a meeting or null if not possible to enter the meeting,
* The returned URL depends on the meeting current status (waiting, started or finished) and the current user.
*
* @param Meeting $meeting
*
* @throws OptimisticLockException
* @throws Exception
*
* @return string|null
*/
public function getStartOrJoinMeetingURL($meeting)
{
$status = $meeting->getMeetingInfoGet()->status;
$userId = api_get_user_id();
$currentUser = api_get_user_entity($userId);
$isGlobal = 'true' === $this->get('enableGlobalConference') && $meeting->isGlobalMeeting();
switch ($status) {
case 'ended':
if ($this->userIsConferenceManager($meeting)) {
return $meeting->getMeetingInfoGet()->start_url;
}
break;
case 'waiting':
// Zoom does not allow for a new meeting to be started on first participant join.
// It requires the host to start the meeting first.
// Therefore for global meetings we must make the first participant the host
// that is use start_url rather than join_url.
// the participant will not be registered and will appear as the Zoom user account owner.
// For course and user meetings, only the host can start the meeting.
if ($this->userIsConferenceManager($meeting)) {
return $meeting->getMeetingInfoGet()->start_url;
}
break;
case 'started':
// User per conference.
if ($currentUser === $meeting->getUser()) {
return $meeting->getMeetingInfoGet()->join_url;
}
// The participant is not registered, he can join only the global meeting (automatic registration).
if ($isGlobal) {
return $this->registerUser($meeting, $currentUser)->getCreatedRegistration()->join_url;
}
if ($meeting->isCourseMeeting()) {
if ($this->userIsCourseConferenceManager()) {
return $meeting->getMeetingInfoGet()->start_url;
}
$sessionId = api_get_session_id();
$courseCode = api_get_course_id();
if (empty($sessionId)) {
$isSubscribed = CourseManager::is_user_subscribed_in_course(
$userId,
$courseCode,
false
);
} else {
$isSubscribed = CourseManager::is_user_subscribed_in_course(
$userId,
$courseCode,
true,
$sessionId
);
}
if ($isSubscribed) {
if ($meeting->isCourseGroupMeeting()) {
$isInGroup = GroupManager::isUserInGroup($userId, $meeting->getGroup());
if (false === $isInGroup) {
throw new Exception($this->get_lang('YouAreNotRegisteredToThisMeeting'));
}
}
if (\Chamilo\PluginBundle\Zoom\API\Meeting::TYPE_INSTANT == $meeting->getMeetingInfoGet()->type) {
return $meeting->getMeetingInfoGet()->join_url;
}
return $this->registerUser($meeting, $currentUser)->getCreatedRegistration()->join_url;
}
throw new Exception($this->get_lang('YouAreNotRegisteredToThisMeeting'));
}
//if ('true' === $this->get('enableParticipantRegistration')) {
//if ('true' === $this->get('enableParticipantRegistration') && $meeting->requiresRegistration()) {
// the participant must be registered
$registrant = $meeting->getRegistrant($currentUser);
if (null == $registrant) {
throw new Exception($this->get_lang('YouAreNotRegisteredToThisMeeting'));
}
// the participant is registered
return $registrant->getCreatedRegistration()->join_url;
//}
break;
}
return null;
}
/**
* @param Meeting $meeting
*
* @return bool whether the logged-in user can manage conferences in this context, that is either
* the current course or session coach, the platform admin or the current course admin
*/
public function userIsConferenceManager($meeting)
{
if (null === $meeting) {
return false;
}
if (api_is_coach() || api_is_platform_admin()) {
return true;
}
if ($meeting->isCourseMeeting() && api_get_course_id() && api_is_course_admin()) {
return true;
}
return $meeting->isUserMeeting() && $meeting->getUser()->getId() == api_get_user_id();
}
/**
* @return bool whether the logged-in user can manage conferences in this context, that is either
* the current course or session coach, the platform admin or the current course admin
*/
public function userIsCourseConferenceManager()
{
if (api_is_coach() || api_is_platform_admin()) {
return true;
}
if (api_get_course_id() && api_is_course_admin()) {
return true;
}
return false;
}
/**
* Update local recording list from remote Zoom server's version.
* Kept to implement a future administration button ("import existing data from zoom server").
*
* @param DateTime $startDate
* @param DateTime $endDate
*
* @throws OptimisticLockException
* @throws Exception
*/
public function reloadPeriodRecordings($startDate, $endDate)
{
$em = Database::getManager();
$recordingRepo = $this->getRecordingRepository();
$meetingRepo = $this->getMeetingRepository();
$recordings = RecordingList::loadPeriodRecordings($startDate, $endDate);
foreach ($recordings as $recordingMeeting) {
$recordingEntity = $recordingRepo->findOneBy(['uuid' => $recordingMeeting->uuid]);
if (null === $recordingEntity) {
$recordingEntity = new Recording();
$meeting = $meetingRepo->findOneBy(['meetingId' => $recordingMeeting->id]);
if (null === $meeting) {
try {
$meetingInfoGet = MeetingInfoGet::fromId($recordingMeeting->id);
} catch (Exception $exception) {
$meetingInfoGet = null; // deleted meeting with recordings
}
if (null !== $meetingInfoGet) {
$meeting = $this->createMeetingFromMeeting(
(new Meeting())->setMeetingInfoGet($meetingInfoGet)
);
$em->persist($meeting);
}
}
if (null !== $meeting) {
$recordingEntity->setMeeting($meeting);
}
}
$recordingEntity->setRecordingMeeting($recordingMeeting);
$em->persist($recordingEntity);
}
$em->flush();
}
/**
* @return RecordingRepository|EntityRepository
*/
public static function getRecordingRepository()
{
return Database::getManager()->getRepository(Recording::class);
}
public function getToolbar($returnUrl = '')
{
if (!api_is_platform_admin()) {
return '';
}
$actionsLeft = '';
$back = '';
$courseId = api_get_course_id();
if (empty($courseId)) {
$actionsLeft .=
Display::url(
Display::getMdiIcon(ToolIcon::VIDEOCONFERENCE, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, $this->get_lang('Meetings')),
api_get_path(WEB_PLUGIN_PATH).'zoom/meetings.php'
);
} else {
$actionsLeft .=
Display::url(
Display::getMdiIcon(ToolIcon::VIDEOCONFERENCE, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, $this->get_lang('Meetings')),
api_get_path(WEB_PLUGIN_PATH).'zoom/start.php?'.api_get_cidreq()
);
}
if (!empty($returnUrl)) {
$back = Display::url(
Display::getMdiIcon(ActionIcon::BACK, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Back')),
$returnUrl
);
}
if (api_is_platform_admin()) {
$actionsLeft .=
Display::url(
Display::getMdiIcon(ToolIcon::SETTINGS, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Settings')),
api_get_path(WEB_CODE_PATH).'admin/configure_plugin.php?name=zoom'
).$back;
}
return Display::toolbarAction('toolbar', [$actionsLeft]);
}
public function getRecordingSetting()
{
$recording = (string) $this->get('enableCloudRecording');
if (in_array($recording, [self::RECORDING_TYPE_LOCAL, self::RECORDING_TYPE_CLOUD], true)) {
return $recording;
}
return self::RECORDING_TYPE_NONE;
}
public function hasRecordingAvailable()
{
$recording = $this->getRecordingSetting();
return self::RECORDING_TYPE_NONE !== $recording;
}
/**
* Updates meeting registrants list. Adds the missing registrants and removes the extra.
*
* @param Meeting $meeting
* @param User[] $users list of users to be registered
*
* @throws Exception
*/
private function updateRegistrantList($meeting, $users)
{
$usersToAdd = [];
foreach ($users as $user) {
$found = false;
foreach ($meeting->getRegistrants() as $registrant) {
if ($registrant->getUser() === $user) {
$found = true;
break;
}
}
if (!$found) {
$usersToAdd[] = $user;
}
}
$registrantsToRemove = [];
foreach ($meeting->getRegistrants() as $registrant) {
$found = false;
foreach ($users as $user) {
if ($registrant->getUser() === $user) {
$found = true;
break;
}
}
if (!$found) {
$registrantsToRemove[] = $registrant;
}
}
$this->registerUsers($meeting, $usersToAdd);
$this->unregister($meeting, $registrantsToRemove);
}
/**
* Register users to a meeting.
*
* @param Meeting $meeting
* @param User[] $users
*
* @throws OptimisticLockException
*
* @return User[] failed registrations [ user id => errorMessage ]
*/
private function registerUsers($meeting, $users)
{
$failedUsers = [];
foreach ($users as $user) {
try {
$this->registerUser($meeting, $user, false);
} catch (Exception $exception) {
$failedUsers[$user->getId()] = $exception->getMessage();
}
}
Database::getManager()->flush();
return $failedUsers;
}
/**
* @throws Exception
* @throws OptimisticLockException
*
* @return Registrant
*/
private function registerUser(Meeting $meeting, User $user, $andFlush = true)
{
if (empty($user->getEmail())) {
throw new Exception($this->get_lang('CannotRegisterWithoutEmailAddress'));
}
$meetingRegistrant = MeetingRegistrant::fromEmailAndFirstName(
$user->getEmail(),
$user->getFirstname(),
$user->getLastname()
);
$registrantEntity = (new Registrant())
->setMeeting($meeting)
->setUser($user)
->setMeetingRegistrant($meetingRegistrant)
->setCreatedRegistration($meeting->getMeetingInfoGet()->addRegistrant($meetingRegistrant));
Database::getManager()->persist($registrantEntity);
if ($andFlush) {
Database::getManager()->flush($registrantEntity);
}
return $registrantEntity;
}
/**
* Removes registrants from a meeting.
*
* @param Meeting $meeting
* @param Registrant[] $registrants
*
* @throws Exception
*/
private function unregister($meeting, $registrants)
{
$meetingRegistrants = [];
foreach ($registrants as $registrant) {
$meetingRegistrants[] = $registrant->getMeetingRegistrant();
}
$meeting->getMeetingInfoGet()->removeRegistrants($meetingRegistrants);
$em = Database::getManager();
foreach ($registrants as $registrant) {
$em->remove($registrant);
}
$em->flush();
}
/**
* Starts a new instant meeting and redirects to its start url.
*
* @param string $topic
* @param User|null $user
* @param Course|null $course
* @param CGroup|null $group
* @param Session|null $session
*
* @throws Exception
*/
private function startInstantMeeting($topic, $user = null, $course = null, $group = null, $session = null)
{
$meetingInfoGet = MeetingInfoGet::fromTopicAndType($topic, MeetingInfoGet::TYPE_INSTANT);
//$meetingInfoGet->settings->approval_type = MeetingSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE;
$meeting = $this->createMeetingFromMeeting(
(new Meeting())
->setMeetingInfoGet($meetingInfoGet)
->setUser($user)
->setGroup($group)
->setCourse($course)
->setSession($session)
);
api_location($meeting->getMeetingInfoGet()->start_url);
}
/**
* Creates a meeting on Zoom servers and stores it in the local database.
*
* @param Meeting $meeting a new, unsaved meeting with at least a type and a topic
*
* @throws Exception
*
* @return Meeting
*/
private function createMeetingFromMeeting($meeting)
{
$currentUser = api_get_user_entity(api_get_user_id());
$meeting->getMeetingInfoGet()->settings->contact_email = $currentUser->getEmail();
$meeting->getMeetingInfoGet()->settings->contact_name = $currentUser->getFullname();
$meeting->getMeetingInfoGet()->settings->auto_recording = $this->getRecordingSetting();
$meeting->getMeetingInfoGet()->settings->registrants_email_notification = false;
//$meeting->getMeetingInfoGet()->host_email = $currentUser->getEmail();
//$meeting->getMeetingInfoGet()->settings->alternative_hosts = $currentUser->getEmail();
// Send create to Zoom.
$meeting->setMeetingInfoGet($meeting->getMeetingInfoGet()->create());
Database::getManager()->persist($meeting);
Database::getManager()->flush();
return $meeting;
}
/**
* @throws Exception
*
* @return Meeting
*/
private function createGlobalMeeting()
{
$meetingInfoGet = MeetingInfoGet::fromTopicAndType(
$this->get_lang('GlobalMeeting'),
MeetingInfoGet::TYPE_SCHEDULED
);
$meetingInfoGet->start_time = (new DateTime())->format(DateTimeInterface::ISO8601);
$meetingInfoGet->duration = 60;
$meetingInfoGet->settings->approval_type =
('true' === $this->get('enableParticipantRegistration'))
? MeetingSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE
: MeetingSettings::APPROVAL_TYPE_NO_REGISTRATION_REQUIRED;
// $meetingInfoGet->settings->host_video = true;
$meetingInfoGet->settings->participant_video = true;
$meetingInfoGet->settings->join_before_host = true;
$meetingInfoGet->settings->registrants_email_notification = false;
return $this->createMeetingFromMeeting((new Meeting())->setMeetingInfoGet($meetingInfoGet));
}
/**
* Schedules a meeting and returns it.
* set $course, $session and $user to null in order to create a global meeting.
*
* @param DateTime $startTime meeting local start date-time (configure local timezone on your Zoom account)
* @param int $duration in minutes
* @param string $topic short title of the meeting, required
* @param string $agenda ordre du jour
* @param string $password meeting password
*
* @throws Exception
*
* @return Meeting meeting
*/
private function createScheduleMeeting(
User $user = null,
Course $course = null,
CGroup $group = null,
Session $session = null,
$startTime,
$duration,
$topic,
$agenda,
$password
) {
$meetingInfoGet = MeetingInfoGet::fromTopicAndType($topic, MeetingInfoGet::TYPE_SCHEDULED);
$meetingInfoGet->duration = $duration;
$meetingInfoGet->start_time = $startTime->format(DateTimeInterface::ISO8601);
$meetingInfoGet->agenda = $agenda;
$meetingInfoGet->password = $password;
$meetingInfoGet->settings->approval_type = MeetingSettings::APPROVAL_TYPE_NO_REGISTRATION_REQUIRED;
if ('true' === $this->get('enableParticipantRegistration')) {
$meetingInfoGet->settings->approval_type = MeetingSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE;
}
return $this->createMeetingFromMeeting(
(new Meeting())
->setMeetingInfoGet($meetingInfoGet)
->setUser($user)
->setCourse($course)
->setGroup($group)
->setSession($session)
);
}
/**
* Registers all the course users to a course meeting.
*
* @param Meeting $meeting
*
* @throws OptimisticLockException
*/
private function registerAllCourseUsers($meeting)
{
$this->registerUsers($meeting, $meeting->getRegistrableUsers());
}
}