Merge pull request #3383 from sebastiendu/17288-plugin-zoom-global-user-meetings

Plugin: Zoom: global and user Zoom meetings - refs BT#17288
pull/3393/head
Yannick Warnier 5 years ago committed by GitHub
commit 1cd9918920
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      main/inc/lib/userportal.lib.php
  2. 508
      plugin/zoom/Entity/MeetingEntity.php
  3. 185
      plugin/zoom/Entity/RecordingEntity.php
  4. 260
      plugin/zoom/Entity/RegistrantEntity.php
  5. 70
      plugin/zoom/README.code.md
  6. 36
      plugin/zoom/README.md
  7. 9
      plugin/zoom/admin.php
  8. 100
      plugin/zoom/endpoint.php
  9. 12
      plugin/zoom/global.php
  10. 39
      plugin/zoom/join_meeting.php
  11. 30
      plugin/zoom/lang/english.php
  12. 29
      plugin/zoom/lang/french.php
  13. 30
      plugin/zoom/lang/spanish.php
  14. 27
      plugin/zoom/lib/API/Client.php
  15. 3
      plugin/zoom/lib/API/JWTClient.php
  16. 16
      plugin/zoom/lib/API/JsonDeserializableTrait.php
  17. 10
      plugin/zoom/lib/API/Meeting.php
  18. 47
      plugin/zoom/lib/API/MeetingInfoGet.php
  19. 14
      plugin/zoom/lib/API/MeetingInstance.php
  20. 7
      plugin/zoom/lib/API/MeetingInstances.php
  21. 7
      plugin/zoom/lib/API/MeetingList.php
  22. 7
      plugin/zoom/lib/API/MeetingRegistrant.php
  23. 7
      plugin/zoom/lib/API/MeetingRegistrantList.php
  24. 3
      plugin/zoom/lib/API/MeetingSettings.php
  25. 5
      plugin/zoom/lib/API/Pagination.php
  26. 5
      plugin/zoom/lib/API/PaginationToken.php
  27. 4
      plugin/zoom/lib/API/ParticipantList.php
  28. 11
      plugin/zoom/lib/API/PastMeeting.php
  29. 6
      plugin/zoom/lib/API/RecordingFile.php
  30. 4
      plugin/zoom/lib/API/RecordingList.php
  31. 10
      plugin/zoom/lib/API/RecordingMeeting.php
  32. 69
      plugin/zoom/lib/CourseMeeting.php
  33. 57
      plugin/zoom/lib/CourseMeetingInfoGet.php
  34. 26
      plugin/zoom/lib/CourseMeetingList.php
  35. 31
      plugin/zoom/lib/CourseMeetingListItem.php
  36. 142
      plugin/zoom/lib/CourseMeetingTrait.php
  37. 74
      plugin/zoom/lib/DisplayableMeetingTrait.php
  38. 28
      plugin/zoom/lib/File.php
  39. 144
      plugin/zoom/lib/MeetingEntityRepository.php
  40. 65
      plugin/zoom/lib/Recording.php
  41. 69
      plugin/zoom/lib/RecordingEntityRepository.php
  42. 51
      plugin/zoom/lib/RecordingList.php
  43. 34
      plugin/zoom/lib/RegistrantEntityRepository.php
  44. 51
      plugin/zoom/lib/UserMeetingRegistrant.php
  45. 43
      plugin/zoom/lib/UserMeetingRegistrantList.php
  46. 30
      plugin/zoom/lib/UserMeetingRegistrantListItem.php
  47. 89
      plugin/zoom/lib/UserMeetingRegistrantTrait.php
  48. 1142
      plugin/zoom/lib/zoom_plugin.class.php
  49. 31
      plugin/zoom/meeting.php
  50. 13
      plugin/zoom/meeting_from_user.php
  51. 22
      plugin/zoom/start.php
  52. 32
      plugin/zoom/user.php
  53. 18
      plugin/zoom/view/admin.tpl
  54. 28
      plugin/zoom/view/meeting.tpl
  55. 6
      plugin/zoom/view/start.tpl

@ -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)

