Actualización

This commit is contained in:
Xes
2025-04-10 12:36:07 +02:00
parent 1da7c3f3b9
commit 4aff98e77b
3147 changed files with 320647 additions and 0 deletions

View File

@@ -0,0 +1,706 @@
<?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\CoreBundle\Entity\SysAnnouncement;
use Chamilo\CourseBundle\Entity\CGroupInfo;
use Chamilo\PluginBundle\Zoom\API\BaseMeetingTrait;
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\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
use Exception;
/**
* Class Meeting.
*
* @ORM\Entity(repositoryClass="Chamilo\PluginBundle\Zoom\MeetingRepository")
* @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
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="type", type="string")
* @ORM\DiscriminatorMap({"meeting" = "Chamilo\PluginBundle\Zoom\Meeting", "webinar" = "Chamilo\PluginBundle\Zoom\Webinar"})
*/
class Meeting
{
/** @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
* @ORM\Column(type="integer", name="id")
* @ORM\Id
* @ORM\GeneratedValue()
*/
protected $id;
/**
* @var int the remote zoom meeting identifier
* @ORM\Column(name="meeting_id", type="string")
*/
protected $meetingId;
/**
* @var User
* @ORM\ManyToOne(targetEntity="Chamilo\UserBundle\Entity\User")
* @ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=true)
*/
protected $user;
/**
* @var Course
* @ORM\ManyToOne(targetEntity="Chamilo\CoreBundle\Entity\Course")
* @ORM\JoinColumn(name="course_id", referencedColumnName="id", nullable=true)
*/
protected $course;
/**
* @var CGroupInfo
* @ORM\ManyToOne(targetEntity="Chamilo\CourseBundle\Entity\CGroupInfo")
* @ORM\JoinColumn(name="group_id", referencedColumnName="iid", nullable=true)
*/
protected $group;
/**
* @var Session
* @ORM\ManyToOne(targetEntity="Chamilo\CoreBundle\Entity\Session")
* @ORM\JoinColumn(name="session_id", referencedColumnName="id", nullable=true)
*/
protected $session;
/**
* @var string
* @ORM\Column(type="text", name="meeting_list_item_json", nullable=true)
*/
protected $meetingListItemJson;
/**
* @var string
* @ORM\Column(type="text", name="meeting_info_get_json", nullable=true)
*/
protected $meetingInfoGetJson;
/**
* @var bool
*
* @ORM\Column(type="boolean", name="sign_attendance")
*/
protected $signAttendance;
/**
* @var string|null
*
* @ORM\Column(type="text", name="reason_to_sign_attendance", nullable=true)
*/
protected $reasonToSignAttendance;
/** @var MeetingListItem */
protected $meetingListItem;
/** @var MeetingInfoGet */
protected $meetingInfoGet;
/**
* @var MeetingActivity[]|ArrayCollection
* @ORM\OrderBy({"createdAt" = "DESC"})
* @ORM\OneToMany(targetEntity="MeetingActivity", mappedBy="meeting", cascade={"persist", "remove"})
*/
protected $activities;
/**
* @var Registrant[]|ArrayCollection
*
* @ORM\OneToMany(targetEntity="Registrant", mappedBy="meeting", cascade={"persist", "remove"})
*/
protected $registrants;
/**
* @var Recording[]|ArrayCollection
*
* @ORM\OneToMany(targetEntity="Recording", mappedBy="meeting", cascade={"persist"}, orphanRemoval=true)
*/
protected $recordings;
/**
* @var string|null
*
* @ORM\Column(type="string", name="account_email", nullable=true)
*/
protected $accountEmail;
/**
* @var SysAnnouncement|null
*
* @ORM\OneToOne(targetEntity="Chamilo\CoreBundle\Entity\SysAnnouncement")
* @ORM\JoinColumn(name="sys_announcement_id", referencedColumnName="id", onDelete="SET NULL")
*/
protected $sysAnnouncement;
public function __construct()
{
$this->registrants = new ArrayCollection();
$this->recordings = new ArrayCollection();
$this->activities = new ArrayCollection();
$this->signAttendance = false;
$this->sysAnnouncement = null;
}
/**
* @return string
*/
public function __toString()
{
return sprintf('Meeting %d', $this->id);
}
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @return int
*/
public function getMeetingId()
{
return $this->meetingId;
}
/**
* @param int $meetingId
*
* @return Meeting
*/
public function setMeetingId($meetingId)
{
$this->meetingId = $meetingId;
return $this;
}
/**
* @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 Registrant[]|ArrayCollection
*/
public function getRegistrants()
{
return $this->registrants->filter(function (Registrant $registrant) {
return !$registrant instanceof Presenter;
});
}
/**
* @return ArrayCollection<int, Presenter>
*/
public function getPresenters(): ArrayCollection
{
return $this->registrants->filter(function (Registrant $registrant) {
return $registrant instanceof Presenter;
});
}
public function hasUserAsPresenter(User $user): bool
{
$presenters = $this->getPresenters();
$criteria = Criteria::create();
$criteria->where(
Criteria::expr()->eq('user', $user)
);
return $presenters->matching($criteria)->count() > 0;
}
/**
* @return Recording[]|ArrayCollection
*/
public function getRecordings()
{
return $this->recordings;
}
/**
* @return MeetingActivity[]|ArrayCollection
*/
public function getActivities()
{
return $this->activities;
}
public function addActivity(MeetingActivity $activity)
{
$activity->setMeeting($this);
$this->activities[] = $activity;
}
/**
* @param MeetingActivity[]|ArrayCollection $activities
*
* @return Meeting
*/
public function setActivities($activities)
{
$this->activities = $activities;
return $this;
}
/**
* @ORM\PostLoad
*
* @throws Exception
*/
public function postLoad()
{
if (null !== $this->meetingListItemJson) {
$this->meetingListItem = MeetingListItem::fromJson($this->meetingListItemJson);
}
if (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 (null !== $this->meetingListItem) {
$this->meetingListItemJson = json_encode($this->meetingListItem);
}
if (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;
}
/**
* @return CGroupInfo
*/
public function getGroup()
{
return $this->group;
}
/**
* @param CGroupInfo $group
*
* @return Meeting
*/
public function setGroup($group)
{
$this->group = $group;
return $this;
}
/**
* @param MeetingListItem $meetingListItem
*
* @throws Exception
*
* @return Meeting
*/
public function setMeetingListItem($meetingListItem)
{
if (null === $this->meetingId) {
$this->meetingId = $meetingListItem->id;
} elseif ($this->meetingId != $meetingListItem->id) {
throw new Exception('the Meeting identifier differs from the MeetingListItem identifier');
}
$this->meetingListItem = $meetingListItem;
return $this;
}
/**
* @param MeetingInfoGet|BaseMeetingTrait $meetingInfoGet
*
* @throws Exception
*
* @return Meeting
*/
public function setMeetingInfoGet($meetingInfoGet)
{
if (null === $this->meetingId) {
$this->meetingId = $meetingInfoGet->id;
} elseif ($this->meetingId != $meetingInfoGet->id) {
throw new Exception('the Meeting identifier differs from the MeetingInfoGet identifier');
}
$this->meetingInfoGet = $meetingInfoGet;
$this->initializeDisplayableProperties();
return $this;
}
/**
* @return bool
*/
public function isCourseMeeting()
{
return null !== $this->course;
}
/**
* @return bool
*/
public function isCourseGroupMeeting()
{
return null !== $this->course && null !== $this->group;
}
/**
* @return bool
*/
public function isUserMeeting()
{
return null !== $this->user && null === $this->course;
}
/**
* @return bool
*/
public function isGlobalMeeting()
{
return null === $this->user && 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()
{
$users = [];
if (!$this->isCourseMeeting()) {
$criteria = ['active' => true];
$users = Database::getManager()->getRepository('ChamiloUserBundle:User')->findBy($criteria);
} elseif (null === $this->session) {
if (null !== $this->course) {
/** @var CourseRelUser $courseRelUser */
foreach ($this->course->getUsers() as $courseRelUser) {
$users[] = $courseRelUser->getUser();
}
}
} else {
if (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;
}
public function requiresRegistration(): bool
{
return true; //MeetingSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE === $this->meetingInfoGet->settings->approval_type;
/*return
MeetingSettings::APPROVAL_TYPE_NO_REGISTRATION_REQUIRED != $this->meetingInfoGet->settings->approval_type;*/
}
/**
* @return bool
*/
public function hasCloudAutoRecordingEnabled()
{
return \ZoomPlugin::RECORDING_TYPE_NONE !== $this->meetingInfoGet->settings->auto_recording;
}
public function getRegistrantByUser(User $user): ?Registrant
{
$criteria = Criteria::create()
->where(
Criteria::expr()->eq('user', $user)
)
;
return $this->registrants->matching($criteria)->first() ?: 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->getTopic()).PHP_EOL;
if (!$this->isGlobalMeeting()) {
if (!empty($this->formattedStartTime)) {
$introduction .= $this->formattedStartTime;
if (!empty($this->formattedDuration)) {
$introduction .= ' ('.$this->formattedDuration.')';
}
$introduction .= PHP_EOL;
}
}
if ($this->user) {
$introduction .= sprintf('<p>%s</p>', $this->user->getFullname()).PHP_EOL;
} elseif ($this->isCourseMeeting()) {
if (null === $this->session) {
$introduction .= sprintf('<p class="main">%s</p>', $this->course).PHP_EOL;
} else {
$introduction .= sprintf('<p class="main">%s (%s)</p>', $this->course, $this->session).PHP_EOL;
}
}
if (!empty($this->getAgenda())) {
$introduction .= sprintf('<p>%s</p>', $this->getAgenda()).PHP_EOL;
}
return $introduction;
}
public function isSignAttendance(): bool
{
return $this->signAttendance;
}
public function setSignAttendance(bool $signAttendance): Meeting
{
$this->signAttendance = $signAttendance;
return $this;
}
public function getAccountEmail(): ?string
{
return $this->accountEmail;
}
public function setAccountEmail(?string $accountEmail): self
{
$this->accountEmail = $accountEmail;
return $this;
}
public function getReasonToSignAttendance(): ?string
{
return $this->reasonToSignAttendance;
}
public function setReasonToSignAttendance(string $reasonToSignAttendance): Meeting
{
$this->reasonToSignAttendance = $reasonToSignAttendance;
return $this;
}
public function getTopic(): string
{
return $this->meetingInfoGet->topic;
}
public function getAgenda(): ?string
{
return $this->meetingInfoGet->agenda;
}
public function getSysAnnouncement(): ?SysAnnouncement
{
return $this->sysAnnouncement;
}
public function setSysAnnouncement(?SysAnnouncement $sysAnnouncement): Meeting
{
$this->sysAnnouncement = $sysAnnouncement;
return $this;
}
/**
* @throws Exception on unexpected start_time or duration
*/
protected function initializeDisplayableProperties()
{
$zoomPlugin = new \ZoomPlugin();
$typeList = [
API\Meeting::TYPE_INSTANT => $zoomPlugin->get_lang('InstantMeeting'),
API\Meeting::TYPE_SCHEDULED => $zoomPlugin->get_lang('ScheduledMeeting'),
API\Meeting::TYPE_RECURRING_WITH_NO_FIXED_TIME => $zoomPlugin->get_lang('RecurringWithNoFixedTime'),
API\Meeting::TYPE_RECURRING_WITH_FIXED_TIME => $zoomPlugin->get_lang('RecurringWithFixedTime'),
];
$this->typeName = $typeList[$this->meetingInfoGet->type];
if (property_exists($this, 'status')) {
$statusList = [
'waiting' => $zoomPlugin->get_lang('Waiting'),
'started' => $zoomPlugin->get_lang('Started'),
'finished' => $zoomPlugin->get_lang('Finished'),
];
$this->statusName = $statusList[$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(api_get_timezone()));
$this->formattedStartTime = $this->startDateTime->format('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 = $now->diff($later);
$this->formattedDuration = $this->durationInterval->format($zoomPlugin->get_lang('DurationFormat'));
}
}
}

View File

@@ -0,0 +1,208 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
/**
* Class Meeting.
*
* @ORM\Entity()
* @ORM\Table(name="plugin_zoom_meeting_activity")
* @ORM\HasLifecycleCallbacks
*/
class MeetingActivity
{
/**
* @var int
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue()
*/
protected $id;
/**
* @var Meeting
*
* @ORM\ManyToOne(targetEntity="Meeting", inversedBy="activities")
* @ORM\JoinColumn(name="meeting_id")
*/
protected $meeting;
/**
* @var Meeting
*
* @ORM\ManyToOne(targetEntity="Chamilo\UserBundle\Entity\User")
* @ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
protected $user;
/**
* @var string
*
* @ORM\Column(type="string", name="name", length=255, nullable=false)
*/
protected $name;
/**
* @var string
*
* @ORM\Column(type="string", name="type", length=255, nullable=false)
*/
protected $type;
/**
* @var string
*
* @ORM\Column(type="text", name="event", nullable=true)
*/
protected $event;
/**
* @var \DateTime
*
* @ORM\Column(name="created_at", type="datetime", nullable=false)
*/
protected $createdAt;
public function __construct()
{
$this->createdAt = new \DateTime();
}
/**
* @return string
*/
public function __toString()
{
return sprintf('Activity %d', $this->id);
}
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @return Meeting
*/
public function getMeeting()
{
return $this->meeting;
}
/**
* @param Meeting $meeting
*
* @return MeetingActivity
*/
public function setMeeting($meeting)
{
$this->meeting = $meeting;
return $this;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @param string $name
*
* @return MeetingActivity
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* @return string
*/
public function getType()
{
return $this->type;
}
/**
* @param string $type
*
* @return MeetingActivity
*/
public function setType($type)
{
$this->type = $type;
return $this;
}
/**
* @return DateTime
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* @return string
*/
public function getEvent()
{
return $this->event;
}
/**
* @return Meeting
*/
public function getUser()
{
return $this->user;
}
/**
* @param Meeting $user
*
* @return MeetingActivity
*/
public function setUser($user)
{
$this->user = $user;
return $this;
}
public function getEventDecoded()
{
if (!empty($this->event)) {
return json_decode($this->event);
}
return '';
}
/**
* @param string $event
*
* @return MeetingActivity
*/
public function setEvent($event)
{
$this->event = $event;
return $this;
}
}

View File

@@ -0,0 +1,38 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use Doctrine\ORM\Mapping as ORM;
use Exception;
/**
* @ORM\Entity()
* @ORM\HasLifecycleCallbacks()
*/
class Presenter extends Registrant
{
public function __toString()
{
return sprintf('Presenter %d', $this->id);
}
/**
* @ORM\PostLoad()
*
* @throws Exception
*/
public function postLoad()
{
parent::postLoad();
}
/**
* @ORM\PreFlush()
*/
public function preFlush()
{
parent::preFlush();
}
}

View File

@@ -0,0 +1,193 @@
<?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.
*
* @ORM\Entity(repositoryClass="Chamilo\PluginBundle\Zoom\RecordingRepository")
* @ORM\Table(
* name="plugin_zoom_recording",
* indexes={
* @ORM\Index(name="meeting_id_index", columns={"meeting_id"}),
* }
* )
* @ORM\HasLifecycleCallbacks
*/
class Recording
{
/** @var DateTime */
public $startDateTime;
/** @var string */
public $formattedStartTime;
/** @var DateInterval */
public $durationInterval;
/** @var string */
public $formattedDuration;
/**
* @var string
*
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue()
*/
protected $id;
/**
* @var string
*
* @ORM\Column(type="string")
*/
protected $uuid;
/**
* @var Meeting
*
* @ORM\ManyToOne(targetEntity="Meeting", inversedBy="recordings")
* @ORM\JoinColumn(name="meeting_id")
*/
protected $meeting;
/**
* @var string
*
* @ORM\Column(type="text", name="recording_meeting_json", nullable=true)
*/
protected $recordingMeetingJson;
/** @var RecordingMeeting */
protected $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 Meeting
*/
public function getMeeting()
{
return $this->meeting;
}
/**
* @throws Exception
*
* @return RecordingMeeting
*/
public function getRecordingMeeting()
{
return $this->recordingMeeting;
}
/**
* @param Meeting $meeting
*
* @return $this
*/
public function setMeeting($meeting)
{
$this->meeting = $meeting;
$this->meeting->getRecordings()->add($this);
return $this;
}
/**
* @param RecordingMeeting $recordingMeeting
*
* @throws Exception
*
* @return Recording
*/
public function setRecordingMeeting($recordingMeeting)
{
if (null === $this->uuid) {
$this->uuid = $recordingMeeting->uuid;
} elseif ($this->uuid !== $recordingMeeting->uuid) {
throw new Exception('the RecordingEntity identifier differs from the RecordingMeeting identifier');
}
if (null === $this->meeting) {
$this->meeting = Database::getManager()->getRepository(Meeting::class)->find($recordingMeeting->id);
} elseif ($this->meeting->getMeetingId() != $recordingMeeting->id) {
// $this->meeting remains null when the remote RecordingMeeting refers to a deleted meeting.
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 (null !== $this->recordingMeetingJson) {
$this->recordingMeeting = RecordingMeeting::fromJson($this->recordingMeetingJson);
}
$this->initializeExtraProperties();
}
/**
* @ORM\PreFlush
*/
public function preFlush()
{
if (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(api_get_timezone()));
$this->formattedStartTime = $this->startDateTime->format('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'));
}
}

View File

@@ -0,0 +1,283 @@
<?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.
*
* @ORM\Entity(repositoryClass="RegistrantRepository")
* @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
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="type", type="string")
* @ORM\DiscriminatorMap({"registrant" = "Chamilo\PluginBundle\Zoom\Registrant", "presenter" = "Chamilo\PluginBundle\Zoom\Presenter"})
*/
class Registrant
{
/** @var string */
public $fullName;
/**
* @var int
* @ORM\Column(type="integer", name="id")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @var User
* @ORM\ManyToOne(targetEntity="Chamilo\UserBundle\Entity\User")
* @ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=false)
*/
protected $user;
/**
* @var Meeting
* @ORM\ManyToOne(targetEntity="Meeting", inversedBy="registrants")
* @ORM\JoinColumn(name="meeting_id", referencedColumnName="id")
*/
protected $meeting;
/**
* @var string
* @ORM\Column(type="text", name="created_registration_json", nullable=true)
*/
protected $createdRegistrationJson;
/**
* @var string
* @ORM\Column(type="text", name="meeting_registrant_list_item_json", nullable=true)
*/
protected $meetingRegistrantListItemJson;
/**
* @var string
* @ORM\Column(type="text", name="meeting_registrant_json", nullable=true)
*/
protected $meetingRegistrantJson;
/**
* @var Signature|null
*
* @ORM\OneToOne(targetEntity="Chamilo\PluginBundle\Zoom\Signature", mappedBy="registrant", orphanRemoval=true)
*/
protected $signature;
/** @var CreatedRegistration */
protected $createdRegistration;
/** @var MeetingRegistrant */
protected $meetingRegistrant;
/** @var MeetingRegistrantListItem */
protected $meetingRegistrantListItem;
/**
* @return string
*/
public function __toString()
{
return sprintf('Registrant %d', $this->id);
}
/**
* @return Meeting
*/
public function getMeeting()
{
return $this->meeting;
}
/**
* @param Meeting $meeting
*
* @return $this
*/
public function setMeeting($meeting)
{
$this->meeting = $meeting;
$this->meeting->getRegistrants()->add($this);
return $this;
}
/**
* @return User
*/
public function getUser()
{
return $this->user;
}
/**
* @param User $user
*
* @return $this
*/
public function setUser($user)
{
$this->user = $user;
return $this;
}
/**
* @throws Exception
*
* @return MeetingRegistrantListItem
*/
public function getMeetingRegistrantListItem()
{
return $this->meetingRegistrantListItem;
}
/**
* @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;
}
public function computeFullName()
{
$this->fullName = api_get_person_name(
$this->meetingRegistrant->first_name,
$this->meetingRegistrant->last_name
);
}
public function getJoinUrl()
{
if (!$this->createdRegistration) {
return '';
}
return $this->createdRegistration->join_url;
}
/**
* @throws Exception
*
* @return CreatedRegistration
*/
public function getCreatedRegistration()
{
return $this->createdRegistration;
}
/**
* @param CreatedRegistration $createdRegistration
*
* @throws Exception
*
* @return $this
*/
public function setCreatedRegistration($createdRegistration)
{
if (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;
}
/**
* @throws Exception
*
* @return MeetingRegistrant
*/
public function getMeetingRegistrant()
{
return $this->meetingRegistrant;
}
/**
* @throws Exception
*/
public function setMeetingRegistrant(API\RegistrantSchema $meetingRegistrant): Registrant
{
$this->meetingRegistrant = $meetingRegistrant;
$this->computeFullName();
return $this;
}
/**
* @ORM\PostLoad
*
* @throws Exception
*/
public function postLoad()
{
if (null !== $this->meetingRegistrantJson) {
$this->meetingRegistrant = MeetingRegistrant::fromJson($this->meetingRegistrantJson);
}
if (null !== $this->createdRegistrationJson) {
$this->createdRegistration = CreatedRegistration::fromJson($this->createdRegistrationJson);
}
if (null !== $this->meetingRegistrantListItemJson) {
$this->meetingRegistrantListItem = MeetingRegistrantListItem::fromJson(
$this->meetingRegistrantListItemJson
);
}
$this->computeFullName();
}
/**
* @ORM\PreFlush
*/
public function preFlush()
{
if (null !== $this->meetingRegistrant) {
$this->meetingRegistrantJson = json_encode($this->meetingRegistrant);
}
if (null !== $this->createdRegistration) {
$this->createdRegistrationJson = json_encode($this->createdRegistration);
}
if (null !== $this->meetingRegistrantListItem) {
$this->meetingRegistrantListItemJson = json_encode($this->meetingRegistrantListItem);
}
}
public function setSignature(Signature $signature): void
{
$this->signature = $signature;
$signature->setRegistrant($this);
}
public function getSignature(): ?Signature
{
return $this->signature;
}
}

View File

@@ -0,0 +1,84 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity()
* @ORM\Table(name="plugin_zoom_signature")
*/
class Signature
{
/**
* @var int
*
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var Registrant
*
* @ORM\OneToOne(targetEntity="Chamilo\PluginBundle\Zoom\Registrant", inversedBy="signature")
* @ORM\JoinColumn(name="registrant_id", referencedColumnName="id")
*/
private $registrant;
/**
* @var string
*
* @ORM\Column(name="signature", type="text")
*/
private $file;
/**
* @var DateTime
*
* @ORM\Column(name="registered_at", type="datetime")
*/
private $registeredAt;
public function getId(): int
{
return $this->id;
}
public function getRegistrant(): Registrant
{
return $this->registrant;
}
public function setRegistrant(Registrant $registrant): Signature
{
$this->registrant = $registrant;
return $this;
}
public function getFile(): string
{
return $this->file;
}
public function setFile(string $file): Signature
{
$this->file = $file;
return $this;
}
public function getRegisteredAt(): DateTime
{
return $this->registeredAt;
}
public function setRegisteredAt(DateTime $registeredAt): Signature
{
$this->registeredAt = $registeredAt;
return $this;
}
}

View File

@@ -0,0 +1,138 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use Chamilo\PluginBundle\Zoom\API\BaseMeetingTrait;
use Chamilo\PluginBundle\Zoom\API\WebinarSchema;
use Chamilo\PluginBundle\Zoom\API\WebinarSettings;
use DateInterval;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
use Exception;
use ZoomPlugin;
/**
* @ORM\Entity()
* @ORM\HasLifecycleCallbacks
*/
class Webinar extends Meeting
{
/**
* @var string
*
* @ORM\Column(name="webinar_schema_json", type="text", nullable=true)
*/
protected $webinarSchemaJson;
/**
* @var WebinarSchema
*/
protected $webinarSchema;
public function preFlush()
{
if (null !== $this->webinarSchema) {
$this->webinarSchemaJson = json_encode($this->webinarSchema);
}
}
public function postLoad()
{
if (null !== $this->webinarSchemaJson) {
$this->webinarSchema = WebinarSchema::fromJson($this->webinarSchemaJson);
}
$this->initializeDisplayableProperties();
}
/**
* @param WebinarSchema|BaseMeetingTrait $webinarSchema
*
* @throws Exception
*/
public function setWebinarSchema(WebinarSchema $webinarSchema): Webinar
{
if (null === $this->meetingId) {
$this->meetingId = $webinarSchema->id;
} elseif ($this->meetingId != $webinarSchema->id) {
throw new Exception('the Meeting identifier differs from the MeetingInfoGet identifier');
}
$this->webinarSchema = $webinarSchema;
$this->initializeDisplayableProperties();
return $this;
}
public function getWebinarSchema(): WebinarSchema
{
return $this->webinarSchema;
}
public function hasCloudAutoRecordingEnabled(): bool
{
return $this->webinarSchema->settings->auto_recording !== ZoomPlugin::RECORDING_TYPE_NONE;
}
public function requiresDateAndDuration(): bool
{
return WebinarSchema::TYPE_WEBINAR == $this->webinarSchema->type;
}
public function requiresRegistration(): bool
{
return in_array(
$this->webinarSchema->settings->approval_type,
[
WebinarSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE,
WebinarSettings::APPROVAL_TYPE_MANUALLY_APPROVE,
]
);
}
public function getTopic(): string
{
return $this->webinarSchema->topic;
}
public function getAgenda(): ?string
{
return $this->webinarSchema->agenda;
}
protected function initializeDisplayableProperties()
{
$zoomPlugin = ZoomPlugin::create();
$namedTypes = [
WebinarSchema::TYPE_WEBINAR => $zoomPlugin->get_lang('Webinar'),
WebinarSchema::TYPE_RECURRING_NO_FIXED_TIME => $zoomPlugin->get_lang('RecurringWithNoFixedTime'),
WebinarSchema::TYPE_RECURRING_FIXED_TIME => $zoomPlugin->get_lang('RecurringWithFixedTime'),
];
$this->typeName = $namedTypes[$this->webinarSchema->type];
if ($this->webinarSchema->start_time) {
$this->startDateTime = new DateTime(
$this->webinarSchema->start_time,
new \DateTimeZone(api_get_timezone())
);
$this->formattedStartTime = $this->startDateTime->format('Y-m-d H:i');
}
if ($this->webinarSchema->duration) {
$now = new DateTime();
$later = new DateTime();
$later->add(
new DateInterval('PT'.$this->webinarSchema->duration.'M')
);
$this->durationInterval = $now->diff($later);
$this->formattedDuration = $this->durationInterval->format(
$zoomPlugin->get_lang('DurationFormat')
);
}
}
}

View File

@@ -0,0 +1,70 @@
The Chamilo Zoom plugin class itself is defined in _plugin/zoom/lib/ZoomPlugin.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.

148
plugin/zoom/README.md Normal file
View File

@@ -0,0 +1,148 @@
This plugin adds Zoom meetings, user registration to meetings and meeting recordings.
> This plugin requires a Zoom account to manage meetings.
Once enabled, it will show as an additional course tool in all courses' homepage: teachers will be able to launch a
conference and student to join it.
## Configuration
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 or a Server-to-Sever OAuth app:
> From June 1, 2023, Zoom recommend that you create a Server-to-Server OAuth application to replace the funcionality of
> a JWT app in your account.
1. Log into your [Zoom profile page](https://zoom.us/profile)
2. Click on Advanced / Application Marketplace
3. Click on [Develop / Build App](https://marketplace.zoom.us/develop/create)
4. Choose JWT or Server-to-Serve OAuth and then Create
5. Information: Fill in fields about your "App" (application and company names, contact name and email address)
6. Click on Continue
7. App Credentials
1. For a JWT application: Copy your API Key and Secret to the plugin configuration
2. For a Server-to-Server OAuth application: Copy your *Account ID*, *Client ID* anf *Client secret* to the plugin
configuration
8. Click on Continue
9. Feature: enable Event Subscriptions to add a new one with endpoint
URL `https://your.chamilo.url/plugin/zoom/endpoint.php` (validate the endpoint to allow to activate the app) and add
these event types:
- Start Meeting
- End Meeting
- Participant/Host joined meeting
- Participant/Host left meeting
- Start Webinar
- End Webinar
- Participant/Host joined webinar
- Participant/Host left webinar
- All Recordings have completed
- Recording transcript files have completed
Then click on Done then on Save and copy your *Verification Token* if you have a JWT application or the *Secret
Token* if you have an Server-to-Server OAuth application to the plugin configuration
10. click on Continue
11. Scopes (only for Server-to-Server OAuth application): Click on Add Scopes and select *meeting:write:admin*,
*webinar:write:admin*, *recording:write:admin*. Then click on Done.
## Changelog
**v0.5**
* Added the ability to create a system announcement.
**v0.4**
* The creation of webinars is now allowed.
* Added signed attendance to allow you to configure an attendance sheet where participants register their signature. The
signed attendance functionality is similar to that found in the Exercise Signature plugin but does not reuse it.
* Allows you to use multiple accounts and subaccounts to create meetings/webinars
## Meetings - Webinars
A **meeting** or **webinar** can be linked to a local **user** and/or a local **course**/**session**:
* a meeting/webinar with a course is a _course meeting/webinar_;
* a meeting/webinar with a user and no course is a _user meeting/webinar_;
* a meeting/webinar with no course nor user is a _global meeting/webinar_.
A webinar only can be creadted when your Zoom account has a plan with the webinars feature.
## Registrants
A **registrant** is the registration of a local user to a meeting/webinar.
Users do not register themselves to meetings.
* They are registered to a course meeting/webinar by the course manager.
* They are registered to a user meeting/webinar by that user.
* They are registered automatically to the global meeting/webinar, when they enter it.
## Recordings
A **recording** is the list of files created during a past meeting/webinar instance.
Course meeting/webinar 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.
The user that starts the meeting/webinar will be identified as the Zoom account that is defined in the plugin. Socreate
a generic account that works for all the users that start meetings.
# Databace changelog
Please, execute this queries in your database:
**Updatnig to v0.6 from v.0.5**
```sql
UPDATE plugin_zoom_registrant SET type = 'registrant';
ALTER TABLE plugin_zoom_registrant
ADD type VARCHAR(255) NOT NULL;
```
**Updating to v0.5 from v.0.4**
```sql
ALTER TABLE plugin_zoom_meeting
ADD sys_announcement_id INT DEFAULT NULL;
ALTER TABLE plugin_zoom_meeting
ADD CONSTRAINT FK_3448092778FB10C FOREIGN KEY (sys_announcement_id) REFERENCES sys_announcement (id) ON DELETE SET NULL;
CREATE INDEX IDX_3448092778FB10C ON plugin_zoom_meeting (sys_announcement_id);
```
**Updating to v0.4 from v0.3**
```sql
ALTER TABLE plugin_zoom_meeting
ADD account_email VARCHAR(255) DEFAULT NULL,
ADD type VARCHAR(255) NOT NULL,
ADD webinar_schema_json LONGTEXT DEFAULT NULL;
CREATE TABLE plugin_zoom_signature (
id INT AUTO_INCREMENT NOT NULL,
registrant_id INT DEFAULT NULL,
signature LONGTEXT NOT NULL,
registered_at DATETIME NOT NULL,
UNIQUE INDEX UNIQ_D41895893304A716 (registrant_id),
PRIMARY KEY (id)
)
DEFAULT CHARACTER SET utf8
COLLATE `utf8_unicode_ci`
ENGINE = InnoDB;
ALTER TABLE plugin_zoom_signature
ADD CONSTRAINT FK_D41895893304A716 FOREIGN KEY (registrant_id) REFERENCES plugin_zoom_registrant (id);
ALTER TABLE plugin_zoom_meeting
ADD sign_attendance TINYINT(1) NOT NULL,
ADD reason_to_sign_attendance LONGTEXT DEFAULT NULL;
```
# Contributing
Read README.code.md for an introduction to the plugin's code.

55
plugin/zoom/activity.php Normal file
View File

@@ -0,0 +1,55 @@
<?php
/* For license terms, see /license.txt */
use Chamilo\PluginBundle\Zoom\Meeting;
$course_plugin = 'zoom'; // needed in order to load the plugin lang variables
require_once __DIR__.'/config.php';
$plugin = ZoomPlugin::create();
$tool_name = $plugin->get_lang('ZoomVideoConferences');
$meetingId = isset($_REQUEST['meetingId']) ? (int) $_REQUEST['meetingId'] : 0;
if (empty($meetingId)) {
api_not_allowed(true);
}
$content = '';
/** @var Meeting $meeting */
$meeting = $plugin->getMeetingRepository()->findOneBy(['meetingId' => $meetingId]);
if (null === $meeting) {
api_not_allowed(true);
}
if (!$plugin->userIsConferenceManager($meeting)) {
api_not_allowed(true);
}
$returnURL = 'meetings.php';
$urlExtra = '';
if ($meeting->isCourseMeeting()) {
api_protect_course_script(true);
$urlExtra = api_get_cidreq();
$returnURL = 'start.php?'.$urlExtra;
if (api_is_in_group()) {
$interbreadcrumb[] = [
'url' => api_get_path(WEB_CODE_PATH).'group/group.php?'.api_get_cidreq(),
'name' => get_lang('Groups'),
];
$interbreadcrumb[] = [
'url' => api_get_path(WEB_CODE_PATH).'group/group_space.php?'.api_get_cidreq(),
'name' => get_lang('GroupSpace').' '.$meeting->getGroup()->getName(),
];
}
}
$interbreadcrumb[] = [
'url' => $returnURL,
'name' => $plugin->get_lang('ZoomVideoConferences'),
];
$tpl = new Template($meeting->getMeetingId());
$tpl->assign('actions', $plugin->getToolbar());
$tpl->assign('meeting', $meeting);
$tpl->assign('url_extra', $urlExtra);
$tpl->assign('content', $tpl->fetch('zoom/view/activity.tpl'));
$tpl->display_one_col_template();

181
plugin/zoom/attendance.php Normal file
View File

@@ -0,0 +1,181 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Zoom\Meeting;
use Chamilo\PluginBundle\Zoom\Registrant;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
require_once __DIR__.'/config.php';
api_block_anonymous_users();
$httpRequest = HttpRequest::createFromGlobals();
$meetingId = $httpRequest->get('meetingId', 0);
if (empty($meetingId)) {
api_not_allowed(true);
}
$plugin = ZoomPlugin::create();
$em = Database::getManager();
/** @var Meeting $meeting */
$meeting = $plugin->getMeetingRepository()->findOneBy(['meetingId' => $meetingId]);
$registrantsRepo = $em->getRepository(Registrant::class);
if (null === $meeting) {
api_not_allowed(
true,
Display::return_message($plugin->get_lang('MeetingNotFound'), 'error')
);
}
if (!$plugin->userIsConferenceManager($meeting)
|| !$meeting->isSignAttendance()
) {
api_not_allowed(
true,
Display::return_message(get_lang('NotAvailable'), 'warning')
);
}
$getNumberOfSignatures = function () use ($meeting) {
return $meeting->getRegistrants()->count();
};
$getSignaturesData = function (
$from,
$limit,
$column,
$direction
) use ($registrantsRepo, $meeting) {
if (0 === $column) {
$columnField = 'u.lastname';
} elseif (1 === $column) {
$columnField = 'u.firstname';
} else {
$columnField = 's.registeredAt';
}
$result = $registrantsRepo->findByMeetingPaginated($meeting, $from, $limit, $columnField, $direction);
return array_map(
function (Registrant $registrant) {
$signature = $registrant->getSignature();
return [
$registrant->getUser()->getLastname(),
$registrant->getUser()->getFirstname(),
$signature ? $signature->getRegisteredAt() : null,
$signature ? $signature->getFile() : null,
];
},
$result
);
};
if ($httpRequest->query->has('export')) {
$plugin->exportSignatures(
$meeting,
$httpRequest->query->getAlnum('export')
);
}
$table = new SortableTable('zoom_signatures', $getNumberOfSignatures, $getSignaturesData, 2);
$table->set_header(0, get_lang('LastName'));
$table->set_header(1, get_lang('FirstName'));
$table->set_header(2, get_lang('DateTime'), true, ['class' => 'text-center'], ['class' => 'text-center']);
$table->set_header(3, $plugin->get_lang('Signature'), false, ['style' => 'width: 200px', 'class' => 'text-center']);
$table->set_additional_parameters(
array_filter(
$httpRequest->query->all(),
function ($key): bool {
return strpos($key, 'zoom_signatures_') === false;
},
ARRAY_FILTER_USE_KEY
)
);
$table->set_column_filter(
2,
function ($dateTime) {
return $dateTime ? api_convert_and_format_date($dateTime, DATE_TIME_FORMAT_LONG) : null;
}
);
$table->set_column_filter(
3,
function ($imgData) use ($plugin) {
if (empty($imgData)) {
return null;
}
return Display::img(
$imgData,
$plugin->get_lang('SignatureDone'),
['class' => 'img-thumbnail'],
false
);
}
);
$cidReq = api_get_cidreq();
$queryParams = 'meetingId='.$meeting->getMeetingId().'&'.$cidReq;
$returnURL = 'meetings.php';
if ($meeting->isCourseMeeting()) {
api_protect_course_script(true);
$this_section = SECTION_COURSES;
$returnURL = 'start.php?'.$cidReq;
if (api_is_in_group()) {
$interbreadcrumb[] = [
'url' => api_get_path(WEB_CODE_PATH).'group/group.php?'.$cidReq,
'name' => get_lang('Groups'),
];
$interbreadcrumb[] = [
'url' => api_get_path(WEB_CODE_PATH).'group/group_space.php?'.$cidReq,
'name' => get_lang('GroupSpace').' '.$meeting->getGroup()->getName(),
];
}
}
$interbreadcrumb[] = [
'url' => $returnURL,
'name' => $plugin->get_lang('ZoomVideoConferences'),
];
$interbreadcrumb[] = [
'url' => 'meeting.php?'.$queryParams,
'name' => $meeting->getMeetingInfoGet()->topic,
];
$exportPdfLink = Display::url(
Display::return_icon('pdf.png', get_lang('ExportToPDF'), [], ICON_SIZE_MEDIUM),
api_get_self().'?'.$queryParams.'&export=pdf'
);
$exportXlsLink = Display::url(
Display::return_icon('excel.png', get_lang('ExportAsXLS'), [], ICON_SIZE_MEDIUM),
api_get_self().'?'.$queryParams.'&export=xls'
);
$pageTitle = $plugin->get_lang('Attendance');
$content = '
<dl>
<dt>'.$plugin->get_lang('ReasonToSign').'</dt>
<dd>'.$meeting->getReasonToSignAttendance().'</dd>
</dl>
'.$table->return_table();
$tpl = new Template($pageTitle);
$tpl->assign(
'actions',
Display::toolbarAction(
'attendance-actions',
[$exportPdfLink.PHP_EOL.$exportXlsLink]
)
);
$tpl->assign('header', $pageTitle);
$tpl->assign('content', $content);
$tpl->display_one_col_template();

View File

@@ -0,0 +1,66 @@
<?php
/* For license terms, see /license.txt */
use Chamilo\PluginBundle\Zoom\API\BaseMeetingTrait;
use Chamilo\PluginBundle\Zoom\Meeting;
use Chamilo\PluginBundle\Zoom\Webinar;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
$course_plugin = 'zoom'; // needed in order to load the plugin lang variables
$cidReset = true;
require_once __DIR__.'/config.php';
api_protect_admin_script();
$request = HttpRequest::createFromGlobals();
$plugin = ZoomPlugin::create();
$user = api_get_user_entity(api_get_user_id());
$action = $request->get('a');
if ($action == 'get_events') {
$startDate = $request->query->get('start');
$endDate = $request->query->get('end');
$startDate = api_get_utc_datetime($startDate, true, true);
$endDate = api_get_utc_datetime($endDate, true, true);
$meetings = $plugin
->getMeetingRepository()
->periodMeetings($startDate, $endDate);
$meetingsAsEvents = array_map(
function (Meeting $conference) {
$isWebinar = $conference instanceof Webinar;
/** @var BaseMeetingTrait $schema */
$schema = $isWebinar ? $conference->getWebinarSchema() : $conference->getMeetingInfoGet();
$endDate = new DateTime($conference->formattedStartTime);
$endDate->add($conference->durationInterval);
return [
'id' => 'meeting_'.$conference->getId(),
'title' => $schema->topic,
'typeName' => $conference->typeName,
'editable' => false,
'start' => $conference->formattedStartTime,
'start_date_localtime' => $conference->formattedStartTime,
'end' => $endDate->format('Y-m-d H:i'),
'end_date_localtime' => $endDate->format('Y-m-d H:i'),
'duration' => $conference->formattedDuration,
'description' => $schema->agenda,
'allDay' => false,
'accountEmail' => $conference->getAccountEmail(),
];
},
$meetings
);
$response = JsonResponse::create($meetingsAsEvents);
$response->send();
}

47
plugin/zoom/calendar.php Normal file
View File

@@ -0,0 +1,47 @@
<?php
/* For license terms, see /license.txt */
$course_plugin = 'zoom'; // needed in order to load the plugin lang variables
$cidReset = true;
require_once __DIR__.'/config.php';
api_protect_admin_script();
$plugin = ZoomPlugin::create();
$toolName = $plugin->get_lang('ZoomVideoConferences');
$defaultView = api_get_setting('default_calendar_view');
if (empty($defaultView)) {
$defaultView = 'month';
}
$regionValue = api_get_language_isocode();
$htmlHeadXtra[] = api_get_asset('qtip2/jquery.qtip.min.js');
$htmlHeadXtra[] = api_get_asset('fullcalendar/dist/fullcalendar.js');
$htmlHeadXtra[] = api_get_asset('fullcalendar/dist/locale-all.js');
$htmlHeadXtra[] = api_get_css_asset('fullcalendar/dist/fullcalendar.min.css');
$htmlHeadXtra[] = api_get_css_asset('qtip2/jquery.qtip.min.css');
$tpl = new Template($toolName);
$tpl->assign('web_agenda_ajax_url', 'calendar.ajax.php?sec_token='.Security::get_token());
$tpl->assign('default_view', $defaultView);
$tpl->assign('region_value', 'en' === $regionValue ? 'en-GB' : $regionValue);
$onHoverInfo = Agenda::returnOnHoverInfo();
$tpl->assign('on_hover_info', $onHoverInfo);
$extraSettings = Agenda::returnFullCalendarExtraSettings();
$tpl->assign('fullcalendar_settings', $extraSettings);
$content = $tpl->fetch('zoom/view/calendar.tpl');
$tpl->assign('actions', $plugin->getToolbar());
$tpl->assign('content', $content);
$tpl->display_one_col_template();

5
plugin/zoom/config.php Normal file
View File

@@ -0,0 +1,5 @@
<?php
/* For licensing terms, see /license.txt */
require_once __DIR__.'/../../main/inc/global.inc.php';

176
plugin/zoom/endpoint.php Normal file
View File

@@ -0,0 +1,176 @@
<?php
/* For license terms, see /license.txt */
use Chamilo\PluginBundle\Zoom\API\RecordingMeeting;
use Chamilo\PluginBundle\Zoom\Meeting;
use Chamilo\PluginBundle\Zoom\MeetingActivity;
use Chamilo\PluginBundle\Zoom\Recording;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response;
require_once __DIR__.'/config.php';
$request = HttpRequest::createFromGlobals();
if (!$request->isMethod('POST')) {
http_response_code(Response::HTTP_NOT_FOUND);
exit;
}
$configAccountId = api_get_plugin_setting('zoom', ZoomPlugin::SETTING_ACCOUNT_ID);
$configClientId = api_get_plugin_setting('zoom', ZoomPlugin::SETTING_CLIENT_ID);
$configClientSecret = api_get_plugin_setting('zoom', ZoomPlugin::SETTING_CLIENT_SECRET);
$configSecretToken = api_get_plugin_setting('zoom', ZoomPlugin::SETTING_SECRET_TOKEN);
$isS2SApp = !empty($configAccountId) && !empty($configClientId) && !empty($configClientSecret);
$isJwtApp = !$isS2SApp;
$authorizationHeaderValue = $request->headers->get('Authorization');
if ($isJwtApp && api_get_plugin_setting('zoom', 'verificationToken') !== $authorizationHeaderValue) {
error_log('verificationToken not valid, please check your zoom configuration');
http_response_code(Response::HTTP_UNAUTHORIZED);
exit;
}
$body = file_get_contents('php://input');
$decoded = json_decode($body);
if ('endpoint.url_validation' === $decoded->event) {
$json = json_encode([
'plainToken' => $decoded->payload->plainToken,
'encryptedToken' => hash_hmac('sha256', $decoded->payload->plainToken, $configSecretToken),
]);
echo $json;
exit();
}
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(Response::HTTP_UNPROCESSABLE_ENTITY);
exit;
}
$object = $decoded->payload->object;
list($objectType, $action) = explode('.', $decoded->event);
$em = Database::getManager();
$meetingRepository = $em->getRepository(Meeting::class);
$meeting = null;
if ($object->id) {
/** @var Meeting $meeting */
$meeting = $meetingRepository->findOneBy(['meetingId' => $object->id]);
}
if (null === $meeting) {
error_log("Meeting not found");
error_log(sprintf('Event "%s" on %s was unhandled: %s', $action, $objectType, $body));
http_response_code(Response::HTTP_NOT_FOUND);
exit;
}
$activity = new MeetingActivity();
$activity->setName($action);
$activity->setType($objectType);
$activity->setEvent(json_encode($object));
switch ($objectType) {
case 'meeting':
error_log('Meeting '.$action.' - '.$meeting->getId());
error_log(print_r($object, 1));
switch ($action) {
case 'deleted':
$em->remove($meeting);
break;
case 'ended':
case 'started':
$meeting->setStatus($action);
$meeting->addActivity($activity);
$em->persist($meeting);
break;
default:
$meeting->addActivity($activity);
$em->persist($meeting);
break;
}
$em->flush();
break;
case 'webinar':
$meeting->addActivity($activity);
$em->persist($meeting);
$em->flush();
break;
case 'recording':
$recordingRepository = $em->getRepository(Recording::class);
$recordingEntity = null;
if ($object->uuid) {
/** @var Recording $recordingEntity */
$recordingEntity = $recordingRepository->findOneBy(['uuid' => $object->uuid, 'meeting' => $meeting]);
}
error_log("Recording: $action");
error_log(print_r($object, 1));
switch ($action) {
case 'completed':
$recording = new Recording();
$recording->setRecordingMeeting(RecordingMeeting::fromObject($object));
$recording->setMeeting($meeting);
$meeting->addActivity($activity);
$em->persist($meeting);
$em->persist($recording);
$em->flush();
break;
case 'recovered':
/*if (null === $recordingEntity) {
$em->persist(
(new Recording())->setRecordingMeeting(RecordingMeeting::fromObject($object))
);
$em->flush();
}*/
break;
case 'trashed':
case 'deleted':
$meeting->addActivity($activity);
if (null !== $recordingEntity) {
$recordMeeting = $recordingEntity->getRecordingMeeting();
$recordingToDelete = RecordingMeeting::fromObject($object);
$files = [];
if ($recordingToDelete->recording_files) {
foreach ($recordingToDelete->recording_files as $fileToDelete) {
foreach ($recordMeeting->recording_files as $file) {
if ($fileToDelete->id != $file->id) {
$files[] = $file;
}
}
}
}
if (empty($files)) {
$em->remove($recordingEntity);
} else {
$recordMeeting->recording_files = $files;
$recordingEntity->setRecordingMeeting($recordMeeting);
$em->persist($recordingEntity);
}
}
$em->persist($meeting);
$em->flush();
break;
default:
$meeting->addActivity($activity);
$em->persist($meeting);
$em->flush();
break;
}
break;
default:
error_log(sprintf('Event "%s" on %s was unhandled: %s', $action, $objectType, $body));
http_response_code(501); // Not Implemented
break;
}

13
plugin/zoom/global.php Normal file
View File

@@ -0,0 +1,13 @@
<?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);
}
api_location('join_meeting.php?meetingId='.ZoomPlugin::create()->getGlobalMeeting()->getMeetingId());

1
plugin/zoom/index.php Normal file
View File

@@ -0,0 +1 @@
<?php

8
plugin/zoom/install.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
/* For license terms, see /license.txt */
if (!api_is_platform_admin()) {
exit('You must have admin permissions to install plugins');
}
ZoomPlugin::create()->install();

View File

@@ -0,0 +1,114 @@
<?php
/* For license terms, see /license.txt */
use Chamilo\PluginBundle\Zoom\Meeting;
require_once __DIR__.'/config.php';
api_block_anonymous_users();
$course_plugin = 'zoom'; // needed in order to load the plugin lang variables
$meetingId = isset($_REQUEST['meetingId']) ? (int) $_REQUEST['meetingId'] : 0;
if (empty($meetingId)) {
api_not_allowed(true);
}
$plugin = ZoomPlugin::create();
/** @var Meeting $meeting */
$meeting = $plugin->getMeetingRepository()->findOneBy(['meetingId' => $meetingId]);
if (null === $meeting) {
api_not_allowed(true, $plugin->get_lang('MeetingNotFound'));
}
if ($meeting->isCourseMeeting()) {
api_protect_course_script(true);
if (api_is_in_group()) {
$interbreadcrumb[] = [
'url' => api_get_path(WEB_CODE_PATH).'group/group.php?'.api_get_cidreq(),
'name' => get_lang('Groups'),
];
$interbreadcrumb[] = [
'url' => api_get_path(WEB_CODE_PATH).'group/group_space.php?'.api_get_cidreq(),
'name' => get_lang('GroupSpace').' '.$meeting->getGroup()->getName(),
];
}
}
$startJoinURL = '';
$detailsURL = '';
$signature = '';
$btnAnnouncement = '';
$currentUser = api_get_user_entity(api_get_user_id());
$isConferenceManager = $plugin->userIsConferenceManager($meeting);
try {
$startJoinURL = $plugin->getStartOrJoinMeetingURL($meeting);
if (empty($startJoinURL)) {
Display::addFlash(
Display::return_message($plugin->get_lang('ConferenceNotAvailable'), 'warning')
);
}
if ($meeting->isSignAttendance() && !$isConferenceManager) {
$registrant = $meeting->getRegistrantByUser($currentUser);
$signature = $registrant ? $registrant->getSignature() : null;
Security::get_token('zoom_signature');
}
if ($isConferenceManager) {
$detailsURL = api_get_path(WEB_PLUGIN_PATH).'zoom/meeting.php?meetingId='.$meeting->getMeetingId();
}
$allowAnnouncementsToSessionAdmin = api_get_configuration_value('session_admin_access_system_announcement');
if (api_is_platform_admin($allowAnnouncementsToSessionAdmin)) {
$announcementUrl = '';
if ($announcement = $meeting->getSysAnnouncement()) {
$announcementUrl = api_get_path(WEB_CODE_PATH).'admin/system_announcements.php?'
.http_build_query(
[
'action' => 'edit',
'id' => $announcement->getId(),
]
);
} else {
$announcementUrl = api_get_path(WEB_CODE_PATH).'admin/system_announcements.php?'
.http_build_query(
[
'action' => 'add',
'type' => 'zoom_conference',
'meeting' => $meeting->getMeetingId(),
]
);
}
$btnAnnouncement = Display::toolbarButton(
$announcement ? get_lang('EditSystemAnnouncement') : get_lang('AddSystemAnnouncement'),
$announcementUrl,
'bullhorn'
);
}
} catch (Exception $exception) {
Display::addFlash(
Display::return_message($exception->getMessage(), 'warning')
);
}
$htmlHeadXtra[] = api_get_asset('signature_pad/signature_pad.umd.js');
$tpl = new Template($meeting->getMeetingId());
$tpl->assign('meeting', $meeting);
$tpl->assign('start_url', $startJoinURL);
$tpl->assign('details_url', $detailsURL);
$tpl->assign('btn_announcement', $btnAnnouncement);
$tpl->assign('is_conference_manager', $isConferenceManager);
$tpl->assign('signature', $signature);
$content = $tpl->fetch('zoom/view/join.tpl');
$tpl->assign('actions', $plugin->getToolbar());
$tpl->assign('content', $content);
$tpl->display_one_col_template();

View File

@@ -0,0 +1,191 @@
<?php
/* License: see /license.txt */
// Needed in order to show the plugin title
$strings['plugin_title'] = "Zoom Videoconference";
$strings['plugin_comment'] = "Zoom Videoconference integration in courses and sessions";
$strings['tool_enable'] = 'Zoom videoconference tool enabled';
$strings['apiKey'] = 'API Key';
$strings['apiKey_help'] = 'For a JWT application type (<small>this app type will be deprecated on 6/1/2023</small>)';
$strings['apiSecret'] = 'API Secret';
$strings['apiSecret_help'] = 'For a JWT application type (<small>this app type will be deprecated on 6/1/2023</small>)';
$strings['verificationToken'] = 'Verification Token';
$strings['verificationToken_help'] = 'For a JWT application type (<small>this app type will be deprecated on 6/1/2023</small>)';
$strings[ZoomPlugin::SETTING_ACCOUNT_ID] = 'Account ID';
$strings[ZoomPlugin::SETTING_ACCOUNT_ID.'_help'] = 'For a Server-to-Server OAuth application type';
$strings[ZoomPlugin::SETTING_CLIENT_ID] = 'Client ID';
$strings[ZoomPlugin::SETTING_CLIENT_ID.'_help'] = 'For a Server-to-Server OAuth application type';
$strings[ZoomPlugin::SETTING_CLIENT_SECRET] = 'Client secret';
$strings[ZoomPlugin::SETTING_CLIENT_SECRET.'_help'] = 'For a Server-to-Server OAuth application type';
$strings[ZoomPlugin::SETTING_SECRET_TOKEN] = 'Secret token';
$strings[ZoomPlugin::SETTING_SECRET_TOKEN.'_help'] = 'For a Server-to-Server OAuth application type';
$strings['enableParticipantRegistration'] = 'Enable participant registration';
$strings['enablePresenter'] = 'Enable presenter';
$strings['enablePresenter_help'] = 'It requires that <i>Enable participant registration</i> settings is enabled.';
$strings['enableCloudRecording'] = 'Automatic recording type';
$strings['enableGlobalConference'] = 'Enable global conference';
$strings['enableGlobalConferencePerUser'] = 'Enable global conference per user';
$strings['globalConferenceAllowRoles'] = "Global conference link only visible for these user roles";
$strings['globalConferencePerUserAllowRoles'] = "Global conference per user link only visible for these user roles";
$strings['accountSelector'] = 'Account selector';
$strings['accountSelector_help'] = 'It allows you to declare the emails of the different accounts with whom you want to open the Zoom videos. Separated by semicolons (account_one@example.come;account_two@exaple.com).';
$strings['tool_enable_help'] = "Choose whether you want to enable the Zoom videoconference tool.
Once enabled, it will show as an additional course tool in all courses' homepage :
teachers will be able to <strong>launch</strong> a conference and student to <strong>join</strong> it.
<br/>
This plugin requires a Zoom account to manage meetings.
<p>The Zoom API uses JSON Web Tokens (JWT) to authenticate account-level access. To get them, create a JWT App or a Server-to-Sever OAuth app:</p>
<blockquote>
<p>From June 1, 2023, Zoom recommend that you create a Server-to-Server OAuth application to replace the funcionality of
a JWT app in your account.</p>
</blockquote>
<ol>
<li>Log into your <a href=\"https://zoom.us/profile\">Zoom profile page</a></li>
<li>Click on Advanced / Application Marketplace</li>
<li>Click on <a href=\"https://marketplace.zoom.us/develop/create\">Develop / Build App</a></li>
<li>Choose JWT or Server-to-Serve OAuth and then Create</li>
<li>Information: Fill in fields about your \"App\" (application and company names, contact name and email address)</li>
<li>Click on Continue</li>
<li>App Credentials
<ol>
<li>For a JWT application: Copy your API Key and Secret to the plugin configuration</li>
<li>For a Server-to-Server OAuth application: Copy your <em>Account ID</em>, <em>Client ID</em> and <em>Client secret</em> to the plugin
configuration</li>
</ol></li>
<li>Click on Continue</li>
<li><p>Feature: enable <em>Event Subscriptions</em> to add a new one with endpoint URL
<code>https://your.chamilo.url/plugin/zoom/endpoint.php</code> (validate the endpoint to allow to activate the app) and add
these event types:</p>
<ul>
<li>Start Meeting</li>
<li>End Meeting</li>
<li>Participant/Host joined meeting</li>
<li>Participant/Host left meeting</li>
<li>Start Webinar</li>
<li>End Webinar</li>
<li>Participant/Host joined webinar</li>
<li>Participant/Host left webinar</li>
<li>All Recordings have completed</li>
<li>Recording transcript files have completed</li>
</ul>
<p>Then click on Done then on Save and copy your <em>Verification Token</em> if you have a JWT application or the <em>Secret
Token</em> if you have an Server-to-Server OAuth application to the plugin configuration</p></li>
<li>click on Continue</li>
<li>Scopes (only for Server-to-Server OAuth application): Click on <em>Add Scopes</em> and select <em>meeting:write:admin</em>,
<em>webinar:write:admin</em>, <em>recording:write:admin</em>. Then click on Done.</li>
</ol>
<br/>
<strong>Attention</strong>:
<br/>Zoom is <em>NOT</em> 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 <em>basic</em> profile.";
$strings['enableCloudRecording_help'] = "Requires a paying Zoom profile.
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";
$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 conference";
$strings['DateMeetingTitle'] = "%s: %s";
$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['EnterMeeting'] = "Enter meeting";
$strings['ViewMeeting'] = "View 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 conference";
$strings['GlobalMeetingPerUser'] = "Global conference per user";
$strings['GroupUsersWereRegistered'] = "Group members were registered";
$strings['InstantMeeting'] = "Instant meeting";
$strings['Join'] = "Join";
$strings['JoinGlobalVideoConference'] = "Join global conference";
$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['RegisteredPresenters'] = "Registered presenters";
$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['ScheduleAMeeting'] = "Schedule a meeting";
$strings['SearchMeeting'] = "Search meeting";
$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['YouAreNotRegisteredToThisMeeting'] = "You are not registered to this meeting";
$strings['ZoomVideoConferences'] = "Zoom Video Conferences";
$strings['Recordings'] = "Recordings";
$strings['CreateGlobalVideoConference'] = "Create global video conference";
$strings['ConferenceNotStarted'] = "Conference not started";
$strings['MeetingNotFound'] = "Meeting not found";
$strings['JoinURLNotAvailable'] = "URL not available";
$strings['Meetings'] = "Meetings";
$strings['ConferenceType'] = "Conference type";
$strings['ForEveryone'] = "Everyone";
$strings['SomeUsers'] = "Some users (Select later)";
$strings['Activity'] = "Activity";
$strings['ConferenceNotAvailable'] = "Conference not available";
$strings['SignAttendance'] = "Sign attendance";
$strings['ReasonToSign'] = 'Reason to sign attendance';
$strings['ConferenceWithAttendance'] = "Conference with attendance sign";
$strings['Sign'] = "Sign";
$strings['Signature'] = "Signature";
$strings['Meeting'] = "Meeting";
$strings['Webinar'] = "Webinar";
$strings['AudienceType'] = 'Audience type';
$strings['AccountEmail'] = 'Account email';
$strings['NewWebinarCreated'] = "New webinar created";
$strings['UpdateWebinar'] = 'Update webinar';
$strings['WebinarUpdated'] = "Webinar updated";
$strings['DeleteWebinar'] = "Delete webinar";
$strings['WebinarDeleted'] = "Webinar deleted";
$strings['UrlForSelfRegistration'] = "URL for self registration";
$strings['RegisterMeToConference'] = "Register me to conference";
$strings['UnregisterMeToConference'] = "Unregister me to conference";
$strings['Presenters'] = "Presenters";

173
plugin/zoom/lang/french.php Normal file
View File

@@ -0,0 +1,173 @@
<?php
/* License: see /license.txt */
// Needed in order to show the plugin title
$strings['plugin_title'] = "Conférence vidéo Zoom";
$strings['plugin_comment'] = "Intégration de conférences vidéo Zoom dans les cours et les sessions";
$strings['tool_enable'] = 'Outil de conférence vidéos Zoom activé';
$strings['apiKey'] = "Clé d'API (<em>API Key</em>)";
$strings['apiSecret'] = "Code secret d'API (<em>API Secret</em>)";
$strings['enableParticipantRegistration'] = "Activer l'inscription des participants";
$strings['enableCloudRecording'] = "Type d'enregistrement automatique";
$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['globalConferencePerUserAllowRoles'] = "Visibilité du lien de vidéo conférence global par utilisateur 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 <strong>démarrer</strong> une conférence et les étudiants la <strong>rejoindre</strong>.
<br/>
Ce plugin requiert un compte Zoom pour gérer les conférences.
<p>L'API de Zoom utilise les <em>JSON Web Tokens (JWT)</em> pour autoriser l'accès à un compte.
Pour les obtenir, créez une application JWT ou une application OAuth serveur à serveur :</p>
<blockquote>
<p>À partir du 1er juin 2023, Zoom recommande de créer une application OAuth serveur à serveur
pour remplacer la fonctionnalité d'une application JWT dans votre compte.</p>
</blockquote>
<ol>
<li>Connectez-vous à votre <a href=\"https://zoom.us/profile\">page de profil Zoom</a></li>
<li>Cliquez sur Avancé / Marketplace d'application</li>
<li>Cliquez sur <a href=\"https://marketplace.zoom.us/develop/create\">Développer / Créer une application</a></li>
<li>Choisissez JWT ou OAuth serveur à serveur, puis Créer</li>
<li>Informations : Remplissez les champs sur votre \"App\" (noms de l'application et de la société, nom et adresse e-mail de contact)</li>
<li>Cliquez sur Continuer</li>
<li>Identifiants de l'application
<ol>
<li>Pour une application JWT : Copiez votre clé API (API Key) et votre code secret (API Secret) dans la configuration du plugin</li>
<li>Pour une application OAuth serveur à serveur : Copiez votre <em>ID de compte</em>, votre <em>ID de client</em> et votre <em>secret de client</em> dans la configuration du plugin</li>
</ol></li>
<li>Cliquez sur Continuer</li>
<li><p>Fonctionnalité : activez les <em>Abonnements aux événements / Event Subscriptions</em> pour en ajouter un nouveau avec comme endpoint URL
<code>https://your.chamilo.url/plugin/zoom/endpoint.php</code> (validez le point de terminaison pour permettre l'activation de l'application) et ajoutez
ces types d'événements :</p>
<ul>
<li>Démarrer une réunion / Start Meeting</li>
<li>Terminer une réunion / End Meeting</li>
<li>Participant/hôte a rejoint la réunion / Participant/Host joined meeting</li>
<li>Participant/hôte a quitté la réunion / Participant/Host left meeting</li>
<li>Démarrer un webinar / Start Webinar</li>
<li>Terminer un webinar / End Webinar</li>
<li>Participant/hôte a rejoint le webinar / Participant/Host joined webinar</li>
<li>Participant/hôte a quitté le webinar / Participant/Host left webinar</li>
<li>Toutes les enregistrements sont terminées / All Recordings have completed</li>
<li>Les fichiers de transcription d'enregistrement sont terminés / Recording transcript files have completed</li>
</ul>
<p>Ensuite, cliquez sur Terminé, puis sur Enregistrer et copiez votre <em>Jeton de vérification / Verification Token</em> si vous avez une application JWT ou le <em>Jeton Secret / Secret
Token</em> si vous avez une application OAuth serveur à serveur dans la configuration du plugin</p></li>
<li>cliquez sur Continuer</li>
<li>Scopes (uniquement pour l'application OAuth serveur à serveur) : cliquez sur <em>Ajouter des scopes / Add Scopes</em> et sélectionnez <em>meeting:write:admin</em>, <em>webinar:write:admin</em>, <em>recording:write:admin</em>. Puis cliquez sur Terminé.</li>
</ol>
<br/>
<strong>Attention</strong> :
<br/>Zoom n'est <em>PAS</em> 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 <em>de base</em>.";
$strings['enableCloudRecording_help'] = "Nécessite un profil Zoom payant.
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";
$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";
$strings['DoIt'] = "Fais-le";
$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['ViewMeeting'] = "Voir 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['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";
$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['ScheduleAMeeting'] = "Programmer une conférence";
$strings['SearchMeeting'] = "Rechercher une conférence";
$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['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";
$strings['Recordings'] = "Enregistrements";
$strings['CreateGlobalVideoConference'] = "Créer une conférence global";
$strings['JoinURLNotAvailable'] = "URL pas disponible";
$strings['Meetings'] = "Conférences";
$strings['Activity'] = "Activité";
$strings['ConferenceNotAvailable'] = "Conférence non disponible";
$strings['SignAttendance'] = "Signer la feuille d'émargement";
$strings['ReasonToSign'] = "Explication pour signer la feuille d'émargement";
$strings['ConferenceWithAttendance'] = "Conférence avec signature d'émargement";
$strings['Sign'] = "Signer";
$strings['Signature'] = "Signature";
$strings['Meeting'] = "Meeting";
$strings['Webinar'] = "Webinar";
$strings['AudienceType'] = "Type d'audience";
$strings['AccountEmail'] = "Compte email";
$strings['NewWebinarCreated'] = "Nouveau webinar créé";
$strings['UpdateWebinar'] = "Mettre à jour le webinar";
$strings['WebinarUpdated'] = "Webinar mis à jour";
$strings['DeleteWebinar'] = "Supprimer le webinar";
$strings['WebinarDeleted'] = "Webinar supprimé";
$strings['UrlForSelfRegistration'] = "URL pour l'auto-inscription des participants";
$strings['RegisterMeToConference'] = "M'inscrire à la visio";
$strings['UnregisterMeToConference'] = "Me désinscrire de la visio";
$strings['ForEveryone'] = "Tout le monde";
$strings['SomeUsers'] = "Utilisateurs inscrits (à inscrire après)";
$strings['Presenters'] = "Animateurs";
$strings['RegisteredPresenters'] = "Animateurs enregistrés";
$strings['enablePresenter_help'] = "Il est nécessaire que l'option <i>Activer l'inscription des participants</i> soit sur Oui.";
$strings['enablePresenter'] = 'Activer les animateurs';

View File

@@ -0,0 +1,174 @@
<?php
/* License: see /license.txt */
// Needed in order to show the plugin title
$strings['plugin_title'] = "Videoconferencia Zoom";
$strings['plugin_comment'] = "Integración de videoconferencias Zoom en los cursos y las sesiones";
$strings['tool_enable'] = 'Herramienta activada';
$strings['apiKey'] = "Clave API (<em>API Key</em>)";
$strings['apiSecret'] = "Código secreto de API (<em>API Secret</em>)";
$strings['enableParticipantRegistration'] = "Activar la inscripción de participantes";
$strings['enablePresenter'] = 'Activar presentadores';
$strings['enablePresenter_help'] = 'Requiere que el parametro <i>Activar la inscripción de participantes</i> este activado.';
$strings['enableCloudRecording'] = "Tipo de grabación automática";
$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['globalConferencePerUserAllowRoles'] = "Visibilidad del enlace global de videoconferencia por usuario para los perfiles siguientes";
$strings['accountSelector'] = 'Selector de cuentas';
$strings['accountSelector_help'] = 'Te permite declarar los correos de las diferentes cuentas con las que quieres abrir los videos de Zoom. Separados por punto y coma (account_one@example.come;account_two@exaple.com).';
$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
<strong>iniciar</strong> una conferencia y los alumnos <strong>juntarse</strong> a ella.
<br/>
Este plugin requiere una cuenta Zoom para gestionar las conferencias.
<p>El API de Zoom utiliza los <em>JSON Web Tokens (JWT)</em> para autorizar el acceso a una cuenta.
Para obtenerlos, crea una <em>app JWT</em> o una aplicación Server-to-Server OAuth:</p>
<blockquote>
<p>Desde el 1 de junio de 2023, Zoom recomienda que usted cree una aplicación Server-to-Server OAuth para reemplazar
la funcionalidad de una aplicación JWT en tu cuenta.</p>
</blockquote>
<ol>
<li>logéase en <a href=\"https://zoom.us/profile\">Su perfil Zoom</a></li>
<li>de clic en <em>Avanzado / Marketplace de aplicaciones</em></li>
<li>de clic en <em><a href=\"https://marketplace.zoom.us/develop/create\">Develop / build App</a></em></li>
<li>escoja <em>JWT o Sever-to-Server OAuth y luego Create</em></li>
<li>Information: ingrese algunas informaciones sobre vuestra \"App\"
(nombres de la aplicación, de la empresa, nombre y dirección de correo de contacto)
<li>de clic en <em>Continue</em></li>
<li>App Credentials:
<ol>
<li>Para una aplicación JWT: copia la clave (API Key) y el código secreto (API Secret) por ingresar aquí.</li>
<li>Para una aplicación Server-to-Server OAuth: Copia los valores de <em>Account ID</em>, <em>Client ID</em> y <em>Client secret</em> para ingresar aquí</li>
</ol>
<li>de clic en <em>Continue</em></li>
<li><p>Feature : activez <em>Event Subscriptions</em> para agregar uno con su endpoint URL
<code>https://your.chamilo.url/plugin/zoom/endpoint.php</code> (valida el endpoint para permitir activar la aplicación)
y agrega este tipo de eventos:</p>
<ul>
<li>Start Meeting</li>
<li>End Meeting</li>
<li>Participant/Host joined meeting</li>
<li>Participant/Host left meeting</li>
<li>Start Webinar</li>
<li>End Webinar</li>
<li>Participant/Host joined webinar</li>
<li>Participant/Host left webinar</li>
<li>All Recordings have completed</li>
<li>Recording transcript files have completed</li>
</ul>
<p>Luego de clic en <em>Done</em> y luego en <em>Save</em> y copie su <em>Verification Token</em> si tiene una aplicación JWT o el <em>Secret token</em> si tiene una aplicación Server-to-Server OAuth para ingresar aquí</li>
<li>de clic en <em>Continue</em></li>
<li>Scopes (solo para una aplicación Server-to-Server OAuth): click en <em>Add Scopes</em> y seleccione <em>meeting:write:admin</em>,
<em>webinar:write:admin</em>, <em>recording:write:admin</em>. Luego, haga click en Done.</li>
</ol>
<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.
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 <em>base/gratuito</em>.";
$strings['enableCloudRecording_help'] = "Requiere un perfil Zoom de pago.
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";
$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";
$strings['DoIt'] = "Hágalo";
$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['ViewMeeting'] = "Ver 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['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";
$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['RegisteredPresenters'] = "Presentadores registrados";
$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['ScheduleAMeeting'] = "Programar una conferencia";
$strings['SearchMeeting'] = "Buscar una conferencia";
$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['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";
$strings['Recordings'] = "Grabaciones";
$strings['CreateGlobalVideoConference'] = "Crear una videoconferencia global";
$strings['JoinURLNotAvailable'] = "URL no disponible";
$strings['Meetings'] = "Conferencias";
$strings['Activity'] = "Actividad";
$strings['ConferenceNotAvailable'] = "Conferencia no disponible";
$strings['SignAttendance'] = "Firmar asistencia";
$strings['ReasonToSign'] = 'Razón para firmar asistencia';
$strings['ConferenceWithAttendance'] = "Conferencia con registro de asistencia";
$strings['Sign'] = "Firmar";
$strings['Signature'] = "Firma";
$strings['Meeting'] = "Conferencia";
$strings['Webinar'] = "Seminario web";
$strings['AudienceType'] = 'Tipo de público';
$strings['AccountEmail'] = 'Correo electrónico de la cuenta';
$strings['NewWebinarCreated'] = "Nuevo seminario web creado";
$strings['UpdateWebinar'] = 'Actualizar seminario web';
$strings['WebinarUpdated'] = "Seminario web actualizado";
$strings['DeleteWebinar'] = "Borrar seminario web";
$strings['WebinarDeleted'] = "Seminario web borrado";
$strings['UrlForSelfRegistration'] = "URL para auto registro";
$strings['RegisterMeToConference'] = "Registrarme a la conferencia";
$strings['UnregisterMeToConference'] = "Cancelar registro a la conferencia";
$strings['Presenters'] = "Presentadores";

View File

@@ -0,0 +1,67 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
trait Api2RequestTrait
{
/**
* @throws Exception
*/
public function send($httpMethod, $relativePath, $parameters = [], $requestBody = null): string
{
$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;
}
}

View File

@@ -0,0 +1,41 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
/**
* Trait BaseMeetingTrait.
* Common meeting properties definitions.
*/
trait BaseMeetingTrait
{
/** @var string */
public $topic;
/** @var int */
public $type;
/** @var string "yyyy-MM-dd'T'HH:mm:ss'Z'" for GMT, same without 'Z' for local time (as set on zoom account) */
public $start_time;
/** @var int in minutes, for scheduled meetings only */
public $duration;
/** @var string the timezone for start_time */
public $timezone;
/** @var string description */
public $agenda;
/** @var string description */
public $host_email;
/**
* @throws Exception
*/
public function update(): void
{
}
}

View File

@@ -0,0 +1,56 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
/**
* Interface Client.
* Two implementations are currently possible : OAuth and JWT.
*
* @see https://marketplace.zoom.us/docs/api-reference/zoom-api
*/
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.
*
* On success, returns the body of the response
* On error, throws an exception with an detailed error message
*
* @param string $httpMethod GET, POST, PUT, DELETE ...
* @param string $relativePath to append to https://api.zoom.us/v2/
* @param array $parameters request query parameters
* @param object $requestBody json-encoded body of the request
*
* @throws Exception describing the error (message and code)
*
* @return string response body (not json-decoded)
*/
abstract public function send($httpMethod, $relativePath, $parameters = [], $requestBody = null);
/**
* Registers an initialized Client.
*
* @param Client $instance
*/
protected static function register($instance)
{
self::$instance = $instance;
}
}

View File

@@ -0,0 +1,42 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
/**
* Class CreatedRegistration.
* An instance of this class is returned by the Zoom server upon recording a registrant to a meeting.
*/
class CreatedRegistration
{
use JsonDeserializableTrait;
/** @var int meeting ID */
public $id;
/** @var string Unique URL for this registrant to join the meeting.
* This URL should only be shared with the registrant for whom the API request was made.
* If the meeting was created with manual approval type (1), the join URL will not be returned in the response.
*/
public $join_url;
/** @var string Unique identifier of the registrant */
public $registrant_id;
/** @var string The start time for the meeting. */
public $start_time;
/** @var string Topic of the meeting. */
public $topic;
/**
* {@inheritdoc}
*/
protected function itemClass($propertyName)
{
throw new Exception("no such array property $propertyName");
}
}

View File

@@ -0,0 +1,18 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
/**
* Class CustomQuestion.
* A list of instances of this class is included in a MeetingRegistrant instance.
*/
class CustomQuestion
{
/** @var string */
public $title;
/** @var string */
public $value;
}

View File

@@ -0,0 +1,26 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
class FollowUpUsers
{
use JsonDeserializableTrait;
/**
* @var bool
*/
public $enable;
/**
* @var
*/
public $type;
public function itemClass($propertyName)
{
throw new Exception("No such array property $propertyName");
}
}

View File

@@ -0,0 +1,39 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
/**
* Class GlobalDialInNumber.
* A list of these is included in a meeting settings.
*/
class GlobalDialInNumber
{
use JsonDeserializableTrait;
/** @var string Country code. For example, BR. */
public $country;
/** @var string Full name of country. For example, Brazil. */
public $country_name;
/** @var string City of the number, if any. For example, Chicago. */
public $city;
/** @var string Phone number. For example, +1 2332357613. */
public $number;
/** @var string Type of number. Either "toll" or "tollfree". */
public $type;
/**
* {@inheritdoc}
*/
public function itemClass($propertyName)
{
throw new Exception("No such array property $propertyName");
}
}

View File

@@ -0,0 +1,38 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Firebase\JWT\JWT;
/**
* Class JWTClient.
*
* @see https://marketplace.zoom.us/docs/guides/auth/jwt
*/
class JWTClient extends Client
{
use Api2RequestTrait;
public $token;
/**
* JWTClient constructor.
* Requires JWT app credentials.
*
* @param string $apiKey JWT API Key
* @param string $apiSecret JWT API Secret
*/
public function __construct($apiKey, $apiSecret)
{
$this->token = JWT::encode(
[
'iss' => $apiKey,
'exp' => (time() + 60) * 1000, // will expire in one minute
],
$apiSecret
);
self::register($this);
}
}

View File

@@ -0,0 +1,122 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
/**
* Trait JsonDeserializableTrait.
* Utility functions to help convert server-generated JSON to API class instances.
*/
trait JsonDeserializableTrait
{
/**
* Builds a class instance from the Json description of the object.
*
* @param string $json
*
* @throws Exception on JSON-decode error or unexpected object property
*
* @return static
*/
public static function fromJson($json)
{
if (empty($json)) {
throw new Exception('Cannot JSON-decode empty string');
}
$object = json_decode($json);
if (null === $object) {
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);
return $instance;
}
/**
* Returns the class name of the items to be found in the named array property.
*
* To override in classes that have a property of type array
*
* @param string $propertyName array property name
*
* @throws Exception if not implemented for this propertyName
*
* @return string class name of the items to be found in the named array property
*/
abstract public function itemClass($propertyName);
/**
* Initializes properties that can be calculated from json-decoded properties.
*
* Called at the end of method recursivelyCopyObjectProperties()
* and indirectly at the end of static method fromJson().
*
* By default it does nothing.
*/
public function initializeExtraProperties()
{
// default does nothing
}
/**
* Copies values from another object properties to an instance, recursively.
*
* @param object $source source object
* @param object $destination specific class instance, with already initialized properties
*
* @throws Exception when the source object has an unexpected property
*/
protected static function recursivelyCopyObjectProperties($source, &$destination)
{
foreach (get_object_vars($source) as $name => $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 {
error_log("Source object has property $name, which was not expected: ".json_encode($source));
}
}
$destination->initializeExtraProperties();
}
}

View File

@@ -0,0 +1,88 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
/**
* Class Meeting, minimal meeting definition required to create one from scratch or update an existing one
* Also referred to as MeetingUpdate in the API documentation
* Does not represent an actual created meeting.
*/
class Meeting
{
use BaseMeetingTrait;
use JsonDeserializableTrait;
public const TYPE_INSTANT = 1;
public const TYPE_SCHEDULED = 2;
public const TYPE_RECURRING_WITH_NO_FIXED_TIME = 3;
public const TYPE_RECURRING_WITH_FIXED_TIME = 8;
/** @var string password to join. [a-z A-Z 0-9 @ - _ *]. Max of 10 characters. */
public $password;
/** @var TrackingField[] Tracking fields */
public $tracking_fields;
/** @var object, only for a recurring meeting with fixed time (type 8) */
public $recurrence;
/** @var MeetingSettings */
public $settings;
/**
* Meeting constructor.
*/
protected function __construct()
{
$this->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.
*
* @throws Exception describing the error (message and code)
*
* @return MeetingInfoGet meeting
*/
public function create($userId = null)
{
$userId = empty($userId) ? 'me' : $userId;
return MeetingInfoGet::fromJson(
Client::getInstance()->send('POST', "users/$userId/meetings", [], $this)
);
}
/**
* Creates a Meeting instance from a topic.
*
* @param string $topic
* @param int $type
*
* @return static
*/
public static function fromTopicAndType($topic, $type = self::TYPE_SCHEDULED)
{
$instance = new static();
$instance->topic = $topic;
$instance->type = $type;
return $instance;
}
}

View File

@@ -0,0 +1,34 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
/**
* Class MeetingInfo
* Used to define MeetingInfoGet
* Does not seem to be used directly.
*/
class MeetingInfo extends Meeting
{
/** @var string */
public $created_at;
/** @var string, allows host to start the meeting as the host (without password) - not to be shared */
public $start_url;
/** @var string, for participants to join the meeting - to share with users to invite */
public $join_url;
/** @var string undocumented */
public $registration_url;
/** @var string H.323/SIP room system password */
public $h323_password;
/** @var int Personal Meeting Id. Only used for scheduled meetings and recurring meetings with no fixed time */
public $pmi;
/** @var object[] */
public $occurrences;
}

View File

@@ -0,0 +1,148 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
/**
* Class MeetingInfoGet
* Full Meeting as returned by the server, with unique identifiers and current status.
*/
class MeetingInfoGet extends MeetingInfo
{
/** @var string unique meeting instance ID */
public $uuid;
/** @var string meeting number */
public $id;
/** @var string host Zoom user id */
public $host_id;
/** @var string meeting status, either "waiting", "started" or "finished" */
public $status;
/** @var string undocumented */
public $pstn_password;
/** @var string Encrypted password for third party endpoints (H323/SIP). */
public $encrypted_password;
/**
* Retrieves a meeting from its numeric identifier.
*
* @param int $id
*
* @throws Exception
*
* @return static the meeting
*/
public static function fromId($id)
{
return static::fromJson(Client::getInstance()->send('GET', "meetings/$id"));
}
/**
* Updates the meeting on server.
*
* @throws Exception
*/
public function update(): void
{
Client::getInstance()->send('PATCH', 'meetings/'.$this->id, [], $this);
}
/**
* Ends the meeting on server.
*
* @throws Exception
*/
public function endNow()
{
Client::getInstance()->send('PUT', "meetings/$this->id/status", [], (object) ['action' => 'end']);
}
/**
* Deletes the meeting on server.
*
* @throws Exception
*/
public function delete()
{
Client::getInstance()->send('DELETE', "meetings/$this->id");
}
/**
* Adds a registrant to the meeting.
*
* @param RegistrantSchema $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(RegistrantSchema $registrant, string $occurrenceIds = ''): CreatedRegistration
{
return CreatedRegistration::fromJson(
Client::getInstance()->send(
'POST',
"meetings/$this->id/registrants",
empty($occurrenceIds) ? [] : ['occurrence_ids' => $occurrenceIds],
$registrant
)
);
}
/**
* Removes registrants from the meeting.
*
* @param MeetingRegistrant[] $registrants registrants to remove (id and email)
* @param string $occurrenceIds separated by comma
*
* @throws Exception
*/
public function removeRegistrants($registrants, $occurrenceIds = '')
{
if (!empty($registrants)) {
Client::getInstance()->send(
'PUT',
"meetings/$this->id/registrants/status",
empty($occurrenceIds) ? [] : ['occurrence_ids' => $occurrenceIds],
(object) [
'action' => 'cancel',
'registrants' => $registrants,
]
);
}
}
/**
* Retrieves meeting registrants.
*
* @throws Exception
*
* @return MeetingRegistrantListItem[] the meeting registrants
*/
public function getRegistrants()
{
return MeetingRegistrantList::loadMeetingRegistrants($this->id);
}
/**
* Retrieves the meeting's instances.
*
* @throws Exception
*
* @return MeetingInstance[]
*/
public function getInstances()
{
return MeetingInstances::fromMeetingId($this->id)->meetings;
}
}

View File

@@ -0,0 +1,50 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
/**
* Class MeetingInstance
* A meeting (numerical id) can have one or more instances (string UUID).
* Each instance has its own start time, participants and recording files.
*
* @see MeetingInstances
* @see PastMeeting for the full record
*/
class MeetingInstance
{
/** @var string */
public $uuid;
/** @var string */
public $start_time;
/**
* Retrieves the recording of the instance.
*
* @throws Exception with code 404 when there is no recording for this meeting
*
* @return RecordingMeeting the recording
*/
public function getRecordings()
{
return RecordingMeeting::fromJson(
Client::getInstance()->send('GET', 'meetings/'.htmlentities($this->uuid).'/recordings')
);
}
/**
* Retrieves the instance's participants.
*
* @throws Exception
*
* @return ParticipantListItem[]
*/
public function getParticipants()
{
return ParticipantList::loadInstanceParticipants($this->uuid);
}
}

View File

@@ -0,0 +1,53 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
/**
* Class MeetingInstances. The list of one meeting's ended instances.
*
* @see MeetingInstance
*/
class MeetingInstances
{
use JsonDeserializableTrait;
/** @var MeetingInstance[] List of ended meeting instances. */
public $meetings;
/**
* MeetingInstances constructor.
*/
public function __construct()
{
$this->meetings = [];
}
/**
* Retrieves a meeting's instances.
*
* @param int $meetingId
*
* @throws Exception
*
* @return MeetingInstances the meeting's instances
*/
public static function fromMeetingId($meetingId)
{
return static::fromJson(Client::getInstance()->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");
}
}

View File

@@ -0,0 +1,58 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
/**
* Class MeetingList. Lists Meetings.
*
* @see MeetingListItem
*/
class MeetingList
{
use Pagination;
public const TYPE_SCHEDULED = 'scheduled'; // all valid past meetings (unexpired),
// live meetings and upcoming scheduled meetings.
public const TYPE_LIVE = 'live'; // all the ongoing meetings.
public const TYPE_UPCOMING = 'upcoming'; // all upcoming meetings, including live meetings.
/** @var MeetingListItem[] */
public $meetings;
/**
* MeetingList constructor.
*/
public function __construct()
{
$this->meetings = [];
}
/**
* Retrieves all meetings of a type.
*
* @param int $type TYPE_SCHEDULED, TYPE_LIVE or TYPE_UPCOMING
*
* @throws Exception
*
* @return MeetingListItem[] all meetings
*/
public static function loadMeetings($type)
{
return static::loadItems('meetings', 'users/me/meetings', ['type' => $type]);
}
/**
* {@inheritdoc}
*/
public function itemClass($propertyName)
{
if ('meetings' === $propertyName) {
return MeetingListItem::class;
}
throw new Exception("No such array property $propertyName");
}
}

View File

@@ -0,0 +1,44 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
/**
* Class MeetingListItem. Item of a list of meetings.
*
* @see MeetingList
*/
class MeetingListItem
{
use BaseMeetingTrait;
use JsonDeserializableTrait;
/** @var string unique meeting instance ID */
public $uuid;
/** @var string meeting number */
public $id;
/** @var string host Zoom user id */
public $host_id;
/** @var string */
public $created_at;
/** @var string */
public $join_url;
/** @var string truncated to 250 characters */
// public $agenda;
/**
* {@inheritdoc}
*/
public function itemClass($propertyName)
{
throw new Exception("no such array property $propertyName");
}
}

View File

@@ -0,0 +1,22 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
/**
* Class Registrant.
*
* Structure of the information to send the server in order to register someone to a meeting.
*/
class MeetingRegistrant extends RegistrantSchema
{
public $auto_approve;
public function __construct()
{
parent::__construct();
$this->auto_approve = true;
}
}

View File

@@ -0,0 +1,53 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
/**
* Class MeetingRegistrantList. List of meeting registrants.
*
* @see MeetingRegistrantListItem
*/
class MeetingRegistrantList
{
use Pagination;
/** @var MeetingRegistrantListItem[] */
public $registrants;
/**
* MeetingRegistrantList constructor.
*/
public function __construct()
{
$this->registrants = [];
}
/**
* Retrieves all registrant for a meeting.
*
* @param int $meetingId
*
* @throws Exception
*
* @return MeetingRegistrantListItem[] all registrants of the meeting
*/
public static function loadMeetingRegistrants($meetingId)
{
return static::loadItems('registrants', "meetings/$meetingId/registrants");
}
/**
* {@inheritdoc}
*/
public function itemClass($propertyName)
{
if ('registrants' === $propertyName) {
return MeetingRegistrantListItem::class;
}
throw new Exception("no such array property $propertyName");
}
}

View File

@@ -0,0 +1,29 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
/**
* Class MeetingRegistrantListItem. Item in a list of meeting registrants.
*
* @see MeetingRegistrantList
*/
class MeetingRegistrantListItem extends MeetingRegistrant
{
/** @var string Registrant ID. */
public $id;
/** @var string The status of the registrant's registration.
* `approved`: User has been successfully approved for the webinar.
* `pending`: The registration is still pending.
* `denied`: User has been denied from joining the webinar.
*/
public $status;
/** @var string The time at which the registrant registered. */
public $create_time;
/** @var string The URL using which an approved registrant can join the webinar. */
public $join_url;
}

View File

@@ -0,0 +1,143 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
/**
* Class MeetingSettings. An instance of this class is included in each Meeting instance.
*/
class MeetingSettings
{
use JsonDeserializableTrait;
public const APPROVAL_TYPE_AUTOMATICALLY_APPROVE = 0;
public const APPROVAL_TYPE_MANUALLY_APPROVE = 1;
public const APPROVAL_TYPE_NO_REGISTRATION_REQUIRED = 2;
public const REGISTRATION_TYPE_REGISTER_ONCE_ATTEND_ANY = 1;
public const REGISTRATION_TYPE_REGISTER_EACH = 2;
public const REGISTRATION_TYPE_REGISTER_ONCE_CHOOSE = 3;
/** @var bool Start video when the host joins the meeting */
public $host_video;
/** @var bool Start video when participants join the meeting */
public $participant_video;
/** @var bool Host meeting in China */
public $cn_meeting;
/** @var bool Host meeting in India */
public $in_meeting;
/** @var bool Allow participants to join the meeting before the host starts the meeting.
* Only used for scheduled or recurring meetings.
*/
public $join_before_host;
/** @var bool Mute participants upon entry */
public $mute_upon_entry;
/** @var bool Add watermark when viewing a shared screen */
public $watermark;
/** @var bool Use a personal meeting ID.
* Only used for scheduled meetings and recurring meetings with no fixed time.
*/
public $use_pmi;
/** @var int Enable registration and set approval for the registration.
* Note that this feature requires the host to be of **Licensed** user type.
* **Registration cannot be enabled for a basic user.**
*/
public $approval_type;
/** @var int Used for recurring meeting with fixed time only. */
public $registration_type;
/** @var string either both, telephony or voip */
public $audio;
/** @var string either local, cloud or none */
public $auto_recording;
/** @var bool @deprecated only signed in users can join this meeting */
public $enforce_login;
/** @var string @deprecated only signed in users with specified domains can join meetings */
public $enforce_login_domains;
/** @var string Alternative host's emails or IDs: multiple values separated by a comma. */
public $alternative_hosts;
/** @var bool Close registration after event date */
public $close_registration;
/** @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;
/** @var GlobalDialInNumber[] Global Dial-in Countries/Regions */
public $global_dial_in_numbers;
/** @var string Contact name for registration */
public $contact_name;
/** @var string Contact email for registration */
public $contact_email;
/** @var bool Send confirmation email to registrants upon successful registration */
public $registrants_confirmation_email;
/** @var bool Send email notifications to registrants about approval, cancellation, denial of the registration.
* The value of this field must be set to true in order to use the `registrants_confirmation_email` field.
*/
public $registrants_email_notification;
/** @var bool Only authenticated users can join meetings. */
public $meeting_authentication;
/** @var string Meeting authentication option id. */
public $authentication_option;
/** @var string
* @see https://support.zoom.us/hc/en-us/articles/360037117472-Authentication-Profiles-for-Meetings-and-Webinars#h_5c0df2e1-cfd2-469f-bb4a-c77d7c0cca6f
*/
public $authentication_domains;
/** @var string
* @see https://support.zoom.us/hc/en-us/articles/360037117472-Authentication-Profiles-for-Meetings-and-Webinars#h_5c0df2e1-cfd2-469f-bb4a-c77d7c0cca6f
*/
public $authentication_name;
/**
* MeetingSettings constructor.
*/
public function __construct()
{
$this->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");
}
}

View File

@@ -0,0 +1,22 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
class Ocurrence
{
use JsonDeserializableTrait;
public $occurrence_id;
public $start_time;
public $duration;
public $status;
public function itemClass($propertyName)
{
throw new Exception("No such array property $propertyName");
}
}

View File

@@ -0,0 +1,68 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
/**
* Trait Pagination
* properties for Pagination objects, which are paginated lists of items,
* retrieved in chunks from the server over one or several API calls, one per page.
*/
trait Pagination
{
use JsonDeserializableTrait;
/** @var int The number of pages returned for the request made. */
public $page_count;
/** @var int The page number of the current results, counting from 1 */
public $page_number;
/** @var int The number of records returned with a single API call. Default 30, max 300. */
public $page_size;
/** @var int The total number of all the records available across pages. */
public $total_records;
/**
* Retrieves all items from the server, possibly generating several API calls.
*
* @param string $arrayPropertyName item array property name
* @param string $relativePath relative path to pass to Client::send
* @param array $parameters parameter array to pass to Client::send
*
* @throws Exception
*
* @return array united list of items
*/
protected static function loadItems($arrayPropertyName, $relativePath, $parameters = [])
{
$items = [];
$pageCount = 1;
$pageSize = 300;
$totalRecords = 0;
for ($pageNumber = 1; $pageNumber <= $pageCount; $pageNumber++) {
$response = static::fromJson(
Client::getInstance()->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;
}
}

View File

@@ -0,0 +1,71 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
/**
* Trait PaginationToken
* properties for PaginationToken objects, which are paginated lists of items,
* retrieved in chunks from the server over one or several API calls, one per page.
*/
trait PaginationToken
{
use JsonDeserializableTrait;
/** @var int The number of pages returned for the request made. */
public $page_count;
/** @var int The number of records returned within a single API call. Default 30, max 300. */
public $page_size;
/** @var int The number of all records available across pages. */
public $total_records;
/** @var string The next page token is used to paginate through large result sets.
* A next page token will be returned whenever the set of available results exceeds the current page size.
* The expiration period for this token is 15 minutes.
*/
public $next_page_token;
/**
* Retrieves all items from the server, possibly generating several API calls.
*
* @param string $arrayPropertyName item array property name
* @param string $relativePath relative path to pass to Client::send
* @param array $parameters parameter array to pass to Client::send
*
* @throws Exception
*
* @return array united list of items
*/
protected static function loadItems($arrayPropertyName, $relativePath, $parameters = [])
{
$items = [];
$pageSize = 300;
$totalRecords = 0;
$nextPageToken = '';
do {
$response = static::fromJson(
Client::getInstance()->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;
}
}

View File

@@ -0,0 +1,57 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
/**
* Class ParticipantList
* List of past meeting instance participants.
*
* @see ParticipantListItem;
*/
class ParticipantList
{
use PaginationToken;
/** @var ParticipantListItem[] */
public $participants;
/**
* ParticipantList constructor.
*/
public function __construct()
{
$this->participants = [];
}
/**
* Retrieves a meeting instance's participants.
*
* @param string $instanceUUID
*
* @throws Exception
*
* @return ParticipantListItem[] participants
*/
public static function loadInstanceParticipants($instanceUUID)
{
return static::loadItems(
'participants',
'past_meetings/'.htmlentities($instanceUUID).'/participants'
);
}
/**
* {@inheritdoc}
*/
public function itemClass($propertyName)
{
if ('participants' === $propertyName) {
return ParticipantListItem::class;
}
throw new Exception("No such array property $propertyName");
}
}

View File

@@ -0,0 +1,22 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
/**
* Class ParticipantListItem. Item in a list of past meeting instance participants.
*
* @see ParticipantList
*/
class ParticipantListItem
{
/** @var string participant UUID */
public $id;
/** @var string display name */
public $name;
/** @var string Email address of the user; will be returned if the user logged into Zoom to join the meeting. */
public $user_email;
}

View File

@@ -0,0 +1,74 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
/**
* Class PastMeeting.
* A past meeting, really a past meeting instance, as returned from the server.
*
* Each past meeting instance is identified by its own UUID.
* Many past meeting instances can be part of the same meeting, identified by property 'id'.
* Each instance has its own start time, participants and recording files.
*/
class PastMeeting extends Meeting
{
/** @var string unique meeting instance ID */
public $uuid;
/** @var string meeting number */
public $id;
/** @var string host Zoom user id */
public $host_id;
/** @var string user display name */
public $user_name;
/** @var string */
public $user_email;
/** @var string "yyyy-MM-dd'T'HH:mm:ss'Z'" (GMT) */
public $start_time;
/** @var string "yyyy-MM-dd'T'HH:mm:ss'Z'" (GMT) */
public $end_time;
/** @var int sum of meeting minutes from all participants in the meeting. */
public $total_minutes;
/** @var int number of meeting participants */
public $participants_count;
/** @var string undocumented */
public $dept;
/**
* Retrieves a past meeting instance from its identifier.
*
* @param string $uuid
*
* @throws Exception
*
* @return PastMeeting the past meeting
*/
public static function fromUUID($uuid)
{
return static::fromJson(Client::getInstance()->send('GET', 'past_meetings/'.htmlentities($uuid)));
}
/**
* Retrieves information on participants from a past meeting instance.
*
* @throws Exception
*
* @return ParticipantListItem[] participants
*/
public function getParticipants()
{
return ParticipantList::loadInstanceParticipants($this->uuid);
}
}

View File

@@ -0,0 +1,23 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
class QuestionAndAnswer
{
use JsonDeserializableTrait;
public $enable;
public $allow_anonymous_questions;
public $answer_questions;
public $attendees_can_upvote;
public $attendees_can_comment;
public function itemClass($propertyName)
{
throw new Exception("No such array property $propertyName");
}
}

View File

@@ -0,0 +1,115 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
/**
* Class RecordingFile. A video, audio or text file, part of a past meeting instance recording.
*
* @see RecordingMeeting
*/
class RecordingFile
{
use JsonDeserializableTrait;
/** @var string The recording file ID. Included in the response of general query. */
public $id;
/** @var string The meeting ID. */
public $meeting_id;
/** @var string The recording start time. */
public $recording_start;
/** @var string The recording end time. Response in general query. */
public $recording_end;
/** @var string The recording file type. The value of this field could be one of the following:<br>
* `MP4`: Video file of the recording.<br>
* `M4A` Audio-only file of the recording.<br>
* `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.
*
* @throws Exception
*/
public function delete()
{
Client::getInstance()->send(
'DELETE',
"/meetings/$this->meeting_id/recordings/$this->id",
['action' => 'delete']
);
}
/**
* {@inheritdoc}
*/
public function itemClass($propertyName)
{
throw new Exception("No such array property $propertyName");
}
}

View File

@@ -0,0 +1,65 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use DateTime;
use Exception;
/**
* Class RecordingList. A list of past meeting instance recordings generated between two dates.
*
* @see RecordingMeeting
*/
class RecordingList
{
use PaginationToken;
/** @var string Start Date */
public $from;
/** @var string End Date */
public $to;
/** @var RecordingMeeting[] List of recordings */
public $meetings;
public function __construct()
{
$this->meetings = [];
}
/**
* Retrieves all recordings from a period of time.
*
* @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($startDate, $endDate)
{
return static::loadItems(
'meetings',
'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");
}
}

View File

@@ -0,0 +1,95 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
/**
* Class RecordingMeeting.
* A meeting instance can be recorded, hence creating an instance of this class.
* Contains a list of recording files.
*
* @see PastMeeting
* @see RecordingFile
*/
class RecordingMeeting
{
use JsonDeserializableTrait;
/** @var string Unique Meeting Identifier. Each instance of the meeting will have its own UUID. */
public $uuid;
/** @var string Meeting ID - also known as the meeting number. */
public $id;
/** @var string Unique Identifier of the user account. */
public $account_id;
/** @var string ID of the user set as host of meeting. */
public $host_id;
/** @var string Meeting topic. */
public $topic;
/** @var int undocumented */
public $type;
/** @var string The time at which the meeting started. */
public $start_time;
/** @var string undocumented */
public $timezone;
/** @var int Meeting duration. */
public $duration;
/** @var string Total size of the recording. */
public $total_size;
/** @var string Number of recording files returned in the response of this API call. */
public $recording_count;
/** @var string undocumented */
public $share_url;
/** @var string */
public $password;
/** @var RecordingFile[] List of recording file. */
public $recording_files;
/**
* RecordingMeeting constructor.
*/
public function __construct()
{
$this->recording_files = [];
}
/**
* Deletes the recording on the server.
*
* @throws Exception
*/
public function delete()
{
Client::getInstance()->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");
}
}

View File

@@ -0,0 +1,103 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
abstract class RegistrantSchema
{
use JsonDeserializableTrait;
/** @var string */
public $email;
/**
* @var string
*/
public $status;
/** @var string */
public $first_name;
/** @var string */
public $last_name;
/** @var string */
public $address;
/** @var string */
public $city;
/** @var string */
public $country;
/** @var string */
public $zip;
/** @var string */
public $state;
/** @var string */
public $phone;
/** @var string */
public $industry;
/** @var string */
public $org;
/** @var string */
public $job_title;
/** @var string */
public $purchasing_time_frame;
/** @var string */
public $role_in_purchase_process;
/** @var string */
public $no_of_employees;
/** @var string */
public $comments;
/** @var object[] title => value */
public $custom_questions;
/**
* @var string
*/
public $language;
/**
* MeetingRegistrant constructor.
*/
public function __construct()
{
$this->status = 'approved';
$this->custom_questions = [];
}
public static function fromEmailAndFirstName(string $email, string $firstName, string $lastName = null): RegistrantSchema
{
$instance = new static();
$instance->first_name = $firstName;
$instance->email = $email;
if (null !== $lastName) {
$instance->last_name = $lastName;
}
return $instance;
}
/**
* {@inheritdoc}
*/
public function itemClass($propertyName): string
{
if ('custom_questions' === $propertyName) {
return CustomQuestion::class;
}
throw new Exception("no such array property $propertyName");
}
}

View File

@@ -0,0 +1,84 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
class ServerToServerOAuthClient extends Client
{
use Api2RequestTrait;
/**
* @var string
*/
public $token;
public function __construct(string $accountId, string $clientId, string $clientSecret)
{
try {
$this->token = $this->requireAccessToken($accountId, $clientId, $clientSecret);
} catch (Exception $e) {
error_log('Zoom: Can\'t require access token: '.$e->getMessage());
}
self::register($this);
}
private function requireAccessToken(string $accountId, string $clientId, string $clientSecret)
{
$options = [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_ENCODING => '',
CURLOPT_HTTPHEADER => [
'Authorization: Basic '.base64_encode("$clientId:$clientSecret"),
'Content-Type: application/x-www-form-urlencoded',
'Host: zoom.us',
],
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_MAXREDIRS => 10,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'account_credentials',
'account_id' => $accountId,
]),
];
$url = 'https://zoom.us/oauth/token';
$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);
}
$jsonResponseBody = json_decode($responseBody, true);
if (false === $jsonResponseBody) {
throw new Exception('Could not generate JSON responso body');
}
return $jsonResponseBody['access_token'];
}
}

View File

@@ -0,0 +1,29 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
/**
* Class TrackingField. Instances of this class can be listed in a meeting object.
*/
class TrackingField
{
use JsonDeserializableTrait;
/** @var string Tracking fields type */
public $field;
/** @var string Tracking fields value */
public $value;
/**
* {@inheritdoc}
*/
public function itemClass($propertyName)
{
throw new Exception("no such array property $propertyName");
}
}

View File

@@ -0,0 +1,9 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
class WebinarRegistrantSchema extends RegistrantSchema
{
}

View File

@@ -0,0 +1,137 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
use stdClass;
class WebinarSchema
{
use BaseMeetingTrait;
use JsonDeserializableTrait;
public const TYPE_WEBINAR = 5;
public const TYPE_RECURRING_NO_FIXED_TIME = 6;
public const TYPE_RECURRING_FIXED_TIME = 9;
public $uuid;
public $id;
public $host_id;
public $created_at;
public $start_url;
public $join_url;
public $registration_url;
public $password;
/**
* @var WebinarSettings
*/
public $settings;
public $registrants_confirmation_email;
/**
* @var array<int, TrackingField>
*/
public $tracking_fields;
public $recurrence;
public $template_id;
/**
* @var array<int, Ocurrence>
*/
public $ocurrences;
protected function __construct()
{
$this->tracking_fields = [];
$this->settings = new WebinarSettings();
$this->ocurrences = [];
}
public function itemClass($propertyName): string
{
if ('tracking_fields' === $propertyName) {
return TrackingField::class;
}
if ('ocurrences' === $propertyName) {
return Ocurrence::class;
}
throw new Exception("no such array property $propertyName");
}
public static function fromTopicAndType($topic, $type = self::TYPE_WEBINAR): WebinarSchema
{
$instance = new static();
$instance->topic = $topic;
$instance->type = $type;
return $instance;
}
/**
* @throws Exception
*/
public function create($userId = null): WebinarSchema
{
$client = Client::getInstance();
$userId = empty($userId) ? 'me' : $userId;
return self::fromJson(
$client->send('POST', "users/$userId/webinars", [], $this)
);
}
/**
* @throws Exception
*/
public function update(): void
{
Client::getInstance()->send('PATCH', 'webinars/'.$this->id, [], $this);
}
/**
* @throws Exception
*/
public function delete()
{
Client::getInstance()->send('DELETE', "webinars/$this->id");
}
/**
* @throws Exception
*/
public function addRegistrant(RegistrantSchema $registrant, string $occurrenceIds = ''): CreatedRegistration
{
return CreatedRegistration::fromJson(
Client::getInstance()->send(
'POST',
"webinars/$this->id/registrants",
empty($occurrenceIds) ? [] : ['occurrence_ids' => $occurrenceIds],
$registrant
)
);
}
/**
* @throws Exception
*/
public function removeRegistrants(array $registrants, string $occurrenceIds = '')
{
if (empty($registrants)) {
return;
}
$requestBody = new stdClass();
$requestBody->action = 'cancel';
$requestBody->registrants = $registrants;
Client::getInstance()->send(
'PUT',
"webinars/$this->id/registrants/status",
empty($occurrenceIds) ? [] : ['occurrence_ids' => $occurrenceIds],
$requestBody
);
}
}

View File

@@ -0,0 +1,201 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom\API;
use Exception;
class WebinarSettings
{
use JsonDeserializableTrait;
public const APPROVAL_TYPE_AUTOMATICALLY_APPROVE = 0;
public const APPROVAL_TYPE_MANUALLY_APPROVE = 1;
public const APPROVAL_TYPE_NO_REGISTRATION_REQUIRED = 2;
public const REGISTRATION_TYPE_REGISTER_ONCE_ATTEND_ANY = 1;
public const REGISTRATION_TYPE_REGISTER_EACH = 2;
public const REGISTRATION_TYPE_REGISTER_ONCE_CHOOSE = 3;
/**
* @var bool
*/
public $host_video;
/**
* @var bool
*/
public $panelists_video;
/**
* @var int
*/
public $approval_type;
/**
* @var string
*/
public $audio;
/**
* @var string
*/
public $auto_recording;
/**
* @var bool
*/
public $enforce_login;
/**
* @var string
*/
public $enforce_login_domains;
/**
* @var string
*/
public $alternative_hosts;
/**
* @var bool
*/
public $close_registration;
/**
* @var bool
*/
public $show_share_button;
/**
* @var bool
*/
public $allow_multiple_devices;
/**
* @var bool
*/
public $practice_session;
/**
* @var bool
*/
public $hd_video;
/**
* @var object
*/
public $question_answer;
/**
* @var bool
*/
public $registrants_confirmation_email;
/**
* @var bool
*/
public $on_demand;
/**
* @var bool
*/
public $request_permission_to_unmute_participants;
/**
* @var array<int,string>
*/
public $global_dial_in_countries;
/**
* @var array<int,GlobalDialInNumber>
*/
public $global_dial_in_numbers;
/**
* @var string
*/
public $contact_name;
/**
* @var string
*/
public $contact_email;
/**
* @var int
*/
public $registrants_restrict_number;
/**
* @var bool
*/
public $registrants_email_notification;
/**
* @var bool
*/
public $post_webinar_survey;
/**
* @var bool
*/
public $meeting_authentication;
/**
* @var QuestionAndAnswer
*/
public $question_and_answer;
/**
* @var bool
*/
public $hd_video_for_attendees;
/**
* @var bool
*/
public $send_1080p_video_to_attendees;
/**
* @var string
*/
public $email_language;
/**
* @var bool
*/
public $panelists_invitation_email_notification;
/**
* @var FollowUpUsers
*/
public $attendees_and_panelists_reminder_email_notification;
/**
* @var FollowUpUsers
*/
public $follow_up_attendees_email_notification;
/**
* @var FollowUpUsers
*/
public $follow_up_absentees_email_notification;
/**
* @var int
*/
public $registration_type;
/**
* @var string
*/
public $auto;
/**
* @var string
*/
public $survey_url;
/**
* @var string
*/
public $authentication_option;
/**
* @var string
*/
public $authentication_domains;
/**
* @var string
*/
public $authentication_name;
public function __construct()
{
$this->global_dial_in_countries = [];
$this->global_dial_in_numbers = [];
$this->question_and_answer = new QuestionAndAnswer();
$this->attendees_and_panelists_reminder_email_notification = new FollowUpUsers();
$this->follow_up_absentees_email_notification = new FollowUpUsers();
$this->follow_up_attendees_email_notification = new FollowUpUsers();
}
public function itemClass($propertyName): string
{
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");
}
}

View File

@@ -0,0 +1,215 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CourseBundle\Entity\CGroupInfo;
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;
use Doctrine\ORM\QueryBuilder;
/**
* Class MeetingRepository.
*/
class MeetingRepository extends EntityRepository
{
/**
* Retrieves information about meetings having a start_time between two dates.
*
* @param DateTime $startDate
* @param DateTime $endDate
*
* @return Meeting[]
*/
public function periodMeetings($startDate, $endDate)
{
$matching = [];
$all = $this->findAll();
/** @var Meeting $candidate */
foreach ($all as $candidate) {
if (API\Meeting::TYPE_INSTANT === $candidate->getMeetingInfoGet()->type) {
continue;
}
$cantidateEndDate = clone $candidate->startDateTime;
$cantidateEndDate->add($candidate->durationInterval);
if (($candidate->startDateTime >= $startDate && $candidate->startDateTime <= $endDate)
|| ($candidate->startDateTime <= $startDate && $cantidateEndDate >= $startDate)
) {
$matching[] = $candidate;
}
}
return $matching;
}
/**
* @return ArrayCollection|Collection|Meeting[]
*/
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|Meeting[]
*/
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 QueryBuilder
*/
public function userMeetings($user = null)
{
$qb = $this->createQueryBuilder('m');
$qb
->select('m')
->leftJoin('m.registrants', 'r');
//$qb->select('m');
/*$criteria = Criteria::create()->where(
Criteria::expr()->andX(
Criteria::expr()->isNull('course'),
Criteria::expr()->orX(
Criteria::expr()->isNull('user'),
Criteria::expr()->eq('user', $user)
)
));*/
/*$qb->where(Criteria::expr()->andX(
Criteria::expr()->isNull('course'),
Criteria::expr()->orX(
Criteria::expr()->isNull('user'),
Criteria::expr()->eq('user', $user)
)
));*/
$qb
->andWhere('m.course IS NULL')
->andWhere('m.user IS NULL OR m.user = :user OR r.user = :user');
$qb->setParameters(['user' => $user]);
return $qb;
/*return $this->matching(
,
Criteria::expr()->andX(
Criteria::expr()->eq('registrants', null),
Criteria::expr()->orX(
Criteria::expr()->eq('user', null),
Criteria::expr()->eq('user', $user)
)
)
)
);*/
/*return $this->matching(
Criteria::create()->where(
Criteria::expr()->andX(
Criteria::expr()->eq('course', null),
Criteria::expr()->orX(
Criteria::expr()->eq('user', null),
Criteria::expr()->eq('user', $user)
)
)
)
);*/
}
/**
* @param User|null $user
*
* @return Meeting[]
*/
public function unfinishedUserMeetings($user = null)
{
/*return $this->userMeetings($user)->filter(
function ($meeting) {
return 'finished' !== $meeting->getMeetingInfoGet()->status;
}
);*/
$results = @$this->userMeetings($user)->getQuery()->getResult();
$list = [];
foreach ($results as $meeting) {
if ('finished' === $meeting->getMeetingInfoGet()->status) {
$list[] = $meeting;
}
}
return $list;
}
/**
* @param DateTime $start
* @param DateTime $end
* @param User $user
*
* @return ArrayCollection|Collection|Meeting[]
*/
public function periodUserMeetings($start, $end, $user = null)
{
/*return $this->userMeetings($user)->filter(
function ($meeting) use ($start, $end) {
return $meeting->startDateTime >= $start && $meeting->startDateTime <= $end;
}
);*/
$results = @$this->userMeetings($user)->getQuery()->getResult();
$list = [];
if ($results) {
foreach ($results as $meeting) {
if ($meeting->startDateTime >= $start && $meeting->startDateTime <= $end) {
$list[] = $meeting;
}
}
}
return $list;
}
/**
* Returns either a course's meetings or all course meetings.
*
* @return ArrayCollection|Collection|Meeting[]
*/
public function courseMeetings(Course $course, CGroupInfo $group = null, Session $session = null)
{
return $this->matching(
Criteria::create()->where(
Criteria::expr()->andX(
Criteria::expr()->eq('group', $group),
Criteria::expr()->eq('course', $course),
Criteria::expr()->eq('session', $session)
)
)
);
}
}

View File

@@ -0,0 +1,65 @@
<?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\ORM\EntityRepository;
/**
* Class RecordingRepository.
*/
class RecordingRepository extends EntityRepository
{
public function getPeriodRecordings($startDate, $endDate)
{
$matching = [];
$all = $this->findAll();
foreach ($all 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|Recording[]
*/
/*public function userRecordings($user)
{
return $this->matching(
Criteria::create()->where(
Criteria::expr()->in(
'meeting',
$this->getEntityManager()->getRepository(Meeting::class)->userMeetings($user)->toArray()
)
)
);
}*/
/**
* @param DateTime $start
* @param DateTime $end
* @param User $user
*
* @return ArrayCollection|Recording[]
*/
/*public function getPeriodUserRecordings($start, $end, $user = null)
{
return $this->userRecordings($user)->filter(
function ($meeting) use ($start, $end) {
return $meeting->startDateTime >= $start && $meeting->startDateTime <= $end;
}
);
}*/
}

View File

@@ -0,0 +1,49 @@
<?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.
*/
class RegistrantRepository extends EntityRepository
{
/**
* Returns the upcoming meeting registrations for the given user.
*
* @param User $user
*
* @return array|Registrant[]
*/
public function meetingsComingSoonRegistrationsForUser($user)
{
$start = new DateTime();
$end = new DateTime();
$end->add(new DateInterval('P7D'));
$meetings = $this->getEntityManager()->getRepository(Meeting::class)->periodMeetings($start, $end);
return $this->findBy(['meeting' => $meetings, 'user' => $user]);
}
public function findByMeetingPaginated(Meeting $meeting, int $from, int $limit, string $column, string $direction)
{
$queryBuilder = $this->createQueryBuilder('r')
->join('r.user', 'u')
->leftJoin('r.signature', 's')
->where('r.meeting = :meeting')
->setFirstResult($from)
->setMaxResults($limit)
->orderBy($column, $direction)
;
$queryBuilder->setParameter('meeting', $meeting);
return $queryBuilder->getQuery()->getResult();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Zoom\Meeting;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
require_once __DIR__.'/config.php';
api_block_anonymous_users(false);
$httpRequest = HttpRequest::createFromGlobals();
$meetingId = $httpRequest->get('meetingId', 0);
if (empty($meetingId)) {
api_not_allowed();
}
$plugin = ZoomPlugin::create();
/** @var Meeting $meeting */
$meeting = $plugin->getMeetingRepository()->findOneBy(['meetingId' => $meetingId]);
$currentUserId = api_get_user_id();
$currentUser = api_get_user_entity($currentUserId);
if (null === $meeting) {
api_not_allowed(false, $plugin->get_lang('MeetingNotFound'));
}
switch ($httpRequest->get('a')) {
case 'sign_attempt':
$registrant = $meeting->getRegistrantByUser($currentUser);
if (!$meeting->isSignAttendance() ||
null === $registrant
) {
api_not_allowed();
}
$file = $httpRequest->request->get('file', '');
$secToken = Security::get_token('zoom_signature');
if (!Security::check_token($secToken, null, 'zoom_signature')) {
api_not_allowed();
}
echo (int) $plugin->saveSignature($registrant, $file);
exit;
}

110
plugin/zoom/meeting.php Normal file
View File

@@ -0,0 +1,110 @@
<?php
/* For license terms, see /license.txt */
use Chamilo\PluginBundle\Zoom\Meeting;
use Chamilo\PluginBundle\Zoom\Webinar;
require_once __DIR__.'/config.php';
$meetingId = isset($_REQUEST['meetingId']) ? (int) $_REQUEST['meetingId'] : 0;
if (empty($meetingId)) {
api_not_allowed(true);
}
$plugin = ZoomPlugin::create();
/** @var Meeting $meeting */
$meeting = $plugin->getMeetingRepository()->findOneBy(['meetingId' => $meetingId]);
if (null === $meeting) {
api_not_allowed(true, $plugin->get_lang('MeetingNotFound'));
}
$course_plugin = 'zoom'; // needed in order to load the plugin lang variables
$returnURL = 'meetings.php';
$urlExtra = '';
if ($meeting->isCourseMeeting()) {
api_protect_course_script(true);
$this_section = SECTION_COURSES;
$urlExtra = api_get_cidreq();
$returnURL = 'start.php?'.$urlExtra;
if (api_is_in_group()) {
$interbreadcrumb[] = [
'url' => api_get_path(WEB_CODE_PATH).'group/group.php?'.$urlExtra,
'name' => get_lang('Groups'),
];
$interbreadcrumb[] = [
'url' => api_get_path(WEB_CODE_PATH).'group/group_space.php?'.$urlExtra,
'name' => get_lang('GroupSpace').' '.$meeting->getGroup()->getName(),
];
}
}
$logInfo = [
'tool' => 'Videoconference Zoom',
];
Event::registerLog($logInfo);
$interbreadcrumb[] = [
'url' => $returnURL,
'name' => $plugin->get_lang('ZoomVideoConferences'),
];
$tpl = new Template($meeting->getMeetingId());
if ($plugin->userIsConferenceManager($meeting)) {
// user can edit, start and delete meeting
$tpl->assign('isConferenceManager', true);
$tpl->assign('editMeetingForm', $plugin->getEditConferenceForm($meeting)->returnForm());
if ($meeting instanceof Webinar) {
$tpl->assign('deleteMeetingForm', $plugin->getDeleteWebinarForm($meeting, $returnURL)->returnForm());
} elseif ($meeting instanceof Meeting) {
$tpl->assign('deleteMeetingForm', $plugin->getDeleteMeetingForm($meeting, $returnURL)->returnForm());
}
$pluginEnableParticipantRegistration = 'true' === $plugin->get('enableParticipantRegistration');
if ($pluginEnableParticipantRegistration && $meeting->requiresRegistration()) {
if (false === $meeting->isGlobalMeeting()
&& false == $meeting->isCourseMeeting()
) {
$tpl->assign('registerParticipantForm', $plugin->getRegisterParticipantForm($meeting)->returnForm());
$tpl->assign('registrants', $meeting->getRegistrants());
}
if ('true' === $plugin->get('enablePresenter') && !$meeting->isCourseMeeting()) {
$tpl->assign('registerPresenterForm', $plugin->getRegisterPresenterForm($meeting)->returnForm());
$tpl->assign('presenters', $meeting->getPresenters());
}
}
if (ZoomPlugin::RECORDING_TYPE_NONE !== $plugin->getRecordingSetting() &&
$meeting->hasCloudAutoRecordingEnabled()
) {
$tpl->assign('fileForm', $plugin->getFileForm($meeting, $returnURL)->returnForm());
$tpl->assign('recordings', $meeting->getRecordings());
}
} elseif ($meeting->requiresRegistration()) {
$userId = api_get_user_id();
try {
foreach ($meeting->getRegistrants() as $registrant) {
if ($registrant->getUser()->getId() == $userId) {
$tpl->assign('currentUserJoinURL', $registrant->getJoinUrl());
break;
}
}
} catch (Exception $exception) {
Display::addFlash(
Display::return_message($exception->getMessage(), 'error')
);
}
}
$tpl->assign('actions', $plugin->getToolbar());
$tpl->assign('meeting', $meeting);
$tpl->assign('url_extra', $urlExtra);
$tpl->assign('content', $tpl->fetch('zoom/view/meeting.tpl'));
$tpl->display_one_col_template();

28
plugin/zoom/meetings.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
/* For license terms, see /license.txt */
$course_plugin = 'zoom'; // needed in order to load the plugin lang variables
$cidReset = true;
require_once __DIR__.'/config.php';
if (!ZoomPlugin::currentUserCanJoinGlobalMeeting()) {
api_not_allowed(true);
}
$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());
$scheduleForm = $plugin->getScheduleMeetingForm($user);
$tpl = new Template();
$tpl->assign('meetings', $plugin->getMeetingRepository()->periodUserMeetings($startDate, $endDate, $user));
$tpl->assign('allow_recording', $plugin->hasRecordingAvailable());
$tpl->assign('actions', $plugin->getToolbar());
$tpl->assign('search_form', $form->returnForm());
$tpl->assign('schedule_form', $scheduleForm->returnForm());
$tpl->assign('content', $tpl->fetch('zoom/view/meetings.tpl'));
$tpl->display_one_col_template();

4
plugin/zoom/plugin.php Normal file
View File

@@ -0,0 +1,4 @@
<?php
/* For license terms, see /license.txt */
$plugin_info = ZoomPlugin::create()->get_info();

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,291 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="124"
height="124"
viewBox="0 0 124 124"
version="1.1"
preserveAspectRatio="xMidYMid"
id="svg68"
sodipodi:docname="googlemeet.svg"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
<metadata
id="metadata72">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1860"
inkscape:window-height="1016"
id="namedview70"
showgrid="false"
inkscape:zoom="3.5336739"
inkscape:cx="20.920415"
inkscape:cy="96.925373"
inkscape:window-x="60"
inkscape:window-y="1107"
inkscape:window-maximized="1"
inkscape:current-layer="g965" />
<defs
id="defs37">
<linearGradient
inkscape:collect="always"
id="linearGradient950">
<stop
style="stop-color:#007266;stop-opacity:1;"
offset="0"
id="stop946" />
<stop
style="stop-color:#007266;stop-opacity:0;"
offset="1"
id="stop948" />
</linearGradient>
<linearGradient
x1="71.77459"
y1="187.32983"
x2="206.34541"
y2="53.078278"
id="linearGradient-1"
gradientTransform="matrix(0.35264551,0,0,0.40800991,10.373845,5.6478575)"
gradientUnits="userSpaceOnUse">
<stop
stop-color="#058B7E"
offset="0%"
id="stop2" />
<stop
stop-color="#058D80"
offset="0%"
id="stop4" />
<stop
stop-color="#058D7F"
offset="100%"
id="stop6" />
</linearGradient>
<path
d="M 127.68269,0 C 57.165748,0 0,55.790933 0,124.61227 0,193.43392 57.165748,249.22453 127.68269,249.22453 l 0.27148,46.91374 c 65.60585,-37.27075 128,-87.01882 128,-171.526 C 255.95417,55.790933 198.19994,0 127.68269,0 Z"
id="path-2"
inkscape:connector-curvature="0" />
<filter
x="-0.0040000002"
y="-0.003"
width="1.008"
height="1.007"
filterUnits="objectBoundingBox"
id="filter-3">
<feOffset
dx="0"
dy="2"
in="SourceAlpha"
result="shadowOffsetInner1"
id="feOffset10" />
<feComposite
in="shadowOffsetInner1"
in2="SourceAlpha"
operator="arithmetic"
k2="-1"
k3="1"
result="shadowInnerInner1"
id="feComposite12"
k1="0"
k4="0" />
<feColorMatrix
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.2 0"
type="matrix"
in="shadowInnerInner1"
id="feColorMatrix14" />
</filter>
<path
d="M 127.68269,0 C 57.165748,0 0,55.790933 0,124.61227 0,193.43392 57.165748,249.22453 127.68269,249.22453 l 0.27148,46.91374 c 65.60585,-37.27075 128,-87.01882 128,-171.526 C 255.95417,55.790933 198.19994,0 127.68269,0 Z"
id="path-4"
inkscape:connector-curvature="0" />
<filter
x="-0.0040000002"
y="-0.003"
width="1.008"
height="1.007"
filterUnits="objectBoundingBox"
id="filter-5">
<feOffset
dx="0"
dy="-2"
in="SourceAlpha"
result="shadowOffsetInner1"
id="feOffset18" />
<feComposite
in="shadowOffsetInner1"
in2="SourceAlpha"
operator="arithmetic"
k2="-1"
k3="1"
result="shadowInnerInner1"
id="feComposite20"
k1="0"
k4="0" />
<feColorMatrix
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
type="matrix"
in="shadowInnerInner1"
id="feColorMatrix22" />
</filter>
<path
d="M 127.68269,0 C 57.165748,0 0,55.790933 0,124.61227 0,193.43392 57.165748,249.22453 127.68269,249.22453 l 0.27148,46.91374 c 65.60585,-37.27075 128,-87.01882 128,-171.526 C 255.95417,55.790933 198.19994,0 127.68269,0 Z"
id="path-6"
inkscape:connector-curvature="0" />
<linearGradient
x1="119.3371"
y1="153.50986"
x2="236.07732"
y2="282.50131"
id="linearGradient-8"
gradientTransform="scale(1.1271732,0.88717512)"
gradientUnits="userSpaceOnUse">
<stop
stop-color="#000000"
offset="0%"
id="stop26" />
<stop
stop-color="#D8D8D8"
stop-opacity="0"
offset="100%"
id="stop28" />
</linearGradient>
<path
d="m 55.580192,128.1054 v 34.23858 c 0,9.03901 7.395555,16.43457 16.434567,16.43457 h 85.016021 c 9.03901,0 16.43457,-7.39556 16.43457,-16.43457 v -21.62283 l 33.65926,33.65925 v -46.275 z"
id="path-9"
inkscape:connector-curvature="0" />
<filter
x="-0.0099999998"
y="-0.029999999"
width="1.026"
height="1.118"
filterUnits="objectBoundingBox"
id="filter-10">
<feOffset
dx="1"
dy="3"
in="SourceAlpha"
result="shadowOffsetOuter1"
id="feOffset32" />
<feColorMatrix
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
type="matrix"
in="shadowOffsetOuter1"
id="feColorMatrix34" />
</filter>
<mask
id="mask-7"
fill="white">
<use
xlink:href="#path-6"
id="use49"
x="0"
y="0"
width="100%"
height="100%" />
</mask>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient950"
id="linearGradient952"
x1="80.086624"
y1="68.221016"
x2="96.365685"
y2="82.936577"
gradientUnits="userSpaceOnUse" />
</defs>
<g
id="g1001"
inkscape:export-filename="/var/www/chamilo11/plugin/google_meet/resources/img/64/meet.png"
inkscape:export-xdpi="49.548386"
inkscape:export-ydpi="49.548386">
<rect
y="0"
x="0"
height="124"
width="124"
id="rect967"
style="opacity:0;fill:#008072;fill-opacity:1;stroke:none;stroke-width:1.63019824;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:1.63019823, 3.26039649000000020;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers" />
<g
id="g965">
<g
style="fill:#000000;fill-opacity:1"
id="g43"
transform="matrix(0.37931894,0,0,0.37931894,10.373845,5.6478575)">
<use
height="100%"
width="100%"
y="0"
x="0"
style="filter:url(#filter-3)"
id="use41"
xlink:href="#path-2" />
</g>
<path
style="fill:#5599ff;stroke-width:0.37931895"
inkscape:connector-curvature="0"
id="path39"
d="m 58.806308,5.6478575 c -26.748411,0 -48.432463,21.1625575 -48.432463,47.2677945 0,26.105355 21.684052,47.267788 48.432463,47.267788 v 0 c 26.086173,-0.13013 48.655802,-15.212614 48.655802,-47.267788 0,-26.105237 -21.907273,-47.2677945 -48.655802,-47.2677945 z"
sodipodi:nodetypes="ssccss" />
<g
id="g916"
transform="matrix(0.96698632,0,0,0.96698632,2.218741,3.1352548)">
<g
transform="matrix(0.39226919,0,0,0.39226919,8.433526,2.598385)"
id="g62">
<use
xlink:href="#path-9"
id="use58"
style="fill:#000000;fill-opacity:1;filter:url(#filter-10)"
x="0"
y="0"
width="100%"
height="100%" />
</g>
<path
d="M 89.682141,52.850186 V 34.646356 L 76.47865,47.821703 v -8.402255 c 0,-3.54585 -2.90105,-6.446774 -6.446775,-6.446774 H 36.68271 c -1.772863,0 -5.479585,-0.03472 -6.43699,-0.0086 0.04687,1.181868 -0.0098,4.682401 -0.0098,6.455326 v 13.430738 z"
id="path64"
inkscape:connector-curvature="0"
style="fill:#e2e2e2;stroke-width:0.39226919"
sodipodi:nodetypes="cccssscscc" />
<use
transform="matrix(0.39226919,0,0,0.39226919,8.4335257,2.5983849)"
xlink:href="#path-9"
id="use60"
style="fill:#f6f6f6;fill-rule:evenodd"
x="0"
y="0"
width="100%"
height="100%" />
<rect
style="opacity:1;fill:#5599ff;fill-opacity:1;stroke:none;stroke-width:1.36803973;stroke-opacity:1"
id="rect1681"
width="1.0864193"
height="28.55731"
x="76.49678"
y="38.07761" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.1 KiB

118
plugin/zoom/start.php Normal file
View File

@@ -0,0 +1,118 @@
<?php
/* For license terms, see /license.txt */
$course_plugin = 'zoom'; // needed in order to load the plugin lang variables
require_once __DIR__.'/config.php';
api_protect_course_script(true);
$this_section = SECTION_COURSES;
$logInfo = [
'tool' => 'Videoconference Zoom',
];
Event::registerLog($logInfo);
$course = api_get_course_entity();
if (null === $course) {
api_not_allowed(true);
}
$group = api_get_group_entity();
$session = api_get_session_entity();
$plugin = ZoomPlugin::create();
if (null !== $group) {
$interbreadcrumb[] = [
'url' => api_get_path(WEB_CODE_PATH).'group/group.php?'.api_get_cidreq(),
'name' => get_lang('Groups'),
];
$interbreadcrumb[] = [
'url' => api_get_path(WEB_CODE_PATH).'group/group_space.php?'.api_get_cidreq(),
'name' => get_lang('GroupSpace').' '.$group->getName(),
];
}
$url = api_get_self().'?'.api_get_cidreq(true, false).'&gidReq=';
$htmlHeadXtra[] = '<script>
$(function() {
$("#group_select").on("change", function() {
var groupId = $(this).find("option:selected").val();
var url = "'.$url.'";
window.location.replace(url+groupId);
});
});
</script>';
$tool_name = $plugin->get_lang('ZoomVideoConferences');
$tpl = new Template($tool_name);
$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : '';
$isManager = $plugin->userIsCourseConferenceManager();
if ($isManager) {
$groupId = api_get_group_id();
$groups = GroupManager::get_groups();
if (!empty($groups)) {
$form = new FormValidator('group_filter');
$groupList[0] = get_lang('Select');
foreach ($groups as $groupData) {
$itemGroupId = $groupData['iid'];
/*if (isset($meetingsGroup[$itemGroupId]) && $meetingsGroup[$itemGroupId] == 1) {
$groupData['name'] .= ' ('.get_lang('Active').')';
}*/
$groupList[$itemGroupId] = $groupData['name'];
}
$form->addSelect('group_id', get_lang('Groups'), $groupList, ['id' => 'group_select']);
$form->setDefaults(['group_id' => $groupId]);
$formToString = $form->returnForm();
$tpl->assign('group_form', $formToString);
}
switch ($action) {
case 'delete':
$meeting = $plugin->getMeetingRepository()->findOneBy(['meetingId' => $_REQUEST['meetingId']]);
if ($meeting && $meeting->isCourseMeeting()) {
$plugin->deleteMeeting($meeting, api_get_self().'?'.api_get_cidreq());
}
break;
}
$user = api_get_user_entity(api_get_user_id());
$tpl->assign(
'instant_meeting_form',
$plugin->getCreateInstantMeetingForm(
$user,
$course,
$group,
$session
)->returnForm()
);
$tpl->assign(
'schedule_meeting_form',
$plugin->getScheduleMeetingForm(
$user,
$course,
$group,
$session
)->returnForm()
);
}
try {
$tpl->assign(
'meetings',
$plugin->getMeetingRepository()->courseMeetings($course, $group, $session)
);
} catch (Exception $exception) {
Display::addFlash(
Display::return_message('Could not retrieve scheduled meeting list: '.$exception->getMessage(), 'error')
);
}
$tpl->assign('is_manager', $isManager);
$tpl->assign('content', $tpl->fetch('zoom/view/start.tpl'));
$tpl->display_one_col_template();

View File

@@ -0,0 +1,117 @@
<?php
/* For license terms, see /license.txt */
use Chamilo\PluginBundle\Zoom\Meeting;
use Chamilo\UserBundle\Entity\User;
require_once __DIR__.'/config.php';
api_block_anonymous_users();
$course_plugin = 'zoom'; // needed in order to load the plugin lang variables
if (empty($_REQUEST['meetingId'])) {
api_not_allowed(true);
}
$plugin = ZoomPlugin::create();
/** @var Meeting $meeting */
$meeting = $plugin->getMeetingRepository()->findOneBy(['meetingId' => $_REQUEST['meetingId']]);
if (null === $meeting) {
api_not_allowed(true, $plugin->get_lang('MeetingNotFound'));
}
if (false !== $meeting->isGlobalMeeting()
|| false != $meeting->isCourseMeeting()
|| 'true' !== $plugin->get('enableParticipantRegistration')
|| !$meeting->requiresRegistration()
) {
api_not_allowed(true);
}
$currentUser = api_get_user_entity(api_get_user_id());
$userRegistrant = $meeting->getRegistrantByUser($currentUser);
if ($meeting->isCourseMeeting()) {
api_protect_course_script(true);
if (api_is_in_group()) {
$interbreadcrumb[] = [
'url' => api_get_path(WEB_CODE_PATH).'group/group.php?'.api_get_cidreq(),
'name' => get_lang('Groups'),
];
$interbreadcrumb[] = [
'url' => api_get_path(WEB_CODE_PATH).'group/group_space.php?'.api_get_cidreq(),
'name' => get_lang('GroupSpace').' '.$meeting->getGroup()->getName(),
];
}
}
$form = new FormValidator('subscription');
$form->addHidden('meetingId', $meeting->getMeetingId());
if (!empty($userRegistrant)) {
$form->addButton(
'unregister',
$plugin->get_lang('UnregisterMeToConference'),
'user-times',
'warning'
);
$form->addHtml(
'<div class="form-group"><div class="col-sm-8 col-sm-offset-2">'
.Display::url(
$plugin->get_lang('ViewMeeting'),
api_get_path(WEB_PLUGIN_PATH).'zoom/join_meeting.php?meetingId='.$meeting->getMeetingId(),
['class' => 'btn btn-primary']
)
.'</div></div>'
);
} else {
$filtered = array_filter(
$meeting->getRegistrableUsers(),
function (User $registableUser) use ($currentUser) {
return $registableUser->getId() === $currentUser->getId();
}
);
if (empty($filtered)) {
api_not_allowed(true);
}
$form->addButton(
'register',
$plugin->get_lang('RegisterMeToConference'),
'user-plus',
'success'
);
}
if ($form->validate()) {
$values = $form->exportValues();
if (isset($values['unregister'])) {
$plugin->unregister($meeting, [$userRegistrant]);
} else {
$plugin->registerUsers($meeting, [$currentUser]);
}
Display::addFlash(
Display::return_message($plugin->get_lang('RegisteredUserListWasUpdated'), 'success')
);
api_location('?meetingId='.$meeting->getMeetingId());
} else {
$form->protect();
}
$view = new Template('');
$view->assign('meeting', $meeting);
$view->assign('frm_register_unregister', $form->returnForm());
$content = $view->fetch('zoom/view/subscription.tpl');
$view->assign('content', $content);
$view->display_one_col_template();

View File

@@ -0,0 +1,8 @@
<?php
/* For license terms, see /license.txt */
if (!api_is_platform_admin()) {
exit('You must have admin permissions to uninstall plugins');
}
ZoomPlugin::create()->uninstall();

View File

@@ -0,0 +1,57 @@
<h4>
{{ meeting.typeName }} {{ meeting.meetingId }}
</h4>
<a class="btn btn-primary" href="meeting.php?meetingId={{ meeting.meetingId }}&{{ url_extra }}">
{{ 'Edit'|get_lang }}
</a>
<table class="table">
<thead>
<tr>
<th>{{ 'Type'|get_lang }}</th>
<th>{{ 'Action'|get_plugin_lang('ZoomPlugin') }}</th>
{# <th>{{ 'User'|get_lang }}</th>#}
<th>{{ 'Date'|get_lang }}</th>
<th>{{ 'Details'|get_lang }} </th>
</tr>
</thead>
<tbody>
{% for activity in meeting.activities %}
<tr>
<td>
{{ activity.type }}
</td>
<td>
{{ activity.name }}
</td>
<td>
{{ activity.createdAt | api_convert_and_format_date(3)}}
</td>
{# <td>#}
{# {% if _u.is_admin %}#}
{# <a href="{{ _p.web_main }}admin/user_information.php?user_id={{ activity.user.id }}" >#}
{# {{ activity.user.firstname }} {{ activity.user.lastname }} ({{ activity.user.username }})#}
{# </a>#}
{# {% else %}#}
{# {{ activity.user.firstname }} {{ activity.user.lastname }} ({{ activity.user.username }})#}
{# {% endif %}#}
{# </td>#}
<td>
{% if activity.eventDecoded.registrant %}
{{ 'User' | get_lang }} :
{{ activity.eventDecoded.registrant.first_name }} -
{{ activity.eventDecoded.registrant.last_name }} -
{{ activity.eventDecoded.registrant.email }} -
{{ activity.eventDecoded.registrant.status }}
{% endif %}
{% if activity.eventDecoded.participant %}
{{ 'User' | get_lang }} :
{{ activity.eventDecoded.participant.user_name }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@@ -0,0 +1,324 @@
<div id="loading" style="margin-left:150px;position:absolute;display:none">
{{ "Loading"|get_lang }} &hellip;
</div>
<div id="calendar"></div>
<div class="modal fade" tabindex="-1" role="dialog" id="simple-dialog-form">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="{{ 'Close'|get_lang }}">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">{{ 'Details'|get_lang }}</h4>
</div>
<div class="modal-body">
<form class="form-horizontal">
<div class="form-group">
<label class="col-sm-4 control-label">{{ 'Type' }}</label>
<div class="col-sm-8">
<p id="simple_type"></p>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{{ "Date"|get_lang }}</label>
<div class="col-sm-8">
<p>
<span id="simple_start_date"></span>
<span id="simple_end_date"></span>
</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{{ "Title"|get_lang }}</label>
<div class="col-sm-8">
<p id="simple_title"></p>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{{ "Description"|get_lang }}</label>
<div class="col-sm-8">
<p id="simple_content"></p>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{{ "AccountEmail"|get_plugin_lang('ZoomPlugin') }}</label>
<div class="col-sm-8">
<p id="simple_account"></p>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'Close'|get_lang }}</button>
</div>
</div>
</div>
</div>
<script>
$(function () {
var cookieData = Cookies.getJSON('agenda_cookies');
var defaultView = (cookieData && cookieData.view) || '{{ default_view }}';
var defaultStartDate = (cookieData && cookieData.start) || moment.now();
var CustomListViewGrid = ListViewGrid.extend({
fgSegHtml: function (seg) {
var view = this.view;
var classes = ['fc-list-item'].concat(this.getSegCustomClasses(seg));
var bgColor = this.getSegBackgroundColor(seg);
var event = seg.event;
var url = event.url;
var timeHtml;
if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day
if (seg.isStart || seg.isEnd) { // outer segment that probably lasts part of the day
timeHtml = htmlEscape(this.getEventTimeText(seg));
} else { // inner segment that lasts the whole day
timeHtml = view.getAllDayHtml();
}
} else {
// Display the normal time text for the *event's* times
timeHtml = htmlEscape(this.getEventTimeText(event));
}
if (url) {
classes.push('fc-has-url');
}
return '<tr class="' + classes.join(' ') + '">' +
(this.displayEventTime
? '<td class="fc-list-item-time ' + view.widgetContentClass + '">' + (timeHtml || '') + '</td>'
: ''
) +
'<td class="fc-list-item-marker ' + view.widgetContentClass + '">' +
'<span class="fc-event-dot"' +
(bgColor ? ' style="background-color:' + bgColor + '"' : '') +
'></span>' +
'</td>' +
'<td class="fc-list-item-title ' + view.widgetContentClass + '">' +
'<a' + (url ? ' href="' + htmlEscape(url) + '"' : '') + '>' +
htmlEscape(seg.event.title || '') + (seg.event.description || '') +
'</a>' +
'</td>' +
'</tr>';
},
// render the event segments in the view
renderSegList: function (allSegs) {
var segsByDay = this.groupSegsByDay(allSegs); // sparse array
var dayIndex;
var daySegs;
var i;
var tableEl = $('<table class="fc-list-table"><tbody/></table>');
var tbodyEl = tableEl.find('tbody');
var eventList = [];
for (dayIndex = 0; dayIndex < segsByDay.length; dayIndex++) {
daySegs = segsByDay[dayIndex];
if (daySegs) { // sparse array, so might be undefined
this.sortEventSegs(daySegs);
for (i = 0; i < daySegs.length; i++) {
var event = daySegs[i].event;
if (jQuery.inArray(event.id, eventList) !== -1) {
continue;
}
eventList.push(event.id);
// append a day header
tbodyEl.append(this.dayHeaderHtml(
this.view.start.clone().add(dayIndex, 'days'),
event
));
tbodyEl.append(daySegs[i].el); // append event row
}
}
}
this.el.empty().append(tableEl);
},
// generates the HTML for the day headers that live amongst the event rows
dayHeaderHtml: function (dayDate, event) {
var view = this.view;
var mainFormat = 'LL';
var altFormat = 'dddd';
var checkIfSame = true;
if (event.end) {
checkIfSame = event.end.format(mainFormat) === dayDate.format(mainFormat);
}
return '<tr class="fc-list-heading" data-date="' + dayDate.format('YYYY-MM-DD') + '">' +
'<td class="' + view.widgetHeaderClass + '" colspan="3">' +
(
mainFormat
? view.buildGotoAnchorHtml(
dayDate,
{ 'class': 'fc-list-heading-main' },
htmlEscape(dayDate.format(mainFormat)) // inner HTML
)
: ''
) +
(
(checkIfSame === false && mainFormat)
? view.buildGotoAnchorHtml(
dayDate,
{ 'class': 'fc-list-heading-main' },
'&nbsp;-&nbsp; ' + htmlEscape(event.end.format(mainFormat)) // inner HTML
)
: ''
) +
(
altFormat
? view.buildGotoAnchorHtml(
dayDate,
{ 'class': 'fc-list-heading-alt' },
htmlEscape(dayDate.format(altFormat)) // inner HTML
)
: ''
) +
'</td>' +
'</tr>'
}
})
var FC = $.fullCalendar; // a reference to FullCalendar's root namespace
var View = ListView; // the class that all views must inherit from
var CustomView; // our subclass
CustomView = View.extend({ // make a subclass of View
initialize: function () {
this.grid = new CustomListViewGrid(this);
this.scroller = new Scroller({
overflowX: 'hidden',
overflowY: 'auto'
});
}
})
FC.views.CustomView = CustomView; // register our class with the view system
var height = '';
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
height = 'auto';
}
$('#calendar').fullCalendar({
height: height,
header: {
left: 'today,prev,next',
center: 'title',
right: 'month,agendaWeek,agendaDay,CustomView'
},
views: {
CustomView: { // name of view
type: 'list',
buttonText: '{{ 'AgendaList'|get_lang | escape('js') }}',
duration: { month: 1 },
defaults: {
'listDayAltFormat': 'dddd' // day-of-week is nice-to-have
}
},
month: {
'displayEventEnd': true
}
},
locale: '{{ region_value }}',
defaultView: defaultView,
defaultDate: defaultStartDate,
firstHour: 8,
firstDay: 1,
{% if fullcalendar_settings %}
{{ fullcalendar_settings }}
{% endif %}
selectable: false,
selectHelper: true,
viewRender: function (view, element) {
var data = {
'view': view.name,
'start': view.intervalStart.format('YYYY-MM-DD')
};
Cookies.set('agenda_cookies', data, 1); // Expires 1 day
},
eventRender: function (event, element) {
{% if on_hover_info.description %}
if (event.description) {
element.qtip({
content: event.description,
position: {
at: 'top center',
my: 'bottom center',
viewport: $(window)
}
});
}
{% endif %}
},
eventClick: function (calEvent, jsEvent, view) {
var start = calEvent.start;
var end = calEvent.end;
var diffDays = moment(end).diff(start, 'days');
var endDateMinusOne = '';
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
// If event is not editable then just return the qtip
{% if on_hover_info.description %}
if (calEvent.description) {
$(this).qtip({
overwrite: false,
show: { ready: true },
content: calEvent.description,
position: {
at: 'top center',
my: 'bottom center',
viewport: $(window)
}
});
}
{% endif %}
return;
}
var clone = end.clone();
endDateMinusOne = clone.subtract(1, 'days').format('{{ js_format_date }}');
var startDateToString = start.format("{{ js_format_date }}");
// Simple form
$('#simple_start_date').text(startDateToString);
if (diffDays > 1) {
$('#simple_end_date').text(' - ' + endDateMinusOne);
} else if (diffDays == 0) {
var start_date_value = start.format('ll');
var startTime = start.format('LT');
var endTime = end.format('LT');
$('#simple_start_date').html('');
$('#simple_end_date').html(start_date_value + ' (' + startTime + ' - ' + endTime + ') ');
} else {
$('#simple_end_date').text('');
}
$('#simple_type').text(calEvent.typeName);
$('#simple_title').text(calEvent.title);
$('#simple_content').empty().text(calEvent.description);
if (calEvent.accountEmail) {
$('#simple_account').text(calEvent.accountEmail).parents('.form-group').show();
} else {
$('#simple_account').empty().parents('.form-group').hide();
}
$('#simple-dialog-form').modal('show');
},
editable: false,
events: "{{ web_agenda_ajax_url }}&a=get_events",
axisFormat: 'H(:mm)', // pm-am format -> h(:mm)a
timeFormat: 'H:mm', // pm-am format -> h:mm
loading: function (bool) {
if (bool) {
$('#loading').show();
} else {
$('#loading').hide();
}
}
})
})
</script>

196
plugin/zoom/view/join.tpl Normal file
View File

@@ -0,0 +1,196 @@
{% include 'zoom/view/meeting_details.tpl' %}
{% if is_conference_manager and meeting.isSignAttendance %}
<p class="text-info">
<span class="fa fa-list-alt"></span>
{{ 'ConferenceWithAttendance'|get_plugin_lang('ZoomPlugin') }}
</p>
{% endif %}
<hr>
{% set btn_start = '' %}
{% if start_url %}
{% set btn_start %}
<a href="{{ start_url }}" class="btn btn-primary">
{{ 'EnterMeeting'|get_plugin_lang('ZoomPlugin') }}
</a>
{% endset %}
{% endif %}
{% if not is_conference_manager %}
{% if meeting.isSignAttendance %}
<div class="row">
<div class="col-md-offset-3 col-md-6">
<div class="panel panel-info">
<div class="panel-heading">
<h3 class="panel-title">
<span class="fa fa-pencil-square-o fa-fw" aria-hidden="true"></span>
{{ 'Attendance'|get_lang }}
</h3>
</div>
<div class="panel-body">
<p>{{ meeting.reasonToSignAttendance }}</p>
{% if signature %}
<div class="thumbnail">
<img src="{{ signature.file }}"
alt="{{ 'SignatureDone'|get_plugin_lang('ZoomPlugin') }}">
<div class="caption text-center">
{{ signature.registeredAt|api_convert_and_format_date(constant('DATE_TIME_FORMAT_LONG')) }}
</div>
</div>
{% else %}
{% set btn_start = '' %}
{% if 'started' == meeting.meetingInfoGet.status %}
<button class="btn btn-info" id="btn-sign" data-toggle="modal"
data-target="#signature-modal">
<i class="fa fa-pencil fa-fw" aria-hidden="true"></i>
{{ 'Sign'|get_plugin_lang('ZoomPlugin') }}
</button>
<div class="modal fade" tabindex="-1" role="dialog" id="signature-modal"
data-backdrop="static">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">{{ 'SignAttendance'|get_plugin_lang('ZoomPlugin') }}</h4>
</div>
<div class="modal-body">
<div id="signature-modal--signature-area" class="well">
<canvas></canvas>
</div>
</div>
<div class="modal-footer">
<span id="signature-modal--loader" aria-hidden="true"
class="fa fa-refresh fa-spin"
aria-label="{{ 'Loading'|get_lang }}" style="display: none;">
</span>
<span id="signature-modal--save-controls">
<button id="signature-modal--btn-save" class="btn btn-primary">
<em class="fa fa-save" aria-hidden="true"></em>
{{ 'Save'|get_lang }}
</button>
<button id="signature-modal--btn-clean" class="btn btn-default">
<em class="fa fa-eraser" aria-hidden="true"></em>
{{ 'Clean'|get_lang }}
</button>
</span>
<div id="signature-modal--close-controls" style="display: none;">
<span id="signature-modal--results"></span>
<button class="btn btn-default"
data-dismiss="modal">{{ 'Close'|get_lang }}</button>
</div>
</div>
</div>
</div>
</div>
<script>
$(function () {
var $signatureArea = $('#signature-modal--signature-area')
var $loader = $('#signature-modal--loader')
var $saveControls = $('#signature-modal--save-controls')
var $btnSave = $('#signature-modal--btn-save')
var $btnClean = $('#signature-modal--btn-clean')
var $closeControls = $('#signature-modal--close-controls')
var $txtResults = $('#signature-modal--results')
var imageFormat = 'image/png'
var canvas = document.querySelector('#signature-modal--signature-area canvas')
var signaturePad = new SignaturePad(canvas)
$('#signature-modal')
.on('shown.bs.modal', function (e) {
var parentWidth = $signatureArea.width()
var parentHeight = $signatureArea.height()
canvas.setAttribute('width', parentWidth + 'px')
canvas.setAttribute('height', parentHeight + 'px')
signaturePad = new SignaturePad(canvas)
})
.on('hide.bs.modal', function (e) {
$loader.hide()
$saveControls.show()
$closeControls.hide()
$signatureArea.show()
$btnSave.prop('disabled', false)
$btnClean.prop('disabled', false)
})
$btnClean.on('click', function () {
signaturePad.clear()
})
$btnSave.on('click', function () {
if (signaturePad.isEmpty()) {
alert('{{ 'ProvideASignatureFirst'|get_plugin_lang('ZoomPlugin')|e('js') }}')
return false
}
var dataURL = signaturePad.toDataURL(imageFormat)
$.ajax({
beforeSend: function () {
$loader.show()
$btnSave.prop('disabled', true)
$btnClean.prop('disabled', true)
},
type: 'POST',
url: 'meeting.ajax.php?{{ _p.web_cid_query }}',
data: {
a: 'sign_attempt',
meetingId: {{ meeting.meetingId }},
file: dataURL
},
success: function (data) {
$btnSave.prop('disabled', false)
$btnClean.prop('disabled', false)
$loader.hide()
$saveControls.hide()
$signatureArea.hide()
signaturePad.clear()
if ('1' === data) {
$txtResults.html('{{ 'Saved'|get_lang }}')
window.location.reload()
} else {
$txtResults.html('{{ 'Error'|get_lang }}')
}
$closeControls.show()
},
})
})
})
</script>
{% endif %}
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
{% endif %}
{{ btn_start }}
{% if details_url %}
<a href="{{ details_url }}" class="btn btn-default">
{{ 'Details'|get_lang }}
</a>
{% endif %}
{% if btn_announcement %}
{{ btn_announcement }}
{% endif %}

View File

@@ -0,0 +1,136 @@
<h4>
{{ meeting.typeName }} {{ meeting.meetingId }}
{% if meeting.meetingInfoGet.status %}
({{ meeting.meetingInfoGet.status }})
{% endif %}
</h4>
<div class="btn-group" role="group">
{% if meeting.meetingInfoGet.status != 'finished' %}
<a class="btn btn-primary" href="join_meeting.php?meetingId={{ meeting.meetingId }}&{{ url_extra }}">
{{ 'ViewMeeting'|get_plugin_lang('ZoomPlugin') }}
</a>
{% endif %}
{% if isConferenceManager %}
{% if meeting.status == 'waiting' %}
<a class="btn btn-primary" href="{{ meeting.meetingInfoGet.start_url }}" target="_blank">
{{ 'StartMeeting'|get_plugin_lang('ZoomPlugin') }}
</a>
{% endif %}
<a class="btn btn-default" href="activity.php?meetingId={{ meeting.meetingId }}&{{ url_extra }}">
{{ 'Activity'|get_plugin_lang('ZoomPlugin') }}
</a>
<a href="attendance.php?meetingId={{ meeting.meetingId ~ '&' ~ url_extra }}" class="btn btn-info">
{{ 'Attendance'|get_lang }}
</a>
{% endif %}
</div>
{% if isConferenceManager %}
<br />
<br />
<div class="panel panel-default conference">
<div class="panel-body">
<div class="share">
{{ 'JoinURLToSendToParticipants'| get_plugin_lang('ZoomPlugin') }}
</div>
<div class="form-inline">
<div class="form-group">
<input id="share_button_flash" type="text"
style="width:460px"
class="form-control" readonly
value="{{ _p.web }}plugin/zoom/join_meeting.php?meetingId={{ meeting.meetingId }}&{{ url_extra }}"
>
<button onclick="copyTextToClipBoard('share_button_flash');" class="btn btn-default">
<span class="fa fa-copy"></span> {{ 'CopyTextToClipboard' | get_lang }}
</button>
</div>
</div>
</div>
</div>
{% endif %}
{% if currentUserJoinURL %}
{#<p>#}
{# <a href="{{ currentUserJoinURL }}" target="_blank">#}
{# {{ 'JoinMeeting'|get_plugin_lang('ZoomPlugin') }}#}
{# </a>#}
{#</p>#}
{% endif %}
{% if isConferenceManager %}
{{ editMeetingForm }}
{{ deleteMeetingForm }}
{% if registerParticipantForm %}
<hr>
{{ registerParticipantForm }}
{% endif %}
{% if registerPresenterForm %}
{{ registerPresenterForm }}
{% endif %}
{{ fileForm }}
<div class="row">
<div class="col-m6">
{% if presenters %}
<h3>{{ 'Presenters'|get_plugin_lang('ZoomPlugin') }}</h3>
<table class="table">
{% for presenter in presenters %}
<tr>
<td>
{{ presenter.fullName }}
</td>
</tr>
{% endfor %}
</table>
{% endif %}
</div>
<div class="col-m6">
{# {% if registrants and meeting.meetingInfoGet.settings.approval_type != 2 %}#}
{% if registrants.count > 0 %}
<script>
function copyJoinURL(event, url) {
event.target.textContent = '{{ 'CopyingJoinURL'|get_plugin_lang('ZoomPlugin')|escape }}';
navigator.clipboard.writeText(url).then(
function() {
event.target.textContent = '{{ 'JoinURLCopied'|get_plugin_lang('ZoomPlugin')|escape }}';
}, function() {
event.target.textContent = '{{ 'CouldNotCopyJoinURL'|get_plugin_lang('ZoomPlugin')|escape }}' + ' ' + url;
}
);
}
</script>
<h3>{{ 'Users' | get_lang }}</h3>
<br />
<table class="table">
{% for registrant in registrants %}
<tr>
<td>
{{ registrant.fullName }}
</td>
<td>
{# {% if registrant.joinUrl %}#}
{# <a class="btn btn-primary" onclick="copyJoinURL(event, '{{ registrant.joinUrl }}')">#}
{# {{ 'CopyJoinAsURL'|get_plugin_lang('ZoomPlugin') }}#}
{# </a>#}
{# {% else %}#}
{# <a class="btn btn-primary disabled" >#}
{# {{ 'JoinURLNotAvailable'|get_plugin_lang('ZoomPlugin') }}#}
{# </a>#}
{# {% endif %}#}
</td>
</tr>
{% endfor %}
</table>
{% endif %}
</div>
</div>
{% else %}
{% include 'zoom/view/meeting_details.tpl' %}
{% endif %}

View File

@@ -0,0 +1,23 @@
<h2 class="page-header">
{{ meeting.topic}}
<small>{{ meeting.typeName }}</small>
</h2>
<dl class="meeting_properties dl-horizontal">
{% if meeting.requiresDateAndDuration %}
<dt>{{ 'StartTime'|get_lang }}</dt>
<dd>{{ meeting.formattedStartTime }}</dd>
<dt>{{ 'Duration'|get_lang }}</dt>
<dd>{{ meeting.formattedDuration }}</dd>
{% endif %}
{% if meeting.accountEmail %}
<dt>{{ 'AccountEmail'|get_lang }}</dt>
<dd>{{ meeting.accountEmail }}</dd>
{% endif %}
</dl>
{% if meeting.agenda %}
<p class="lead">{{ meeting.agenda|nl2br }}</p>
{% endif %}

View File

@@ -0,0 +1,64 @@
{% import "default/document/recycle.tpl" as macro %}
{{ schedule_form }}
{{ search_form }}
{% if meetings %}
<h4>{{ 'MeetingsFound'|get_plugin_lang('ZoomPlugin') }}: </h4>
<table class="table table-hover table-striped">
<thead>
<tr>
<th>{{ 'Type'|get_lang }}</th>
<th>{{ 'Topic'|get_plugin_lang('ZoomPlugin') }}</th>
<th>{{ 'StartTime'|get_lang }}</th>
<th>{{ 'ForEveryone'|get_plugin_lang('ZoomPlugin') }}</th>
{# <th>{{ 'Course'|get_lang }}</th>#}
{# <th>{{ 'Session'|get_lang }}</th>#}
{% if allow_recording %}
<th>{{ 'Recordings'|get_plugin_lang('ZoomPlugin') }}</th>
{% endif %}
<th></th>
</tr>
</thead>
<tbody>
{% for meeting in meetings %}
<tr>
<td>{{ meeting.typeName }}</td>
<td>{{ meeting.topic }}</td>
<td>{{ meeting.formattedStartTime }}</td>
<td>{{ meeting.user ? 'No'|get_lang : 'Yes'|get_lang }}</td>
{# <td>{{ meeting.course ? meeting.course : '-' }}</td>#}
{# <td>{{ meeting.session ? meeting.session : '-' }}</td>#}
<td>
{% if allow_recording and meeting.recordings.count > 0 %}
{% for recording in meeting.recordings %}
<dl>
<dt>
{{ recording.formattedStartTime }} ({{ recording.formattedDuration }}) {{ 'Password' | get_lang }}: {{ recording.recordingMeeting.password }}
</dt>
<dd>
<ul>
{% for file in recording.recordingMeeting.recording_files %}
<li>
<a href="{{ file.play_url }}" target="_blank">
{{ file.recording_type }}.{{ file.file_type }}
({{ macro.bytesToSize(file.file_size) }})
</a>
</li>
{% endfor %}
</ul>
</dd>
</dl>
{% endfor %}
{% endif %}
</td>
<td>
<a class="btn btn-primary" href="meeting.php?meetingId={{ meeting.meetingId }}">
{{ 'Details'|get_lang }}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}

View File

@@ -0,0 +1,60 @@
{% if instant_meeting_form %}
{{ instant_meeting_form }}
{% endif %}
{% if group_form %}
{{ group_form }}
{% endif %}
{% if meetings.count %}
<div class="page-header">
<h2>{{ 'ScheduledMeetings'|get_lang }}</h2>
</div>
<table class="table">
<tr>
<th>{{ 'Type'|get_lang }}</th>
<th>{{ 'Topic'|get_plugin_lang('ZoomPlugin') }}</th>
<th>{{ 'Agenda'|get_plugin_lang('ZoomPlugin') }}</th>
<th>{{ 'StartTime'|get_lang }}</th>
<th>{{ 'Duration'|get_lang }}</th>
<th>{{ 'Actions'|get_lang }}</th>
</tr>
{% for meeting in meetings %}
<tr>
<td>{{ meeting.typeName }}</td>
<td>
{{ meeting.meetingInfoGet.topic }}
{{ meeting.webinarSchema.topic }}
</td>
<td>
{{ meeting.meetingInfoGet.agenda|nl2br }}
{{ meeting.webinarSchema.agenda|nl2br }}
</td>
<td>{{ meeting.formattedStartTime }}</td>
<td>{{ meeting.formattedDuration }}</td>
<td>
<a class="btn btn-primary" href="join_meeting.php?meetingId={{ meeting.meetingId }}&{{ _p.web_cid_query }}">
{{ 'Join'|get_plugin_lang('ZoomPlugin') }}
</a>
{% if is_manager %}
<a class="btn btn-default" href="meeting.php?meetingId={{ meeting.meetingId }}&{{ _p.web_cid_query }}">
{{ 'Details'|get_plugin_lang('ZoomPlugin') }}
</a>
<a class="btn btn-danger"
href="start.php?action=delete&meetingId={{ meeting.meetingId }}&{{ _p.web_cid_query }}"
onclick="javascript:if(!confirm('{{ 'AreYouSureToDelete' | get_lang }}')) return false;"
>
{{ 'Delete'|get_lang }}
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% if schedule_meeting_form %}
{{ schedule_meeting_form }}
{% endif %}

View File

@@ -0,0 +1,3 @@
{% include 'zoom/view/meeting_details.tpl' %}
{{ frm_register_unregister }}