Passed
Push — master ( 702f24...55adc1 )
by Yannick
09:28
created

BbbPlugin::get_name()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\AccessUrlRelPlugin;
6
use Chamilo\CoreBundle\Entity\ConferenceMeeting;
7
use Chamilo\CoreBundle\Entity\ConferenceRecording;
8
use Chamilo\CoreBundle\Framework\Container;
9
use Chamilo\CourseBundle\Entity\CCourseSetting;
10
use Chamilo\CoreBundle\Entity\Course;
11
12
/**
13
 * BigBlueButton plugin configuration class.
14
 * Handles plugin options and course settings.
15
 */
16
class BbbPlugin extends Plugin
17
{
18
    const ROOM_OPEN = 0;
19
    const ROOM_CLOSE = 1;
20
    const ROOM_CHECK = 2;
21
22
    public $isCoursePlugin = true;
23
24
    // Default course settings when creating a new course
25
    public $course_settings = [
26
        ['name' => 'big_blue_button_record_and_store', 'type' => 'checkbox'],
27
        ['name' => 'bbb_enable_conference_in_groups', 'type' => 'checkbox'],
28
        ['name' => 'bbb_force_record_generation', 'type' => 'checkbox'],
29
        ['name' => 'big_blue_button_students_start_conference_in_groups', 'type' => 'checkbox'],
30
    ];
31
32
    /**
33
     * BBBPlugin constructor.
34
     * Defines all available plugin settings.
35
     */
36
    protected function __construct()
37
    {
38
        parent::__construct(
39
            '2.9',
40
            'Julio Montoya, Yannick Warnier, Angel Fernando Quiroz Campos, Jose Angel Ruiz',
41
            [
42
                'tool_enable' => 'boolean',
43
                'host' => 'text',
44
                'salt' => 'text',
45
                'enable_global_conference' => 'boolean',
46
                'enable_global_conference_per_user' => 'boolean',
47
                'enable_conference_in_course_groups' => 'boolean',
48
                'enable_global_conference_link' => 'boolean',
49
                'disable_download_conference_link' => 'boolean',
50
                'max_users_limit' => 'text',
51
                'global_conference_allow_roles' => [
52
                    'type' => 'select',
53
                    'options' => [
54
                        PLATFORM_ADMIN => get_lang('Administrator'),
55
                        COURSEMANAGER => get_lang('Teacher'),
56
                        STUDENT => get_lang('Student'),
57
                        STUDENT_BOSS => get_lang('StudentBoss'),
58
                    ],
59
                    'attributes' => ['multiple' => 'multiple'],
60
                ],
61
                'allow_regenerate_recording' => 'boolean',
62
                'big_blue_button_record_and_store' => 'checkbox',
63
                'bbb_enable_conference_in_groups' => 'checkbox',
64
                'bbb_force_record_generation' => 'checkbox',
65
                'disable_course_settings' => 'boolean',
66
                'meeting_duration' => 'text',
67
                'delete_recordings_on_course_delete' => 'boolean',
68
                'hide_conference_link' => 'boolean',
69
            ]
70
        );
71
72
        $this->isAdminPlugin = true;
73
    }
74
75
    /**
76
     * Returns a singleton instance of the plugin.
77
     */
78
    public static function create(): self
79
    {
80
        static $result = null;
81
        return $result ??= new self();
82
    }
83
84
    /**
85
     * Validates if a course setting is enabled depending on global plugin configuration.
86
     */
87
    public function validateCourseSetting($variable): bool
88
    {
89
        if ($this->get('disable_course_settings') === 'true') {
90
            return false;
91
        }
92
93
        switch ($variable) {
94
            case 'bbb_enable_conference_in_groups':
95
                return $this->get('enable_conference_in_course_groups') === 'true';
96
            case 'bbb_force_record_generation':
97
                return $this->get('allow_regenerate_recording') === 'true';
98
            default:
99
                return true;
100
        }
101
    }
102
103
    /**
104
     * Returns course-level plugin settings if not disabled globally.
105
     */
106
    public function getCourseSettings(): array
107
    {
108
        if ($this->get('disable_course_settings') === 'true') {
109
            return [];
110
        }
111
112
        return parent::getCourseSettings();
113
    }
114
115
    /**
116
     * Performs automatic updates to all course settings after configuration changes.
117
     */
118
    public function performActionsAfterConfigure(): self
119
    {
120
        if ($this->get('disable_course_settings') === 'true') {
121
            self::updateCourseFieldInAllCourses(
122
                'bbb_enable_conference_in_groups',
123
                $this->get('enable_conference_in_course_groups') === 'true' ? 1 : 0
124
            );
125
            self::updateCourseFieldInAllCourses(
126
                'bbb_force_record_generation',
127
                $this->get('allow_regenerate_recording') === 'true' ? 1 : 0
128
            );
129
            self::updateCourseFieldInAllCourses(
130
                'big_blue_button_record_and_store',
131
                $this->get('big_blue_button_record_and_store') === 'true' ? 1 : 0
132
            );
133
        }
134
135
        return $this;
136
    }
137
138
    /**
139
     * Updates a course setting value across all existing courses.
140
     */
141
    public static function updateCourseFieldInAllCourses(string $variable, string $value): void
142
    {
143
        $entityManager = Database::getManager();
144
        $courseRepo = $entityManager->getRepository(Course::class);
145
        $settingRepo = $entityManager->getRepository(CCourseSetting::class);
146
147
        $courses = $courseRepo->createQueryBuilder('c')
148
            ->select('c.id')
149
            ->orderBy('c.id', 'ASC')
150
            ->getQuery()
151
            ->getArrayResult();
152
153
        foreach ($courses as $course) {
154
            $setting = $settingRepo->findOneBy([
155
                'variable' => $variable,
156
                'cId' => $course['id'],
157
            ]);
158
159
            if ($setting) {
160
                $setting->setValue($value);
161
                $entityManager->persist($setting);
162
            }
163
        }
164
165
        $entityManager->flush();
166
    }
167
168
    // Hook called when a course is deleted
169
    public function doWhenDeletingCourse($courseId): void
170
    {
171
        // Check if the setting is enabled
172
        if ($this->get('delete_recordings_on_course_delete') !== 'true') {
173
            return;
174
        }
175
176
        $this->removeBbbRecordingsForCourse($courseId);
177
    }
178
179
    // Hook called when a session is deleted
180
    public function doWhenDeletingSession($sessionId): void
181
    {
182
        // Check if the setting is enabled
183
        if ($this->get('delete_recordings_on_course_delete') !== 'true') {
184
            return;
185
        }
186
187
        $this->removeBbbRecordingsForSession($sessionId);
188
    }
189
190
    // Remove BBB recordings linked to a specific course
191
    private function removeBbbRecordingsForCourse(int $courseId): void
192
    {
193
        $em = Database::getManager();
194
        $meetingRepo = $em->getRepository(ConferenceMeeting::class);
195
        $recordingRepo = $em->getRepository(ConferenceRecording::class);
196
197
        // Get all BBB meetings for this course
198
        $meetings = $meetingRepo->createQueryBuilder('m')
199
            ->where('m.course = :cid')
200
            ->andWhere('m.serviceProvider = :sp')
201
            ->setParameters(['cid' => $courseId, 'sp' => 'bbb'])
202
            ->getQuery()
203
            ->getResult();
204
205
        foreach ($meetings as $meeting) {
206
            // Get all recordings of this meeting
207
            $recordings = $recordingRepo->findBy([
208
                'meeting' => $meeting,
209
                'formatType' => 'bbb',
210
            ]);
211
212
            foreach ($recordings as $rec) {
213
                // Try to extract the record ID from the URL
214
                if ($recordId = $this->extractRecordId($rec->getResourceUrl())) {
215
                    $this->deleteRecording($recordId); // Call BBB API to delete
216
                }
217
218
                $em->remove($rec); // Remove local record
219
            }
220
221
            $em->remove($meeting); // Optionally remove the meeting entity
222
        }
223
224
        $em->flush(); // Save all removals
225
    }
226
227
    // Remove BBB recordings linked to a specific session
228
    private function removeBbbRecordingsForSession(int $sessionId): void
229
    {
230
        $em = Database::getManager();
231
        $meetingRepo = $em->getRepository(ConferenceMeeting::class);
232
        $recordingRepo = $em->getRepository(ConferenceRecording::class);
233
234
        // Get all BBB meetings for this session
235
        $meetings = $meetingRepo->findBy([
236
            'session' => $sessionId,
237
            'serviceProvider' => 'bbb',
238
        ]);
239
240
        foreach ($meetings as $meeting) {
241
            $recordings = $recordingRepo->findBy([
242
                'meeting' => $meeting,
243
                'formatType' => 'bbb',
244
            ]);
245
246
            foreach ($recordings as $rec) {
247
                if ($recordId = $this->extractRecordId($rec->getResourceUrl())) {
248
                    $this->deleteRecording($recordId);
249
                }
250
251
                $em->remove($rec);
252
            }
253
254
            $em->remove($meeting);
255
        }
256
257
        $em->flush();
258
    }
259
260
    // Extracts the recordID from the BBB recording URL
261
    private function extractRecordId(string $url): ?string
262
    {
263
        // Match parameter ?recordID=xxx
264
        if (preg_match('/[?&]recordID=([\w-]+)/', $url, $matches)) {
265
            return $matches[1];
266
        }
267
268
        // Optional: match paths like .../recordingID-123456
269
        if (preg_match('/recordingID[-=](\d+)/', $url, $matches)) {
270
            return $matches[1];
271
        }
272
273
        return null;
274
    }
275
276
    // Sends a deleteRecordings API request to BigBlueButton
277
    private function deleteRecording(string $recordId): void
278
    {
279
        $host = rtrim($this->get('host'), '/');
280
        $salt = $this->get('salt');
281
282
        $query = "recordID={$recordId}";
283
        $checksum = sha1('deleteRecordings' . $query . $salt);
284
        $url = "{$host}/bigbluebutton/api/deleteRecordings?{$query}&checksum={$checksum}";
285
286
        // Send the request (silently)
287
        @file_get_contents($url);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for file_get_contents(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

287
        /** @scrutinizer ignore-unhandled */ @file_get_contents($url);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
288
    }
289
290
    /**
291
     * Installs the plugin
292
     */
293
    public function install(): void
294
    {
295
        $entityManager = Database::getManager();
296
297
        $pluginRepo = Container::getPluginRepository();
298
        $plugin = $pluginRepo->findOneByTitle($this->get_name());
299
300
        if (!$plugin) {
301
            // Create the plugin only if it does not exist
302
            $plugin = new \Chamilo\CoreBundle\Entity\Plugin();
303
            $plugin->setTitle($this->get_name());
304
            $plugin->setInstalled(true);
305
            $plugin->setInstalledVersion($this->get_version());
306
            $plugin->setSource(\Chamilo\CoreBundle\Entity\Plugin::SOURCE_OFFICIAL);
307
308
            $entityManager->persist($plugin);
309
            $entityManager->flush();
310
        } else {
311
            // Ensure Doctrine manages it in the current UnitOfWork
312
            $plugin = $entityManager->merge($plugin);
313
        }
314
315
        // Check if the plugin has relations for access URLs
316
        $accessUrlRepo = Container::getAccessUrlRepository();
317
        $accessUrlRelPluginRepo = Container::getAccessUrlRelPluginRepository();
318
319
        $accessUrls = $accessUrlRepo->findAll();
320
321
        foreach ($accessUrls as $accessUrl) {
322
            $rel = $accessUrlRelPluginRepo->findOneBy([
323
                'plugin' => $plugin,
324
                'url' => $accessUrl,
325
            ]);
326
327
            if (!$rel) {
328
                $rel = new AccessUrlRelPlugin();
329
                $rel->setPlugin($plugin);
330
                $rel->setUrl($accessUrl);
331
                $rel->setActive(true);
332
333
                $configuration = [];
334
                foreach ($this->fields as $name => $type) {
335
                    $defaultValue = '';
336
337
                    if (is_array($type)) {
338
                        $defaultValue = $type['type'] === 'boolean' ? 'false' : '';
339
                    } else {
340
                        switch ($type) {
341
                            case 'boolean':
342
                            case 'checkbox':
343
                                $defaultValue = 'false';
344
                                break;
345
                            default:
346
                                $defaultValue = '';
347
                                break;
348
                        }
349
                    }
350
351
                    $configuration[$name] = $defaultValue;
352
                }
353
354
                $rel->setConfiguration($configuration);
355
356
                $entityManager->persist($rel);
357
            }
358
        }
359
360
        $entityManager->flush();
361
    }
362
363
    public function canCurrentUserSeeGlobalConferenceLink(): bool
364
    {
365
        $allowedStatuses = $this->get('global_conference_allow_roles') ?? [];
366
367
        if (empty($allowedStatuses)) {
368
            return api_is_platform_admin();
369
        }
370
371
        foreach ($allowedStatuses as $status) {
372
            switch ((int) $status) {
373
                case PLATFORM_ADMIN:
374
                    if (api_is_platform_admin()) {
375
                        return true;
376
                    }
377
                    break;
378
                case COURSEMANAGER:
379
                    if (api_is_teacher()) {
380
                        return true;
381
                    }
382
                    break;
383
                case STUDENT:
384
                    if (api_is_student()) {
385
                        return true;
386
                    }
387
                    break;
388
                case STUDENT_BOSS:
389
                    if (api_is_student_boss()) {
390
                        return true;
391
                    }
392
                    break;
393
            }
394
        }
395
396
        return false;
397
    }
398
399
    public function get_name(): string
400
    {
401
        return 'Bbb';
402
    }
403
}
404