@ -0,0 +1,508 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\CourseRelUser;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CoreBundle\Entity\SessionRelCourseRelUser;
use Chamilo\PluginBundle\Zoom\API\MeetingInfoGet;
use Chamilo\PluginBundle\Zoom\API\MeetingListItem;
use Chamilo\PluginBundle\Zoom\API\MeetingSettings;
use Chamilo\UserBundle\Entity\User;
use Database;
use DateInterval;
use DateTime;
use DateTimeZone;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Exception;
/**
* Class MeetingEntity.
*
* @package Chamilo\PluginBundle\Zoom
* @ORM\Entity(repositoryClass="Chamilo\PluginBundle\Zoom\MeetingEntityRepository")
* @ORM\Table(
* name="plugin_zoom_meeting",
* indexes={
* @ORM\Index(name="user_id_index", columns={"user_id"}),
* @ORM\Index(name="course_id_index", columns={"course_id"}),
* @ORM\Index(name="session_id_index", columns={"session_id"})
* }
* )
* @ORM\HasLifecycleCallbacks
*/
class MeetingEntity
{
/** @var string meeting type name */
public $typeName;
/** @var DateTime meeting start time as a DateTime instance */
public $startDateTime;
/** @var string meeting formatted start time */
public $formattedStartTime;
/** @var DateInterval meeting duration as a DateInterval instance */
public $durationInterval;
/** @var string meeting formatted duration */
public $formattedDuration;
/** @var string */
public $statusName;
/**
* @var int the remote zoom meeting identifier
* @ORM\Column(type="bigint")
* @ORM\Id
*/
private $id;
/**
* @var User
* @ORM\ManyToOne(
* targetEntity="Chamilo\UserBundle\Entity\User",
* )
* @ORM\JoinColumn(name="user_id", nullable=true)
*/
private $user;
/**
* @var Course
* @ORM\ManyToOne(
* targetEntity="Chamilo\CoreBundle\Entity\Course",
* )
* @ORM\JoinColumn(name="course_id", nullable=true)
*/
private $course;
/**
* @var Session
* @ORM\ManyToOne(
* targetEntity="Chamilo\CoreBundle\Entity\Session",
* )
* @ORM\JoinColumn(name="session_id", nullable=true)
*/
private $session;
/**
* @var string
* @ORM\Column(type="text", name="meeting_list_item_json", nullable=true)
*/
private $meetingListItemJson;
/** @var MeetingListItem */
private $meetingListItem;
/**
* @var string
* @ORM\Column(type="text", name="meeting_info_get_json", nullable=true)
*/
private $meetingInfoGetJson;
/** @var MeetingInfoGet */
private $meetingInfoGet;
/**
* @var RegistrantEntity[]|ArrayCollection
* @ORM\OneToMany(
* targetEntity="RegistrantEntity",
* mappedBy="meeting",
* cascade={"persist", "remove"}
* )
*/
private $registrants;
/**
* @var RecordingEntity[]|ArrayCollection
* @ORM\OneToMany(
* targetEntity="RecordingEntity",
* mappedBy="meeting",
* )
*/
private $recordings;
public function __construct()
{
$this->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);
}
public function setStatus($status)
{
$this->meetingInfoGet->status = $status;
}
/**
* 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()
{
/** @var User[] $users */
$users = [];
if (!$this->isCourseMeeting()) {
$users = Database::getManager()->getRepository('ChamiloUserBundle:User')->findBy(['active' => true]);
} elseif (is_null($this->session)) {
if (!is_null($this->course)) {
/** @var CourseRelUser $courseRelUser */
foreach ($this->course->getUsers() as $courseRelUser) {
$users[] = $courseRelUser->getUser();
}
}
} 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();
}
}
}
}
$activeUsersWithEmail = [];
foreach ($users as $user) {
if ($user->isActive() && !empty($user->getEmail())) {
$activeUsersWithEmail[] = $user;
}
}
return $activeUsersWithEmail;
}
/**
* @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)
{
return $this->getRegistrants()->exists(
function (RegistrantEntity $registrantEntity) use (&$user) {
return $registrantEntity->getUser() === $user;
}
);
}
/**
* @param User $user
*
* @return RegistrantEntity|null
*/
public function getRegistrant($user)
{
foreach ($this->getRegistrants() as $registrant) {
if ($registrant->getUser() === $user) {
return $registrant;
}
}
return null;
}
/**
* Generates a short presentation of the meeting for the future participant.
* To be displayed above the "Enter meeting" link.
*
* @return string
*/
public function getIntroduction()
{
$introduction = sprintf('<h1>%s</h1>', $this->meetingInfoGet->topic);
if (!$this->isGlobalMeeting()) {
$introduction .= sprintf('<p>%s (%s)</p>', $this->formattedStartTime, $this->formattedDuration);
}
if ($this->user) {
$introduction .= sprintf('<p>%s</p>', $this->user->getFullname());
} elseif ($this->isCourseMeeting()) {
if (is_null($this->session)) {
$introduction .= sprintf('<p class="main">%s</p>', $this->course);
} else {
$introduction .= sprintf('<p class="main">%s (%s)</p>', $this->course, $this->session);
}
}
if (!empty($this->meetingInfoGet->agenda)) {
$introduction .= sprintf('<p>%s</p>', $this->meetingInfoGet->agenda);
}
return $introduction;
}
/**
* @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'));
}
}
}

@ -0,0 +1,185 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use Chamilo\PluginBundle\Zoom\API\RecordingMeeting;
use Database;
use DateInterval;
use DateTime;
use DateTimeZone;
use Doctrine\ORM\Mapping as ORM;
use Exception;
/**
* Class RecordingEntity.
*
* @package Chamilo\PluginBundle\Zoom
* @ORM\Entity(repositoryClass="Chamilo\PluginBundle\Zoom\RecordingEntityRepository")
* @ORM\Table(
* name="plugin_zoom_recording",
* indexes={
* @ORM\Index(name="meeting_id_index", columns={"meeting_id"}),
* }
* )
* @ORM\HasLifecycleCallbacks
*/
class RecordingEntity
{
/** @var DateTime */
public $startDateTime;
/** @var string */
public $formattedStartTime;
/** @var DateInterval */
public $durationInterval;
/** @var string */
public $formattedDuration;
/**
* @var string
* @ORM\Column(type="string")
* @ORM\Id
*/
private $uuid;
/**
* @var MeetingEntity
* @ORM\ManyToOne(
* targetEntity="MeetingEntity",
* inversedBy="recordings",
* )
* @ORM\JoinColumn(name="meeting_id")
*/
private $meeting;
/**
* @var string
* @ORM\Column(type="text", name="recording_meeting_json", nullable=true)
*/
private $recordingMeetingJson;
/** @var RecordingMeeting */
private $recordingMeeting;
/**
* @param $name
*
* @throws Exception
*
* @return mixed
*/
public function __get($name)
{
$object = $this->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;
}
/**
* @throws Exception
*
* @return RecordingMeeting
*/
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'));
}
}

