diff --git a/plugin/zoom/README.md b/plugin/zoom/README.md new file mode 100644 index 0000000000..d00088f335 --- /dev/null +++ b/plugin/zoom/README.md @@ -0,0 +1 @@ +This plugin allows teachers to launch a Zoom conference at any time and students to join it. \ No newline at end of file diff --git a/plugin/zoom/admin.php b/plugin/zoom/admin.php new file mode 100644 index 0000000000..673f45c065 --- /dev/null +++ b/plugin/zoom/admin.php @@ -0,0 +1,32 @@ +getAdminSearchForm(); +$startDate = new DateTime($form->getElement('start')->getValue()); +$endDate = new DateTime($form->getElement('end')->getValue()); +$type = $form->getElement('type')->getValue(); + +$tpl = new Template($tool_name); +$tpl->assign('meetings', $plugin->getPeriodMeetings($type, $startDate, $endDate)); +if ($plugin->get('enableCloudRecording')) { + $tpl->assign('recordings', $plugin->getRecordings($startDate, $endDate)); +} +$tpl->assign('search_form', $form->returnForm()); +$tpl->assign('content', $tpl->fetch('zoom/view/admin.tpl')); +$tpl->display_one_col_template(); diff --git a/plugin/zoom/config.php b/plugin/zoom/config.php new file mode 100755 index 0000000000..74bf8a43ef --- /dev/null +++ b/plugin/zoom/config.php @@ -0,0 +1,3 @@ +install(); diff --git a/plugin/zoom/lang/english.php b/plugin/zoom/lang/english.php new file mode 100755 index 0000000000..78d36753c2 --- /dev/null +++ b/plugin/zoom/lang/english.php @@ -0,0 +1,108 @@ +launch a conference and student to join it. +
+This plugin requires a Zoom account to manage meetings. +The Zoom API uses JSON Web Tokens (JWT) to authenticate account-level access. +
+JWT apps provide an API Key and Secret required to authenticate with JWT. + +To get them, create a JWT App : +
1. log into your Zoom profile page +
2. click on Advanced / Application Marketplace +
3. click on Develop / build App +
4. choose JWT / Create +
5. fill in information about your \"App\" +(application and company names, contact name and email address) +
6. click on Continue +Locate your API Key and Secret in the App Credentials page. +
+Attention: +
Zoom is NOT free software and specific rules apply to personal data protection. +Please check with Zoom and make sure they satisfy you and learning users."; + +$strings['enableParticipantRegistration_help'] = "Requires a paying Zoom profile. +Will not work for a basic profile."; + +$strings['enableCloudRecording_help'] = "Requires a paying Zoom profile. +Will not work for a basic profile."; + +// please keep these lines alphabetically sorted +$strings['AllCourseUsersWereRegistered'] = "All course students were registered"; +$strings['Agenda'] = "Agenda"; +$strings['CopyingJoinURL'] = "Copying join URL"; +$strings['CopyJoinAsURL'] = "Copy 'join as' URL"; +$strings['CopyToCourse'] = "Copy to course"; +$strings['CouldNotCopyJoinURL'] = "Could not copy join URL"; +$strings['Course'] = "Cours"; +$strings['CreatedAt'] = "Created at"; +$strings['CreateLinkInCourse'] = "Create link(s) in course"; +$strings['DeleteMeeting'] = "Delete meeting"; +$strings['DeleteFile'] = "Delete file(s)"; +$strings['Details'] = "Details"; +$strings['DoIt'] = "Do it"; +$strings['Duration'] = "Duration"; +$strings['DurationFormat'] = "%hh%I"; +$strings['DurationInMinutes'] = "Duration (in minutes)"; +$strings['EndDate'] = "End Date"; +$strings['Files'] = "Files"; +$strings['Finished'] = "finished"; +$strings['FileWasCopiedToCourse'] = "The file was copied to the course"; +$strings['FileWasDeleted'] = "The file was deleted"; +$strings['InstantMeeting'] = "Instant meeting"; +$strings['Join'] = "Join"; +$strings['JoinMeetingAsMyself'] = "Join meeting as myself"; +$strings['JoinURLCopied'] = "Join URL copied"; +$strings['JoinURLToSendToParticipants'] = "Join URL to send to participants"; +$strings['LiveMeetings'] = "Live meetings"; +$strings['LinkToFileWasCreatedInCourse'] = "A link to the file was added to the course"; +$strings['MeetingDeleted'] = "Meeting deleted"; +$strings['MeetingsFound'] = "Meetings found"; +$strings['MeetingUpdated'] = "Meeting updated"; +$strings['NewMeetingCreated'] = "New meeting created"; +$strings['Password'] = "Password"; +$strings['RecurringWithFixedTime'] = "Recurring with fixed time"; +$strings['RecurringWithNoFixedTime'] = "Recurring with no fixed time"; +$strings['RegisterAllCourseUsers'] = "Register all course users"; +$strings['RegisteredUserListWasUpdated'] = "Registered user list updated"; +$strings['RegisteredUsers'] = "Registered users"; +$strings['RegisterNoUser'] = "Register no user"; +$strings['RegisterTheseGroupMembers'] = "Register these group members"; +$strings['ScheduleAMeeting'] = "Schedule a meeting"; +$strings['ScheduledMeeting'] = "Scheduled meeting"; +$strings['ScheduledMeetings'] = "Scheduled Meetings"; +$strings['ScheduleTheMeeting'] = "Schedule the meeting"; +$strings['Search'] = "Search"; +$strings['Session'] = "Session"; +$strings['StartDate'] = "Start Date"; +$strings['Started'] = "started"; +$strings['StartInstantMeeting'] = "Start instant meeting"; +$strings['StartMeeting'] = "Start meeting"; +$strings['StartTime'] = "Start time"; +$strings['Topic'] = "Topic"; +$strings['TopicAndAgenda'] = "Topic and agenda"; +$strings['Type'] = "Type"; +$strings['UpcomingMeetings'] = "Upcoming meetings"; +$strings['UpdateMeeting'] = "Update meeting"; +$strings['UpdateRegisteredUserList'] = "Update registered user list"; +$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['ZoomVideoConferences'] = "Zoom Video Conferences"; diff --git a/plugin/zoom/lang/french.php b/plugin/zoom/lang/french.php new file mode 100755 index 0000000000..58b1076e63 --- /dev/null +++ b/plugin/zoom/lang/french.php @@ -0,0 +1,107 @@ +API Key)"; +$strings['apiSecret'] = "Code secret d'API (API Secret)"; +$strings['enableParticipantRegistration'] = "Activer l'inscription des participants"; +$strings['enableCloudRecording'] = "Activer l'enregistrement sur les serveurs de Zoom"; +$strings['enableGlobalConference'] = "Activer les conférences globales"; +$strings['enableGlobalConferencePerUser'] = "Activer les conférences globales par utilisateur"; +$strings['globalConferenceAllowRoles'] = "Visibilité du lien de vidéo conférence global pour les profils suivant"; + +$strings['tool_enable_help'] = "Choisissez si vous voulez activer l'outil de conférence vidéo Zoom. +Une fois activé, il apparaitra dans les pages d'accueil de tous les cours : +les enseignants pourront démarrer une conférence et les étudiants la rejoindre. +
+Ce plugin requiert un compte Zoom pour gérer les conférences. +L'API de Zoom utilise les JSON Web Tokens (JWT) pour autoriser l'accès à un compte. +Une clé et un code secret d'API sont requis pour s'authentifier avec JWT. +Pour les obtenir, créez une JWT app : +
1. logguez vous sur Votre profil Zoom +
2. cliquez sur Avancé / Marketplace d'application +
3. cliquez sur Develop / build App +
4. choisissez JWT / Create +
5. saisissez quelques informations sur votre \"App\" +(noms de l'application, de l'entreprise, nom et adresse de courriel de contact) +
6. cliquez sur Continue +
La page App Credentials affiche la clé (API Key) et le code secret (API Secret) à saisir ici. +
+Attention : +
Zoom n'est PAS un logiciel libre +et des règles spécifiques de protection des données personnelles s'y appliquent. +Merci de vérifier auprès de Zoom qu'elles sont satisfaisantes pour vous et les apprenants qui l'utiliseront."; + +$strings['enableParticipantRegistration_help'] = "Nécessite un profil Zoom payant. +Ne fonctionnera pas pour un profil de base."; + +$strings['enableCloudRecording_help'] = "Nécessite un profil Zoom payant. +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['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"; +$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['DeleteMeeting'] = "Effacer la conférence"; +$strings['DeleteFile'] = "Supprimer ce(s) fichier(s)"; +$strings['Details'] = "Détail"; +$strings['DoIt'] = "Fais-le"; +$strings['Duration'] = "Durée"; +$strings['DurationFormat'] = "%hh%I"; +$strings['DurationInMinutes'] = "Durée (en minutes)"; +$strings['EndDate'] = "Date de fin"; +$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['InstantMeeting'] = "Conférence instantanée"; +$strings['Join'] = "Rejoindre"; +$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)"; +$strings['LiveMeetings'] = "Conférences en cours"; +$strings['LinkToFileWasCreatedInCourse'] = "Un lien vers le fichier a été ajouter au cours"; +$strings['MeetingDeleted'] = "Conférence effacée"; +$strings['MeetingsFound'] = "Conférences trouvées"; +$strings['MeetingUpdated'] = "Conférence mise à jour"; +$strings['NewMeetingCreated'] = "Nouvelle conférence créée"; +$strings['Password'] = "Mot de passe"; +$strings['RecurringWithFixedTime'] = "Recurrent, à heure fixe"; +$strings['RecurringWithNoFixedTime'] = "Recurrent, sans heure fixe"; +$strings['RegisterAllCourseUsers'] = "Inscrire tous les utilisateurs du cours"; +$strings['RegisteredUserListWasUpdated'] = "Liste des utilisateurs inscrits mise à jour"; +$strings['RegisteredUsers'] = "Utilisateurs inscrits"; +$strings['RegisterNoUser'] = "N'inscrire aucun utilisateur"; +$strings['RegisterTheseGroupMembers'] = "Inscrire les membres de ces groupes"; +$strings['ScheduleAMeeting'] = "Programmer une conférence"; +$strings['ScheduledMeeting'] = "Conférence programmée"; +$strings['ScheduledMeetings'] = "Conférences programmées"; +$strings['ScheduleTheMeeting'] = "Programmer la conférence"; +$strings['Search'] = "Rechercher"; +$strings['Session'] = "Session"; +$strings['StartDate'] = "Date de début"; +$strings['Started'] = "démarrée"; +$strings['StartInstantMeeting'] = "Démarrer une conférence instantanée"; +$strings['StartMeeting'] = "Démarrer la conférence"; +$strings['StartTime'] = "Heure de début"; +$strings['Topic'] = "Objet"; +$strings['TopicAndAgenda'] = "Objet et ordre du jour"; +$strings['Type'] = "Type"; +$strings['UpcomingMeeting'] = "Conférences à venir"; +$strings['UpdateMeeting'] = "Mettre à jour la conférence"; +$strings['UpdateRegisteredUserList'] = "Mettre à jour la liste des utilisateurs inscrits"; +$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['ZoomVideoConferences'] = "Conférences vidéo Zoom"; diff --git a/plugin/zoom/lang/spanish.php b/plugin/zoom/lang/spanish.php new file mode 100644 index 0000000000..a90a0ab0f0 --- /dev/null +++ b/plugin/zoom/lang/spanish.php @@ -0,0 +1,106 @@ +API Key)"; +$strings['apiSecret'] = "Código secreto de API (API Secret)"; +$strings['enableParticipantRegistration'] = "Activar la inscripción de participantes"; +$strings['enableCloudRecording'] = "Activar la grabación en los servidores de Zoom"; +$strings['enableGlobalConference'] = "Activar las conferencias globales"; +$strings['enableGlobalConferencePerUser'] = "Activar las conferencias globales por usuario"; +$strings['globalConferenceAllowRoles'] = "Visibilidad del enlace global de videoconferencia para los perfiles siguientes"; + +$strings['tool_enable_help'] = "Escoja si desea activar la herramienta Zoom. +Una vez activada, aparecerá en las páginas principales de todos los cursos. Los profesores podrán +iniciar una conferencia y los alumnos juntarse a ella. +
+Este plugin requiere una cuenta Zoom para gestionar las conferencias. +El API de Zoom utiliza los JSON Web Tokens (JWT) para autorizar el acceso a una cuenta. +Una clave y un código secreto de API son necesarios para identificarse con JWT. +Para obtenerlos, crea una app JWT : +
1. logéase en Su perfil Zoom +
2. de clic en Avanzado / Marketplace de aplicaciones +
3. de clic en Develop / build App +
4. escoja JWT / Create +
5. ingrese algunas informaciones sobre vuestra \"App\" +(nombres de la aplicación, de la empresa, nombre y dirección de correo de contacto) +
6. de clic en Continue +
La página App Credentials muestra la clave (API Key) y el código secreto (API Secret) por ingresar aquí. +
+Atención : +
Zoom NO ES un software libre, y reglas específicas de protección de datos se aplican a este. +Por favor verifique con Zoom que éstas le den satisfacción a Usted y los alumnos que la usarán."; + +$strings['enableParticipantRegistration_help'] = "Requiere un perfil Zoom de pago. +No funcionará para un perfil base/gratuito."; + +$strings['enableCloudRecording_help'] = "Requiere un perfil Zoom de pago. +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['CopyingJoinURL'] = "Copia de la URL para ingresar"; +$strings['CopyJoinAsURL'] = "Copiar la URL para 'ingresar como'"; +$strings['CopyToCourse'] = "Copiar en el curso"; +$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['DeleteMeeting'] = "Borrar la conferencia"; +$strings['DeleteFile'] = "Borrar este/estos archivo(s)"; +$strings['Details'] = "Detalle"; +$strings['DoIt'] = "Hágalo"; +$strings['Duration'] = "Duración"; +$strings['DurationFormat'] = "%hh%I"; +$strings['DurationInMinutes'] = "Duración (en minutos)"; +$strings['EndDate'] = "Fecha de fin"; +$strings['Files'] = "Archivos"; +$strings['Finished'] = "terminada"; +$strings['FileWasCopiedToCourse'] = "El archivo ha sido copiado en el curso"; +$strings['FileWasDeleted'] = "El archivo ha sido borrado"; +$strings['InstantMeeting'] = "Conferencia instantánea"; +$strings['Join'] = "Ingresar"; +$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)"; +$strings['LiveMeetings'] = "Conferencias activas"; +$strings['LinkToFileWasCreatedInCourse'] = "Un enlace al archivo ha sido añadido al curso"; +$strings['MeetingDeleted'] = "Conferencia borrada"; +$strings['MeetingsFound'] = "Conferencias encontradas"; +$strings['MeetingUpdated'] = "Conferencias actualizadas"; +$strings['NewMeetingCreated'] = "Nueva conferencia creada"; +$strings['Password'] = "Contraseña"; +$strings['RecurringWithFixedTime'] = "Recurrente, a una hora fija"; +$strings['RecurringWithNoFixedTime'] = "Recurrente, sin hora fija"; +$strings['RegisterAllCourseUsers'] = "Inscribir todos los usuarios del curso"; +$strings['RegisteredUserListWasUpdated'] = "Lista de usuarios inscritos actualizada"; +$strings['RegisteredUsers'] = "Usuarios inscritos"; +$strings['RegisterNoUser'] = "No inscribir ningún usuario"; +$strings['RegisterTheseGroupMembers'] = "Inscribir los miembros de estos grupos"; +$strings['ScheduleAMeeting'] = "Programar una conferencia"; +$strings['ScheduledMeeting'] = "Conferencia programada"; +$strings['ScheduledMeetings'] = "Conferencias programadas"; +$strings['ScheduleTheMeeting'] = "Programar la conferencia"; +$strings['Search'] = "Buscar"; +$strings['Session'] = "Sesión"; +$strings['StartDate'] = "Fecha de inicio"; +$strings['Started'] = "iniciada"; +$strings['StartInstantMeeting'] = "Iniciar una conferencia instantánea"; +$strings['StartMeeting'] = "Iniciar la conferencia"; +$strings['StartTime'] = "Hora de inicio"; +$strings['Topic'] = "Objeto"; +$strings['TopicAndAgenda'] = "Objeto y orden del día"; +$strings['Type'] = "Tipo"; +$strings['UpcomingMeeting'] = "Próximas conferencias"; +$strings['UpdateMeeting'] = "Actualizar la conferencia"; +$strings['UpdateRegisteredUserList'] = "Actualizar la lista de usuarios inscritos"; +$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['ZoomVideoConferences'] = "Videoconferencias Zoom"; diff --git a/plugin/zoom/lib/API/BaseMeetingTrait.php b/plugin/zoom/lib/API/BaseMeetingTrait.php new file mode 100644 index 0000000000..d1b0d952da --- /dev/null +++ b/plugin/zoom/lib/API/BaseMeetingTrait.php @@ -0,0 +1,31 @@ +token = JWT::encode( + [ + 'iss' => $apiKey, + 'exp' => (time() + 60) * 1000, // will expire in one minute + ], + $apiSecret + ); + } + + /** + * {@inheritdoc} + */ + public function send($httpMethod, $relativePath, $parameters = [], $requestBody = null) + { + $options = [ + CURLOPT_CUSTOMREQUEST => $httpMethod, + CURLOPT_ENCODING => '', + CURLOPT_HTTPHEADER => [ + 'authorization: Bearer '.$this->token, + 'content-type: application/json', + ], + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_MAXREDIRS => 10, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + ]; + if (!is_null($requestBody)) { + $jsonRequestBody = json_encode($requestBody); + if (false === $jsonRequestBody) { + throw new Exception('Could not generate JSON request body'); + } + $options[CURLOPT_POSTFIELDS] = $jsonRequestBody; + } + + $url = "https://api.zoom.us/v2/$relativePath"; + if (!empty($parameters)) { + $url .= '?'.http_build_query($parameters); + } + $curl = curl_init($url); + if (false === $curl) { + throw new Exception("curl_init returned false"); + } + curl_setopt_array($curl, $options); + $responseBody = curl_exec($curl); + $responseCode = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); + $curlError = curl_error($curl); + curl_close($curl); + + if ($curlError) { + throw new Exception("cURL Error: $curlError"); + } + + if (false === $responseBody || !is_string($responseBody)) { + throw new Exception('cURL Error'); + } + + if (empty($responseCode) + || $responseCode < 200 + || $responseCode >= 300 + ) { + throw new Exception($responseBody, $responseCode); + } + + return $responseBody; + } +} diff --git a/plugin/zoom/lib/API/JsonDeserializableTrait.php b/plugin/zoom/lib/API/JsonDeserializableTrait.php new file mode 100644 index 0000000000..203938d48e --- /dev/null +++ b/plugin/zoom/lib/API/JsonDeserializableTrait.php @@ -0,0 +1,109 @@ + $value) { + if (property_exists($destination, $name)) { + if (is_object($value)) { + if (is_object($destination->$name)) { + static::recursivelyCopyObjectProperties($value, $destination->$name); + } else { + throw new Exception("Source property $name is an object, which is not expected"); + } + } elseif (is_array($value)) { + if (is_array($destination->$name)) { + $itemClass = $destination->itemClass($name); + foreach ($value as $sourceItem) { + if ('string' === $itemClass) { + $destination->$name[] = $sourceItem; + } else { + $item = new $itemClass(); + static::recursivelyCopyObjectProperties($sourceItem, $item); + $destination->$name[] = $item; + } + } + } else { + throw new Exception("Source property $name is an array, which is not expected"); + } + } else { + $destination->$name = $value; + } + } else { + throw new Exception("Source object has property $name, which was not expected."); + } + } + $destination->initializeExtraProperties(); + } +} diff --git a/plugin/zoom/lib/API/Meeting.php b/plugin/zoom/lib/API/Meeting.php new file mode 100644 index 0000000000..c26c24d92d --- /dev/null +++ b/plugin/zoom/lib/API/Meeting.php @@ -0,0 +1,89 @@ +tracking_fields = []; + $this->settings = new MeetingSettings(); + } + + /** + * {@inheritdoc} + */ + public function itemClass($propertyName) + { + if ('tracking_fields' === $propertyName) { + return TrackingField::class; + } + throw new Exception("no such array property $propertyName"); + } + + /** + * 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) + { + return MeetingInfoGet::fromJson($client->send('POST', 'users/me/meetings', [], $this)); + } + + /** + * Creates a Meeting instance from a topic. + * + * @param string $topic + * @param int $type + * + * @throws Exception + * + * @return static + */ + protected static function fromTopicAndType($topic, $type = self::TYPE_SCHEDULED) + { + $instance = new static(); + $instance->topic = $topic; + $instance->type = $type; + + return $instance; + } +} diff --git a/plugin/zoom/lib/API/MeetingInfo.php b/plugin/zoom/lib/API/MeetingInfo.php new file mode 100644 index 0000000000..58bbd57b25 --- /dev/null +++ b/plugin/zoom/lib/API/MeetingInfo.php @@ -0,0 +1,35 @@ +send('GET', "meetings/$id")); + } + + /** + * Updates the meeting on server. + * + * @param Client $client + * + * @throws Exception + */ + public function update($client) + { + $client->send('PATCH', 'meetings/'.$this->id, [], $this); + } + + /** + * Ends the meeting on server. + * + * @param Client $client + * + * @throws Exception + */ + public function endNow($client) + { + $client->send('PUT', "meetings/$this->id/status", [], (object) ['action' => 'end']); + } + + /** + * Deletes the meeting on server. + * + * @param Client $client + * + * @throws Exception + */ + public function delete($client) + { + $client->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 + * (at least while using profile "Pro") + * @param string $occurrenceIds separated by comma + * + * @throws Exception + * + * @return CreatedRegistration with unique join_url and registrant_id properties + */ + public function addRegistrant($client, $registrant, $occurrenceIds = '') + { + return CreatedRegistration::fromJson( + $client->send( + 'POST', + "meetings/$this->id/registrants", + empty($occurrenceIds) ? [] : ['occurrence_ids' => $occurrenceIds], + $registrant + ) + ); + } + + /** + * 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 = '') + { + if (!empty($registrants)) { + $client->send( + 'PUT', + "meetings/$this->id/registrants/status", + empty($occurrenceIds) ? [] : ['occurrence_ids' => $occurrenceIds], + (object) [ + 'action' => 'cancel', + 'registrants' => $registrants, + ] + ); + } + } + + /** + * Retrieves meeting registrants. + * + * @param Client $client + * + * @throws Exception + * + * @return MeetingRegistrantListItem[] the meeting registrants + */ + public function getRegistrants($client) + { + return MeetingRegistrantList::loadMeetingRegistrants($client, $this->id); + } + + /** + * Retrieves the meeting's instances. + * + * @param Client $client + * + * @throws Exception + * + * @return MeetingInstance[] + */ + public function getInstances($client) + { + return MeetingInstances::fromMeetingId($client, $this->id)->meetings; + } +} diff --git a/plugin/zoom/lib/API/MeetingInstance.php b/plugin/zoom/lib/API/MeetingInstance.php new file mode 100644 index 0000000000..8be93df7c0 --- /dev/null +++ b/plugin/zoom/lib/API/MeetingInstance.php @@ -0,0 +1,53 @@ +send('GET', 'meetings/'.htmlentities($this->uuid).'/recordings')); + } + + /** + * Retrieves the instance's participants. + * + * @param Client $client + * + * @throws Exception + * + * @return ParticipantListItem[] + */ + public function getParticipants($client) + { + return ParticipantList::loadInstanceParticipants($client, $this->uuid); + } +} diff --git a/plugin/zoom/lib/API/MeetingInstances.php b/plugin/zoom/lib/API/MeetingInstances.php new file mode 100644 index 0000000000..3f1ceaafc7 --- /dev/null +++ b/plugin/zoom/lib/API/MeetingInstances.php @@ -0,0 +1,55 @@ +meetings = []; + } + + /** + * 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) + { + return static::fromJson($client->send('GET', "past_meetings/$meetingId/instances")); + } + + /** + * {@inheritdoc} + */ + public function itemClass($propertyName) + { + if ('meetings' === $propertyName) { + return MeetingInstance::class; + } + throw new Exception("No such array property $propertyName"); + } +} diff --git a/plugin/zoom/lib/API/MeetingList.php b/plugin/zoom/lib/API/MeetingList.php new file mode 100644 index 0000000000..50d6f4def9 --- /dev/null +++ b/plugin/zoom/lib/API/MeetingList.php @@ -0,0 +1,60 @@ +meetings = []; + } + + /** + * 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) + { + return static::loadItems('meetings', $client, 'users/me/meetings', ['type' => $type]); + } + + /** + * {@inheritdoc} + */ + public function itemClass($propertyName) + { + if ('meetings' === $propertyName) { + return MeetingListItem::class; + } + throw new Exception("No such array property $propertyName"); + } +} diff --git a/plugin/zoom/lib/API/MeetingListItem.php b/plugin/zoom/lib/API/MeetingListItem.php new file mode 100644 index 0000000000..92f58845dc --- /dev/null +++ b/plugin/zoom/lib/API/MeetingListItem.php @@ -0,0 +1,45 @@ + value */ + public $custom_questions; + + /** + * MeetingRegistrant constructor. + */ + public function __construct() + { + $this->custom_questions = []; + } + + /** + * @param string $email + * @param string $firstName + * + * @return MeetingRegistrant + */ + public static function fromEmailAndFirstName($email, $firstName) + { + $instance = new static(); + $instance->first_name = $firstName; + $instance->email = $email; + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function itemClass($propertyName) + { + if ('custom_questions' == $propertyName) { + return CustomQuestion::class; + } + throw new Exception("no such array property $propertyName"); + } +} diff --git a/plugin/zoom/lib/API/MeetingRegistrantList.php b/plugin/zoom/lib/API/MeetingRegistrantList.php new file mode 100644 index 0000000000..5bc4cf75c0 --- /dev/null +++ b/plugin/zoom/lib/API/MeetingRegistrantList.php @@ -0,0 +1,55 @@ +registrants = []; + } + + /** + * 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) + { + return static::loadItems('registrants', $client, "meetings/$meetingId/registrants"); + } + + /** + * {@inheritdoc} + */ + public function itemClass($propertyName) + { + if ('registrants' === $propertyName) { + return MeetingRegistrantListItem::class; + } + throw new Exception("no such array property $propertyName"); + } +} diff --git a/plugin/zoom/lib/API/MeetingRegistrantListItem.php b/plugin/zoom/lib/API/MeetingRegistrantListItem.php new file mode 100644 index 0000000000..a852fea80c --- /dev/null +++ b/plugin/zoom/lib/API/MeetingRegistrantListItem.php @@ -0,0 +1,30 @@ +global_dial_in_countries = []; + $this->global_dial_in_numbers = []; + } + + /** + * {@inheritdoc} + */ + public function itemClass($propertyName) + { + if ('global_dial_in_countries' === $propertyName) { + return 'string'; + } + if ('global_dial_in_numbers' === $propertyName) { + return GlobalDialInNumber::class; + } + throw new Exception("No such array property $propertyName"); + } +} diff --git a/plugin/zoom/lib/API/Pagination.php b/plugin/zoom/lib/API/Pagination.php new file mode 100644 index 0000000000..14a42163b5 --- /dev/null +++ b/plugin/zoom/lib/API/Pagination.php @@ -0,0 +1,70 @@ +send( + 'GET', + $relativePath, + array_merge(['page_size' => $pageSize, 'page_number' => $pageNumber], $parameters) + ) + ); + $items = array_merge($items, $response->$arrayPropertyName); + if (0 === $totalRecords) { + $pageCount = $response->page_count; + $pageSize = $response->page_size; + $totalRecords = $response->total_records; + } + } + if (count($items) !== $totalRecords) { + error_log('Zoom announced '.$totalRecords.' records but returned '.count($items)); + } + + return $items; + } +} diff --git a/plugin/zoom/lib/API/PaginationToken.php b/plugin/zoom/lib/API/PaginationToken.php new file mode 100644 index 0000000000..3194f45a27 --- /dev/null +++ b/plugin/zoom/lib/API/PaginationToken.php @@ -0,0 +1,73 @@ +send( + 'GET', + $relativePath, + array_merge(['page_size' => $pageSize, 'next_page_token' => $nextPageToken], $parameters) + ) + ); + $items = array_merge($items, $response->$arrayPropertyName); + $nextPageToken = $response->next_page_token; + if (0 === $totalRecords) { + $pageSize = $response->page_size; + $totalRecords = $response->total_records; + } + } while (!empty($nextPagetoken)); + if (count($items) !== $totalRecords) { + error_log('Zoom announced '.$totalRecords.' records but returned '.count($items)); + } + + return $items; + } +} diff --git a/plugin/zoom/lib/API/ParticipantList.php b/plugin/zoom/lib/API/ParticipantList.php new file mode 100644 index 0000000000..10b9107401 --- /dev/null +++ b/plugin/zoom/lib/API/ParticipantList.php @@ -0,0 +1,60 @@ +participants = []; + } + + /** + * Retrieves a meeting instance's participants. + * + * @param Client $client + * @param string $instanceUUID + * + * @throws Exception + * + * @return ParticipantListItem[] participants + */ + public static function loadInstanceParticipants($client, $instanceUUID) + { + return static::loadItems( + 'participants', + $client, + 'past_meetings/'.htmlentities($instanceUUID).'/participants' + ); + } + + /** + * {@inheritdoc} + */ + public function itemClass($propertyName) + { + if ('participants' === $propertyName) { + return ParticipantListItem::class; + } + throw new Exception("No such array property $propertyName"); + } +} diff --git a/plugin/zoom/lib/API/ParticipantListItem.php b/plugin/zoom/lib/API/ParticipantListItem.php new file mode 100644 index 0000000000..4a8e5fc391 --- /dev/null +++ b/plugin/zoom/lib/API/ParticipantListItem.php @@ -0,0 +1,23 @@ +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) + { + return ParticipantList::loadInstanceParticipants($client, $this->uuid); + } +} diff --git a/plugin/zoom/lib/API/RecordingFile.php b/plugin/zoom/lib/API/RecordingFile.php new file mode 100644 index 0000000000..f725da8518 --- /dev/null +++ b/plugin/zoom/lib/API/RecordingFile.php @@ -0,0 +1,118 @@ + + * `MP4`: Video file of the recording.
+ * `M4A` Audio-only file of the recording.
+ * `TIMELINE`: Timestamp file of the recording. + * To get a timeline file, the "Add a timestamp to the recording" setting must be enabled in the recording settings + * (https://support.zoom.us/hc/en-us/articles/203741855-Cloud-recording#h_3f14c3a4-d16b-4a3c-bbe5-ef7d24500048). + * The time will display in the host's timezone, set on their Zoom profile. + * `TRANSCRIPT`: Transcription file of the recording. + * `CHAT`: A TXT file containing in-meeting chat messages that were sent during the meeting. + * `CC`: File containing closed captions of the recording. + * A recording file object with file type of either `CC` or `TIMELINE` **does not have** the following properties: + * `id`, `status`, `file_size`, `recording_type`, and `play_url`. + */ + public $file_type; + + /** @var int The recording file size. */ + public $file_size; + + /** @var string The URL using which a recording file can be played. */ + public $play_url; + + /** @var string The URL using which the recording file can be downloaded. + * To access a private or password protected cloud recording, you must use a [Zoom JWT App Type] + * (https://marketplace.zoom.us/docs/guides/getting-started/app-types/create-jwt-app). + * Use the generated JWT token as the value of the `access_token` query parameter + * and include this query parameter at the end of the URL as shown in the example. + * Example: `https://api.zoom.us/recording/download/{{ Download Path }}?access_token={{ JWT Token }}` + */ + public $download_url; + + /** @var string The recording status. "completed". */ + public $status; + + /** @var string The time at which recording was deleted. Returned in the response only for trash query. */ + public $deleted_time; + + /** @var string The recording type. The value of this field can be one of the following: + * `shared_screen_with_speaker_view(CC)` + * `shared_screen_with_speaker_view` + * `shared_screen_with_gallery_view` + * `speaker_view` + * `gallery_view` + * `shared_screen` + * `audio_only` + * `audio_transcript` + * `chat_file` + * `TIMELINE` + */ + public $recording_type; + + /** + * Builds the recording file download URL with the access_token query parameter. + * + * @see RecordingFile::$download_url + * + * @param string $token + * + * @return string full URL + */ + public function getFullDownloadURL($token) + { + return $this->download_url.'?access_token='.$token; + } + + /** + * Deletes the file. + * + * @param Client $client + * + * @throws Exception + */ + public function delete($client) + { + $client->send( + 'DELETE', + "/meetings/$this->meeting_id/recordings/$this->id", + ['action' => 'delete'] + ); + } + + /** + * {@inheritdoc} + */ + public function itemClass($propertyName) + { + throw new Exception("No such array property $propertyName"); + } +} diff --git a/plugin/zoom/lib/API/RecordingList.php b/plugin/zoom/lib/API/RecordingList.php new file mode 100644 index 0000000000..ec82442151 --- /dev/null +++ b/plugin/zoom/lib/API/RecordingList.php @@ -0,0 +1,68 @@ +meetings = []; + } + + /** + * 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 + * + * @throws Exception + * + * @return RecordingMeeting[] all recordings from that period + */ + public static function loadPeriodRecordings($client, $startDate, $endDate) + { + return static::loadItems( + 'meetings', + $client, + 'users/me/recordings', + [ + 'from' => $startDate->format('Y-m-d'), + 'to' => $endDate->format('Y-m-d'), + ] + ); + } + + /** + * {@inheritdoc} + */ + public function itemClass($propertyName) + { + if ('meetings' === $propertyName) { + return RecordingMeeting::class; + } + throw new Exception("No such array property $propertyName"); + } +} diff --git a/plugin/zoom/lib/API/RecordingMeeting.php b/plugin/zoom/lib/API/RecordingMeeting.php new file mode 100644 index 0000000000..155d4e06e5 --- /dev/null +++ b/plugin/zoom/lib/API/RecordingMeeting.php @@ -0,0 +1,91 @@ +recording_files = []; + } + + /** + * Deletes the recording on the server. + * + * @param Client $client + * + * @throws Exception + */ + public function delete($client) + { + $client->send('DELETE', 'meetings/'.htmlentities($this->uuid).'/recordings', ['action' => 'delete']); + } + + /** + * {@inheritdoc} + */ + public function itemClass($propertyName) + { + if ('recording_files' === $propertyName) { + return RecordingFile::class; + } + throw new Exception("No such array property $propertyName"); + } +} diff --git a/plugin/zoom/lib/API/TrackingField.php b/plugin/zoom/lib/API/TrackingField.php new file mode 100644 index 0000000000..931ab771cf --- /dev/null +++ b/plugin/zoom/lib/API/TrackingField.php @@ -0,0 +1,30 @@ +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 new file mode 100644 index 0000000000..3b263d4253 --- /dev/null +++ b/plugin/zoom/lib/CourseMeetingInfoGet.php @@ -0,0 +1,57 @@ +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 new file mode 100644 index 0000000000..af9a7e56ca --- /dev/null +++ b/plugin/zoom/lib/CourseMeetingList.php @@ -0,0 +1,26 @@ +decodeAndRemoveTag(); + $this->initializeDisplayableProperties(); + } +} diff --git a/plugin/zoom/lib/CourseMeetingTrait.php b/plugin/zoom/lib/CourseMeetingTrait.php new file mode 100644 index 0000000000..1e189b85b4 --- /dev/null +++ b/plugin/zoom/lib/CourseMeetingTrait.php @@ -0,0 +1,142 @@ +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 new file mode 100644 index 0000000000..5761ed4e1b --- /dev/null +++ b/plugin/zoom/lib/DisplayableMeetingTrait.php @@ -0,0 +1,74 @@ +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 new file mode 100644 index 0000000000..f3b9d64813 --- /dev/null +++ b/plugin/zoom/lib/File.php @@ -0,0 +1,28 @@ +formattedFileSize = format_file_size($this->file_size); + } +} diff --git a/plugin/zoom/lib/Recording.php b/plugin/zoom/lib/Recording.php new file mode 100644 index 0000000000..826953db3f --- /dev/null +++ b/plugin/zoom/lib/Recording.php @@ -0,0 +1,65 @@ +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/RecordingList.php b/plugin/zoom/lib/RecordingList.php new file mode 100644 index 0000000000..63b10aaac7 --- /dev/null +++ b/plugin/zoom/lib/RecordingList.php @@ -0,0 +1,51 @@ + $startDate->format('Y-m-d'), + 'to' => $endDate->format('Y-m-d'), + ] + ); + } +} diff --git a/plugin/zoom/lib/UserMeetingRegistrant.php b/plugin/zoom/lib/UserMeetingRegistrant.php new file mode 100644 index 0000000000..5604623a1e --- /dev/null +++ b/plugin/zoom/lib/UserMeetingRegistrant.php @@ -0,0 +1,51 @@ +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 new file mode 100644 index 0000000000..4f164544c9 --- /dev/null +++ b/plugin/zoom/lib/UserMeetingRegistrantList.php @@ -0,0 +1,43 @@ +decodeAndRemoveTag(); + $this->computeFullName(); + } +} diff --git a/plugin/zoom/lib/UserMeetingRegistrantTrait.php b/plugin/zoom/lib/UserMeetingRegistrantTrait.php new file mode 100644 index 0000000000..09dcdc3d5a --- /dev/null +++ b/plugin/zoom/lib/UserMeetingRegistrantTrait.php @@ -0,0 +1,89 @@ +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 new file mode 100755 index 0000000000..098e119f06 --- /dev/null +++ b/plugin/zoom/lib/zoom_plugin.class.php @@ -0,0 +1,1043 @@ + 'boolean', + 'apiKey' => 'text', + 'apiSecret' => 'text', + 'enableParticipantRegistration' => 'boolean', + 'enableCloudRecording' => 'boolean', + 'enableGlobalConference' => 'boolean', + 'enableGlobalConferencePerUser' => 'boolean', + 'globalConferenceAllowRoles' => [ + 'type' => 'select', + 'options' => [ + PLATFORM_ADMIN => get_lang('Administrator'), + COURSEMANAGER => get_lang('Teacher'), + STUDENT => get_lang('Student'), + STUDENT_BOSS => get_lang('StudentBoss'), + ], + 'attributes' => ['multiple' => 'multiple'], + ], + ] + ); + + $this->isAdminPlugin = true; + } + + /** + * Caches and returns an instance of this class. + * + * @return ZoomPlugin the instance to use + */ + public static function create() + { + static $instance = null; + + return $instance ? $instance : $instance = new self(); + } + + /** + * Creates this plugin's related data and data structure in the internal database. + */ + public function install() + { + $this->install_course_fields_in_all_courses(); + } + + /** + * Drops this plugins' related data from the internal database. + */ + public function uninstall() + { + $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'. + * + * @return FormValidator + */ + 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->addButtonSearch(get_lang('Search')); + $oneMonth = new DateInterval('P1M'); + if ($form->validate()) { + try { + $start = new DateTime($startDatePicker->getValue()); + } catch (Exception $exception) { + $start = new DateTime(); + $start->sub($oneMonth); + } + try { + $end = new DateTime($endDatePicker->getValue()); + } 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, + ]); + } catch (Exception $exception) { + error_log(join(':', [__FILE__, __LINE__, $exception])); + } + + return $form; + } + + /** + * Generates a meeting edit form and updates the meeting on validation. + * + * @param CourseMeetingInfoGet $meeting the meeting + * + * @return FormValidator + */ + public function getEditMeetingForm(&$meeting) + { + $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')); + $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->addButtonUpdate(get_lang('UpdateMeeting')); + if ($form->validate()) { + if ($withTimeAndDuration) { + $meeting->start_time = $startTimeDatePicker->getValue(); + $meeting->timezone = date_default_timezone_get(); + $meeting->duration = $durationNumeric->getValue(); + } + $meeting->topic = $topicText->getValue(); + $meeting->agenda = $agendaTextArea->getValue(); + try { + $this->updateMeeting($meeting); + Display::addFlash( + Display::return_message(get_lang('MeetingUpdated'), 'confirm') + ); + } catch (Exception $exception) { + Display::addFlash( + Display::return_message($exception->getMessage(), 'error') + ); + } + $meeting = $this->getMeeting($meeting->id); + } + $defaults = [ + 'topic' => $meeting->topic, + 'agenda' => $meeting->agenda, + ]; + if ($withTimeAndDuration) { + $defaults['start_time'] = $meeting->startDateTime->format('c'); + $defaults['duration'] = $meeting->duration; + } + $form->setDefaults($defaults); + + return $form; + } + + /** + * Generates a meeting delete form and deletes the meeting on validation. + * + * @param CourseMeetingInfoGet $meeting + * @param string $returnURL where to redirect to on successful deletion + * + * @return FormValidator + */ + public function getDeleteMeetingForm($meeting, $returnURL) + { + $form = new FormValidator('delete', 'post', $_SERVER['REQUEST_URI']); + $form->addButtonDelete(get_lang('DeleteMeeting')); + if ($form->validate()) { + try { + $this->deleteMeeting($meeting); + Display::addFlash( + Display::return_message(get_lang('MeetingDeleted'), 'confirm') + ); + location($returnURL); + } catch (Exception $exception) { + Display::addFlash( + Display::return_message($exception->getMessage(), 'error') + ); + } + } + + return $form; + } + + /** + * Generates a registrant list update form listing course and session users. + * Updates the list on validation. + * + * @param CourseMeetingInfoGet $meeting + * + * @return array a list of two elements: + * FormValidator the form + * UserMeetingRegistrantListItem[] the up-to-date list of registrants + */ + public function getRegisterParticipantForm($meeting) + { + $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(); + 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(); + $selectedUsers = []; + foreach ($users as $user) { + if (in_array($user->getId(), $selectedUserIds)) { + $selectedUsers[] = $user; + } + } + try { + $this->updateRegistrantList($meeting, $selectedUsers); + Display::addFlash( + Display::return_message(get_lang('RegisteredUserListWasUpdated'), 'confirm') + ); + } catch (Exception $exception) { + Display::addFlash( + 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; + } + $userIdSelect->setSelected($registeredUserIds); + + + return [ $form, $registrants ]; + } + + /** + * Generates a meeting recording files management form. + * Takes action on validation. + * + * @param CourseMeetingInfoGet $meeting + * + * @return array a list of two elements: + * FormValidator the form + * Recording[] the up-to-date list of recordings + */ + 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)) { + $fileIdSelect = $form->addSelect('fileIds', get_lang('Files')); + $fileIdSelect->setMultiple(true); + foreach ($recordings as &$recording) { + // $recording->instanceDetails = $plugin->getPastMeetingInstanceDetails($instance->uuid); + $options = []; + foreach ($recording->recording_files as $file) { + $options[] = [ + 'text' => sprintf( + '%s.%s (%s)', + $file->recording_type, + $file->file_type, + $file->formattedFileSize + ), + 'value' => $file->id, + ]; + } + $fileIdSelect->addOptGroup( + $options, + sprintf("%s (%s)", $recording->formattedStartTime, $recording->formattedDuration) + ); + } + $actionRadio = $form->addRadio( + 'action', + get_lang('Action'), + [ + 'CreateLinkInCourse' => get_lang('CreateLinkInCourse'), + 'CopyToCourse' => get_lang('CopyToCourse'), + 'DeleteFile' => get_lang('DeleteFile'), + ] + ); + $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())) { + $name = sprintf( + get_lang('XRecordingOfMeetingXFromXDurationXDotX'), + $file->recording_type, + $meeting->id, + $recording->formattedStartTime, + $recording->formattedDuration, + $file->file_type + ); + if ('CreateLinkInCourse' === $actionRadio->getValue()) { + try { + $this->createLinkToFileInCourse($meeting, $file, $name); + Display::addFlash( + Display::return_message(get_lang('LinkToFileWasCreatedInCourse'), 'success') + ); + } catch (Exception $exception) { + Display::addFlash( + Display::return_message($exception->getMessage(), 'error') + ); + } + } elseif ('CopyToCourse' === $actionRadio->getValue()) { + try { + $this->copyFileToCourse($meeting, $file, $name); + Display::addFlash( + Display::return_message(get_lang('FileWasCopiedToCourse'), 'confirm') + ); + } catch (Exception $exception) { + Display::addFlash( + Display::return_message($exception->getMessage(), 'error') + ); + } + } elseif ('DeleteFile' === $actionRadio->getValue()) { + try { + $this->deleteFile($file); + Display::addFlash( + Display::return_message(get_lang('FileWasDeleted'), 'confirm') + ); + } catch (Exception $exception) { + Display::addFlash( + Display::return_message($exception->getMessage(), 'error') + ); + } + } + } + } + } + try { + $recordings = $this->getMeetingRecordings($meeting); + } catch (Exception $exception) { + Display::addFlash( + Display::return_message($exception->getMessage(), 'error') + ); + $recordings = []; + } + } + } + + return [$form, $recordings]; + } + + /** + * Generates a form to fast and easily create and start an instant meeting. + * On validation, create it then redirect to it and exit. + * + * @return FormValidator + */ + public function getCreateInstantMeetingForm() + { + $form = new FormValidator('createInstantMeetingForm', 'post', '', '_blank'); + $form->addButton('startButton', get_lang('StartInstantMeeting')); + + if ($form->validate()) { + try { + $newInstantMeeting = $this->createInstantMeeting(); + location($newInstantMeeting->start_url); + } catch (Exception $exception) { + Display::addFlash( + Display::return_message($exception->getMessage(), 'error') + ); + } + } + + return $form; + } + + /** + * Generates a form to schedule a meeting. + * On validation, creates it. + * + * @throws Exception + * + * @return FormValidator + */ + public function getScheduleMeetingForm() + { + $form = new FormValidator('scheduleMeetingForm'); + $startTimeDatePicker = $form->addDateTimePicker('start_time', 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]); + // $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( + <<getAttribute('id'), + $userRegistrationRadio->getelements()[1]->getAttribute('id') + ); + + $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() + ); + 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))); + } + $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->id); + } catch (Exception $exception) { + Display::addFlash( + Display::return_message($exception->getMessage(), 'error') + ); + } + } else { + $form->setDefaults( + [ + 'duration' => 60, + 'topic' => api_get_course_info()['title'], + 'userRegistration' => 'RegisterAllCourseUsers', + ] + ); + } + + return $form; + } + + /** + * 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 + * + * @return CourseMeetingListItem[] matching meetings + */ + public function getPeriodMeetings($type, $startDate, $endDate) + { + $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 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() + { + return api_is_coach() + || api_is_platform_admin() + || api_get_course_id() && api_is_course_admin(); + } + + /** + * Retrieves a meeting. + * + * @param int $id the meeting numeric identifier + * + * @throws Exception + * + * @return CourseMeetingInfoGet + */ + public function getMeeting($id) + { + return CourseMeetingInfoGet::fromId($this->jwtClient(), $id); + } + + /** + * Retrieves a past meeting instance details. + * + * @param string $instanceUUID + * + * @throws Exception + * + * @return PastMeeting + */ + public function getPastMeetingInstanceDetails($instanceUUID) + { + return PastMeeting::fromUUID($this->jwtClient(), $instanceUUID); + } + + /** + * Retrieves all live meetings linked to current course and session. + * + * @throws Exception on API error + * + * @return CourseMeetingListItem[] matching meetings + */ + public function getLiveMeetings() + { + return $this->getMeetings(CourseMeetingList::TYPE_LIVE); + } + + /** + * 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); + } + + /** + * Retrieves all upcoming meetings linked to current course and session. + * + * @throws Exception on API error + * + * @return CourseMeetingListItem[] matching meetings + */ + public function getUpcomingMeetings() + { + return $this->getMeetings(CourseMeetingList::TYPE_UPCOMING); + } + + /** + * Creates an instant meeting and returns it. + * + * @throws Exception describing the error (message and code) + * + * @return CourseMeetingInfoGet meeting + */ + public function createInstantMeeting() + { + // default meeting topic is based on session name, course title and current date + $topic = ''; + $sessionName = api_get_session_name(); + if ($sessionName) { + $topic = $sessionName.', '; + } + $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); + } + + /** + * Schedules a meeting and returns it. + * + * @param DateTime $startTime meeting local start date-time (configure local timezone on your Zoom account) + * @param int $duration in minutes + * @param string $topic short title of the meeting, required + * @param string $agenda ordre du jour + * @param string $password meeting password + * + * @throws Exception describing the error (message and code) + * + * @return CourseMeetingInfoGet meeting + */ + public function createScheduledMeeting($startTime, $duration, $topic, $agenda = '', $password = '') + { + $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; + + return $this->createMeeting($meeting); + } + + /** + * Updates a meeting. + * + * @param CourseMeetingInfoGet $meeting the meeting with updated properties + * + * @throws Exception on API error + */ + public function updateMeeting($meeting) + { + $meeting->update($this->jwtClient()); + } + + /** + * Deletes a meeting. + * + * @param CourseMeetingInfoGet $meeting + * + * @throws Exception on API error + */ + public function deleteMeeting($meeting) + { + $meeting->delete($this->jwtClient()); + } + + /** + * Retrieves all recordings from a period of time. + * + * @param DateTime $startDate start date + * @param DateTime $endDate end date + * + * @throws Exception + * + * @return Recording[] all recordings + */ + public function getRecordings($startDate, $endDate) + { + return RecordingList::loadRecordings($this->jwtClient(), $startDate, $endDate); + } + + /** + * Retrieves a meetings instances' recordings. + * + * @param CourseMeetingInfoGet $meeting + * + * @throws Exception + * + * @return Recording[] meeting instances' recordings + */ + public function getMeetingRecordings($meeting) + { + $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; + } + + /** + * Retrieves a meeting instance's participants. + * + * @param string $instanceUUID the meeting instance UUID + * + * @throws Exception + * + * @return ParticipantListItem[] + */ + public function getParticipants($instanceUUID) + { + return ParticipantList::loadInstanceParticipants($this->jwtClient(), $instanceUUID); + } + + /** + * Retrieves a meeting's registrants. + * + * @param CourseMeetingInfoGet $meeting + * + * @throws Exception + * + * @return UserMeetingRegistrantListItem[] the meeting registrants + */ + public function getRegistrants($meeting) + { + return $meeting->getUserRegistrants($this->jwtClient()); + } + + /** + * Registers users to a meeting. + * + * @param CourseMeetingInfoGet $meeting + * @param \Chamilo\UserBundle\Entity\User[] $users + * + * @throws Exception + * + * @return CreatedRegistration[] the created registrations + */ + public function addRegistrants($meeting, $users) + { + $createdRegistrations = []; + foreach ($users as $user) { + $registrant = UserMeetingRegistrant::fromUser($user); + $registrant->tagEmail(); + $createdRegistrations[] = $meeting->addRegistrant($this->jwtClient(), $registrant); + } + + return $createdRegistrations; + } + + /** + * Removes registrants from a meeting. + * + * @param CourseMeetingInfoGet $meeting + * @param UserMeetingRegistrant[] $registrants + * + * @throws Exception + */ + public function removeRegistrants($meeting, $registrants) + { + $meeting->removeRegistrants($this->jwtClient(), $registrants); + } + + /** + * 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 + * + * @throws Exception + */ + public function updateRegistrantList($meeting, $users) + { + $registrants = $this->getRegistrants($meeting); + $usersToAdd = []; + foreach ($users as $user) { + $found = false; + foreach ($registrants as $registrant) { + if ($registrant->matches($user->getId())) { + $found = true; + break; + } + } + if (!$found) { + $usersToAdd[] = $user; + } + } + $registrantsToRemove = []; + foreach ($registrants as $registrant) { + $found = false; + foreach ($users as $user) { + if ($registrant->matches($user->getId())) { + $found = true; + break; + } + } + if (!$found) { + $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()); + } +} diff --git a/plugin/zoom/meeting.php b/plugin/zoom/meeting.php new file mode 100755 index 0000000000..999cb8f00a --- /dev/null +++ b/plugin/zoom/meeting.php @@ -0,0 +1,68 @@ + 'Videoconference Zoom', +]; + +Event::registerLog($logInfo); + +$interbreadcrumb[] = [ // used in templates + 'url' => $returnURL, + 'name' => get_lang('ZoomVideoConferences'), +]; + +if (!array_key_exists('meetingId', $_REQUEST)) { + throw new Exception('MeetingNotFound'); +} +$plugin = ZoomPlugin::create(); + +$meeting = $plugin->getMeeting($_REQUEST['meetingId']); + +$tpl = new Template($meeting->id); + +if ($plugin->userIsConferenceManager()) { + // 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('enableCloudRecording') + && 'cloud' === $meeting->settings->auto_recording + // && 'finished' === $meeting->status + ) { + list($fileForm, $recordings) = $plugin->getFileForm($meeting); + $tpl->assign('fileForm', $fileForm->returnForm()); + $tpl->assign('recordings', $recordings); + } +} elseif (MeetingSettings::APPROVAL_TYPE_NO_REGISTRATION_REQUIRED != $meeting->settings->approval_type) { + $userId = api_get_user_id(); + try { + foreach ($plugin->getRegistrants($meeting) as $registrant) { + if ($registrant->userId == $userId) { + $tpl->assign('currentUserJoinURL', $registrant->join_url); + break; + } + } + } catch (Exception $exception) { + Display::addFlash( + Display::return_message($exception->getMessage(), 'error') + ); + } +} +$tpl->assign('meeting', $meeting); +$tpl->assign('content', $tpl->fetch('zoom/view/meeting.tpl')); +$tpl->display_one_col_template(); diff --git a/plugin/zoom/meeting_from_admin.php b/plugin/zoom/meeting_from_admin.php new file mode 100644 index 0000000000..c06b135dff --- /dev/null +++ b/plugin/zoom/meeting_from_admin.php @@ -0,0 +1,13 @@ +get_info(); diff --git a/plugin/zoom/start.php b/plugin/zoom/start.php new file mode 100755 index 0000000000..90b5374ada --- /dev/null +++ b/plugin/zoom/start.php @@ -0,0 +1,41 @@ + 'Videoconference Zoom', +]; + +Event::registerLog($logInfo); + +$tool_name = get_lang('ZoomVideoconferences'); +$tpl = new Template($tool_name); + +$plugin = ZoomPlugin::create(); + +if ($plugin->userIsConferenceManager()) { + // user can create a new meeting + $tpl->assign('createInstantMeetingForm', $plugin->getCreateInstantMeetingForm()->returnForm()); + $tpl->assign('scheduleMeetingForm', $plugin->getScheduleMeetingForm()->returnForm()); +} + +try { + $tpl->assign('scheduledMeetings', $plugin->getScheduledMeetings()); +} catch (Exception $exception) { + Display::addFlash( + Display::return_message('Could not retrieve scheduled meeting list: '.$exception->getMessage(), 'error') + ); +} + +$tpl->assign('content', $tpl->fetch('zoom/view/start.tpl')); +$tpl->display_one_col_template(); diff --git a/plugin/zoom/uninstall.php b/plugin/zoom/uninstall.php new file mode 100755 index 0000000000..d6ef421e77 --- /dev/null +++ b/plugin/zoom/uninstall.php @@ -0,0 +1,8 @@ +uninstall(); diff --git a/plugin/zoom/view/admin.tpl b/plugin/zoom/view/admin.tpl new file mode 100644 index 0000000000..00745df5e0 --- /dev/null +++ b/plugin/zoom/view/admin.tpl @@ -0,0 +1,55 @@ +{{ search_form }} + + + + + + + + + {% if recordings %} + + {% endif %} + + + + + {% for meeting in meetings %} + + + + + + {% if recordings %} + + {% endif %} + + + {% endfor %} + +
{{ 'MeetingsFound'|get_lang }}
{{ 'StartTime'|get_lang }}{{ 'Course'|get_lang }}{{ 'Session'|get_lang }}{{ 'Topic'|get_lang }}{{ 'Recordings'|get_lang }}
{{ meeting.formattedStartTime }}{{ meeting.course ? meeting.course.title : '-' }}{{ meeting.session ? meeting.session.name : '-' }}{{ meeting.topic }} + {% for recording in recordings %} + {% if recording.id == meeting.id %} +
+
+ {{ recording.formattedStartTime }} + ({{ recording.formattedDuration }}) +
+
+
    + {% for file in recording.recording_files %} +
  • + {{ file.recording_type }}.{{ file.file_type }} + ({{ file.formattedFileSize }}) +
  • + {% endfor %} +
+
+
+ {% endif %} + {% endfor %} +
+ + {{ 'Details'|get_lang }} + +
\ No newline at end of file diff --git a/plugin/zoom/view/meeting.tpl b/plugin/zoom/view/meeting.tpl new file mode 100644 index 0000000000..806a4a5d86 --- /dev/null +++ b/plugin/zoom/view/meeting.tpl @@ -0,0 +1,78 @@ +

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

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

+ + {{ 'StartMeeting'|get_lang }} + +

+{% endif %} + +{% if currentUserJoinURL %} +

+ + {{ 'JoinMeetingAsMyself'|get_lang }} + +

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

+ +

+{% endif %} + + +{% if isConferenceManager %} + +{{ editMeetingForm }} +{{ deleteMeetingForm }} +{{ registerParticipantForm }} +{{ fileForm }} +{% if registrants and meeting.settings.approval_type != 2 %} + + +{% endif %} + +{% else %} + +

{{ meeting.topic }}

+{% if meeting.agenda %} +
{{ meeting.agenda| nl2br }}
+{% endif %} + +{% if meeting.type == 2 or meeting.type == 8 %} +
+
{{ 'StartTime'|get_lang }}
+
{{ meeting.formattedStartTime }}
+ +
{{ 'Duration'|get_lang }}
+
{{ meeting.formattedDuration }}
+
+{% endif %} + +{% endif %} + diff --git a/plugin/zoom/view/start.tpl b/plugin/zoom/view/start.tpl new file mode 100644 index 0000000000..8a5d00dd46 --- /dev/null +++ b/plugin/zoom/view/start.tpl @@ -0,0 +1,44 @@ +{% if createInstantMeetingForm %} +{{ createInstantMeetingForm }} +{% endif %} +{% if scheduledMeetings %} + + + + + + + + + + + {% for meeting in scheduledMeetings %} + + + + + + + + + {% endfor %} +
{{ 'StartTime'|get_lang }}{{ 'Duration'|get_lang }}{{ 'TopicAndAgenda'|get_lang }}
{{ meeting.formattedStartTime }}{{ meeting.formattedDuration }} + {{ meeting.topic }} +

{{ meeting.agenda|nl2br }}

+
+ + {{ 'Details'|get_lang }} + + + {{ 'Join'|get_lang }} + +
+{% else %} + +{% endif %} +{% if scheduleMeetingForm %} +

{{ 'ScheduleAMeeting'|get_lang }}

+{{ scheduleMeetingForm }} +{% endif %} \ No newline at end of file