'boolean', 'apiKey' => 'text', 'apiSecret' => 'text', 'verificationToken' => 'text', self::SETTING_ACCOUNT_ID => 'text', self::SETTING_CLIENT_ID => 'text', self::SETTING_CLIENT_SECRET => 'text', self::SETTING_SECRET_TOKEN => 'text', 'enableParticipantRegistration' => 'boolean', 'enablePresenter' => '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'), SESSIONADMIN => get_lang('SessionsAdmin'), ], 'attributes' => ['multiple' => 'multiple'], ], 'accountSelector' => 'text', ] ); $this->isAdminPlugin = true; $accountId = $this->get(self::SETTING_ACCOUNT_ID); $clientId = $this->get(self::SETTING_CLIENT_ID); $clientSecret = $this->get(self::SETTING_CLIENT_SECRET); if (!empty($accountId) && !empty($clientId) && !empty($clientSecret)) { $this->jwtClient = new ServerToServerOAuthClient($accountId, $clientId, $clientSecret); } else { $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(): ZoomPlugin { 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') && api_user_is_login(); 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::return_icon( 'bbb.png', 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->getTopic() )] = sprintf($linkTemplate, $meeting->getMeetingId()); } 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', 'plugin_zoom_signature', ] ); if ($tablesExists) { return; } $em = Database::getManager(); (new SchemaTool($em))->createSchema( [ $em->getClassMetadata(Meeting::class), $em->getClassMetadata(Webinar::class), $em->getClassMetadata(MeetingActivity::class), $em->getClassMetadata(Recording::class), $em->getClassMetadata(Registrant::class), $em->getClassMetadata(Signature::class), ] ); // Copy icons into the main/img/icons folder $iconName = 'zoom_meet'; $iconsList = [ '64/'.$iconName.'.png', '64/'.$iconName.'_na.png', '32/'.$iconName.'.png', '32/'.$iconName.'_na.png', '22/'.$iconName.'.png', '22/'.$iconName.'_na.png', ]; $sourceDir = api_get_path(SYS_PLUGIN_PATH).'zoom/resources/img/'; $destinationDir = api_get_path(SYS_CODE_PATH).'img/icons/'; foreach ($iconsList as $icon) { $src = $sourceDir.$icon; $dest = $destinationDir.$icon; copy($src, $dest); } $this->install_course_fields_in_all_courses(true, 'zoom_meet.png'); } /** * Drops this plugins' related tables from the internal database. * Uninstalls course fields in all courses(). */ public function uninstall() { $em = Database::getManager(); (new SchemaTool($em))->dropSchema( [ $em->getClassMetadata(Meeting::class), $em->getClassMetadata(Webinar::class), $em->getClassMetadata(MeetingActivity::class), $em->getClassMetadata(Recording::class), $em->getClassMetadata(Registrant::class), $em->getClassMetadata(Signature::class), ] ); $this->uninstall_course_fields_in_all_courses(); // Remove icons from the main/img/icons folder $iconName = 'zoom_meet'; $iconsList = [ '64/'.$iconName.'.png', '64/'.$iconName.'_na.png', '32/'.$iconName.'.png', '32/'.$iconName.'_na.png', '22/'.$iconName.'.png', '22/'.$iconName.'_na.png', ]; $destinationDir = api_get_path(SYS_CODE_PATH).'img/icons/'; foreach ($iconsList as $icon) { $dest = $destinationDir.$icon; if (is_file($dest)) { @unlink($dest); } } } /** * 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; } /** * @throws Exception */ public function getEditConferenceForm(Meeting $conference): FormValidator { $isWebinar = $conference instanceof Webinar; $requiresDateAndDuration = $conference->requiresDateAndDuration(); /** @var BaseMeetingTrait $schema */ $schema = $isWebinar ? $conference->getWebinarSchema() : $conference->getMeetingInfoGet(); $form = new FormValidator('edit', 'post', $_SERVER['REQUEST_URI']); $form->addHeader( $isWebinar ? $this->get_lang('UpdateWebinar') : $this->get_lang('UpdateMeeting') ); $form->addLabel(get_lang('Type'), $conference->typeName); if ($conference->getAccountEmail()) { $form->addLabel( $this->get_lang('AccountEmail'), $conference->getAccountEmail() ); } $form->addText('topic', $this->get_lang('Topic')); if ($requiresDateAndDuration) { $startTimeDatePicker = $form->addDateTimePicker('startTime', get_lang('StartTime')); $durationNumeric = $form->addNumeric('duration', $this->get_lang('DurationInMinutes')); $form->setRequired($startTimeDatePicker); $form->setRequired($durationNumeric); } $form->addTextarea('agenda', get_lang('Agenda'), ['maxlength' => 2000]); $form->addCheckBox('sign_attendance', $this->get_lang('SignAttendance'), get_lang('Yes')); $form->addTextarea('reason_to_sign', $this->get_lang('ReasonToSign'), ['rows' => 5]); $form->addButtonUpdate(get_lang('Update')); if ($form->validate()) { $formValues = $form->exportValues(); $em = Database::getManager(); if ($requiresDateAndDuration) { $schema->start_time = (new DateTime($formValues['startTime']))->format(DATE_ATOM); $schema->timezone = date_default_timezone_get(); $schema->duration = (int) $formValues['duration']; } $schema->topic = $formValues['topic']; $schema->agenda = $formValues['agenda']; $conference ->setSignAttendance(isset($formValues['sign_attendance'])) ->setReasonToSignAttendance($formValues['reason_to_sign']); try { $schema->update(); if ($isWebinar) { $conference->setWebinarSchema($schema); } else { $conference->setMeetingInfoGet($schema); } $em->persist($conference); $em->flush(); Display::addFlash( Display::return_message( $isWebinar ? $this->get_lang('WebinarUpdated') : $this->get_lang('MeetingUpdated'), 'confirm' ) ); } catch (Exception $exception) { Display::addFlash( Display::return_message($exception->getMessage(), 'error') ); } } $defaults = [ 'topic' => $schema->topic, 'agenda' => $schema->agenda, ]; if ($requiresDateAndDuration) { $defaults['startTime'] = $conference->startDateTime->format('Y-m-d H:i'); $defaults['duration'] = $schema->duration; } $defaults['sign_attendance'] = $conference->isSignAttendance(); $defaults['reason_to_sign'] = $conference->getReasonToSignAttendance(); $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; } public function getDeleteWebinarForm(Webinar $webinar, string $returnURL): FormValidator { $id = $webinar->getMeetingId(); $form = new FormValidator('delete', 'post', api_get_self()."?meetingId=$id"); $form->addButtonDelete($this->get_lang('DeleteWebinar')); if ($form->validate()) { $this->deleteWebinar($webinar, $returnURL); } return $form; } /** * @param Meeting $meeting * @param string $returnURL */ public function deleteMeeting($meeting, $returnURL): bool { if (null === $meeting) { return false; } // No need to delete a instant meeting. if (\Chamilo\PluginBundle\Zoom\API\Meeting::TYPE_INSTANT == $meeting->getMeetingInfoGet()->type) { return false; } try { $meeting->getMeetingInfoGet()->delete(); } catch (Exception $exception) { $this->handleException($exception); } $em = Database::getManager(); $em->remove($meeting); $em->flush(); Display::addFlash( Display::return_message($this->get_lang('MeetingDeleted'), 'confirm') ); api_location($returnURL); return true; } public function deleteWebinar(Webinar $webinar, string $returnURL) { try { $webinar->getWebinarSchema()->delete(); } catch (Exception $exception) { $this->handleException($exception); } $em = Database::getManager(); $em->remove($webinar); $em->flush(); Display::addFlash( Display::return_message($this->get_lang('WebinarDeleted'), 'success') ); api_location($returnURL); } /** * @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')); $selfRegistrationUrl = api_get_path(WEB_PLUGIN_PATH) .'zoom/subscription.php?meetingId='.$meeting->getMeetingId(); $form->addHtml( '