@ -0,0 +1,260 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use Chamilo\PluginBundle\Zoom\API\CreatedRegistration;
use Chamilo\PluginBundle\Zoom\API\MeetingRegistrant;
use Chamilo\PluginBundle\Zoom\API\MeetingRegistrantListItem;
use Chamilo\UserBundle\Entity\User;
use Doctrine\ORM\Mapping as ORM;
use Exception;
/**
* Class RegistrantEntity.
*
* @package Chamilo\PluginBundle\Zoom
* @ORM\Entity(repositoryClass="Chamilo\PluginBundle\Zoom\RegistrantEntityRepository")
* @ORM\Table(
* name="plugin_zoom_registrant",
* indexes={
* @ORM\Index(name="user_id_index", columns={"user_id"}),
* @ORM\Index(name="meeting_id_index", columns={"meeting_id"}),
* }
* )
* @ORM\HasLifecycleCallbacks
*/
class RegistrantEntity
{
/** @var string */
public $fullName;
/**
* @var string
* @ORM\Column(type="string")
* @ORM\Id
*/
private $id;
/**
* @var User
* @ORM\ManyToOne(
* targetEntity="Chamilo\UserBundle\Entity\User",
* )
* @ORM\JoinColumn(name="user_id", nullable=false)
*/
private $user;
/**
* @var MeetingEntity
* @ORM\ManyToOne(
* targetEntity="MeetingEntity",
* inversedBy="registrants",
* )
* @ORM\JoinColumn(name="meeting_id")
*/
private $meeting;
/**
* @var string
* @ORM\Column(type="text", name="created_registration_json", nullable=true)
*/
private $createdRegistrationJson;
/**
* @var string
* @ORM\Column(type="text", name="meeting_registrant_list_item_json", nullable=true)
*/
private $meetingRegistrantListItemJson;
/**
* @var string
* @ORM\Column(type="text", name="meeting_registrant_json", nullable=true)
*/
private $meetingRegistrantJson;
/** @var CreatedRegistration */
private $createdRegistration;
/** @var MeetingRegistrant */
private $meetingRegistrant;
/** @var MeetingRegistrantListItem */
private $meetingRegistrantListItem;
/**
* @return string
*/
public function __toString()
{
return sprintf('Registrant %d', $this->id);
}
/**
* @return MeetingEntity
*/
public function getMeeting()
{
return $this->meeting;
}
/**
* @return User
*/
public function getUser()
{
return $this->user;
}
/**
* @throws Exception
*
* @return MeetingRegistrantListItem
*/
public function getMeetingRegistrantListItem()
{
return $this->meetingRegistrantListItem;
}
/**
* @throws Exception
*
* @return CreatedRegistration
*/
public function getCreatedRegistration()
{
return $this->createdRegistration;
}
/**
* @throws Exception
*
* @return MeetingRegistrant
*/
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
);
}
}

@ -0,0 +1,70 @@
The Chamilo Zoom plugin class itself is defined in _plugin/zoom/lib/zoom_plugin.class.php_
It manipulates both **remote Zoom server objects** and **local database entities**:
# Local database entities
The local entities map the remote objects to Chamilo courses/sessions and users.
They also maintain a cache of the matching remote objects.
_Entity/*Entity.php_ are the local database entity classes.
Doctrine entity manager repository classes are in _lib/*EntityRepository.php_.
# Remote Zoom server objets
_lib/API/*.php_ contains the Zoom API data structure definition classes,
based on Zoom's own API specification:
* https://marketplace.zoom.us/docs/api-reference/zoom-api
* https://marketplace.zoom.us/docs/api-reference/zoom-api/Zoom%20API.oas2.json
These classes provide methods to list, create, update and delete the remote objects.
# JWT Client
API class methods use a JWT client implemented in _lib/API/JWTClient.php_.
The plugin constructor initializes the JWT Client, giving it required API key and secret.
# Event notification handler
_endpoint.php_ is the Zoom API event notification web hook end point.
It handles notifications sent by Zoom servers on useful events :
* meeting start and end,
* registrant join and leave,
* recordings created and deleted
# Administrative interface
_admin.php_ is the administrative interface.
It lists all meetings and recordings.
# Course tool
_start.php_ is the **course** tool target:
* to the course teacher, it shows a course meeting management interface;
* to the course learners, it shows the list of scheduled course meetings.
# Home page's profile block (also on "My Courses" page)
This plugin can add 3 kinds of links to "profile block" :
1. _join_meeting.php?meetingId=…_ links to upcoming meetings accessible to the current user.
_join_meeting.php_ presents the meeting and shows a link to enter the meeting.
2. _user.php_ is the **user**'s own meeting management interface.
3. _global.php_ directs the user to _join_meeting.php_ with the **global** meeting.
# Meeting management page
_admin.php_, _start.php_ and _user.php_ link to _meeting.php_.
_meeting.php_ is the meeting management page, where one can manage
* the meeting properties,
* the list of its registrants and
* its recordings.

@ -1 +1,35 @@
This plugin allows teachers to launch a Zoom conference at any time and students to join it.
This plugin adds Zoom meetings, user registration to meetings and meeting recordings.
## Meetings
A **meeting** can be linked to a local **user** and/or a local **course**/**session**:
* a meeting with a course is a _course meeting_;
* a meeting with a user and no course is a _user meeting_;
* a meeting with no course nor user is a _global meeting_.
## Registrants
A **registrant** is the registration of a local user to a meeting.
Users do not register themselves to meetings.
* They are registered to a course meeting by the course manager.
* They are registered to a user meeting by that user.
* They are registered automatically to the global meeting, when they enter it.
## Recordings
A **recording** is the list of files created during a past meeting instance.
Course meeting files can be copied to the course by the course manager.
# Required Zoom user account
Recordings and user registration are only available to paying Zoom customers.
For a non-paying Zoom user, this plugin still works but participants will join anonymously.
# Contributing
Read README.code.md for an introduction to the plugin's code.

@ -16,14 +16,13 @@ $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'));
$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'));

