From eb80bfdd9c9714faef12262072dc657b8c95ea70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ducoulombier?= Date: Thu, 16 Jul 2020 19:57:35 +0200 Subject: [PATCH 1/9] global and user Zoom meetings - refs BT#17288 Introducing 3 local tables to link remote Zoom API meetings to local user, course and sessions and remote registrants to local users. Better than using remote storage. Opens new possibilities but would work better with "webhooks" to maintain local copies of Zoom object. --- main/inc/lib/userportal.lib.php | 6 + plugin/zoom/Entity/MeetingEntity.php | 468 +++++++ plugin/zoom/Entity/RecordingEntity.php | 184 +++ plugin/zoom/Entity/RegistrantEntity.php | 258 ++++ plugin/zoom/admin.php | 13 +- plugin/zoom/global.php | 30 + plugin/zoom/join_meeting.php | 42 + plugin/zoom/lang/english.php | 7 + plugin/zoom/lang/french.php | 7 + plugin/zoom/lang/spanish.php | 7 + plugin/zoom/lib/API/Client.php | 27 +- plugin/zoom/lib/API/JWTClient.php | 3 +- plugin/zoom/lib/API/Meeting.php | 10 +- plugin/zoom/lib/API/MeetingInfoGet.php | 45 +- plugin/zoom/lib/API/MeetingInstance.php | 14 +- plugin/zoom/lib/API/MeetingInstances.php | 5 +- plugin/zoom/lib/API/MeetingList.php | 5 +- plugin/zoom/lib/API/MeetingRegistrant.php | 7 +- plugin/zoom/lib/API/MeetingRegistrantList.php | 5 +- plugin/zoom/lib/API/Pagination.php | 5 +- plugin/zoom/lib/API/PaginationToken.php | 5 +- plugin/zoom/lib/API/ParticipantList.php | 4 +- plugin/zoom/lib/API/PastMeeting.php | 11 +- plugin/zoom/lib/API/RecordingFile.php | 6 +- plugin/zoom/lib/API/RecordingList.php | 4 +- plugin/zoom/lib/API/RecordingMeeting.php | 10 +- plugin/zoom/lib/CourseMeeting.php | 69 - plugin/zoom/lib/CourseMeetingInfoGet.php | 57 - plugin/zoom/lib/CourseMeetingList.php | 26 - plugin/zoom/lib/CourseMeetingListItem.php | 31 - plugin/zoom/lib/CourseMeetingTrait.php | 142 -- plugin/zoom/lib/DisplayableMeetingTrait.php | 74 -- plugin/zoom/lib/File.php | 28 - plugin/zoom/lib/MeetingEntityRepository.php | 141 ++ plugin/zoom/lib/Recording.php | 65 - plugin/zoom/lib/RecordingEntityRepository.php | 66 + plugin/zoom/lib/RecordingList.php | 51 - .../zoom/lib/RegistrantEntityRepository.php | 33 + plugin/zoom/lib/UserMeetingRegistrant.php | 51 - plugin/zoom/lib/UserMeetingRegistrantList.php | 43 - .../lib/UserMeetingRegistrantListItem.php | 30 - .../zoom/lib/UserMeetingRegistrantTrait.php | 89 -- plugin/zoom/lib/zoom_plugin.class.php | 1177 +++++++++-------- plugin/zoom/meeting.php | 31 +- plugin/zoom/meeting_from_user.php | 13 + plugin/zoom/start.php | 22 +- plugin/zoom/user.php | 36 + plugin/zoom/view/admin.tpl | 18 +- plugin/zoom/view/meeting.tpl | 18 +- plugin/zoom/view/start.tpl | 6 +- 50 files changed, 2084 insertions(+), 1421 deletions(-) create mode 100644 plugin/zoom/Entity/MeetingEntity.php create mode 100644 plugin/zoom/Entity/RecordingEntity.php create mode 100644 plugin/zoom/Entity/RegistrantEntity.php create mode 100644 plugin/zoom/global.php create mode 100644 plugin/zoom/join_meeting.php delete mode 100644 plugin/zoom/lib/CourseMeeting.php delete mode 100644 plugin/zoom/lib/CourseMeetingInfoGet.php delete mode 100644 plugin/zoom/lib/CourseMeetingList.php delete mode 100644 plugin/zoom/lib/CourseMeetingListItem.php delete mode 100644 plugin/zoom/lib/CourseMeetingTrait.php delete mode 100644 plugin/zoom/lib/DisplayableMeetingTrait.php delete mode 100644 plugin/zoom/lib/File.php create mode 100644 plugin/zoom/lib/MeetingEntityRepository.php delete mode 100644 plugin/zoom/lib/Recording.php create mode 100644 plugin/zoom/lib/RecordingEntityRepository.php delete mode 100644 plugin/zoom/lib/RecordingList.php create mode 100644 plugin/zoom/lib/RegistrantEntityRepository.php delete mode 100644 plugin/zoom/lib/UserMeetingRegistrant.php delete mode 100644 plugin/zoom/lib/UserMeetingRegistrantList.php delete mode 100644 plugin/zoom/lib/UserMeetingRegistrantListItem.php delete mode 100644 plugin/zoom/lib/UserMeetingRegistrantTrait.php create mode 100644 plugin/zoom/meeting_from_user.php create mode 100644 plugin/zoom/user.php diff --git a/main/inc/lib/userportal.lib.php b/main/inc/lib/userportal.lib.php index 48c92c7a7a..490e7753d6 100755 --- a/main/inc/lib/userportal.lib.php +++ b/main/inc/lib/userportal.lib.php @@ -952,6 +952,12 @@ class IndexManager ]; } + if ('true' === api_get_plugin_setting('zoom', 'tool_enable')) { + foreach (ZoomPlugin::getProfileBlockItems() as $item) { + $items[] = $item; + } + } + if ( true === api_get_configuration_value('whispeak_auth_enabled') && !WhispeakAuthPlugin::checkUserIsEnrolled($userId) diff --git a/plugin/zoom/Entity/MeetingEntity.php b/plugin/zoom/Entity/MeetingEntity.php new file mode 100644 index 0000000000..591d95e4cf --- /dev/null +++ b/plugin/zoom/Entity/MeetingEntity.php @@ -0,0 +1,468 @@ +registrants = new ArrayCollection(); + $this->recordings = new ArrayCollection(); + } + + /** + * @return string + */ + public function __toString() + { + return sprintf('Meeting %d', $this->id); + } + + /** + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * @return User + */ + public function getUser() + { + return $this->user; + } + + /** + * @return Course + */ + public function getCourse() + { + return $this->course; + } + + /** + * @return Session + */ + public function getSession() + { + return $this->session; + } + + /** + * @return RegistrantEntity[]|ArrayCollection + */ + public function getRegistrants() + { + return $this->registrants; + } + + /** + * @return RecordingEntity[]|ArrayCollection + */ + public function getRecordings() + { + return $this->recordings; + } + + /** + * @ORM\PostLoad + * + * @throws Exception + */ + public function postLoad() + { + if (!is_null($this->meetingListItemJson)) { + $this->meetingListItem = MeetingListItem::fromJson($this->meetingListItemJson); + } + if (!is_null($this->meetingInfoGetJson)) { + $this->meetingInfoGet = MeetingInfoGet::fromJson($this->meetingInfoGetJson); + } + $this->initializeDisplayableProperties(); + } + + /** + * @ORM\PostUpdate + * @throws Exception + */ + public function postUpdate() + { + $this->initializeDisplayableProperties(); + } + + /** + * @ORM\PreFlush + */ + public function preFlush() + { + if (!is_null($this->meetingListItem)) { + $this->meetingListItemJson = json_encode($this->meetingListItem); + } + if (!is_null($this->meetingInfoGet)) { + $this->meetingInfoGetJson = json_encode($this->meetingInfoGet); + } + } + + /** + * @return MeetingListItem + */ + public function getMeetingListItem() + { + return $this->meetingListItem; + } + + /** + * @return MeetingInfoGet + */ + public function getMeetingInfoGet() + { + return $this->meetingInfoGet; + } + + /** + * @param User $user + * + * @return $this + */ + public function setUser($user) + { + $this->user = $user; + + return $this; + } + + /** + * @param Course $course + * + * @return $this + */ + public function setCourse($course) + { + $this->course = $course; + + return $this; + } + + /** + * @param Session $session + * + * @return $this + */ + public function setSession($session) + { + $this->session = $session; + + return $this; + } + + /** + * @param MeetingListItem $meetingListItem + * + * @throws Exception + * + * @return MeetingEntity + */ + public function setMeetingListItem($meetingListItem) + { + if (is_null($this->id)) { + $this->id = $meetingListItem->id; + } elseif ($this->id != $meetingListItem->id) { + throw new Exception('the MeetingEntity identifier differs from the MeetingListItem identifier'); + } + $this->meetingListItem = $meetingListItem; + + return $this; + } + + /** + * @param MeetingInfoGet $meetingInfoGet + * + * @throws Exception + * + * @return MeetingEntity + */ + public function setMeetingInfoGet($meetingInfoGet) + { + if (is_null($this->id)) { + $this->id = $meetingInfoGet->id; + } elseif ($this->id != $meetingInfoGet->id) { + throw new Exception('the MeetingEntity identifier differs from the MeetingInfoGet identifier'); + } + $this->meetingInfoGet = $meetingInfoGet; + $this->initializeDisplayableProperties(); + + return $this; + } + + /** + * @return bool + */ + public function isCourseMeeting() + { + return !is_null($this->course); + } + + /** + * @return bool + */ + public function isUserMeeting() + { + return !is_null($this->user) && is_null($this->course); + } + + /** + * @return bool + */ + public function isGlobalMeeting() + { + return is_null($this->user) && is_null($this->course); + } + + /** + * Builds the list of users that can register into this meeting. + * Zoom requires an email address, therefore users without an email address are excluded from the list. + * + * @return User[] the list of users + */ + public function getRegistrableUsers() + { + $users = []; + if (!$this->isCourseMeeting()) { + $users = Database::getManager()->getRepository('ChamiloUserBundle:User')->findBy(['active' => true]); + } + if (is_null($this->session)) { + if (!is_null($this->course)) { + $users = $this->course->getUsers()->matching( + Criteria::create()->where(Criteria::expr()->isNull('email')) + ); + } + } else { + if (!is_null($this->course)) { + $subscriptions = $this->session->getUserCourseSubscriptionsByStatus($this->course, Session::STUDENT); + if ($subscriptions) { + /** @var SessionRelCourseRelUser $sessionCourseUser */ + foreach ($subscriptions as $sessionCourseUser) { + $users[] = $sessionCourseUser->getUser(); + } + } + } + } + + return $users; + } + + /** + * @return bool + */ + public function requiresDateAndDuration() + { + return MeetingInfoGet::TYPE_SCHEDULED === $this->meetingInfoGet->type + || MeetingInfoGet::TYPE_RECURRING_WITH_FIXED_TIME === $this->meetingInfoGet->type; + } + + /** + * @return bool + */ + public function requiresRegistration() + { + return + MeetingSettings::APPROVAL_TYPE_NO_REGISTRATION_REQUIRED != $this->meetingInfoGet->settings->approval_type; + } + + /** + * @return bool + */ + public function hasCloudAutoRecordingEnabled() + { + return 'cloud' === $this->meetingInfoGet->settings->auto_recording; + } + + /** + * @param User $user + * + * @return bool + */ + public function hasRegisteredUser(User $user) + { + return $this->getRegistrants()->exists( + function (RegistrantEntity $registrantEntity) use (&$user) { + return $registrantEntity->getUser() === $user; + } + ); + } + + /** + * @param User $user + * + * @return RegistrantEntity|null + */ + public function getRegistrant(User $user) + { + foreach ($this->getRegistrants() as $registrant) { + if ($registrant->getUser() === $user) { + return $registrant; + } + } + return null; + } + + /** + * @throws Exception on unexpected start_time or duration + */ + private function initializeDisplayableProperties() + { + $this->typeName = [ + API\Meeting::TYPE_INSTANT => get_lang('InstantMeeting'), + API\Meeting::TYPE_SCHEDULED => get_lang('ScheduledMeeting'), + API\Meeting::TYPE_RECURRING_WITH_NO_FIXED_TIME => get_lang('RecurringWithNoFixedTime'), + API\Meeting::TYPE_RECURRING_WITH_FIXED_TIME => get_lang('RecurringWithFixedTime'), + ][$this->meetingInfoGet->type]; + if (property_exists($this, 'status')) { + $this->statusName = [ + 'waiting' => get_lang('Waiting'), + 'started' => get_lang('Started'), + 'finished' => get_lang('Finished'), + ][$this->meetingInfoGet->status]; + } + $this->startDateTime = null; + $this->formattedStartTime = ''; + $this->durationInterval = null; + $this->formattedDuration = ''; + if (!empty($this->meetingInfoGet->start_time)) { + $this->startDateTime = new DateTime($this->meetingInfoGet->start_time); + $this->startDateTime->setTimezone(new DateTimeZone(date_default_timezone_get())); + $this->formattedStartTime = $this->startDateTime->format(get_lang('Y-m-d H:i')); + } + if (!empty($this->meetingInfoGet->duration)) { + $now = new DateTime(); + $later = new DateTime(); + $later->add(new DateInterval('PT'.$this->meetingInfoGet->duration.'M')); + $this->durationInterval = $later->diff($now); + $this->formattedDuration = $this->durationInterval->format(get_lang('DurationFormat')); + } + } +} diff --git a/plugin/zoom/Entity/RecordingEntity.php b/plugin/zoom/Entity/RecordingEntity.php new file mode 100644 index 0000000000..4f688da408 --- /dev/null +++ b/plugin/zoom/Entity/RecordingEntity.php @@ -0,0 +1,184 @@ +getRecordingMeeting(); + if (property_exists($object, $name)) { + return $object->$name; + } + throw new Exception(sprintf('%s does not know property %s', $this, $name)); + } + + /** + * @return string + */ + public function __toString() + { + return sprintf('Recording %d', $this->uuid); + } + + /** + * @return MeetingEntity + */ + public function getMeeting() + { + return $this->meeting; + } + + /** + * @return RecordingMeeting + * + * @throws Exception + */ + public function getRecordingMeeting() + { + return $this->recordingMeeting; + } + + /** + * @param MeetingEntity $meeting + * + * @return $this + */ + public function setMeeting($meeting) + { + $this->meeting = $meeting; + $this->meeting->getRecordings()->add($this); + + return $this; + } + + /** + * @param RecordingMeeting $recordingMeeting + * + * @throws Exception + * + * @return RecordingEntity + */ + public function setRecordingMeeting($recordingMeeting) + { + if (is_null($this->uuid)) { + $this->uuid = $recordingMeeting->uuid; + } elseif ($this->uuid !== $recordingMeeting->uuid) { + throw new Exception('the RecordingEntity identifier differs from the RecordingMeeting identifier'); + } + if (is_null($this->meeting)) { + $this->meeting = Database::getManager()->getRepository(MeetingEntity::class)->find($recordingMeeting->id); + // $this->meeting remains null when the remote RecordingMeeting refers to a deleted meeting + } elseif ($this->meeting->getId() != $recordingMeeting->id) { + throw new Exception('The RecordingEntity meeting id differs from the RecordingMeeting meeting id'); + } + $this->recordingMeeting = $recordingMeeting; + + return $this; + } + + /** + * @ORM\PostLoad + * + * @throws Exception + */ + public function postLoad() + { + if (!is_null($this->recordingMeetingJson)) { + $this->recordingMeeting = RecordingMeeting::fromJson($this->recordingMeetingJson); + } + $this->initializeExtraProperties(); + } + + /** + * @ORM\PreFlush + */ + public function preFlush() + { + if (!is_null($this->recordingMeeting)) { + $this->recordingMeetingJson = json_encode($this->recordingMeeting); + } + } + + /** + * @throws Exception + */ + public function initializeExtraProperties() + { + $this->startDateTime = new DateTime($this->recordingMeeting->start_time); + $this->startDateTime->setTimezone(new DateTimeZone(date_default_timezone_get())); + $this->formattedStartTime = $this->startDateTime->format(get_lang('Y-m-d H:i')); + + $now = new DateTime(); + $later = new DateTime(); + $later->add(new DateInterval('PT'.$this->recordingMeeting->duration.'M')); + $this->durationInterval = $later->diff($now); + $this->formattedDuration = $this->durationInterval->format(get_lang('DurationFormat')); + } +} diff --git a/plugin/zoom/Entity/RegistrantEntity.php b/plugin/zoom/Entity/RegistrantEntity.php new file mode 100644 index 0000000000..48bcf4b66b --- /dev/null +++ b/plugin/zoom/Entity/RegistrantEntity.php @@ -0,0 +1,258 @@ +id); + } + + /** + * @return MeetingEntity + */ + public function getMeeting() + { + return $this->meeting; + } + + /** + * @return User + */ + public function getUser() + { + return $this->user; + } + + /** + * @return MeetingRegistrantListItem + * + * @throws Exception + */ + public function getMeetingRegistrantListItem() + { + return $this->meetingRegistrantListItem; + } + + /** + * @return CreatedRegistration + * + * @throws Exception + */ + public function getCreatedRegistration() + { + return $this->createdRegistration; + } + + /** + * @return MeetingRegistrant + * + * @throws Exception + */ + public function getMeetingRegistrant() + { + return $this->meetingRegistrant; + } + + /** + * @param MeetingEntity $meeting + * + * @return $this + */ + public function setMeeting($meeting) + { + $this->meeting = $meeting; + $this->meeting->getRegistrants()->add($this); + + return $this; + } + + /** + * @param User $user + * + * @return $this + */ + public function setUser($user) + { + $this->user = $user; + + return $this; + } + + /** + * @param MeetingRegistrantListItem $meetingRegistrantListItem + * + * @throws Exception + * + * @return $this + */ + public function setMeetingRegistrantListItem($meetingRegistrantListItem) + { + if (!is_null($this->meeting) && $this->meeting->getId() != $meetingRegistrantListItem->id) { + throw new Exception('RegistrantEntity meeting id differs from MeetingRegistrantListItem id'); + } + $this->meetingRegistrantListItem = $meetingRegistrantListItem; + $this->computeFullName(); + + return $this; + } + + /** + * @param CreatedRegistration $createdRegistration + * + * @throws Exception + * + * @return $this + */ + public function setCreatedRegistration($createdRegistration) + { + if (is_null($this->id)) { + $this->id = $createdRegistration->registrant_id; + } elseif ($this->id != $createdRegistration->registrant_id) { + throw new Exception('RegistrantEntity id differs from CreatedRegistration identifier'); + } + $this->createdRegistration = $createdRegistration; + + return $this; + } + + /** + * @param MeetingRegistrant $meetingRegistrant + * + * @throws Exception + * + * @return $this + */ + public function setMeetingRegistrant($meetingRegistrant) + { + $this->meetingRegistrant = $meetingRegistrant; + $this->computeFullName(); + + return $this; + } + + /** + * @ORM\PostLoad + * @throws Exception + */ + public function postLoad() + { + if (!is_null($this->meetingRegistrantJson)) { + $this->meetingRegistrant = MeetingRegistrant::fromJson($this->meetingRegistrantJson); + } + if (!is_null($this->createdRegistrationJson)) { + $this->createdRegistration = CreatedRegistration::fromJson($this->createdRegistrationJson); + } + if (!is_null($this->meetingRegistrantListItemJson)) { + $this->meetingRegistrantListItem = MeetingRegistrantListItem::fromJson( + $this->meetingRegistrantListItemJson + ); + } + $this->computeFullName(); + } + + /** + * @ORM\PreFlush + */ + public function preFlush() + { + if (!is_null($this->meetingRegistrant)) { + $this->meetingRegistrantJson = json_encode($this->meetingRegistrant); + } + if (!is_null($this->createdRegistration)) { + $this->createdRegistrationJson = json_encode($this->createdRegistration); + } + if (!is_null($this->meetingRegistrantListItem)) { + $this->meetingRegistrantListItemJson = json_encode($this->meetingRegistrantListItem); + } + } + + public function computeFullName() + { + $this->fullName = api_get_person_name( + $this->meetingRegistrant->first_name, + $this->meetingRegistrant->last_name + ); + } +} diff --git a/plugin/zoom/admin.php b/plugin/zoom/admin.php index e8affc4b04..b000f2e821 100644 --- a/plugin/zoom/admin.php +++ b/plugin/zoom/admin.php @@ -16,14 +16,17 @@ $plugin = ZoomPlugin::create(); $this_section = SECTION_PLATFORM_ADMIN; $form = $plugin->getAdminSearchForm(); -$startDate = new DateTime($form->getElement('start')->getValue()); -$endDate = new DateTime($form->getElement('end')->getValue()); -$type = $form->getElement('type')->getValue(); +$startDate = new DateTime($form->getSubmitValue('start')); +$endDate = new DateTime($form->getSubmitValue('end')); +$reloadRecordingLists = $form->getSubmitValue('reloadRecordingLists'); +if ($reloadRecordingLists) { + $plugin->reloadPeriodRecordings($startDate, $endDate); +} $tpl = new Template($tool_name); -$tpl->assign('meetings', $plugin->getPeriodMeetings($type, $startDate, $endDate)); +$tpl->assign('meetings', $plugin->getMeetingRepository()->periodMeetings($startDate, $endDate)); if ($plugin->get('enableCloudRecording')) { - $tpl->assign('recordings', $plugin->getRecordings($startDate, $endDate)); + $tpl->assign('recordings', $plugin->getRecordingRepository()->getPeriodRecordings($startDate, $endDate)); } $tpl->assign('search_form', $form->returnForm()); $tpl->assign('content', $tpl->fetch('zoom/view/admin.tpl')); diff --git a/plugin/zoom/global.php b/plugin/zoom/global.php new file mode 100644 index 0000000000..3ef67e5de5 --- /dev/null +++ b/plugin/zoom/global.php @@ -0,0 +1,30 @@ +get_title()); + +try { + printf( + '
+ %s +
', + $plugin->getGlobalMeetingURL(), + get_lang('JoinGlobalVideoConference') + ); +} catch (Exception $exception) { + Display::addFlash( + Display::return_message($exception->getMessage(), 'error') + ); +} + +Display::display_footer(); diff --git a/plugin/zoom/join_meeting.php b/plugin/zoom/join_meeting.php new file mode 100644 index 0000000000..a3b2a5511f --- /dev/null +++ b/plugin/zoom/join_meeting.php @@ -0,0 +1,42 @@ +get_title()); + +if (array_key_exists('meetingId', $_REQUEST)) { + /** @var MeetingEntity $meeting */ + $meeting = $plugin->getMeetingRepository()->find($_REQUEST['meetingId']); + try { + if (is_null($meeting)) { + throw new Exception('Meeting not found'); + } + // TODO introduce the meeting + printf( + '
+ %s +
', + $plugin->getUserMeetingURL($meeting), + get_lang('JoinMeetingAsMyself') + ); + + } catch (Exception $exception) { + Display::addFlash( + Display::return_message($exception->getMessage(), 'error') + ); + } +} + +Display::display_footer(); diff --git a/plugin/zoom/lang/english.php b/plugin/zoom/lang/english.php index 78d36753c2..c17978674d 100755 --- a/plugin/zoom/lang/english.php +++ b/plugin/zoom/lang/english.php @@ -46,6 +46,7 @@ Will not work for a basic profile."; // please keep these lines alphabetically sorted $strings['AllCourseUsersWereRegistered'] = "All course students were registered"; $strings['Agenda'] = "Agenda"; +$strings['CannotRegisterWithoutEmailAddress'] = "Cannot register without email address"; $strings['CopyingJoinURL'] = "Copying join URL"; $strings['CopyJoinAsURL'] = "Copy 'join as' URL"; $strings['CopyToCourse'] = "Copy to course"; @@ -53,6 +54,8 @@ $strings['CouldNotCopyJoinURL'] = "Could not copy join URL"; $strings['Course'] = "Cours"; $strings['CreatedAt'] = "Created at"; $strings['CreateLinkInCourse'] = "Create link(s) in course"; +$strings['CreateUserVideoConference'] = "Create user conferences"; +$strings['DateMeetingTitle'] = "%s: %s"; $strings['DeleteMeeting'] = "Delete meeting"; $strings['DeleteFile'] = "Delete file(s)"; $strings['Details'] = "Details"; @@ -65,8 +68,11 @@ $strings['Files'] = "Files"; $strings['Finished'] = "finished"; $strings['FileWasCopiedToCourse'] = "The file was copied to the course"; $strings['FileWasDeleted'] = "The file was deleted"; +$strings['GlobalMeeting'] = "Global meeting"; +$strings['GroupUsersWereRegistered'] = "Group members were registered"; $strings['InstantMeeting'] = "Instant meeting"; $strings['Join'] = "Join"; +$strings['JoinGlobalVideoConference'] = "Join global conference"; $strings['JoinMeetingAsMyself'] = "Join meeting as myself"; $strings['JoinURLCopied'] = "Join URL copied"; $strings['JoinURLToSendToParticipants'] = "Join URL to send to participants"; @@ -105,4 +111,5 @@ $strings['UserRegistration'] = "User registration"; $strings['Y-m-d H:i'] = "Y-m-d H:i"; $strings['Waiting'] = "waiting"; $strings['XRecordingOfMeetingXFromXDurationXDotX'] = "%s recording of meeting %s from %s (%s).%s"; +$strings['YouAreNotRegisteredToThisMeeting'] = "You are not registered to this meeting"; $strings['ZoomVideoConferences'] = "Zoom Video Conferences"; diff --git a/plugin/zoom/lang/french.php b/plugin/zoom/lang/french.php index 58b1076e63..17c5a5c5a3 100755 --- a/plugin/zoom/lang/french.php +++ b/plugin/zoom/lang/french.php @@ -45,6 +45,7 @@ Ne fonctionnera pas pour un profil de base."; // please keep these lines alphabetically sorted $strings['AllCourseUsersWereRegistered'] = "Tous les étudiants du cours sont inscrits"; $strings['Agenda'] = "Ordre du jour"; +$strings['CannotRegisterWithoutEmailAddress'] = "Impossible d'inscrire un utilisateur sans adresse de courriel"; $strings['CopyingJoinURL'] = "Copie de l'URL pour rejoindre en cours"; $strings['CopyJoinAsURL'] = "Copier l'URL pour 'rejoindre en tant que'"; $strings['CopyToCourse'] = "Copier dans le cours"; @@ -52,6 +53,8 @@ $strings['CouldNotCopyJoinURL'] = "Échec de la copie de l'URL pour rejoindre"; $strings['Course'] = "Cours"; $strings['CreatedAt'] = "Créé à"; $strings['CreateLinkInCourse'] = "Créer dans le cours un ou des lien(s) vers le(s) fichier(s)"; +$strings['CreateUserVideoConference'] = "Créer des conferences par utilisateur"; +$strings['DateMeetingTitle'] = "%s : %s"; $strings['DeleteMeeting'] = "Effacer la conférence"; $strings['DeleteFile'] = "Supprimer ce(s) fichier(s)"; $strings['Details'] = "Détail"; @@ -64,8 +67,11 @@ $strings['Files'] = "Fichiers"; $strings['Finished'] = "terminée"; $strings['FileWasCopiedToCourse'] = "Le fichier a été copié dans le cours"; $strings['FileWasDeleted'] = "Le fichier a été effacé"; +$strings['GlobalMeeting'] = "Conférence globale"; +$strings['GroupUsersWereRegistered'] = "Les membres des groupes ont été inscrits"; $strings['InstantMeeting'] = "Conférence instantanée"; $strings['Join'] = "Rejoindre"; +$strings['JoinGlobalVideoConference'] = "Rejoindre la conférence globale"; $strings['JoinMeetingAsMyself'] = "Rejoindre la conférence en tant que moi-même"; $strings['JoinURLCopied'] = "URL pour rejoindre copiée"; $strings['JoinURLToSendToParticipants'] = "URL pour assister à la conférence (à envoyer aux participants)"; @@ -104,4 +110,5 @@ $strings['UserRegistration'] = "Inscription des utilisateurs"; $strings['Y-m-d H:i'] = "d/m/Y à H\hi"; $strings['Waiting'] = "en attente"; $strings['XRecordingOfMeetingXFromXDurationXDotX'] = "Enregistrement (%s) de la conférence %s de %s (%s).%s"; +$strings['YouAreNotRegisteredToThisMeeting'] = "Vous n'êtes pas inscrit à cette conférence"; $strings['ZoomVideoConferences'] = "Conférences vidéo Zoom"; diff --git a/plugin/zoom/lang/spanish.php b/plugin/zoom/lang/spanish.php index a90a0ab0f0..e78b403053 100644 --- a/plugin/zoom/lang/spanish.php +++ b/plugin/zoom/lang/spanish.php @@ -44,6 +44,7 @@ No funcionará para un perfil base/gratuito."; // please keep these lines alphabetically sorted $strings['AllCourseUsersWereRegistered'] = "Todos los alumnos del curso están inscritos"; $strings['Agenda'] = "Orden del día"; +$strings['CannotRegisterWithoutEmailAddress'] = "No se puede registrar usuario sin dirección de correo electrónico"; $strings['CopyingJoinURL'] = "Copia de la URL para ingresar"; $strings['CopyJoinAsURL'] = "Copiar la URL para 'ingresar como'"; $strings['CopyToCourse'] = "Copiar en el curso"; @@ -51,6 +52,8 @@ $strings['CouldNotCopyJoinURL'] = "Falló la copia de la URL de ingreso"; $strings['Course'] = "Curso"; $strings['CreatedAt'] = "Creado el"; $strings['CreateLinkInCourse'] = "Crear en el curso uno o más vínculos hacia el/los archivo(s)"; +$strings['CreateUserVideoConference'] = "Crear conferencias de usario"; +$strings['DateMeetingTitle'] = "%s: %s"; $strings['DeleteMeeting'] = "Borrar la conferencia"; $strings['DeleteFile'] = "Borrar este/estos archivo(s)"; $strings['Details'] = "Detalle"; @@ -63,8 +66,11 @@ $strings['Files'] = "Archivos"; $strings['Finished'] = "terminada"; $strings['FileWasCopiedToCourse'] = "El archivo ha sido copiado en el curso"; $strings['FileWasDeleted'] = "El archivo ha sido borrado"; +$strings['GlobalMeeting'] = "Conferencia global"; +$strings['GroupUsersWereRegistered'] = "Miembros de los grupos han sido registrados"; $strings['InstantMeeting'] = "Conferencia instantánea"; $strings['Join'] = "Ingresar"; +$strings['JoinGlobalVideoConference'] = "Ingresar la conrencia global"; $strings['JoinMeetingAsMyself'] = "Ingresar la conferencia como yo mismo"; $strings['JoinURLCopied'] = "URL para juntarse copiada"; $strings['JoinURLToSendToParticipants'] = "URL para asistir a la conferencia (para enviar a los participantes)"; @@ -103,4 +109,5 @@ $strings['UserRegistration'] = "Inscripción de los usuarios"; $strings['Y-m-d H:i'] = "d/m/Y a las H\hi"; $strings['Waiting'] = "en espera"; $strings['XRecordingOfMeetingXFromXDurationXDotX'] = "Grabación (%s) de la conferencia %s de %s (%s).%s"; +$strings['YouAreNotRegisteredToThisMeeting'] = "No estás registrado en esta reunión"; $strings['ZoomVideoConferences'] = "Videoconferencias Zoom"; diff --git a/plugin/zoom/lib/API/Client.php b/plugin/zoom/lib/API/Client.php index 53d08b793a..8dabe01f07 100644 --- a/plugin/zoom/lib/API/Client.php +++ b/plugin/zoom/lib/API/Client.php @@ -13,8 +13,31 @@ use Exception; * * @package Chamilo\PluginBundle\Zoom\API */ -interface Client +abstract class Client { + /** @var Client */ + private static $instance; + + /** + * Returns an initialized Client. + * + * @return Client + */ + public static function getInstance() + { + return self::$instance; + } + + /** + * Registers an initialized Client. + * + * @param Client $instance + */ + protected static function register($instance) + { + self::$instance = $instance; + } + /** * Sends a Zoom API-compliant HTTP request and retrieves the response. * @@ -30,5 +53,5 @@ interface Client * * @return string response body (not json-decoded) */ - public function send($httpMethod, $relativePath, $parameters = [], $requestBody = null); + abstract public function send($httpMethod, $relativePath, $parameters = [], $requestBody = null); } diff --git a/plugin/zoom/lib/API/JWTClient.php b/plugin/zoom/lib/API/JWTClient.php index 0176b220c6..ead9545d9d 100755 --- a/plugin/zoom/lib/API/JWTClient.php +++ b/plugin/zoom/lib/API/JWTClient.php @@ -13,7 +13,7 @@ use Firebase\JWT\JWT; * * @package Chamilo\PluginBundle\Zoom */ -class JWTClient implements Client +class JWTClient extends Client { public $token; @@ -33,6 +33,7 @@ class JWTClient implements Client ], $apiSecret ); + self::register($this); } /** diff --git a/plugin/zoom/lib/API/Meeting.php b/plugin/zoom/lib/API/Meeting.php index c26c24d92d..8e8e7e9d80 100644 --- a/plugin/zoom/lib/API/Meeting.php +++ b/plugin/zoom/lib/API/Meeting.php @@ -57,15 +57,13 @@ class Meeting /** * Creates a meeting on the server and returns the resulting MeetingInfoGet. * - * @param Client $client an API client - * * @throws Exception describing the error (message and code) * * @return MeetingInfoGet meeting */ - public function create($client) + public function create() { - return MeetingInfoGet::fromJson($client->send('POST', 'users/me/meetings', [], $this)); + return MeetingInfoGet::fromJson(Client::getInstance()->send('POST', 'users/me/meetings', [], $this)); } /** @@ -74,11 +72,9 @@ class Meeting * @param string $topic * @param int $type * - * @throws Exception - * * @return static */ - protected static function fromTopicAndType($topic, $type = self::TYPE_SCHEDULED) + public static function fromTopicAndType($topic, $type = self::TYPE_SCHEDULED) { $instance = new static(); $instance->topic = $topic; diff --git a/plugin/zoom/lib/API/MeetingInfoGet.php b/plugin/zoom/lib/API/MeetingInfoGet.php index f8fea4c5af..abdfaea8fb 100644 --- a/plugin/zoom/lib/API/MeetingInfoGet.php +++ b/plugin/zoom/lib/API/MeetingInfoGet.php @@ -34,58 +34,50 @@ class MeetingInfoGet extends MeetingInfo /** * Retrieves a meeting from its numeric identifier. * - * @param Client $client * @param int $id * * @throws Exception * * @return static the meeting */ - public static function fromId($client, $id) + public static function fromId($id) { - return static::fromJson($client->send('GET', "meetings/$id")); + return static::fromJson(Client::getInstance()->send('GET', "meetings/$id")); } /** * Updates the meeting on server. * - * @param Client $client - * * @throws Exception */ - public function update($client) + public function update() { - $client->send('PATCH', 'meetings/'.$this->id, [], $this); + Client::getInstance()->send('PATCH', 'meetings/'.$this->id, [], $this); } /** * Ends the meeting on server. * - * @param Client $client - * * @throws Exception */ - public function endNow($client) + public function endNow() { - $client->send('PUT', "meetings/$this->id/status", [], (object) ['action' => 'end']); + Client::getInstance()->send('PUT', "meetings/$this->id/status", [], (object) ['action' => 'end']); } /** * Deletes the meeting on server. * - * @param Client $client - * * @throws Exception */ - public function delete($client) + public function delete() { - $client->send('DELETE', "meetings/$this->id"); + Client::getInstance()->send('DELETE', "meetings/$this->id"); } /** * Adds a registrant to the meeting. * - * @param Client $client * @param MeetingRegistrant $registrant with at least 'email' and 'first_name'. * 'last_name' will also be recorded by Zoom. * Other properties remain ignored, or not returned by Zoom @@ -96,10 +88,10 @@ class MeetingInfoGet extends MeetingInfo * * @return CreatedRegistration with unique join_url and registrant_id properties */ - public function addRegistrant($client, $registrant, $occurrenceIds = '') + public function addRegistrant($registrant, $occurrenceIds = '') { return CreatedRegistration::fromJson( - $client->send( + Client::getInstance()->send( 'POST', "meetings/$this->id/registrants", empty($occurrenceIds) ? [] : ['occurrence_ids' => $occurrenceIds], @@ -111,16 +103,15 @@ class MeetingInfoGet extends MeetingInfo /** * Removes registrants from the meeting. * - * @param Client $client * @param MeetingRegistrant[] $registrants registrants to remove (id and email) * @param string $occurrenceIds separated by comma * * @throws Exception */ - public function removeRegistrants($client, $registrants, $occurrenceIds = '') + public function removeRegistrants($registrants, $occurrenceIds = '') { if (!empty($registrants)) { - $client->send( + Client::getInstance()->send( 'PUT', "meetings/$this->id/registrants/status", empty($occurrenceIds) ? [] : ['occurrence_ids' => $occurrenceIds], @@ -135,28 +126,24 @@ class MeetingInfoGet extends MeetingInfo /** * Retrieves meeting registrants. * - * @param Client $client - * * @throws Exception * * @return MeetingRegistrantListItem[] the meeting registrants */ - public function getRegistrants($client) + public function getRegistrants() { - return MeetingRegistrantList::loadMeetingRegistrants($client, $this->id); + return MeetingRegistrantList::loadMeetingRegistrants($this->id); } /** * Retrieves the meeting's instances. * - * @param Client $client - * * @throws Exception * * @return MeetingInstance[] */ - public function getInstances($client) + public function getInstances() { - return MeetingInstances::fromMeetingId($client, $this->id)->meetings; + return MeetingInstances::fromMeetingId($this->id)->meetings; } } diff --git a/plugin/zoom/lib/API/MeetingInstance.php b/plugin/zoom/lib/API/MeetingInstance.php index 8be93df7c0..c0e98da2dc 100644 --- a/plugin/zoom/lib/API/MeetingInstance.php +++ b/plugin/zoom/lib/API/MeetingInstance.php @@ -26,28 +26,26 @@ class MeetingInstance /** * Retrieves the recording of the instance. * - * @param Client $client - * * @throws Exception with code 404 when there is no recording for this meeting * * @return RecordingMeeting the recording */ - public function getRecordings($client) + public function getRecordings() { - return RecordingMeeting::fromJson($client->send('GET', 'meetings/'.htmlentities($this->uuid).'/recordings')); + return RecordingMeeting::fromJson( + Client::getInstance()->send('GET', 'meetings/'.htmlentities($this->uuid).'/recordings') + ); } /** * Retrieves the instance's participants. * - * @param Client $client - * * @throws Exception * * @return ParticipantListItem[] */ - public function getParticipants($client) + public function getParticipants() { - return ParticipantList::loadInstanceParticipants($client, $this->uuid); + return ParticipantList::loadInstanceParticipants($this->uuid); } } diff --git a/plugin/zoom/lib/API/MeetingInstances.php b/plugin/zoom/lib/API/MeetingInstances.php index 3f1ceaafc7..133d82148b 100644 --- a/plugin/zoom/lib/API/MeetingInstances.php +++ b/plugin/zoom/lib/API/MeetingInstances.php @@ -30,16 +30,15 @@ class MeetingInstances /** * Retrieves a meeting's instances. * - * @param Client $client * @param int $meetingId * * @throws Exception * * @return MeetingInstances the meeting's instances */ - public static function fromMeetingId($client, $meetingId) + public static function fromMeetingId($meetingId) { - return static::fromJson($client->send('GET', "past_meetings/$meetingId/instances")); + return static::fromJson(Client::getInstance()->send('GET', "past_meetings/$meetingId/instances")); } /** diff --git a/plugin/zoom/lib/API/MeetingList.php b/plugin/zoom/lib/API/MeetingList.php index 50d6f4def9..449d275e52 100644 --- a/plugin/zoom/lib/API/MeetingList.php +++ b/plugin/zoom/lib/API/MeetingList.php @@ -35,16 +35,15 @@ class MeetingList /** * Retrieves all meetings of a type. * - * @param Client $client * @param int $type TYPE_SCHEDULED, TYPE_LIVE or TYPE_UPCOMING * * @throws Exception * * @return MeetingListItem[] all meetings */ - public static function loadMeetings($client, $type) + public static function loadMeetings($type) { - return static::loadItems('meetings', $client, 'users/me/meetings', ['type' => $type]); + return static::loadItems('meetings', 'users/me/meetings', ['type' => $type]); } /** diff --git a/plugin/zoom/lib/API/MeetingRegistrant.php b/plugin/zoom/lib/API/MeetingRegistrant.php index 727f26ae08..f551751a79 100644 --- a/plugin/zoom/lib/API/MeetingRegistrant.php +++ b/plugin/zoom/lib/API/MeetingRegistrant.php @@ -77,14 +77,19 @@ class MeetingRegistrant /** * @param string $email * @param string $firstName + * @param string $lastName * * @return MeetingRegistrant */ - public static function fromEmailAndFirstName($email, $firstName) + public static function fromEmailAndFirstName($email, $firstName, $lastName = null) { $instance = new static(); $instance->first_name = $firstName; $instance->email = $email; + $instance->first_name = $firstName; + if (!is_null($lastName)) { + $instance->last_name = $lastName; + } return $instance; } diff --git a/plugin/zoom/lib/API/MeetingRegistrantList.php b/plugin/zoom/lib/API/MeetingRegistrantList.php index 5bc4cf75c0..515ca9437b 100644 --- a/plugin/zoom/lib/API/MeetingRegistrantList.php +++ b/plugin/zoom/lib/API/MeetingRegistrantList.php @@ -30,16 +30,15 @@ class MeetingRegistrantList /** * Retrieves all registrant for a meeting. * - * @param Client $client * @param int $meetingId * * @throws Exception * * @return MeetingRegistrantListItem[] all registrants of the meeting */ - public static function loadMeetingRegistrants($client, $meetingId) + public static function loadMeetingRegistrants($meetingId) { - return static::loadItems('registrants', $client, "meetings/$meetingId/registrants"); + return static::loadItems('registrants', "meetings/$meetingId/registrants"); } /** diff --git a/plugin/zoom/lib/API/Pagination.php b/plugin/zoom/lib/API/Pagination.php index 14a42163b5..9c53b992a2 100644 --- a/plugin/zoom/lib/API/Pagination.php +++ b/plugin/zoom/lib/API/Pagination.php @@ -32,7 +32,6 @@ trait Pagination * Retrieves all items from the server, possibly generating several API calls. * * @param string $arrayPropertyName item array property name - * @param Client $client * @param string $relativePath relative path to pass to Client::send * @param array $parameters parameter array to pass to Client::send * @@ -40,7 +39,7 @@ trait Pagination * * @return array united list of items */ - protected static function loadItems($arrayPropertyName, $client, $relativePath, $parameters = []) + protected static function loadItems($arrayPropertyName, $relativePath, $parameters = []) { $items = []; $pageCount = 1; @@ -48,7 +47,7 @@ trait Pagination $totalRecords = 0; for ($pageNumber = 1; $pageNumber <= $pageCount; $pageNumber++) { $response = static::fromJson( - $client->send( + Client::getInstance()->send( 'GET', $relativePath, array_merge(['page_size' => $pageSize, 'page_number' => $pageNumber], $parameters) diff --git a/plugin/zoom/lib/API/PaginationToken.php b/plugin/zoom/lib/API/PaginationToken.php index 3194f45a27..4c19cb0013 100644 --- a/plugin/zoom/lib/API/PaginationToken.php +++ b/plugin/zoom/lib/API/PaginationToken.php @@ -35,7 +35,6 @@ trait PaginationToken * Retrieves all items from the server, possibly generating several API calls. * * @param string $arrayPropertyName item array property name - * @param Client $client * @param string $relativePath relative path to pass to Client::send * @param array $parameters parameter array to pass to Client::send * @@ -43,7 +42,7 @@ trait PaginationToken * * @return array united list of items */ - protected static function loadItems($arrayPropertyName, $client, $relativePath, $parameters = []) + protected static function loadItems($arrayPropertyName, $relativePath, $parameters = []) { $items = []; $pageSize = 300; @@ -51,7 +50,7 @@ trait PaginationToken $nextPageToken = ''; do { $response = static::fromJson( - $client->send( + Client::getInstance()->send( 'GET', $relativePath, array_merge(['page_size' => $pageSize, 'next_page_token' => $nextPageToken], $parameters) diff --git a/plugin/zoom/lib/API/ParticipantList.php b/plugin/zoom/lib/API/ParticipantList.php index 10b9107401..cad3d26b23 100644 --- a/plugin/zoom/lib/API/ParticipantList.php +++ b/plugin/zoom/lib/API/ParticipantList.php @@ -31,18 +31,16 @@ class ParticipantList /** * Retrieves a meeting instance's participants. * - * @param Client $client * @param string $instanceUUID * * @throws Exception * * @return ParticipantListItem[] participants */ - public static function loadInstanceParticipants($client, $instanceUUID) + public static function loadInstanceParticipants($instanceUUID) { return static::loadItems( 'participants', - $client, 'past_meetings/'.htmlentities($instanceUUID).'/participants' ); } diff --git a/plugin/zoom/lib/API/PastMeeting.php b/plugin/zoom/lib/API/PastMeeting.php index 6163169a52..0dccf7ce3c 100644 --- a/plugin/zoom/lib/API/PastMeeting.php +++ b/plugin/zoom/lib/API/PastMeeting.php @@ -50,29 +50,26 @@ class PastMeeting extends Meeting /** * Retrieves a past meeting instance from its identifier. * - * @param Client $client * @param string $uuid * * @throws Exception * * @return PastMeeting the past meeting */ - public static function fromUUID($client, $uuid) + public static function fromUUID($uuid) { - return static::fromJson($client->send('GET', 'past_meetings/'.htmlentities($uuid))); + return static::fromJson(Client::getInstance()->send('GET', 'past_meetings/'.htmlentities($uuid))); } /** * Retrieves information on participants from a past meeting instance. * - * @param Client $client - * * @throws Exception * * @return ParticipantListItem[] participants */ - public function getParticipants($client) + public function getParticipants() { - return ParticipantList::loadInstanceParticipants($client, $this->uuid); + return ParticipantList::loadInstanceParticipants($this->uuid); } } diff --git a/plugin/zoom/lib/API/RecordingFile.php b/plugin/zoom/lib/API/RecordingFile.php index f725da8518..d853d632a0 100644 --- a/plugin/zoom/lib/API/RecordingFile.php +++ b/plugin/zoom/lib/API/RecordingFile.php @@ -95,13 +95,11 @@ class RecordingFile /** * Deletes the file. * - * @param Client $client - * * @throws Exception */ - public function delete($client) + public function delete() { - $client->send( + Client::getInstance()->send( 'DELETE', "/meetings/$this->meeting_id/recordings/$this->id", ['action' => 'delete'] diff --git a/plugin/zoom/lib/API/RecordingList.php b/plugin/zoom/lib/API/RecordingList.php index ec82442151..5bccf953ed 100644 --- a/plugin/zoom/lib/API/RecordingList.php +++ b/plugin/zoom/lib/API/RecordingList.php @@ -34,7 +34,6 @@ class RecordingList /** * Retrieves all recordings from a period of time. * - * @param Client $client * @param DateTime $startDate first day of the period * @param DateTime $endDate last day of the period * @@ -42,11 +41,10 @@ class RecordingList * * @return RecordingMeeting[] all recordings from that period */ - public static function loadPeriodRecordings($client, $startDate, $endDate) + public static function loadPeriodRecordings($startDate, $endDate) { return static::loadItems( 'meetings', - $client, 'users/me/recordings', [ 'from' => $startDate->format('Y-m-d'), diff --git a/plugin/zoom/lib/API/RecordingMeeting.php b/plugin/zoom/lib/API/RecordingMeeting.php index 155d4e06e5..e2742edc60 100644 --- a/plugin/zoom/lib/API/RecordingMeeting.php +++ b/plugin/zoom/lib/API/RecordingMeeting.php @@ -69,13 +69,15 @@ class RecordingMeeting /** * Deletes the recording on the server. * - * @param Client $client - * * @throws Exception */ - public function delete($client) + public function delete() { - $client->send('DELETE', 'meetings/'.htmlentities($this->uuid).'/recordings', ['action' => 'delete']); + Client::getInstance()->send( + 'DELETE', + 'meetings/'.htmlentities($this->uuid).'/recordings', + ['action' => 'delete'] + ); } /** diff --git a/plugin/zoom/lib/CourseMeeting.php b/plugin/zoom/lib/CourseMeeting.php deleted file mode 100644 index b454e4db4e..0000000000 --- a/plugin/zoom/lib/CourseMeeting.php +++ /dev/null @@ -1,69 +0,0 @@ -setCourseAndSessionId($courseId, $sessionId); - $instance->initializeDisplayableProperties(); - - return $instance; - } - - /** - * {@inheritdoc} - * - * @throws Exception - */ - public function initializeExtraProperties() - { - parent::initializeExtraProperties(); - $this->decodeAndRemoveTag(); - $this->initializeDisplayableProperties(); - } - - /** - * {@inheritdoc} - * - * Creates a tagged meeting - * - * @return CourseMeetingInfoGet - */ - public function create($client) - { - $new = new CourseMeetingInfoGet(); - - $this->tagAgenda(); - static::recursivelyCopyObjectProperties(parent::create($client), $new); - $this->untagAgenda(); - - return $new; - } -} diff --git a/plugin/zoom/lib/CourseMeetingInfoGet.php b/plugin/zoom/lib/CourseMeetingInfoGet.php deleted file mode 100644 index 3b263d4253..0000000000 --- a/plugin/zoom/lib/CourseMeetingInfoGet.php +++ /dev/null @@ -1,57 +0,0 @@ -decodeAndRemoveTag(); - $this->initializeDisplayableProperties(); - } - - /** - * Updates the meeting on server, tagging it so to remember its course and session. - * - * @param API\Client $client - * - * @throws Exception - */ - public function update($client) - { - $this->tagAgenda(); - parent::update($client); - $this->untagAgenda(); - } - - /** - * Retrieves meeting registrants. - * - * @param API\Client $client - * - * @throws Exception - * - * @return UserMeetingRegistrantListItem[] - */ - public function getUserRegistrants($client) - { - return UserMeetingRegistrantList::loadUserMeetingRegistrants($client, $this->id); - } -} diff --git a/plugin/zoom/lib/CourseMeetingList.php b/plugin/zoom/lib/CourseMeetingList.php deleted file mode 100644 index af9a7e56ca..0000000000 --- a/plugin/zoom/lib/CourseMeetingList.php +++ /dev/null @@ -1,26 +0,0 @@ -decodeAndRemoveTag(); - $this->initializeDisplayableProperties(); - } -} diff --git a/plugin/zoom/lib/CourseMeetingTrait.php b/plugin/zoom/lib/CourseMeetingTrait.php deleted file mode 100644 index 1e189b85b4..0000000000 --- a/plugin/zoom/lib/CourseMeetingTrait.php +++ /dev/null @@ -1,142 +0,0 @@ -course = Database::getManager()->getRepository('ChamiloCoreBundle:Course')->find($this->courseId); - } - - public function loadSession() - { - $this->session = $this->sessionId - ? Database::getManager()->getRepository('ChamiloCoreBundle:Session')->find($this->sessionId) - : null; - } - - public function setCourseAndSessionId($courseId, $sessionId) - { - $this->courseId = $courseId; - $this->sessionId = $sessionId; - } - - public function tagAgenda() - { - $this->agenda = $this->getUntaggedAgenda().$this->getTag(); - } - - public function untagAgenda() - { - $this->agenda = $this->getUntaggedAgenda(); - } - - /** - * Builds the list of users that can register into this meeting. - * - * @return User[] the list of users - */ - public function getCourseAndSessionUsers() - { - if ($this->sessionId && is_null($this->session)) { - $this->loadSession(); - } - - if (is_null($this->course)) { - $this->loadCourse(); - } - - $users = []; - - if (is_null($this->session)) { - $users = Database::getManager()->getRepository( - 'ChamiloCoreBundle:Course' - )->getSubscribedUsers($this->course)->getQuery()->getResult(); - } else { - $subscriptions = $this->session->getUserCourseSubscriptionsByStatus($this->course, Session::STUDENT); - if ($subscriptions) { - /** @var SessionRelCourseRelUser $sessionCourseUser */ - foreach ($subscriptions as $sessionCourseUser) { - $users[$sessionCourseUser->getUser()->getUserId()] = $sessionCourseUser->getUser(); - } - } - } - - return $users; - } - - /** - * @param int $courseId - * @param int $sessionId - * - * @return bool whether both values match this CourseMeeting - */ - public function matches($courseId, $sessionId) - { - return $courseId == $this->courseId && $sessionId == $this->sessionId; - } - - protected function decodeAndRemoveTag() - { - $this->isTaggedWithCourseId = preg_match(self::getTagPattern(), $this->agenda, $matches); - if ($this->isTaggedWithCourseId) { - $this->setCourseAndSessionId($matches['courseId'], $matches['sessionId']); - $this->untagAgenda(); - } else { - $this->setCourseAndSessionId(0, 0); - } - $this->course = null; - $this->session = null; - } - - protected function getUntaggedAgenda() - { - return str_replace($this->getTag(), '', $this->agenda); - } - - /** - * @return string a tag to append to a meeting agenda so to link it to a (course, session) tuple - */ - private function getTag() - { - return "\n(course $this->courseId, session $this->sessionId)"; - } - - private static function getTagPattern() - { - return '/course (?P\d+), session (?P\d+)/m'; - } -} diff --git a/plugin/zoom/lib/DisplayableMeetingTrait.php b/plugin/zoom/lib/DisplayableMeetingTrait.php deleted file mode 100644 index 5761ed4e1b..0000000000 --- a/plugin/zoom/lib/DisplayableMeetingTrait.php +++ /dev/null @@ -1,74 +0,0 @@ -typeName = [ - API\Meeting::TYPE_INSTANT => get_lang('InstantMeeting'), - API\Meeting::TYPE_SCHEDULED => get_lang('ScheduledMeeting'), - API\Meeting::TYPE_RECURRING_WITH_NO_FIXED_TIME => get_lang('RecurringWithNoFixedTime'), - API\Meeting::TYPE_RECURRING_WITH_FIXED_TIME => get_lang('RecurringWithFixedTime'), - ][$this->type]; - if (property_exists($this, 'status')) { - $this->statusName = [ - 'waiting' => get_lang('Waiting'), - 'started' => get_lang('Started'), - 'finished' => get_lang('Finished'), - ][$this->status]; - } - $this->startDateTime = null; - $this->formattedStartTime = ''; - $this->durationInterval = null; - $this->formattedDuration = ''; - if (!empty($this->start_time)) { - $this->startDateTime = new DateTime($this->start_time); - $this->startDateTime->setTimezone(new DateTimeZone(date_default_timezone_get())); - $this->formattedStartTime = $this->startDateTime->format(get_lang('Y-m-d H:i')); - } - if (!empty($this->duration)) { - $now = new DateTime(); - $later = new DateTime(); - $later->add(new DateInterval('PT'.$this->duration.'M')); - $this->durationInterval = $later->diff($now); - $this->formattedDuration = $this->durationInterval->format(get_lang('DurationFormat')); - } - } -} diff --git a/plugin/zoom/lib/File.php b/plugin/zoom/lib/File.php deleted file mode 100644 index f3b9d64813..0000000000 --- a/plugin/zoom/lib/File.php +++ /dev/null @@ -1,28 +0,0 @@ -formattedFileSize = format_file_size($this->file_size); - } -} diff --git a/plugin/zoom/lib/MeetingEntityRepository.php b/plugin/zoom/lib/MeetingEntityRepository.php new file mode 100644 index 0000000000..791f7ffa9b --- /dev/null +++ b/plugin/zoom/lib/MeetingEntityRepository.php @@ -0,0 +1,141 @@ +findAll() as $candidate) { + if ($candidate->startDateTime >= $startDate + && $candidate->startDateTime <= $endDate + ) { + $matching[] = $candidate; + } + } + return $matching; + } + + /** + * @return ArrayCollection|Collection|MeetingEntity[] + */ + public function globalMeetings() + { + return $this->matching( + Criteria::create()->where( + Criteria::expr()->andX( + Criteria::expr()->eq('course', null), + Criteria::expr()->eq('user', null) + ) + ) + ); + } + + /** + * @return ArrayCollection|Collection|MeetingEntity[] + */ + public function unfinishedGlobalMeetings() + { + return $this->globalMeetings()->filter( + function ($meeting) { + return 'finished' !== $meeting->getMeetingInfoGet()->status; + } + ); + } + + /** + * Returns either a user's meetings or all user meetings. + * + * @param User|null $user + * @return ArrayCollection|Collection|MeetingEntity[] + */ + public function userMeetings($user = null) + { + return $this->matching( + Criteria::create()->where( + Criteria::expr()->andX( + Criteria::expr()->eq('course', null), + is_null($user) + ? Criteria::expr()->neq('user', null) + : Criteria::expr()->eq('user', $user) + ) + ) + ); + } + + /** + * @param User|null $user + * @return ArrayCollection|Collection|MeetingEntity[] + */ + public function unfinishedUserMeetings($user = null) + { + return $this->userMeetings($user)->filter( + function ($meeting) { + return 'finished' !== $meeting->getMeetingInfoGet()->status; + } + ); + } + + /** + * @param DateTime $start + * @param DateTime $end + * @param User|null $user + * @return ArrayCollection|Collection|MeetingEntity[] + */ + public function periodUserMeetings($start, $end, $user = null) + { + return $this->userMeetings($user)->filter( + function ($meeting) use ($start, $end) { + return $meeting->startDateTime >= $start + && $meeting->startDateTime <= $end; + } + ); + } + + + + /** + * Returns either a course's meetings or all course meetings. + * + * @param Course|null $course + * @param Session|null $session + * + * @return ArrayCollection|Collection|MeetingEntity[] + */ + public function courseMeetings($course = null, $session = null) + { + return $this->matching( + Criteria::create()->where( + is_null($course) + ? Criteria::expr()->neq('course', null) + : Criteria::expr()->andX( + Criteria::expr()->eq('course', $course), + Criteria::expr()->eq('session', $session) + ) + ) + ); + } +} diff --git a/plugin/zoom/lib/Recording.php b/plugin/zoom/lib/Recording.php deleted file mode 100644 index 826953db3f..0000000000 --- a/plugin/zoom/lib/Recording.php +++ /dev/null @@ -1,65 +0,0 @@ -startDateTime = new DateTime($this->start_time); - $this->startDateTime->setTimezone(new DateTimeZone(date_default_timezone_get())); - $this->formattedStartTime = $this->startDateTime->format(get_lang('Y-m-d H:i')); - - $now = new DateTime(); - $later = new DateTime(); - $later->add(new DateInterval('PT'.$this->duration.'M')); - $this->durationInterval = $later->diff($now); - $this->formattedDuration = $this->durationInterval->format(get_lang('DurationFormat')); - } - - /** - * {@inheritdoc} - */ - public function itemClass($propertyName) - { - if ('recording_files' === $propertyName) { - return File::class; - } - - return parent::itemClass($propertyName); - } -} diff --git a/plugin/zoom/lib/RecordingEntityRepository.php b/plugin/zoom/lib/RecordingEntityRepository.php new file mode 100644 index 0000000000..b6ec27834b --- /dev/null +++ b/plugin/zoom/lib/RecordingEntityRepository.php @@ -0,0 +1,66 @@ +findAll() as $candidate) { + if ($candidate->startDateTime >= $startDate + && $candidate->startDateTime <= $endDate + ) { + $matching[] = $candidate; + } + } + return $matching; + } + + /** + * Returns a user's meeting recordings. + * + * @param User $user + * @return ArrayCollection|Collection|RecordingEntity[] + */ + public function userRecordings($user) + { + return $this->matching( + Criteria::create()->where( + Criteria::expr()->in( + 'meeting', + $this->getEntityManager()->getRepository(MeetingEntity::class)->userMeetings($user)->toArray() + ) + ) + ); + } + + /** + * @param DateTime $start + * @param DateTime $end + * @param User $user + * + * @return ArrayCollection|RecordingEntity[] + */ + public function getPeriodUserRecordings($start, $end, $user) + { + return $this->userRecordings($user)->filter( + function ($meeting) use ($start, $end) { + return $meeting->startDateTime >= $start + && $meeting->startDateTime <= $end; + } + ); + } +} diff --git a/plugin/zoom/lib/RecordingList.php b/plugin/zoom/lib/RecordingList.php deleted file mode 100644 index 63b10aaac7..0000000000 --- a/plugin/zoom/lib/RecordingList.php +++ /dev/null @@ -1,51 +0,0 @@ - $startDate->format('Y-m-d'), - 'to' => $endDate->format('Y-m-d'), - ] - ); - } -} diff --git a/plugin/zoom/lib/RegistrantEntityRepository.php b/plugin/zoom/lib/RegistrantEntityRepository.php new file mode 100644 index 0000000000..3e868574d4 --- /dev/null +++ b/plugin/zoom/lib/RegistrantEntityRepository.php @@ -0,0 +1,33 @@ +add(new DateInterval('P7D')); + $meetings = $this->getEntityManager()->getRepository(MeetingEntity::class)->periodMeetings($start, $end); + + return $this->findBy(['meeting' => $meetings, 'user' => $user]); + } +} diff --git a/plugin/zoom/lib/UserMeetingRegistrant.php b/plugin/zoom/lib/UserMeetingRegistrant.php deleted file mode 100644 index 5604623a1e..0000000000 --- a/plugin/zoom/lib/UserMeetingRegistrant.php +++ /dev/null @@ -1,51 +0,0 @@ -decodeAndRemoveTag(); - $this->computeFullName(); - } - - /** - * Creates a UserMeetingRegistrant instance from a user. - * - * @param User $user - * - * @throws Exception - * - * @return static - */ - public static function fromUser($user) - { - $instance = new static(); - $instance->email = $user->getEmail(); - $instance->first_name = $user->getFirstname(); - $instance->last_name = $user->getLastname(); - $instance->userId = $user->getId(); - $instance->user = $user; - $instance->computeFullName(); - - return $instance; - } -} diff --git a/plugin/zoom/lib/UserMeetingRegistrantList.php b/plugin/zoom/lib/UserMeetingRegistrantList.php deleted file mode 100644 index 4f164544c9..0000000000 --- a/plugin/zoom/lib/UserMeetingRegistrantList.php +++ /dev/null @@ -1,43 +0,0 @@ -decodeAndRemoveTag(); - $this->computeFullName(); - } -} diff --git a/plugin/zoom/lib/UserMeetingRegistrantTrait.php b/plugin/zoom/lib/UserMeetingRegistrantTrait.php deleted file mode 100644 index 09dcdc3d5a..0000000000 --- a/plugin/zoom/lib/UserMeetingRegistrantTrait.php +++ /dev/null @@ -1,89 +0,0 @@ -user = Database::getManager()->getRepository('ChamiloUserBundle:User')->find($this->userId); - } - - public function setUserId($userId) - { - $this->userId = $userId; - } - - public function tagEmail() - { - $this->email = str_replace('@', $this->getTag(), $this->getUntaggedEmail()); - } - - public function untagEmail() - { - $this->email = $this->getUntaggedEmail(); - } - - public function matches($userId) - { - return $userId == $this->userId; - } - - public function computeFullName() - { - $this->fullName = api_get_person_name($this->first_name, $this->last_name); - } - - protected function decodeAndRemoveTag() - { - $this->isTaggedWithUserId = preg_match(self::getTagPattern(), $this->email, $matches); - if ($this->isTaggedWithUserId) { - $this->setUserId($matches['userId']); - $this->untagEmail(); - } else { - $this->setUserId(0); - } - $this->user = null; - } - - protected function getUntaggedEmail() - { - return str_replace($this->getTag(), '@', $this->email); - } - - /** - * @return string a tag to append to a registrant comments so to link it to a user - */ - private function getTag() - { - return "+user_$this->userId@"; - } - - private static function getTagPattern() - { - return '/\+user_(?P\d+)@/m'; - } -} diff --git a/plugin/zoom/lib/zoom_plugin.class.php b/plugin/zoom/lib/zoom_plugin.class.php index dd2db00fbd..a9148dba1a 100755 --- a/plugin/zoom/lib/zoom_plugin.class.php +++ b/plugin/zoom/lib/zoom_plugin.class.php @@ -1,22 +1,25 @@ isAdminPlugin = true; + + $this->jwtClient = new JWTClient($this->get('apiKey'), $this->get('apiSecret')); } /** @@ -67,69 +82,174 @@ class ZoomPlugin extends Plugin } /** - * Creates this plugin's related data and data structure in the internal database. + * @return bool + */ + public static function currentUserCanJoinGlobalMeeting() + { + return 'true' === api_get_plugin_setting('zoom', 'enableGlobalConference'); + } + + /** + * @return bool + */ + public static function currentUserCanCreateUserMeeting() + { + $user = api_get_user_entity(api_get_user_id()); + return (!is_null($user) + && 'true' === api_get_plugin_setting('zoom', 'enableGlobalConferencePerUser') + && in_array( + (api_is_platform_admin() ? PLATFORM_ADMIN : $user->getStatus()), + (array)api_get_plugin_setting('zoom', 'globalConferenceAllowRoles') + ) + ); + } + + /** + * @return array [ $title => $link ] + */ + public static function meetingsToWhichCurrentUserIsRegisteredComingSoon() + { + $items = []; + $linkTemplate = api_get_path(WEB_PLUGIN_PATH).'zoom/join_meeting.php?meetingId=%s'; + foreach (self::getRegistrantRepository()->meetingsComingSoonRegistrationsForUser( + api_get_user_entity(api_get_user_id()) + ) as $registrant) { + $meeting = $registrant->getMeeting(); + $items[ + sprintf( + get_lang('DateMeetingTitle'), + $meeting->formattedStartTime, + $meeting->getMeetingInfoGet()->topic + ) + ] = sprintf($linkTemplate, $meeting->getId()); + } + + return $items; + } + + /** + * @return array + */ + public static function getProfileBlockItems() + { + $elements = self::meetingsToWhichCurrentUserIsRegisteredComingSoon(); + if (ZoomPlugin::currentUserCanJoinGlobalMeeting()) { + $elements[get_lang('JoinGlobalVideoConference')] = api_get_path(WEB_PLUGIN_PATH) . 'zoom/global.php'; + } + if (ZoomPlugin::currentUserCanCreateUserMeeting()) { + $elements[get_lang('CreateUserVideoConference')] = api_get_path(WEB_PLUGIN_PATH).'zoom/user.php'; + } + $items = []; + foreach ($elements as $title => $link) { + $items[] = [ + 'class' => 'video-conference', + 'icon' => Display::return_icon( + 'zoom.png', + get_lang('VideoConference') + ), + 'link' => $link, + 'title' => $title, + ]; + } + + return $items; + } + + /** + * @return MeetingEntityRepository|EntityRepository + */ + public static function getMeetingRepository() + { + return Database::getManager()->getRepository(MeetingEntity::class); + } + + /** + * @return RecordingEntityRepository|EntityRepository + */ + public static function getRecordingRepository() + { + return Database::getManager()->getRepository(RecordingEntity::class); + } + + /** + * @return RegistrantEntityRepository|EntityRepository + */ + public static function getRegistrantRepository() + { + return Database::getManager()->getRepository(RegistrantEntity::class); + } + + + /** + * Creates this plugin's related tables in the internal database. + * Installs course fields in all courses. + * + * @throws ToolsException */ public function install() { + (new SchemaTool(Database::getManager()))->createSchema([ + Database::getManager()->getClassMetadata(MeetingEntity::class), + Database::getManager()->getClassMetadata(RecordingEntity::class), + Database::getManager()->getClassMetadata(RegistrantEntity::class), + ]); $this->install_course_fields_in_all_courses(); } /** - * Drops this plugins' related data from the internal database. + * 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(MeetingEntity::class), + Database::getManager()->getClassMetadata(RecordingEntity::class), + Database::getManager()->getClassMetadata(RegistrantEntity::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 a Radio 'type'. + * The form has DatePickers 'start' and 'end' and Checkbox 'reloadRecordingLists'. * - * @return FormValidator + * @return FormValidator the form */ public function getAdminSearchForm() { $form = new FormValidator('search'); - $startDatePicker = $form->addDatePicker('start', get_lang('StartDate')); - $endDatePicker = $form->addDatePicker('end', get_lang('EndDate')); - $typeSelect = $form->addRadio( - 'type', - get_lang('Type'), - [ - CourseMeetingList::TYPE_SCHEDULED => get_lang('ScheduledMeetings'), - CourseMeetingList::TYPE_LIVE => get_lang('LiveMeetings'), - CourseMeetingList::TYPE_UPCOMING => get_lang('UpcomingMeetings'), - ] - ); + $form->addDatePicker('start', get_lang('StartDate')); + $form->addDatePicker('end', get_lang('EndDate')); + // TODO instead of requiring user intervention, implement Zoom API callbacks: + // TODO https://marketplace.zoom.us/docs/guides/build/webhook-only-app + $form->addCheckBox('reloadRecordingLists', '', get_lang('ReloadRecordingLists')); $form->addButtonSearch(get_lang('Search')); $oneMonth = new DateInterval('P1M'); if ($form->validate()) { try { - $start = new DateTime($startDatePicker->getValue()); + $start = new DateTime($form->getSubmitValue('start')); } catch (Exception $exception) { $start = new DateTime(); $start->sub($oneMonth); } try { - $end = new DateTime($endDatePicker->getValue()); + $end = new DateTime($form->getSubmitValue('end')); } catch (Exception $exception) { $end = new DateTime(); $end->add($oneMonth); } - $type = $typeSelect->getValue(); } else { $start = new DateTime(); $start->sub($oneMonth); $end = new DateTime(); $end->add($oneMonth); - $type = CourseMeetingList::TYPE_SCHEDULED; } try { $form->setDefaults([ 'start' => $start->format('Y-m-d'), 'end' => $end->format('Y-m-d'), - 'type' => $type, + 'reloadRecordingLists' => false, ]); } catch (Exception $exception) { error_log(join(':', [__FILE__, __LINE__, $exception])); @@ -141,35 +261,41 @@ class ZoomPlugin extends Plugin /** * Generates a meeting edit form and updates the meeting on validation. * - * @param CourseMeetingInfoGet $meeting the meeting + * @param MeetingEntity $meetingEntity the meeting * * @return FormValidator + * @throws Exception + * */ - public function getEditMeetingForm(&$meeting) + public function getEditMeetingForm($meetingEntity) { + $meetingInfoGet = $meetingEntity->getMeetingInfoGet(); $form = new FormValidator('edit', 'post', $_SERVER['REQUEST_URI']); - $withTimeAndDuration = $meeting::TYPE_SCHEDULED === $meeting->type - || $meeting::TYPE_RECURRING_WITH_FIXED_TIME === $meeting->type; - if ($withTimeAndDuration) { - $startTimeDatePicker = $form->addDateTimePicker('start_time', get_lang('StartTime')); + if ($meetingEntity->requiresDateAndDuration()) { + $startTimeDatePicker = $form->addDateTimePicker('startTime', get_lang('StartTime')); $form->setRequired($startTimeDatePicker); $durationNumeric = $form->addNumeric('duration', get_lang('DurationInMinutes')); $form->setRequired($durationNumeric); } - $topicText = $form->addText('topic', get_lang('Topic')); - $agendaTextArea = $form->addTextarea('agenda', get_lang('Agenda'), ['maxlength' => 2000]); - // $passwordText = $form->addText('password', get_lang('Password'), false, ['maxlength' => '10']); + $form->addText('topic', get_lang('Topic')); + $form->addTextarea('agenda', get_lang('Agenda'), ['maxlength' => 2000]); + // $form->addText('password', get_lang('Password'), false, ['maxlength' => '10']); $form->addButtonUpdate(get_lang('UpdateMeeting')); if ($form->validate()) { - if ($withTimeAndDuration) { - $meeting->start_time = $startTimeDatePicker->getValue(); - $meeting->timezone = date_default_timezone_get(); - $meeting->duration = $durationNumeric->getValue(); + if ($meetingEntity->requiresDateAndDuration()) { + $meetingInfoGet->start_time = (new DateTime($form->getSubmitValue('startTime')))->format( + DateTimeInterface::ISO8601 + ); + $meetingInfoGet->timezone = date_default_timezone_get(); + $meetingInfoGet->duration = (int)$form->getSubmitValue('duration'); } - $meeting->topic = $topicText->getValue(); - $meeting->agenda = $agendaTextArea->getValue(); + $meetingInfoGet->topic = $form->getSubmitValue('topic'); + $meetingInfoGet->agenda = $form->getSubmitValue('agenda'); try { - $this->updateMeeting($meeting); + $meetingInfoGet->update(); + $meetingEntity->setMeetingInfoGet($meetingInfoGet); + Database::getManager()->persist($meetingEntity); + Database::getManager()->flush(); Display::addFlash( Display::return_message(get_lang('MeetingUpdated'), 'confirm') ); @@ -178,15 +304,14 @@ class ZoomPlugin extends Plugin Display::return_message($exception->getMessage(), 'error') ); } - $meeting = $this->getMeeting($meeting->id); } $defaults = [ - 'topic' => $meeting->topic, - 'agenda' => $meeting->agenda, + 'topic' => $meetingInfoGet->topic, + 'agenda' => $meetingInfoGet->agenda, ]; - if ($withTimeAndDuration) { - $defaults['start_time'] = $meeting->startDateTime->format('c'); - $defaults['duration'] = $meeting->duration; + if ($meetingEntity->requiresDateAndDuration()) { + $defaults['startTime'] = $meetingEntity->startDateTime->format('Y-m-d H:i'); + $defaults['duration'] = $meetingInfoGet->duration; } $form->setDefaults($defaults); @@ -196,18 +321,21 @@ class ZoomPlugin extends Plugin /** * Generates a meeting delete form and deletes the meeting on validation. * - * @param CourseMeetingInfoGet $meeting - * @param string $returnURL where to redirect to on successful deletion + * @param MeetingEntity $meetingEntity + * @param string $returnURL where to redirect to on successful deletion * * @return FormValidator + * @throws Exception + * */ - public function getDeleteMeetingForm($meeting, $returnURL) + public function getDeleteMeetingForm($meetingEntity, $returnURL) { $form = new FormValidator('delete', 'post', $_SERVER['REQUEST_URI']); $form->addButtonDelete(get_lang('DeleteMeeting')); if ($form->validate()) { try { - $this->deleteMeeting($meeting); + $meetingEntity->getMeetingInfoGet()->delete(); + Database::getManager()->remove($meetingEntity); Display::addFlash( Display::return_message(get_lang('MeetingDeleted'), 'confirm') ); @@ -226,35 +354,26 @@ class ZoomPlugin extends Plugin * Generates a registrant list update form listing course and session users. * Updates the list on validation. * - * @param CourseMeetingInfoGet $meeting + * @param MeetingEntity $meetingEntity + * + * @return FormValidator + * @throws Exception * - * @return array a list of two elements: - * FormValidator the form - * UserMeetingRegistrantListItem[] the up-to-date list of registrants */ - public function getRegisterParticipantForm($meeting) + public function getRegisterParticipantForm($meetingEntity) { $form = new FormValidator('register', 'post', $_SERVER['REQUEST_URI']); $userIdSelect = $form->addSelect('userIds', get_lang('RegisteredUsers')); $userIdSelect->setMultiple(true); $form->addButtonSend(get_lang('UpdateRegisteredUserList')); - $users = $meeting->getCourseAndSessionUsers(); + $users = $meetingEntity->getRegistrableUsers(); foreach ($users as $user) { $userIdSelect->addOption(api_get_person_name($user->getFirstname(), $user->getLastname()), $user->getId()); } - try { - $registrants = $this->getRegistrants($meeting); - } catch (Exception $exception) { - Display::addFlash( - Display::return_message($exception->getMessage(), 'error') - ); - $registrants = []; - } - if ($form->validate()) { - $selectedUserIds = $userIdSelect->getValue(); + $selectedUserIds = $form->getSubmitValue('userIds'); $selectedUsers = []; foreach ($users as $user) { if (in_array($user->getId(), $selectedUserIds)) { @@ -262,7 +381,7 @@ class ZoomPlugin extends Plugin } } try { - $this->updateRegistrantList($meeting, $selectedUsers); + $this->updateRegistrantList($meetingEntity, $selectedUsers); Display::addFlash( Display::return_message(get_lang('RegisteredUserListWasUpdated'), 'confirm') ); @@ -271,58 +390,43 @@ class ZoomPlugin extends Plugin Display::return_message($exception->getMessage(), 'error') ); } - try { - $registrants = $this->getRegistrants($meeting); - } catch (Exception $exception) { - Display::addFlash( - Display::return_message($exception->getMessage(), 'error') - ); - $registrants = []; - } } $registeredUserIds = []; - foreach ($registrants as $registrant) { - $registeredUserIds[] = $registrant->userId; + foreach ($meetingEntity->getRegistrants() as $registrant) { + $registeredUserIds[] = $registrant->getUser()->getId(); } $userIdSelect->setSelected($registeredUserIds); - return [$form, $registrants]; + return $form; } /** * Generates a meeting recording files management form. * Takes action on validation. * - * @param CourseMeetingInfoGet $meeting + * @param MeetingEntity $meeting + * + * @throws Exception * - * @return array a list of two elements: - * FormValidator the form - * Recording[] the up-to-date list of recordings + * @return FormValidator */ public function getFileForm($meeting) { $form = new FormValidator('fileForm', 'post', $_SERVER['REQUEST_URI']); - try { - $recordings = $this->getMeetingRecordings($meeting); - } catch (Exception $exception) { - Display::addFlash( - Display::return_message($exception->getMessage(), 'error') - ); - $recordings = []; - } - if (!empty($recordings)) { + if (!$meeting->getRecordings()->isEmpty()) { $fileIdSelect = $form->addSelect('fileIds', get_lang('Files')); $fileIdSelect->setMultiple(true); - foreach ($recordings as &$recording) { + foreach ($meeting->getRecordings() as &$recording) { // $recording->instanceDetails = $plugin->getPastMeetingInstanceDetails($instance->uuid); $options = []; - foreach ($recording->recording_files as $file) { + foreach ($recording->getRecordingMeeting()->recording_files as $file) { $options[] = [ 'text' => sprintf( '%s.%s (%s)', $file->recording_type, $file->file_type, - $file->formattedFileSize + //$file->formattedFileSize + $file->file_size ), 'value' => $file->id, ]; @@ -332,29 +436,32 @@ class ZoomPlugin extends Plugin sprintf("%s (%s)", $recording->formattedStartTime, $recording->formattedDuration) ); } - $actionRadio = $form->addRadio( + $actions = []; + if ($meeting->isCourseMeeting()) { + $actions['CreateLinkInCourse'] = get_lang('CreateLinkInCourse'); + $actions['CopyToCourse'] = get_lang('CopyToCourse'); + } + $actions['DeleteFile'] = get_lang('DeleteFile'); + $form->addRadio( 'action', get_lang('Action'), - [ - 'CreateLinkInCourse' => get_lang('CreateLinkInCourse'), - 'CopyToCourse' => get_lang('CopyToCourse'), - 'DeleteFile' => get_lang('DeleteFile'), - ] + $actions ); $form->addButtonUpdate(get_lang('DoIt')); if ($form->validate()) { - foreach ($recordings as $recording) { - foreach ($recording->recording_files as $file) { - if (in_array($file->id, $fileIdSelect->getValue())) { + foreach ($meeting->getRecordings() as $recording) { + foreach ($recording->files as $file) { + if (in_array($file->id, $form->getSubmitValue('fileIds'))) { $name = sprintf( get_lang('XRecordingOfMeetingXFromXDurationXDotX'), $file->recording_type, - $meeting->id, + $meeting->getId(), $recording->formattedStartTime, $recording->formattedDuration, $file->file_type ); - if ('CreateLinkInCourse' === $actionRadio->getValue()) { + $action = $form->getSubmitValue('action'); + if ('CreateLinkInCourse' === $action && $meeting->isCourseMeeting()) { try { $this->createLinkToFileInCourse($meeting, $file, $name); Display::addFlash( @@ -365,7 +472,7 @@ class ZoomPlugin extends Plugin Display::return_message($exception->getMessage(), 'error') ); } - } elseif ('CopyToCourse' === $actionRadio->getValue()) { + } elseif ('CopyToCourse' === $action && $meeting->isCourseMeeting()) { try { $this->copyFileToCourse($meeting, $file, $name); Display::addFlash( @@ -376,9 +483,9 @@ class ZoomPlugin extends Plugin Display::return_message($exception->getMessage(), 'error') ); } - } elseif ('DeleteFile' === $actionRadio->getValue()) { + } elseif ('DeleteFile' === $action) { try { - $this->deleteFile($file); + $file->delete(); Display::addFlash( Display::return_message(get_lang('FileWasDeleted'), 'confirm') ); @@ -391,35 +498,29 @@ class ZoomPlugin extends Plugin } } } - try { - $recordings = $this->getMeetingRecordings($meeting); - } catch (Exception $exception) { - Display::addFlash( - Display::return_message($exception->getMessage(), 'error') - ); - $recordings = []; - } } } - return [$form, $recordings]; + return $form; } /** * Generates a form to fast and easily create and start an instant meeting. * On validation, create it then redirect to it and exit. * + * @param User $user + * @param Course $course + * @param Session $session + * * @return FormValidator */ - public function getCreateInstantMeetingForm() + public function getCreateInstantMeetingForm($user, $course, $session) { $form = new FormValidator('createInstantMeetingForm', 'post', '', '_blank'); $form->addButton('startButton', get_lang('StartInstantMeeting')); - if ($form->validate()) { try { - $newInstantMeeting = $this->createInstantMeeting(); - location($newInstantMeeting->start_url); + $this->startInstantMeeting(get_lang('InstantMeeting'), $user, $course, $session); } catch (Exception $exception) { Display::addFlash( Display::return_message($exception->getMessage(), 'error') @@ -432,84 +533,100 @@ class ZoomPlugin extends Plugin /** * Generates a form to schedule a meeting. - * On validation, creates it. + * On validation, creates it and redirects to its page. + * + * @param User|null $user + * @param Course|null $course + * @param Session|null $session * * @throws Exception * * @return FormValidator */ - public function getScheduleMeetingForm() + public function getScheduleMeetingForm($user, $course = null, $session = null) { $form = new FormValidator('scheduleMeetingForm'); - $startTimeDatePicker = $form->addDateTimePicker('start_time', get_lang('StartTime')); + $startTimeDatePicker = $form->addDateTimePicker('startTime', get_lang('StartTime')); $form->setRequired($startTimeDatePicker); $durationNumeric = $form->addNumeric('duration', get_lang('DurationInMinutes')); $form->setRequired($durationNumeric); - $topicText = $form->addText('topic', get_lang('Topic'), true); - $agendaTextArea = $form->addTextarea('agenda', get_lang('Agenda'), ['maxlength' => 2000]); + $form->addText('topic', get_lang('Topic'), true); + $form->addTextarea('agenda', get_lang('Agenda'), ['maxlength' => 2000]); // $passwordText = $form->addText('password', get_lang('Password'), false, ['maxlength' => '10']); - $registrationOptions = [ - 'RegisterAllCourseUsers' => get_lang('RegisterAllCourseUsers'), - ]; - $groups = GroupManager::get_groups(); - if (!empty($groups)) { - $registrationOptions['RegisterTheseGroupMembers'] = get_lang('RegisterTheseGroupMembers'); - } - $registrationOptions['RegisterNoUser'] = get_lang('RegisterNoUser'); - $userRegistrationRadio = $form->addRadio( - 'userRegistration', - get_lang('UserRegistration'), - $registrationOptions - ); - $groupOptions = []; - foreach ($groups as $group) { - $groupOptions[$group['id']] = $group['name']; - } - $groupIdsSelect = $form->addSelect( - 'groupIds', - 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') + if (!is_null($course)) { + $registrationOptions = [ + 'RegisterAllCourseUsers' => get_lang('RegisterAllCourseUsers'), + ]; + $groups = GroupManager::get_groups(); + if (!empty($groups)) { + $registrationOptions['RegisterTheseGroupMembers'] = get_lang('RegisterTheseGroupMembers'); + } + $registrationOptions['RegisterNoUser'] = get_lang('RegisterNoUser'); + $userRegistrationRadio = $form->addRadio( + 'userRegistration', + get_lang('UserRegistration'), + $registrationOptions ); + $groupOptions = []; + foreach ($groups as $group) { + $groupOptions[$group['id']] = $group['name']; + } + $groupIdsSelect = $form->addSelect( + 'groupIds', + 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->setAttribute('onchange', $jsCode); + } } $form->addButtonCreate(get_lang('ScheduleTheMeeting')); - // meeting scheduling if ($form->validate()) { try { - $newMeeting = $this->createScheduledMeeting( - new DateTime($startTimeDatePicker->getValue()), - $durationNumeric->getValue(), - $topicText->getValue(), - $agendaTextArea->getValue(), - '' // $passwordText->getValue() + $newMeeting = $this->scheduleMeeting( + $user, + $course, + $session, + new DateTime($form->getSubmitValue('startTime')), + $form->getSubmitValue('duration'), + $form->getSubmitValue('topic'), + $form->getSubmitValue('agenda'), + '' ); Display::addFlash( Display::return_message(get_lang('NewMeetingCreated')) ); - if ('RegisterAllCourseUsers' == $userRegistrationRadio->getValue()) { - $this->addRegistrants($newMeeting, $newMeeting->getCourseAndSessionUsers()); - Display::addFlash( - Display::return_message(get_lang('AllCourseUsersWereRegistered')) - ); - } elseif ('RegisterTheseGroupMembers' == $userRegistrationRadio->getValue()) { - $userIds = []; - foreach ($groupIdsSelect->getValue() as $groupId) { - $userIds = array_unique(array_merge($userIds, GroupManager::get_users($groupId))); + if ($newMeeting->isCourseMeeting()) { + if ('RegisterAllCourseUsers' == $form->getSubmitValue('userRegistration')) { + $this->registerAllCourseUsers($newMeeting); + Display::addFlash( + Display::return_message(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(get_lang('GroupUsersWereRegistered')) + ); } - $users = Database::getManager()->getRepository( - 'ChamiloUserBundle:User' - )->matching(Criteria::create()->where(Criteria::expr()->in('id', $userIds)))->getValues(); - $this->addRegistrants($newMeeting, $users); + location('meeting_from_start.php?meetingId='.$newMeeting->getId()); + } elseif (!is_null($user)) { + location('meeting_from_user.php?meetingId='.$newMeeting->getId()); } - location('meeting_from_start.php?meetingId='.$newMeeting->id); } catch (Exception $exception) { Display::addFlash( Display::return_message($exception->getMessage(), 'error') @@ -519,7 +636,6 @@ class ZoomPlugin extends Plugin $form->setDefaults( [ 'duration' => 60, - 'topic' => api_get_course_info()['title'], 'userRegistration' => 'RegisterAllCourseUsers', ] ); @@ -529,38 +645,26 @@ class ZoomPlugin extends Plugin } /** - * Retrieves information about meetings having a start_time between two dates. - * - * @param string $type MeetingList::TYPE_LIVE, MeetingList::TYPE_SCHEDULED or MeetingList::TYPE_UPCOMING - * @param DateTime $startDate - * @param DateTime $endDate - * - * @throws Exception on API error + * @param MeetingEntity $meetingEntity * - * @return CourseMeetingListItem[] matching meetings + * @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 getPeriodMeetings($type, $startDate, $endDate) + public function userIsConferenceManager($meetingEntity) { - $matchingMeetings = []; - /** @var CourseMeetingListItem $meeting */ - foreach (CourseMeetingList::loadMeetings($this->jwtClient(), $type) as $meeting) { - if (property_exists($meeting, 'start_time')) { - if ($startDate <= $meeting->startDateTime && $meeting->startDateTime <= $endDate) { - $meeting->loadCourse(); - $meeting->loadSession(); - $matchingMeetings[] = $meeting; - } - } - } - - return $matchingMeetings; + return api_is_coach() + || api_is_platform_admin() + || $meetingEntity->isCourseMeeting() && api_get_course_id() && api_is_course_admin() + || $meetingEntity->isUserMeeting() && $meetingEntity->getUser()->getId() == api_get_user_id(); } /** + * @param Course $course + * * @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() + public function userIsCourseConferenceManager($course) { return api_is_coach() || api_is_platform_admin() @@ -568,272 +672,438 @@ class ZoomPlugin extends Plugin } /** - * Retrieves a meeting. + * Adds to the meeting course documents a link to a meeting instance recording file. * - * @param int $id the meeting numeric identifier + * @param MeetingEntity $meeting + * @param RecordingFile $file + * @param string $name * * @throws Exception - * - * @return CourseMeetingInfoGet */ - public function getMeeting($id) + public function createLinkToFileInCourse($meeting, $file, $name) { - return CourseMeetingInfoGet::fromId($this->jwtClient(), $id); + $course = $meeting->getCourse(); + if (is_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' + ) + ); + } } /** - * Retrieves a past meeting instance details. + * Copies a recording file to a meeting's course. * - * @param string $instanceUUID + * @param MeetingEntity $meeting + * @param RecordingFile $file + * @param string $name * * @throws Exception - * - * @return PastMeeting */ - public function getPastMeetingInstanceDetails($instanceUUID) + public function copyFileToCourse($meeting, $file, $name) { - return PastMeeting::fromUUID($this->jwtClient(), $instanceUUID); + $course = $meeting->getCourse(); + if (is_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)); + } + $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, + 'type' => $file->file_type, + ], + '/', + api_get_path(SYS_COURSE_PATH) . $courseInfo['path'] . '/document', + api_get_user_id(), + 0, + null, + 0, + '', + true, + false, + null, + $meeting->getSession()->getId(), + true + ); + fclose($tmpFile); + if (false === $newPath) { + throw new Exception('could not handle uploaded document'); + } } /** - * Retrieves all live meetings linked to current course and session. - * - * @throws Exception on API error + * @throws OptimisticLockException + * @throws Exception * - * @return CourseMeetingListItem[] matching meetings + * @return string */ - public function getLiveMeetings() + public function getGlobalMeetingURL() { - return $this->getMeetings(CourseMeetingList::TYPE_LIVE); - } + if (!self::currentUserCanJoinGlobalMeeting()) { + throw new Exception('global meetings are not enabled'); + } + $url = null; + foreach ($this->getMeetingRepository()->unfinishedGlobalMeetings() as $meeting) { + // 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 we must make the global meeting creator the host, that is, redirect to start_url, not join_url + $meetingInfoGet = MeetingInfoGet::fromId($meeting->getMeetingInfoGet()->id); + if ($meeting->getMeetingInfoGet() != $meetingInfoGet) { // keep comparison operator (!=) + $meeting->setMeetingInfoGet($meetingInfoGet); + Database::getManager()->persist($meeting); + Database::getManager()->flush($meeting); + } + if ('waiting' === $meetingInfoGet->status) { + $url = $meeting->getMeetingInfoGet()->start_url; + break; + } elseif ('started' === $meetingInfoGet->status) { + if ('true' === $this->get('enableParticipantRegistration') && $meeting->requiresRegistration()) { + $user = api_get_user_entity(api_get_user_id()); + /** @var RegistrantEntity $registrant */ + $registrant = $meeting->getRegistrant($user); + if (is_null($registrant)) { // not registered yet + $registrant = $this->registerUser($meeting, $user); + } + $url = $registrant->getCreatedRegistration()->join_url; + break; + } else { // no registration possible, join anonymously + $url = $meetingInfoGet->join_url; + break; + } + } // else 'finished' - try next + } + if (is_null($url)) { + $url = $this->createGlobalMeeting()->getMeetingInfoGet()->start_url; + } - /** - * Retrieves all scheduled meetings linked to current course and session. - * - * @throws Exception on API error - * - * @return CourseMeetingListItem[] matching meetings - */ - public function getScheduledMeetings() - { - return $this->getMeetings(CourseMeetingList::TYPE_SCHEDULED); + return $url; } /** - * Retrieves all upcoming meetings linked to current course and session. + * Returns the URL to enter (start or join) a user meeting. * - * @throws Exception on API error + * @param MeetingEntity $meeting + * @param bool $autoRegister * - * @return CourseMeetingListItem[] matching meetings + * @throws Exception + * @throws OptimisticLockException + * + * @return string */ - public function getUpcomingMeetings() + public function getUserMeetingURL($meeting, $autoRegister = false) { - return $this->getMeetings(CourseMeetingList::TYPE_UPCOMING); + $url = null; + $host = $meeting->getUser(); + $participant = api_get_user_entity(api_get_user_id()); + if ($host === $participant) { + $url = $meeting->getMeetingInfoGet()->start_url; + } else { + if ('true' === $this->get('enableParticipantRegistration') && $meeting->requiresRegistration()) { + $registrant = $meeting->getRegistrant($participant); + if (is_null($registrant)) { // not registered yet + if (!$autoRegister) { + throw new Exception(get_lang('YouAreNotRegisteredToThisMeeting')); + } + $registrant = $this->registerUser($meeting, $participant); + } + $url = $registrant->getCreatedRegistration()->join_url; + } else { // no registration possible, join anonymously + $url = $meeting->getMeetingInfoGet()->join_url; + } + } + + return $url; } /** - * Creates an instant meeting and returns it. - * - * @throws Exception describing the error (message and code) + * @param DateTime $startDate + * @param DateTime $endDate * - * @return CourseMeetingInfoGet meeting + * @throws OptimisticLockException + * @throws Exception */ - public function createInstantMeeting() + public function reloadPeriodRecordings(DateTime $startDate, DateTime $endDate) { - // default meeting topic is based on session name, course title and current date - $topic = ''; - $sessionName = api_get_session_name(); - if ($sessionName) { - $topic = $sessionName.', '; + foreach (RecordingList::loadPeriodRecordings($startDate, $endDate) as $recordingMeeting) { + $recordingEntity = $this->getRecordingRepository()->find($recordingMeeting->uuid); + if (is_null($recordingEntity)) { + $recordingEntity = new RecordingEntity(); + $meetingEntity = $this->getMeetingRepository()->find($recordingMeeting->id); + if (is_null($meetingEntity)) { + try { + $meetingInfoGet = MeetingInfoGet::fromId($recordingMeeting->id); + } catch (Exception $exception) { + $meetingInfoGet = null; // deleted meeting with recordings + } + if (!is_null($meetingInfoGet)) { + $meetingEntity = $this->createMeetingFromMeetingEntity( + (new MeetingEntity())->setMeetingInfoGet($meetingInfoGet) + ); + Database::getManager()->persist($meetingEntity); + } + } + if (!is_null($meetingEntity)) { + $recordingEntity->setMeeting($meetingEntity); + } + } + $recordingEntity->setRecordingMeeting($recordingMeeting); + Database::getManager()->persist($recordingEntity); } - $courseInfo = api_get_course_info(); - $topic .= $courseInfo['title'].', '.date('yy-m-d H:i'); - $meeting = CourseMeeting::fromCourseSessionTopicAndType( - api_get_course_int_id(), - api_get_session_id(), - $topic, - CourseMeeting::TYPE_INSTANT - ); - - return $this->createMeeting($meeting); + Database::getManager()->flush(); } /** - * Schedules a meeting and returns it. + * Creates a meeting on Zoom servers and stores it in the local database. * - * @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 + * @param MeetingEntity $meeting a new, unsaved meeting with at least a type and a topic * - * @throws Exception describing the error (message and code) + * @return MeetingEntity + * @throws Exception * - * @return CourseMeetingInfoGet meeting */ - public function createScheduledMeeting($startTime, $duration, $topic, $agenda = '', $password = '') + private function createMeetingFromMeetingEntity($meeting) { - $meeting = CourseMeeting::fromCourseSessionTopicAndType( - api_get_course_int_id(), - api_get_session_id(), - $topic, - CourseMeeting::TYPE_SCHEDULED - ); - $meeting->duration = $duration; - $meeting->start_time = $startTime->format(DateTimeInterface::ISO8601); - $meeting->agenda = $agenda; - $meeting->password = $password; - $meeting->settings->approval_type = $this->get('enableParticipantRegistration') - ? MeetingSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE - : MeetingSettings::APPROVAL_TYPE_NO_REGISTRATION_REQUIRED; + $meeting->getMeetingInfoGet()->settings->auto_recording = $this->get('enableCloudRecording') + ? 'cloud' + : 'local'; + $meeting->getMeetingInfoGet()->settings->registrants_email_notification = false; + $meeting->setMeetingInfoGet($meeting->getMeetingInfoGet()->create()); + Database::getManager()->persist($meeting); + Database::getManager()->flush(); - return $this->createMeeting($meeting); + return $meeting; } /** - * Updates a meeting. + * @return MeetingEntity * - * @param CourseMeetingInfoGet $meeting the meeting with updated properties - * - * @throws Exception on API error + * @throws Exception */ - public function updateMeeting($meeting) + private function createGlobalMeeting() { - $meeting->update($this->jwtClient()); - } + $meetingInfoGet = MeetingInfoGet::fromTopicAndType( + 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; - /** - * Deletes a meeting. - * - * @param CourseMeetingInfoGet $meeting - * - * @throws Exception on API error - */ - public function deleteMeeting($meeting) - { - $meeting->delete($this->jwtClient()); + return $this->createMeetingFromMeetingEntity((new MeetingEntity())->setMeetingInfoGet($meetingInfoGet)); } /** - * Retrieves all recordings from a period of time. + * Starts a new instant meeting and redirects to its start url. * - * @param DateTime $startDate start date - * @param DateTime $endDate end date + * @param string $topic + * @param User|null $user + * @param Course|null $course + * @param Session|null $session * * @throws Exception - * - * @return Recording[] all recordings */ - public function getRecordings($startDate, $endDate) + private function startInstantMeeting($topic, $user = null, $course = null, $session = null) { - return RecordingList::loadRecordings($this->jwtClient(), $startDate, $endDate); + $meeting = $this->createMeetingFromMeetingEntity( + (new MeetingEntity()) + ->setMeetingInfoGet(MeetingInfoGet::fromTopicAndType($topic, MeetingInfoGet::TYPE_INSTANT)) + ->setUser($user) + ->setCourse($course) + ->setSession($session) + ); + location($meeting->getMeetingInfoGet()->start_url); } /** - * Retrieves a meetings instances' recordings. + * Schedules a meeting and returns it. + * set $course, $session and $user to null in order to create a global meeting. * - * @param CourseMeetingInfoGet $meeting + * @param User|null $user the current user, for a course meeting or a user meeting + * @param Course|null $course the course, for a course meeting + * @param Session|null $session the session, for a course 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 * + * @return MeetingEntity meeting * @throws Exception - * - * @return Recording[] meeting instances' recordings */ - public function getMeetingRecordings($meeting) + private function scheduleMeeting($user, $course, $session, $startTime, $duration, $topic, $agenda, $password) { - $interval = new DateInterval('P1M'); - $startDate = clone $meeting->startDateTime; - $startDate->sub($interval); - $endDate = clone $meeting->startDateTime; - $endDate->add($interval); - $recordings = []; - foreach ($this->getRecordings($startDate, $endDate) as $recording) { - if ($recording->id == $meeting->id) { - $recordings[] = $recording; - } - } - - return $recordings; + $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 = $this->get('enableParticipantRegistration') + ? MeetingSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE + : MeetingSettings::APPROVAL_TYPE_NO_REGISTRATION_REQUIRED; + return $this->createMeetingFromMeetingEntity( + (new MeetingEntity()) + ->setMeetingInfoGet($meetingInfoGet) + ->setUser($user) + ->setCourse($course) + ->setSession($session) + ); } /** - * Retrieves a meeting instance's participants. + * @param MeetingEntity $meetingEntity + * @param User $user + * @param bool $andFlush * - * @param string $instanceUUID the meeting instance UUID + * @return RegistrantEntity * + * @throws OptimisticLockException * @throws Exception - * - * @return ParticipantListItem[] */ - public function getParticipants($instanceUUID) + private function registerUser($meetingEntity, $user, $andFlush = true) { - return ParticipantList::loadInstanceParticipants($this->jwtClient(), $instanceUUID); + if (empty($user->getEmail())) { + throw new Exception('CannotRegisterWithoutEmailAddress'); + } + $meetingRegistrant = MeetingRegistrant::fromEmailAndFirstName( + $user->getEmail(), + $user->getFirstname(), + $user->getLastname() + ); + $registrantEntity = (new RegistrantEntity()) + ->setMeeting($meetingEntity) + ->setUser($user) + ->setMeetingRegistrant($meetingRegistrant) + ->setCreatedRegistration($meetingEntity->getMeetingInfoGet()->addRegistrant($meetingRegistrant)); + Database::getManager()->persist($registrantEntity); + if ($andFlush) { + Database::getManager()->flush($registrantEntity); + } + return $registrantEntity; } /** - * Retrieves a meeting's registrants. + * Register users to a meeting. * - * @param CourseMeetingInfoGet $meeting + * @param MeetingEntity $meetingEntity + * @param User[] $users * - * @throws Exception + * @throws OptimisticLockException * - * @return UserMeetingRegistrantListItem[] the meeting registrants + * @return User[] failed registrations [ user id => errorMessage ] */ - public function getRegistrants($meeting) + private function registerUsers($meetingEntity, $users) { - return $meeting->getUserRegistrants($this->jwtClient()); + $failedUsers = []; + foreach ($users as $user) { + try { + $this->registerUser($meetingEntity, $user, false); + } catch (Exception $exception) { + $failedUsers[$user->getId()] = $exception->getMessage(); + } + } + Database::getManager()->flush(); + + return $failedUsers; } /** - * Registers users to a meeting. - * - * @param CourseMeetingInfoGet $meeting - * @param \Chamilo\UserBundle\Entity\User[] $users + * Registers all the course users to a course meeting. * - * @throws Exception + * @param MeetingEntity $meetingEntity * - * @return CreatedRegistration[] the created registrations + * @throws OptimisticLockException */ - public function addRegistrants($meeting, $users) + private function registerAllCourseUsers($meetingEntity) { - $createdRegistrations = []; - foreach ($users as $user) { - $registrant = UserMeetingRegistrant::fromUser($user); - $registrant->tagEmail(); - $createdRegistrations[] = $meeting->addRegistrant($this->jwtClient(), $registrant); - } - - return $createdRegistrations; + $this->registerUsers($meetingEntity, $meetingEntity->getRegistrableUsers()); } /** * Removes registrants from a meeting. * - * @param CourseMeetingInfoGet $meeting - * @param UserMeetingRegistrant[] $registrants + * @param MeetingEntity $meetingEntity + * @param RegistrantEntity[] $registrants * * @throws Exception */ - public function removeRegistrants($meeting, $registrants) + private function unregister($meetingEntity, $registrants) { - $meeting->removeRegistrants($this->jwtClient(), $registrants); + $meetingRegistrants = []; + foreach ($registrants as $registrant) { + $meetingRegistrants[] = $registrant->getMeetingRegistrant(); + } + $meetingEntity->getMeetingInfoGet()->removeRegistrants($meetingRegistrants); + foreach ($registrants as $registrant) { + Database::getManager()->remove($registrant); + } + Database::getManager()->flush(); } /** * Updates meeting registrants list. Adds the missing registrants and removes the extra. * - * @param CourseMeetingInfoGet $meeting - * @param \Chamilo\UserBundle\Entity\User[] $users list of users to be registred + * @param MeetingEntity $meetingEntity + * @param User[] $users list of users to be registered * * @throws Exception */ - public function updateRegistrantList($meeting, $users) + private function updateRegistrantList($meetingEntity, $users) { - $registrants = $this->getRegistrants($meeting); $usersToAdd = []; foreach ($users as $user) { $found = false; - foreach ($registrants as $registrant) { - if ($registrant->matches($user->getId())) { + foreach ($meetingEntity->getRegistrants() as $registrant) { + if ($registrant->getUser() === $user) { $found = true; break; } @@ -843,10 +1113,10 @@ class ZoomPlugin extends Plugin } } $registrantsToRemove = []; - foreach ($registrants as $registrant) { + foreach ($meetingEntity->getRegistrants() as $registrant) { $found = false; foreach ($users as $user) { - if ($registrant->matches($user->getId())) { + if ($registrant->getUser() === $user) { $found = true; break; } @@ -855,176 +1125,7 @@ class ZoomPlugin extends Plugin $registrantsToRemove[] = $registrant; } } - $this->addRegistrants($meeting, $usersToAdd); - $this->removeRegistrants($meeting, $registrantsToRemove); - } - - /** - * Adds to the meeting course documents a link to a meeting instance recording file. - * - * @param CourseMeetingInfoGet $meeting - * @param File $file - * @param string $name - * - * @throws Exception - */ - public function createLinkToFileInCourse($meeting, $file, $name) - { - $courseInfo = api_get_course_info_by_id($meeting->courseId); - 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 CourseMeetingInfoGet $meeting - * @param File $file - * @param string $name - * - * @throws Exception - */ - public function copyFileToCourse($meeting, $file, $name) - { - $courseInfo = api_get_course_info_by_id($meeting->courseId); - 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)); - } - $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, - 'type' => $file->file_type, - ], - '/', - api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document', - api_get_user_id(), - 0, - null, - 0, - '', - true, - false, - null, - $meeting->sessionId, - true - ); - fclose($tmpFile); - if (false === $newPath) { - throw new Exception('could not handle uploaded document'); - } - } - - /** - * Deletes a meeting instance's recordings. - * - * @param Recording $recording - * - * @throws Exception - */ - public function deleteRecordings($recording) - { - $recording->delete($this->jwtClient()); - } - - /** - * Deletes a meeting instance recording file. - * - * @param File $file - * - * @throws Exception - */ - public function deleteFile($file) - { - $file->delete($this->jwtClient()); - } - - /** - * Caches and returns the JWT client instance, initialized with plugin settings. - * - * @return JWTClient object that provides means of communications with the Zoom servers - */ - protected function jwtClient() - { - static $jwtClient = null; - if (is_null($jwtClient)) { - $jwtClient = new JWTClient($this->get('apiKey'), $this->get('apiSecret')); - } - - return $jwtClient; - } - - /** - * Retrieves all meetings of a specific type and linked to current course and session. - * - * @param string $type MeetingList::TYPE_LIVE, MeetingList::TYPE_SCHEDULED or MeetingList::TYPE_UPCOMING - * - * @throws Exception on API error - * - * @return CourseMeetingListItem[] matching meetings - */ - private function getMeetings($type) - { - $matchingMeetings = []; - $courseId = api_get_course_int_id(); - $sessionId = api_get_session_id(); - /** @var CourseMeetingListItem $candidateMeeting */ - foreach (CourseMeetingList::loadMeetings($this->jwtClient(), $type) as $candidateMeeting) { - if ($candidateMeeting->matches($courseId, $sessionId)) { - $matchingMeetings[] = $candidateMeeting; - } - } - - return $matchingMeetings; - } - - /** - * Creates a meeting on the server and returns it. - * - * @param CourseMeeting $meeting a meeting with at least a type and a topic - * - * @throws Exception describing the error (message and code) - * - * @return CourseMeetingInfoGet the new meeting - */ - private function createMeeting($meeting) - { - $meeting->settings->auto_recording = $this->get('enableCloudRecording') - ? 'cloud' - : 'local'; - $meeting->settings->registrants_email_notification = false; - - return $meeting->create($this->jwtClient()); + $this->registerUsers($meetingEntity, $usersToAdd); + $this->unregister($meetingEntity, $registrantsToRemove); } } diff --git a/plugin/zoom/meeting.php b/plugin/zoom/meeting.php index 999cb8f00a..3059797477 100755 --- a/plugin/zoom/meeting.php +++ b/plugin/zoom/meeting.php @@ -1,7 +1,7 @@ getMeeting($_REQUEST['meetingId']); +/** @var MeetingEntity $meeting */ +$meeting = $plugin->getMeetingRepository()->find($_REQUEST['meetingId']); +if (is_null($meeting)) { + throw new Exception('MeetingNotFound'); +} -$tpl = new Template($meeting->id); +$tpl = new Template($meeting->getId()); -if ($plugin->userIsConferenceManager()) { +if ($plugin->userIsConferenceManager($meeting)) { // user can edit, start and delete meeting $tpl->assign('isConferenceManager', true); $tpl->assign('editMeetingForm', $plugin->getEditMeetingForm($meeting)->returnForm()); $tpl->assign('deleteMeetingForm', $plugin->getDeleteMeetingForm($meeting, $returnURL)->returnForm()); - if ($plugin->get('enableParticipantRegistration') - && MeetingSettings::APPROVAL_TYPE_NO_REGISTRATION_REQUIRED != $meeting->settings->approval_type) { - list($registerParticipantForm, $registrants) = $plugin->getRegisterParticipantForm($meeting); - $tpl->assign('registerParticipantForm', $registerParticipantForm->returnForm()); - $tpl->assign('registrants', $registrants); // FIXME cache + if ($plugin->get('enableParticipantRegistration') && $meeting->requiresRegistration()) { + $tpl->assign('registerParticipantForm', $plugin->getRegisterParticipantForm($meeting)->returnForm()); + $tpl->assign('registrants', $meeting->getRegistrants()); } if ($plugin->get('enableCloudRecording') - && 'cloud' === $meeting->settings->auto_recording + && $meeting->hasCloudAutoRecordingEnabled() // && 'finished' === $meeting->status ) { - list($fileForm, $recordings) = $plugin->getFileForm($meeting); - $tpl->assign('fileForm', $fileForm->returnForm()); - $tpl->assign('recordings', $recordings); + $tpl->assign('fileForm', $plugin->getFileForm($meeting)->returnForm()); + $tpl->assign('recordings', $meeting->getRecordings()); } -} elseif (MeetingSettings::APPROVAL_TYPE_NO_REGISTRATION_REQUIRED != $meeting->settings->approval_type) { +} elseif ($meeting->requiresRegistration()) { $userId = api_get_user_id(); try { - foreach ($plugin->getRegistrants($meeting) as $registrant) { + foreach ($meeting->getRegistrants() as $registrant) { if ($registrant->userId == $userId) { $tpl->assign('currentUserJoinURL', $registrant->join_url); break; diff --git a/plugin/zoom/meeting_from_user.php b/plugin/zoom/meeting_from_user.php new file mode 100644 index 0000000000..a4ccd74bc6 --- /dev/null +++ b/plugin/zoom/meeting_from_user.php @@ -0,0 +1,13 @@ +userIsConferenceManager()) { +if ($plugin->userIsCourseConferenceManager(api_get_course_entity())) { // user can create a new meeting - $tpl->assign('createInstantMeetingForm', $plugin->getCreateInstantMeetingForm()->returnForm()); - $tpl->assign('scheduleMeetingForm', $plugin->getScheduleMeetingForm()->returnForm()); + $tpl->assign( + 'createInstantMeetingForm', + $plugin->getCreateInstantMeetingForm( + api_get_user_entity(api_get_user_id()), + api_get_course_entity(), + api_get_session_entity() + )->returnForm() + ); + $tpl->assign('scheduleMeetingForm', $plugin->getScheduleMeetingForm( + api_get_user_entity(api_get_user_id()), + api_get_course_entity(), + api_get_session_entity() + )->returnForm()); } try { - $tpl->assign('scheduledMeetings', $plugin->getScheduledMeetings()); + $tpl->assign( + 'scheduledMeetings', + $plugin->getMeetingRepository()->courseMeetings(api_get_course_entity(), api_get_session_entity()) + ); } catch (Exception $exception) { Display::addFlash( Display::return_message('Could not retrieve scheduled meeting list: '.$exception->getMessage(), 'error') diff --git a/plugin/zoom/user.php b/plugin/zoom/user.php new file mode 100644 index 0000000000..9408510884 --- /dev/null +++ b/plugin/zoom/user.php @@ -0,0 +1,36 @@ +getAdminSearchForm(); +$startDate = new DateTime($form->getElement('start')->getValue()); +$endDate = new DateTime($form->getElement('end')->getValue()); +$reloadRecordingLists = $form->getElement('reloadRecordingLists')->getValue(); +if ($reloadRecordingLists) { + $plugin->reloadPeriodRecordings($startDate, $endDate); +} + +$tpl = new Template(); +$tpl->assign('meetings', $plugin->getMeetingRepository()->periodUserMeetings($startDate, $endDate, $user)); +if ($plugin->get('enableCloudRecording')) { + $tpl->assign( + 'recordings', + $plugin->getRecordingRepository()->getPeriodUserRecordings($startDate, $endDate, $user) + ); +} +$tpl->assign('search_form', $form->returnForm()); +$tpl->assign('schedule_form', $plugin->getScheduleMeetingForm($user)->returnForm()); +$tpl->assign('content', $tpl->fetch('zoom/view/admin.tpl')); +$tpl->display_one_col_template(); diff --git a/plugin/zoom/view/admin.tpl b/plugin/zoom/view/admin.tpl index 00745df5e0..df777442ba 100644 --- a/plugin/zoom/view/admin.tpl +++ b/plugin/zoom/view/admin.tpl @@ -4,6 +4,7 @@ {{ 'StartTime'|get_lang }} + {{ 'User'|get_lang }} {{ 'Course'|get_lang }} {{ 'Session'|get_lang }} {{ 'Topic'|get_lang }} @@ -17,13 +18,14 @@ {% for meeting in meetings %} {{ meeting.formattedStartTime }} - {{ meeting.course ? meeting.course.title : '-' }} - {{ meeting.session ? meeting.session.name : '-' }} - {{ meeting.topic }} + {{ meeting.user ? meeting.user : '-' }} + {{ meeting.course ? meeting.course : '-' }} + {{ meeting.session ? meeting.session : '-' }} + {{ meeting.meetingInfoGet.topic }} {% if recordings %} {% for recording in recordings %} - {% if recording.id == meeting.id %} + {% if recording.recordingMeeting.id == meeting.id %}
{{ recording.formattedStartTime }} @@ -31,10 +33,10 @@
    - {% for file in recording.recording_files %} + {% for file in recording.recordingMeeting.recording_files %}
  • {{ file.recording_type }}.{{ file.file_type }} - ({{ file.formattedFileSize }}) + ({{ file.file_size }})
  • {% endfor %}
@@ -52,4 +54,6 @@ {% endfor %} - \ No newline at end of file + + +{{ schedule_form }} \ No newline at end of file diff --git a/plugin/zoom/view/meeting.tpl b/plugin/zoom/view/meeting.tpl index 806a4a5d86..11d3097d80 100644 --- a/plugin/zoom/view/meeting.tpl +++ b/plugin/zoom/view/meeting.tpl @@ -1,8 +1,8 @@ -

{{ meeting.typeName }} {{ meeting.id }} ({{ meeting.statusName }})

+

{{ meeting.typeName }} {{ meeting.id }} ({{ meeting.meetingInfoGet.status }})

{% if isConferenceManager and meeting.status == 'waiting' %}

- + {{ 'StartMeeting'|get_lang }}

@@ -16,11 +16,11 @@

{% endif %} -{% if meeting.settings.approval_type == 2 %} +{% if meeting.meetingInfoGet.settings.approval_type == 2 %}

{% endif %} @@ -32,7 +32,7 @@ {{ deleteMeetingForm }} {{ registerParticipantForm }} {{ fileForm }} -{% if registrants and meeting.settings.approval_type != 2 %} +{% if registrants and meeting.meetingInfoGet.settings.approval_type != 2 %}