' ); $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; } public function getRegisterPresenterForm(Meeting $meeting): FormValidator { $form = new FormValidator('register_presenter', 'post', $_SERVER['REQUEST_URI']); $presenterIdSelect = $form->addSelect('presenterIds', $this->get_lang('RegisteredPresenters')); $presenterIdSelect->setMultiple(true); $form->addButtonSend($this->get_lang('UpdateRegisteredUserList')); $users = $meeting->getRegistrableUsers(); foreach ($users as $user) { $presenterIdSelect->addOption( api_get_person_name($user->getFirstname(), $user->getLastname()), $user->getId() ); } if ($form->validate()) { $selectedPresenterIds = $form->getSubmitValue('presenterIds') ?: []; $selectedPresenters = []; foreach ($users as $user) { if (in_array($user->getId(), $selectedPresenterIds)) { $selectedPresenters[] = $user; } } try { $this->updatePresenterList($meeting, $selectedPresenters); Display::addFlash( Display::return_message($this->get_lang('RegisteredUserListWasUpdated'), 'confirm') ); } catch (Exception $exception) { Display::addFlash( Display::return_message($exception->getMessage(), 'error') ); } } $registeredPresenterIds = []; foreach ($meeting->getPresenters() as $registrant) { if ($registrant instanceof Presenter) { $registeredPresenterIds[] = $registrant->getUser()->getId(); } } $presenterIdSelect->setSelected($registeredPresenterIds); 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 = 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, CGroupInfo $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, CGroupInfo $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')); $form->addSelect( 'conference_type', $this->get_lang('ConferenceType'), [ 'meeting' => $this->get_lang('Meeting'), 'webinar' => $this->get_lang('Webinar'), ] ); $form->addRule('conference_type', get_lang('ThisFieldIsRequired'), 'required'); $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('AudienceType'), $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->addCheckBox('sign_attendance', $this->get_lang('SignAttendance'), get_lang('Yes')); $form->addTextarea('reason_to_sign', $this->get_lang('ReasonToSign'), ['rows' => 5]); $accountEmails = $this->getAccountEmails(); if (!empty($accountEmails)) { $form->addSelect('account_email', $this->get_lang('AccountEmail'), $accountEmails); } $form->addButtonCreate(get_lang('Save')); if ($form->validate()) { $formValues = $form->exportValues(); $conferenceType = $formValues['conference_type']; $password = substr(uniqid('z', true), 0, 10); switch ($formValues['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; } $accountEmail = $formValues['account_email'] ?? null; $accountEmail = $accountEmail && in_array($accountEmail, $accountEmails) ? $accountEmail : null; try { $startTime = new DateTime($formValues['startTime']); if ('meeting' === $conferenceType) { $newMeeting = $this->createScheduleMeeting( $user, $course, $group, $session, $startTime, $formValues['duration'], $formValues['topic'], $formValues['agenda'], $password, isset($formValues['sign_attendance']), $formValues['reason_to_sign'], $accountEmail ); Display::addFlash( Display::return_message($this->get_lang('NewMeetingCreated')) ); } elseif ('webinar' === $conferenceType) { $newMeeting = $this->createScheduleWebinar( $user, $course, $group, $session, $startTime, $formValues['duration'], $formValues['topic'], $formValues['agenda'], $password, isset($formValues['sign_attendance']), $formValues['reason_to_sign'], $accountEmail ); Display::addFlash( Display::return_message($this->get_lang('NewWebinarCreated')) ); } else { throw new Exception('Invalid conference type'); } 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. * * @throws OptimisticLockException * @throws Exception * * @return string|null */ public function getStartOrJoinMeetingURL(Meeting $meeting) { if ($meeting instanceof Webinar) { $status = 'started'; } else { $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 instanceof Webinar ? $meeting->getWebinarSchema()->start_url : $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 instanceof Webinar ? $meeting->getWebinarSchema()->start_url : $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()) { $groupInfo = GroupManager::get_group_properties($meeting->getGroup()->getIid(), true); $isInGroup = GroupManager::is_user_in_group($userId, $groupInfo); if (false === $isInGroup) { throw new Exception($this->get_lang('YouAreNotRegisteredToThisMeeting')); } } if (!$meeting instanceof Webinar && \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->getRegistrantByUser($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; } $currentUser = api_get_user_entity(api_get_user_id()); if ('true' === $this->get('enableParticipantRegistration') && 'true' === $this->get('enablePresenter') && $currentUser && $meeting->hasUserAsPresenter($currentUser) ) { 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(string $returnUrl = ''): string { $isPlatformOrSessionAdmin = api_is_platform_admin(true); $isSessionAdmin = api_is_session_admin(); if (!$isPlatformOrSessionAdmin) { return ''; } $actionsLeft = ''; $back = ''; $courseId = api_get_course_id(); if (empty($courseId)) { $actionsLeft .= Display::url( Display::return_icon('bbb.png', $this->get_lang('Meetings'), null, ICON_SIZE_MEDIUM), api_get_path(WEB_PLUGIN_PATH).'zoom/meetings.php' ); } else { $actionsLeft .= Display::url( Display::return_icon('bbb.png', $this->get_lang('Meetings'), null, ICON_SIZE_MEDIUM), api_get_path(WEB_PLUGIN_PATH).'zoom/start.php?'.api_get_cidreq() ); } if (!empty($returnUrl)) { $back = Display::url( Display::return_icon('back.png', get_lang('Back'), null, ICON_SIZE_MEDIUM), $returnUrl ); } if (!$isSessionAdmin) { $actionsLeft .= Display::url( Display::return_icon('agenda.png', get_lang('Calendar'), [], ICON_SIZE_MEDIUM), 'calendar.php' ); $actionsLeft .= Display::url( Display::return_icon('settings.png', get_lang('Settings'), null, ICON_SIZE_MEDIUM), api_get_path(WEB_CODE_PATH).'admin/configure_plugin.php?name=zoom' ); } return Display::toolbarAction('toolbar', [$back.PHP_EOL.$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; } /** * @throws OptimisticLockException * @throws \Doctrine\ORM\ORMException */ public function saveSignature(Registrant $registrant, string $file): bool { if (empty($file)) { return false; } $signature = $registrant->getSignature(); if (null !== $signature) { return false; } $signature = new Signature(); $signature ->setFile($file) ->setRegisteredAt(api_get_utc_datetime(null, false, true)) ; $registrant->setSignature($signature); $em = Database::getManager(); $em->persist($signature); $em->flush(); return true; } public function getSignature(int $userId, Meeting $meeting): ?Signature { $signatureRepo = Database::getManager() ->getRepository(Signature::class) ; return $signatureRepo->findOneBy(['user' => $userId, 'meeting' => $meeting]); } public function exportSignatures(Meeting $meeting, $formatToExport) { $signatures = array_map( function (Registrant $registrant) use ($formatToExport) { $signature = $registrant->getSignature(); $item = [ $registrant->getUser()->getLastname(), $registrant->getUser()->getFirstname(), $signature ? api_convert_and_format_date($signature->getRegisteredAt(), DATE_TIME_FORMAT_LONG) : '-', ]; if ('pdf' === $formatToExport) { $item[] = $signature ? Display::img($signature->getFile(), '', ['style' => 'width: 150px;'], false) : '-'; } return $item; }, $meeting->getRegistrants()->toArray() ); $data = array_merge( [ [ get_lang('LastName'), get_lang('FirstName'), get_lang('DateTime'), 'pdf' === $formatToExport ? get_lang('File') : null, ], ], $signatures ); if ('pdf' === $formatToExport) { $params = [ 'filename' => get_lang('Attendance'), 'pdf_title' => get_lang('Attendance'), 'pdf_description' => $meeting->getIntroduction(), 'show_teacher_as_myself' => false, ]; Export::export_table_pdf($data, $params); } if ('xls' === $formatToExport) { $introduction = array_map( function ($line) { return [ strip_tags(trim($line)), ]; }, explode(PHP_EOL, $meeting->getIntroduction()) ); Export::arrayToXls( array_merge($introduction, $data), get_lang('Attendance') ); } } /** * @throws Exception */ public function createWebinarFromSchema(Webinar $webinar, WebinarSchema $schema): Webinar { $currentUser = api_get_user_entity(api_get_user_id()); $schema->settings->contact_email = $currentUser->getEmail(); $schema->settings->contact_name = $currentUser->getFullname(); $schema->settings->auto_recording = $this->getRecordingSetting(); $schema->settings->registrants_email_notification = false; $schema->settings->attendees_and_panelists_reminder_email_notification->enable = false; $schema->settings->follow_up_attendees_email_notification->enable = false; $schema->settings->follow_up_absentees_email_notification->enable = false; $schema = $schema->create($webinar->getAccountEmail()); $webinar->setWebinarSchema($schema); $em = Database::getManager(); $em->persist($webinar); $em->flush(); return $webinar; } public function getAccountEmails(): array { $currentValue = $this->get('accountSelector'); if (empty($currentValue)) { return []; } $emails = explode(';', $currentValue); $trimmed = array_map('trim', $emails); $filtered = array_filter($trimmed); return array_combine($filtered, $filtered); } /** * Register users to a meeting. * * @param User[] $users * * @throws OptimisticLockException * * @return User[] failed registrations [ user id => errorMessage ] */ public function registerUsers(Meeting $meeting, array $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; } /** * @param array $users * * @throws OptimisticLockException * @throws \Doctrine\ORM\ORMException */ public function registerPresenters(Meeting $meeting, array $users): array { $failedUsers = []; foreach ($users as $user) { try { $this->registerUser($meeting, $user, false, true); } catch (Exception $exception) { $failedUsers[$user->getId()] = $exception->getMessage(); } } Database::getManager()->flush(); return $failedUsers; } /** * Removes registrants from a meeting. * * @param Registrant[] $registrants * * @throws Exception */ public function unregister(Meeting $meeting, array $registrants) { $meetingRegistrants = []; foreach ($registrants as $registrant) { $meetingRegistrants[] = $registrant->getMeetingRegistrant(); } if ($meeting instanceof Webinar) { $meeting->getWebinarSchema()->removeRegistrants($meetingRegistrants); } else { $meeting->getMeetingInfoGet()->removeRegistrants($meetingRegistrants); } $em = Database::getManager(); foreach ($registrants as $registrant) { $em->remove($registrant); } $em->flush(); } /** * 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); } private function updatePresenterList($meeting, $users) { /** @var array $presenters */ $presenters = $meeting->getPresenters(); $presenterToAdd = []; foreach ($users as $user) { $foundPresenter = false; foreach ($presenters as $presenter) { if ($presenter->getUser() === $user) { $foundPresenter = true; break; } } if (!$foundPresenter) { $presenterToAdd[] = $user; } } $registrantsToRemove = []; foreach ($presenters as $registrant) { $found = false; foreach ($users as $user) { if ($registrant->getUser() === $user) { $found = true; break; } } if (!$found) { $registrantsToRemove[] = $registrant; } } $this->registerPresenters($meeting, $presenterToAdd); $this->unregister($meeting, $registrantsToRemove); } /** * @throws Exception * @throws OptimisticLockException * * @return Registrant */ private function registerUser(Meeting $meeting, User $user, $andFlush = true, bool $isPresenter = false) { if (empty($user->getEmail())) { throw new Exception($this->get_lang('CannotRegisterWithoutEmailAddress')); } if ($meeting instanceof Webinar) { $meetingRegistrant = WebinarRegistrantSchema::fromEmailAndFirstName( $user->getEmail(), $user->getFirstname(), $user->getLastname() ); } else { $meetingRegistrant = MeetingRegistrant::fromEmailAndFirstName( $user->getEmail(), $user->getFirstname(), $user->getLastname() ); } $registrantEntity = new Registrant(); if ($isPresenter) { $registrantEntity = new Presenter(); } $registrantEntity ->setMeeting($meeting) ->setUser($user) ->setMeetingRegistrant($meetingRegistrant) ; if ($meeting instanceof Webinar) { $registrantEntity->setCreatedRegistration($meeting->getWebinarSchema()->addRegistrant($meetingRegistrant)); } else { $registrantEntity->setCreatedRegistration($meeting->getMeetingInfoGet()->addRegistrant($meetingRegistrant)); } Database::getManager()->persist($registrantEntity); if ($andFlush) { Database::getManager()->flush($registrantEntity); } return $registrantEntity; } /** * Starts a new instant meeting and redirects to its start url. * * @param string $topic * @param User|null $user * @param Course|null $course * @param CGroupInfo|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( $meeting->getAccountEmail() ) ); 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(DATE_ATOM); $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, CGroupInfo $group = null, Session $session = null, $startTime, $duration, $topic, $agenda, $password, bool $signAttendance = false, string $reasonToSignAttendance = '', string $accountEmail = null ) { $meetingInfoGet = MeetingInfoGet::fromTopicAndType($topic, MeetingInfoGet::TYPE_SCHEDULED); $meetingInfoGet->duration = $duration; $meetingInfoGet->start_time = $startTime->format(DATE_ATOM); $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) ->setSignAttendance($signAttendance) ->setReasonToSignAttendance($reasonToSignAttendance) ->setAccountEmail($accountEmail) ); } /** * @throws Exception */ private function createScheduleWebinar( ?User $user, ?Course $course, ?CGroupInfo $group, ?Session $session, DateTime $startTime, $duration, $topic, $agenda, $password, bool $signAttendance = false, string $reasonToSignAttendance = '', string $accountEmail = null ): Webinar { $webinarSchema = WebinarSchema::fromTopicAndType($topic); $webinarSchema->duration = $duration; $webinarSchema->start_time = $startTime->format(DATE_ATOM); $webinarSchema->agenda = $agenda; $webinarSchema->password = $password; if ('true' === $this->get('enableParticipantRegistration')) { $webinarSchema->settings->approval_type = WebinarSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE; } $webinar = (new Webinar()) ->setUser($user) ->setCourse($course) ->setGroup($group) ->setSession($session) ->setSignAttendance($signAttendance) ->setReasonToSignAttendance($reasonToSignAttendance) ->setAccountEmail($accountEmail) ; return $this->createWebinarFromSchema($webinar, $webinarSchema); } /** * Registers all the course users to a course meeting. * * @param Meeting $meeting * * @throws OptimisticLockException */ private function registerAllCourseUsers($meeting) { $this->registerUsers($meeting, $meeting->getRegistrableUsers()); } }