@ -0,0 +1,100 @@
<?php
/* For license terms, see /license.txt */
use Chamilo\PluginBundle\Zoom\API\MeetingInfoGet;
use Chamilo\PluginBundle\Zoom\API\RecordingMeeting;
use Chamilo\PluginBundle\Zoom\MeetingEntity;
use Chamilo\PluginBundle\Zoom\RecordingEntity;
use Chamilo\PluginBundle\Zoom\RegistrantEntity;
if ('POST' !== $_SERVER['REQUEST_METHOD']) {
http_response_code(404); // Not found
exit;
}
$authorizationHeaderValue = apache_request_headers()['authorization']; // TODO handle non-apache installations
require __DIR__.'/config.php';
if (api_get_plugin_setting('zoom', 'verificationToken') !== $authorizationHeaderValue) {
http_response_code(401); // Unauthorized
exit;
}
$body = file_get_contents('php://input');
$decoded = json_decode($body);
if (is_null($decoded) || !is_object($decoded) || !isset($decoded->event) || !isset($decoded->payload->object)) {
error_log(sprintf('Did not recognize event notification: %s', $body));
http_response_code(422); // Unprocessable Entity
exit;
}
$object = $decoded->payload->object;
list($objectType, $action) = explode('.', $decoded->event);
switch ($objectType) {
case 'meeting':
$meetingRepository = $entityManager->getRepository(MeetingEntity::class);
$registrantRepository = $entityManager->getRepository(RegistrantEntity::class);
switch ($action) {
case 'deleted':
$meetingEntity = $meetingRepository->find($object->id);
if (!is_null($meetingEntity)) {
$entityManager->remove($meetingEntity);
}
break;
case 'ended':
case 'started':
$entityManager->persist(
(
$meetingRepository->find($object->id)->setStatus($action)
?: (new MeetingEntity())->setMeetingInfoGet(MeetingInfoGet::fromObject($object))
)
);
$entityManager->flush();
break;
case 'participant_joined':
case 'participant_left':
$registrant = $registrantRepository->find($object->participant->id);
if (!is_null($registrant)) {
// TODO log attendance
}
break;
case 'alert':
default:
error_log(sprintf('Event "%s" on %s was unhandled: %s', $action, $objectType, $body));
http_response_code(501); // Not Implemented
}
break;
case 'recording':
$recordingRepository = $entityManager->getRepository(RecordingEntity::class);
switch ($action) {
case 'completed':
$entityManager->persist(
(new RecordingEntity())->setRecordingMeeting(RecordingMeeting::fromObject($object))
);
$entityManager->flush();
break;
case 'recovered':
if (is_null($recordingRepository->find($object->uuid))) {
$entityManager->persist(
(new RecordingEntity())->setRecordingMeeting(RecordingMeeting::fromObject($object))
);
$entityManager->flush();
}
break;
case 'trashed':
case 'deleted':
$recordingEntity = $recordingRepository->find($object->uuid);
if (!is_null($recordingEntity)) {
$entityManager->remove($recordingEntity);
$entityManager->flush();
}
break;
default:
error_log(sprintf('Event "%s" on %s was unhandled: %s', $action, $objectType, $body));
http_response_code(501); // Not Implemented
}
break;
default:
error_log(sprintf('Event "%s" on %s was unhandled: %s', $action, $objectType, $body));
http_response_code(501); // Not Implemented
}

@ -0,0 +1,12 @@
<?php
/* For license terms, see /license.txt */
$course_plugin = 'zoom'; // needed in order to load the plugin lang variables
require_once __DIR__.'/config.php';
if (!ZoomPlugin::currentUserCanJoinGlobalMeeting()) {
api_not_allowed(true);
}
location('join_meeting.php?meetingId='.ZoomPlugin::create()->getGlobalMeeting()->getId());

@ -0,0 +1,39 @@
<?php
/* For license terms, see /license.txt */
use Chamilo\PluginBundle\Zoom\MeetingEntity;
$course_plugin = 'zoom'; // needed in order to load the plugin lang variables
require_once __DIR__.'/config.php';
if (!api_user_is_login()) {
api_not_allowed(true);
exit(); // just in case
}
$plugin = ZoomPlugin::create();
Display::display_header($plugin->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');
}
printf(
'%s<p><a href="%s" target="_blank">%s</a></p>',
$meeting->getIntroduction(),
$plugin->getStartOrJoinMeetingURL($meeting),
get_lang('EnterMeeting')
);
} catch (Exception $exception) {
Display::addFlash(
Display::return_message($exception->getMessage(), 'error')
);
}
}
Display::display_footer();

@ -8,6 +8,7 @@ $strings['plugin_comment'] = "Zoom Videoconference integration in courses and se
$strings['tool_enable'] = 'Zoom videoconference tool enabled';
$strings['apiKey'] = 'API Key';
$strings['apiSecret'] = 'API Secret';
$strings['verificationToken'] = 'Verification Token';
$strings['enableParticipantRegistration'] = 'Enable participant registration';
$strings['enableCloudRecording'] = 'Enable cloud recording';
$strings['enableGlobalConference'] = 'Enable global conference';
@ -28,10 +29,24 @@ To get them, create a <em>JWT App</em> :
<br/>2. click on <em>Advanced / Application Marketplace</em>
<br/>3. click on <em><a href=\"https://marketplace.zoom.us/develop/create\">Develop / build App</a></em>
<br/>4. choose <em>JWT / Create</em>
<br/>5. fill in information about your \"App\"
<br/>5. Information: fill in fields about your \"App\"
(application and company names, contact name and email address)
<br/>6. click on <em>Continue</em>
Locate your API Key and Secret in the App Credentials page.
<br/>6. Click on <em>Continue</em>
<br/>7. App Credentials: <strong>copy your API Key and Secret to these fields below</strong>
<br/>8. click on <em>Continue</em>
<br/>9. Feature:
enable <em>Event Subscriptions</em> to add a new one with endpoint URL
<code>https://your.chamilo.url/plugin/zoom/endpoint.php</code>
and add these event types:
<br/>- Start Meeting
<br/>- End Meeting
<br/>- Participant/Host joined meeting
<br/>- Participant/Host left meeting
<br/>- All Recordings have completed
<br/>- Recording transcript files have completed
<br/>then click on <em>Done</em> then on <em>Save</em>
and <strong>copy your Verification Token to the field below</strong>.
<br/>10. click on <em>Continue</em>
<br/>
<strong>Attention</strong>:
<br/>Zoom is <em>NOT</em> free software and specific rules apply to personal data protection.
@ -46,6 +61,7 @@ Will not work for a <em>basic</em> 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 +69,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";
@ -61,13 +79,16 @@ $strings['Duration'] = "Duration";
$strings['DurationFormat'] = "%hh%I";
$strings['DurationInMinutes'] = "Duration (in minutes)";
$strings['EndDate'] = "End Date";
$strings['EnterMeeting'] = "Enter meeting";
$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['JoinMeetingAsMyself'] = "Join meeting as myself";
$strings['JoinGlobalVideoConference'] = "Join global conference";
$strings['JoinURLCopied'] = "Join URL copied";
$strings['JoinURLToSendToParticipants'] = "Join URL to send to participants";
$strings['LiveMeetings'] = "Live meetings";
@ -105,4 +126,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";

@ -26,10 +26,25 @@ Pour les obtenir, créez une <em>JWT app</em> :
<br/>2. cliquez sur <em>Avancé / Marketplace d'application</em>
<br/>3. cliquez sur <em><a href=\"https://marketplace.zoom.us/develop/create\">Develop / build App</a></em>
<br/>4. choisissez <em>JWT / Create</em>
<br/>5. saisissez quelques informations sur votre \"App\"
<br/>5. Information: remplissez quelques champs à propos de votre \"App\"
(noms de l'application, de l'entreprise, nom et adresse de courriel de contact)
<br/>6. cliquez sur <em>Continue</em>
<br/>La page <em>App Credentials</em> affiche la clé (API Key) et le code secret (API Secret) à saisir ici.
<br/>7. App Credentials :
<strong>copiez la clé (API Key) et le code secret (API Secret) dans les champs ci-dessous.</strong>
<br/>8. cliquez sur <em>Continue</em>
<br/>9. Feature :
activez <em>Event Subscriptions</em> pour en ajouter une avec comme endpoint URL
<code>https://your.chamilo.url/plugin/zoom/endpoint.php</code>
et ajoutez ces types d'événements :
<br/>- Start Meeting
<br/>- End Meeting
<br/>- Participant/Host joined meeting
<br/>- Participant/Host left meeting
<br/>- All Recordings have completed
<br/>- Recording transcript files have completed
<br/>puis cliquez sur <em>Done</em> puis sur <em>Save</em>
et <strong>copiez votre Verification Token dans le champ ci-dessous</strong>.
<br/>10. cliquez sur <em>Continue</em>
<br/>
<strong>Attention</strong> :
<br/>Zoom n'est <em>PAS</em> un logiciel libre
@ -45,6 +60,7 @@ Ne fonctionnera pas pour un profil <em>de base</em>.";
// 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 +68,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";
@ -60,13 +78,16 @@ $strings['Duration'] = "Durée";
$strings['DurationFormat'] = "%hh%I";
$strings['DurationInMinutes'] = "Durée (en minutes)";
$strings['EndDate'] = "Date de fin";
$strings['EnterMeeting'] = "Entrer dans la conférence";
$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['JoinMeetingAsMyself'] = "Rejoindre la conférence en tant que moi-même";
$strings['JoinGlobalVideoConference'] = "Rejoindre la conférence globale";
$strings['JoinURLCopied'] = "URL pour rejoindre copiée";
$strings['JoinURLToSendToParticipants'] = "URL pour assister à la conférence (à envoyer aux participants)";
$strings['LiveMeetings'] = "Conférences en cours";
@ -102,6 +123,8 @@ $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['verificationToken'] = 'Verification Token';
$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";

@ -26,10 +26,26 @@ Para obtenerlos, crea una <em>app JWT</em> :
<br/>2. de clic en <em>Avanzado / Marketplace de aplicaciones</em>
<br/>3. de clic en <em><a href=\"https://marketplace.zoom.us/develop/create\">Develop / build App</a></em>
<br/>4. escoja <em>JWT / Create</em>
<br/>5. ingrese algunas informaciones sobre vuestra \"App\"
<br/>5. Information: ingrese algunas informaciones sobre vuestra \"App\"
(nombres de la aplicación, de la empresa, nombre y dirección de correo de contacto)
<br/>6. de clic en <em>Continue</em>
<br/>La página <em>App Credentials</em> muestra la clave (API Key) y el código secreto (API Secret) por ingresar aquí.
<br/>7. App Credentials:
muestra la clave (API Key) y el código secreto (API Secret) por ingresar aquí.
<strong>copiez la clé (API Key) et le code secret (API Secret) dans les champs ci-dessous.</strong>
<br/>8. de clic en <em>Continue</em>
<br/>9. Feature :
activez <em>Event Subscriptions</em> para agregar uno con su endpoint URL
<code>https://your.chamilo.url/plugin/zoom/endpoint.php</code>
y agrega este tipo de eventos:
<br/>- Start Meeting
<br/>- End Meeting
<br/>- Participant/Host joined meeting
<br/>- Participant/Host left meeting
<br/>- All Recordings have completed
<br/>- Recording transcript files have completed
<br/>de clic en <em>Done</em> y luego en <em>Save</em>
y <strong>copie su Verification Token en el campo a continuación</strong>.
<br/>10. de clic en <em>Continue</em>
<br/>
<strong>Atención</strong> :
<br/>Zoom <em>NO ES</em> un software libre, y reglas específicas de protección de datos se aplican a este.
@ -44,6 +60,7 @@ No funcionará para un perfil <em>base/gratuito</em>.";
// 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 +68,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";
@ -59,13 +78,16 @@ $strings['Duration'] = "Duración";
$strings['DurationFormat'] = "%hh%I";
$strings['DurationInMinutes'] = "Duración (en minutos)";
$strings['EndDate'] = "Fecha de fin";
$strings['EnterMeeting'] = "Ingresar la conferencia";
$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['JoinMeetingAsMyself'] = "Ingresar la conferencia como yo mismo";
$strings['JoinGlobalVideoConference'] = "Ingresar la conrencia global";
$strings['JoinURLCopied'] = "URL para juntarse copiada";
$strings['JoinURLToSendToParticipants'] = "URL para asistir a la conferencia (para enviar a los participantes)";
$strings['LiveMeetings'] = "Conferencias activas";
@ -101,6 +123,8 @@ $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['verificationToken'] = 'Verification Token';
$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";

@ -13,8 +13,21 @@ 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;
}
/**
* Sends a Zoom API-compliant HTTP request and retrieves the response.
*
@ -30,5 +43,15 @@ 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);
/**
* Registers an initialized Client.
*
* @param Client $instance
*/
protected static function register($instance)
{
self::$instance = $instance;
}
}

@ -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);
}
/**

@ -32,6 +32,20 @@ trait JsonDeserializableTrait
throw new Exception('Could not decode JSON: '.$json);
}
return static::fromObject($object);
}
/**
* Builds a class instance from an already json-decoded object.
*
* @param object $object
*
* @throws Exception on unexpected object property
*
* @return static
*/
public static function fromObject($object)
{
$instance = new static();
static::recursivelyCopyObjectProperties($object, $instance);
@ -101,7 +115,7 @@ trait JsonDeserializableTrait
$destination->$name = $value;
}
} else {
throw new Exception("Source object has property $name, which was not expected.");
error_log("Source object has property $name, which was not expected: ".json_encode($source));
}
}
$destination->initializeExtraProperties();

@ -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;

@ -34,58 +34,50 @@ class MeetingInfoGet extends MeetingInfo
/**
* Retrieves a meeting from its numeric identifier.
*
* @param Client $client
* @param int $id
* @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;
}
}

@ -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);
}
}

@ -30,16 +30,15 @@ class MeetingInstances
/**
* Retrieves a meeting's instances.
*
* @param Client $client
* @param int $meetingId
* @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"));
}
/**

@ -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
* @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]);
}
/**

@ -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;
}

@ -30,16 +30,15 @@ class MeetingRegistrantList
/**
* Retrieves all registrant for a meeting.
*
* @param Client $client
* @param int $meetingId
* @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");
}
/**

@ -80,6 +80,9 @@ class MeetingSettings
/** @var bool Enable waiting room */
public $waiting_room;
/** @var bool undocumented */
public $request_permission_to_unmute_participants;
/** @var string[] List of global dial-in countries */
public $global_dial_in_countries;

@ -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)

@ -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)

@ -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'
);
}

@ -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);
}
}

@ -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']

@ -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'),

@ -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']
);
}
/**

@ -1,69 +0,0 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use Exception;
/**
* Class CourseMeeting. A remote Zoom meeting linked to a local course.
* An instance of this class is required to create a remote meeting from scratch.
*
* @package Chamilo\PluginBundle\Zoom
*/
class CourseMeeting extends API\Meeting
{
use CourseMeetingTrait;
use DisplayableMeetingTrait;
/**
* Creates a CourseMeeting instance from a topic.
*
* @param int $courseId
* @param int $sessionId
* @param string $topic
* @param int $type
*
* @throws Exception
*
* @return static
*/
public static function fromCourseSessionTopicAndType($courseId, $sessionId, $topic, $type)
{
$instance = static::fromTopicAndType($topic, $type);
$instance->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;
}
}

@ -1,57 +0,0 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use Exception;
/**
* Class CourseMeetingInfoGet. A full Meeting as returned by the server and locally linked to a course.
*
* @package Chamilo\PluginBundle\Zoom
*/
class CourseMeetingInfoGet extends API\MeetingInfoGet
{
use CourseMeetingTrait;
use DisplayableMeetingTrait;
/**
* {@inheritdoc}
*
* @throws Exception
*/
public function initializeExtraProperties()
{
parent::initializeExtraProperties();
$this->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);
}
}

@ -1,26 +0,0 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
/**
* Class CourseMeetingList. A List of course meetings.
*
* @see CourseMeetingListItem
*
* @package Chamilo\PluginBundle\Zoom
*/
class CourseMeetingList extends API\MeetingList
{
/**
* {@inheritdoc}
*/
public function itemClass($propertyName)
{
if ('meetings' === $propertyName) {
return CourseMeetingListItem::class;
}
return parent::itemClass($propertyName);
}
}

@ -1,31 +0,0 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use Exception;
/**
* Class CourseMeetingListItem. An item from a course meeting list.
*
* @see CourseMeetingList
*
* @package Chamilo\PluginBundle\Zoom
*/
class CourseMeetingListItem extends API\MeetingListItem
{
use CourseMeetingTrait;
use DisplayableMeetingTrait;
/**
* {@inheritdoc}
*
* @throws Exception
*/
public function initializeExtraProperties()
{
parent::initializeExtraProperties();
$this->decodeAndRemoveTag();
$this->initializeDisplayableProperties();
}
}

@ -1,142 +0,0 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CoreBundle\Entity\SessionRelCourseRelUser;
use Chamilo\UserBundle\Entity\User;
use Database;
/**
* Trait CourseMeetingTrait.
* A Zoom meeting linked to a (course, session) pair.
* The course and session IDs are stored in the meeting agenda on write operations, read and removed on retrieval.
*
* @package Chamilo\PluginBundle\Zoom
*/
trait CourseMeetingTrait
{
use API\BaseMeetingTrait;
/** @var bool whether the agenda contains the course and session identifiers */
public $isTaggedWithCourseId;
/** @var int meeting course id as found in the agenda field */
public $courseId;
/** @var Course meeting course */
public $course;
/** @var int meeting session id as found in the agenda field */
public $sessionId;
/** @var Session meeting session */
public $session;
public function loadCourse()
{
$this->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<courseId>\d+), session (?P<sessionId>\d+)/m';
}
}

@ -1,74 +0,0 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use DateInterval;
use DateTime;
use DateTimeZone;
use Exception;
/**
* Trait DisplayableMeetingTrait. Extra properties to help formatting web views and time operations.
*
* @package Chamilo\PluginBundle\Zoom
*/
trait DisplayableMeetingTrait
{
use API\BaseMeetingTrait;
/** @var string meeting type name */
public $typeName;
/** @var DateTime meeting start time as a DateTime instance */
public $startDateTime;
/** @var string meeting formatted start time */
public $formattedStartTime;
/** @var DateInterval meeting duration as a DateInterval instance */
public $durationInterval;
/** @var string meeting formatted duration */
public $formattedDuration;
/** @var string */
public $statusName;
/**
* @throws Exception on unexpected start_time or duration
*/
public 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->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'));
}
}
}

@ -1,28 +0,0 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use Exception;
/**
* Class File. A RecordingFile with extra help properties for the web view.
*
* @package Chamilo\PluginBundle\Zoom
*/
class File extends API\RecordingFile
{
/** @var string */
public $formattedFileSize;
/**
* {@inheritdoc}
*
* @throws Exception
*/
public function initializeExtraProperties()
{
parent::initializeExtraProperties();
$this->formattedFileSize = format_file_size($this->file_size);
}
}

@ -0,0 +1,144 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\PageBundle\Entity\User;
use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityRepository;
/**
* Class MeetingEntityRepository.
*
* @package Chamilo\PluginBundle\Zoom
*/
class MeetingEntityRepository extends EntityRepository
{
/**
* Retrieves information about meetings having a start_time between two dates.
*
* @param DateTime $startDate
* @param DateTime $endDate
*
* @return MeetingEntity[]
*/
public function periodMeetings($startDate, $endDate)
{
$matching = [];
foreach ($this->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)
)
)
);
}
}

@ -1,65 +0,0 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use DateInterval;
use DateTime;
use DateTimeZone;
use Exception;
/**
* Class Recording. A RecordingMeeting with extra help properties and a list of File instances
* (instead of RecordingFile instances).
*
* @package Chamilo\PluginBundle\Zoom
*/
class Recording extends API\RecordingMeeting
{
/** @var File[] List of recording file. */
public $recording_files;
/** @var DateTime */
public $startDateTime;
/** @var string */
public $formattedStartTime;
/** @var DateInterval */
public $durationInterval;
/** @var string */
public $formattedDuration;
/**
* {@inheritdoc}
*
* @throws Exception
*/
public function initializeExtraProperties()
{
parent::initializeExtraProperties();
$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'));
$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);
}
}

@ -0,0 +1,69 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use Chamilo\ClassificationBundle\Entity\Collection;
use Chamilo\UserBundle\Entity\User;
use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityRepository;
/**
* Class RecordingEntityRepository.
*
* @package Chamilo\PluginBundle\Zoom
*/
class RecordingEntityRepository extends EntityRepository
{
public function getPeriodRecordings($startDate, $endDate)
{
$matching = [];
foreach ($this->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;
}
);
}
}

@ -1,51 +0,0 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use DateTime;
use Exception;
/**
* Class RecordingList. A list of past meeting instance recordings generated between two dates.
*
* @package Chamilo\PluginBundle\Zoom
*/
class RecordingList extends API\RecordingList
{
/**
* {@inheritdoc}
*/
public function itemClass($propertyName)
{
if ('meetings' === $propertyName) {
return Recording::class;
}
return parent::itemClass($propertyName);
}
/**
* Retrieves all recordings from a period of time.
*
* @param API\Client $client
* @param DateTime $startDate first day of the period
* @param DateTime $endDate last day of the period
*
* @throws Exception
*
* @return Recording[] all recordings from that period
*/
public static function loadRecordings($client, $startDate, $endDate)
{
return static::loadItems(
'meetings',
$client,
'users/me/recordings',
[
'from' => $startDate->format('Y-m-d'),
'to' => $endDate->format('Y-m-d'),
]
);
}
}

@ -0,0 +1,34 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use Chamilo\UserBundle\Entity\User;
use DateInterval;
use DateTime;
use Doctrine\ORM\EntityRepository;
/**
* Class RegistrantEntityRepository.
*
* @package Chamilo\PluginBundle\Zoom
*/
class RegistrantEntityRepository extends EntityRepository
{
/**
* Returns the upcoming meeting registrations for the given user.
*
* @param User $user
*
* @return array|RegistrantEntity[]
*/
public function meetingsComingSoonRegistrationsForUser($user)
{
$start = new DateTime();
$end = new DateTime();
$end->add(new DateInterval('P7D'));
$meetings = $this->getEntityManager()->getRepository(MeetingEntity::class)->periodMeetings($start, $end);
return $this->findBy(['meeting' => $meetings, 'user' => $user]);
}
}

@ -1,51 +0,0 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use Chamilo\UserBundle\Entity\User;
use Exception;
/**
* Class UserMeetingRegistrant. Used to register a local user to a meeting.
*
* @package Chamilo\PluginBundle\Zoom
*/
class UserMeetingRegistrant extends API\MeetingRegistrant
{
use UserMeetingRegistrantTrait;
/**
* {@inheritdoc}
*
* @throws Exception
*/
public function initializeExtraProperties()
{
parent::initializeExtraProperties();
$this->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;
}
}

@ -1,43 +0,0 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use Exception;
/**
* Class UserMeetingRegistrantList. A list of users registered to a meeting.
*
* @see UserMeetingRegistrantListItem
*
* @package Chamilo\PluginBundle\Zoom
*/
class UserMeetingRegistrantList extends API\MeetingRegistrantList
{
/**
* {@inheritdoc}
*/
public function itemClass($propertyName)
{
if ('registrants' === $propertyName) {
return UserMeetingRegistrantListItem::class;
}
return parent::itemClass($propertyName);
}
/**
* Retrieves all registrant for a meeting.
*
* @param API\Client $client
* @param int $meetingId
*
* @throws Exception
*
* @return UserMeetingRegistrantListItem[] all registrants of the meeting
*/
public static function loadUserMeetingRegistrants($client, $meetingId)
{
return static::loadItems('registrants', $client, "meetings/$meetingId/registrants");
}
}

@ -1,30 +0,0 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use Exception;
/**
* Class UserMeetingRegistrantListItem. An item of a user registrant list.
*
* @see UserMeetingRegistrantList
*
* @package Chamilo\PluginBundle\Zoom
*/
class UserMeetingRegistrantListItem extends API\MeetingRegistrantListItem
{
use UserMeetingRegistrantTrait;
/**
* {@inheritdoc}
*
* @throws Exception
*/
public function initializeExtraProperties()
{
parent::initializeExtraProperties();
$this->decodeAndRemoveTag();
$this->computeFullName();
}
}

@ -1,89 +0,0 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use Chamilo\UserBundle\Entity\User;
use Database;
/**
* Trait UserMeetingRegistrantTrait.
* A Zoom meeting registrant linked to a local user.
* The user id is stored in the registrant's email address on write operations, read and removed on retrieval.
*
* @package Chamilo\PluginBundle\Zoom
*/
trait UserMeetingRegistrantTrait
{
/** @var bool whether the remote zoom record contains a local user's identifier */
public $isTaggedWithUserId;
/** @var int */
public $userId;
/** @var User */
public $user;
/** @var string */
public $fullName;
public function loadUser()
{
$this->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<userId>\d+)@/m';
}
}

File diff suppressed because it is too large Load Diff

@ -1,7 +1,7 @@
<?php
/* For license terms, see /license.txt */
use Chamilo\PluginBundle\Zoom\API\MeetingSettings;
use Chamilo\PluginBundle\Zoom\MeetingEntity;
if (!isset($returnURL)) {
exit;
@ -25,33 +25,34 @@ if (!array_key_exists('meetingId', $_REQUEST)) {
}
$plugin = ZoomPlugin::create();
$meeting = $plugin->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;

@ -0,0 +1,13 @@
<?php
/* For license terms, see /license.txt */
require_once __DIR__.'/config.php';
api_protect_admin_script();
$returnURL = 'user.php';
// the section (for the tabs)
$this_section = SECTION_MYPROFILE;
include "meeting.php";

@ -21,14 +21,28 @@ $tpl = new Template($tool_name);
$plugin = ZoomPlugin::create();
if ($plugin->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')

@ -0,0 +1,32 @@
<?php
/* For license terms, see /license.txt */
$course_plugin = 'zoom'; // needed in order to load the plugin lang variables
require_once __DIR__.'/config.php';
if (!ZoomPlugin::currentUserCanCreateUserMeeting()) {
api_not_allowed(true);
exit(); // just in case
}
$plugin = ZoomPlugin::create();
$user = api_get_user_entity(api_get_user_id());
$form = $plugin->getAdminSearchForm();
$startDate = new DateTime($form->getElement('start')->getValue());
$endDate = new DateTime($form->getElement('end')->getValue());
$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();

@ -4,6 +4,7 @@
<thead>
<tr>
<th>{{ 'StartTime'|get_lang }}</th>
<th>{{ 'User'|get_lang }}</th>
<th>{{ 'Course'|get_lang }}</th>
<th>{{ 'Session'|get_lang }}</th>
<th>{{ 'Topic'|get_lang }}</th>
@ -17,13 +18,14 @@
{% for meeting in meetings %}
<tr>
<td>{{ meeting.formattedStartTime }}</td>
<td>{{ meeting.course ? meeting.course.title : '-' }}</td>
<td>{{ meeting.session ? meeting.session.name : '-' }}</td>
<td>{{ meeting.topic }}</td>
<td>{{ meeting.user ? meeting.user : '-' }}</td>
<td>{{ meeting.course ? meeting.course : '-' }}</td>
<td>{{ meeting.session ? meeting.session : '-' }}</td>
<td>{{ meeting.meetingInfoGet.topic }}</td>
{% if recordings %}
<td>
{% for recording in recordings %}
{% if recording.id == meeting.id %}
{% if recording.recordingMeeting.id == meeting.id %}
<dl>
<dt>
{{ recording.formattedStartTime }}
@ -31,10 +33,10 @@
</dt>
<dd>
<ul>
{% for file in recording.recording_files %}
{% for file in recording.recordingMeeting.recording_files %}
<li>
{{ file.recording_type }}.{{ file.file_type }}
({{ file.formattedFileSize }})
({{ file.file_size }})
</li>
{% endfor %}
</ul>
@ -52,4 +54,6 @@
</tr>
{% endfor %}
</tbody>
</table>
</table>
{{ schedule_form }}

@ -1,8 +1,17 @@
<p>{{ meeting.typeName }} {{ meeting.id }} ({{ meeting.statusName }})</p>
<p>
{{ meeting.typeName }} {{ meeting.id }} ({{ meeting.meetingInfoGet.status }})
</p>
{% if meeting.meetingInfoGet.status != 'finished' %}
<p>
<a href="join_meeting.php?meetingId={{ meeting.id }}">
{{ 'EnterMeeting'|get_lang }}
</a>
</p>
{% endif %}
{% if isConferenceManager and meeting.status == 'waiting' %}
<p>
<a href="{{ meeting.start_url }}" target="_blank">
<a href="{{ meeting.meetingInfoGet.start_url }}" target="_blank">
{{ 'StartMeeting'|get_lang }}
</a>
</p>
@ -16,11 +25,11 @@
</p>
{% endif %}
{% if meeting.settings.approval_type == 2 %}
{% if meeting.meetingInfoGet.settings.approval_type == 2 %}
<p>
<label>
{{ 'JoinURLToSendToParticipants'|get_lang }}
<input readonly value="{{ meeting.join_url }}">
<input readonly value="{{ meeting.meetingInfoGet.join_url }}">
</label>
</p>
{% endif %}
@ -32,7 +41,7 @@
{{ deleteMeetingForm }}
{{ registerParticipantForm }}
{{ fileForm }}
{% if registrants and meeting.settings.approval_type != 2 %}
{% if registrants and meeting.meetingInfoGet.settings.approval_type != 2 %}
<script>
function copyJoinURL(event, url) {
event.target.textContent = '{{ 'CopyingJoinURL'|get_lang|escape }}';
@ -59,12 +68,12 @@
{% else %}
<h2>{{ meeting.topic }}</h2>
{% if meeting.agenda %}
<blockquote>{{ meeting.agenda| nl2br }}</blockquote>
<h2>{{ meeting.meetingInfoGet.topic }}</h2>
{% if meeting.meetingInfoGet.agenda %}
<blockquote>{{ meeting.meetingInfoGet.agenda| nl2br }}</blockquote>
{% endif %}
{% if meeting.type == 2 or meeting.type == 8 %}
{% if meeting.meetingInfoGet.type == 2 or meeting.meetingInfoGet.type == 8 %}
<dl class="meeting_properties">
<dt>{{ 'StartTime'|get_lang }}</dt>
<dd>{{ meeting.formattedStartTime }}</dd>
@ -75,4 +84,3 @@
{% endif %}
{% endif %}

@ -21,14 +21,14 @@
<td>{{ meeting.formattedDuration }}</td>
<!-- td>{{ meeting.typeName }}</td -->
<td>
<strong>{{ meeting.topic }}</strong>
<p class="small">{{ meeting.agenda|nl2br }}</p>
<strong>{{ meeting.meetingInfoGet.topic }}</strong>
<p class="small">{{ meeting.meetingInfoGet.agenda|nl2br }}</p>
</td>
<td>
<a class="btn" href="meeting_from_start.php?meetingId={{ meeting.id }}">
{{ 'Details'|get_lang }}
</a>
<a class="btn" href="{{ meeting.join_url }}">
<a class="btn" href="{{ meeting.meetingInfoGet.join_url }}">
{{ 'Join'|get_lang }}
</a>
</td>

Loading…
Cancel
Save