Passed
Push — master ( 540590...861e6a )
by Yannick
08:32
created

BbbPlugin   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 422
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 206
dl 0
loc 422
rs 4.08
c 0
b 0
f 0
wmc 59

17 Methods

Rating   Name   Duplication   Size   Complexity  
A updateCourseFieldInAllCourses() 0 25 3
A create() 0 4 1
A getCourseSettings() 0 7 2
A performActionsAfterConfigure() 0 18 5
A validateCourseSetting() 0 13 4
B install() 0 51 8
A uninstall() 0 22 3
B canCurrentUserSeeGlobalConferenceLink() 0 34 11
A removeBbbRecordingsForCourse() 0 34 4
A doWhenDeletingCourse() 0 8 2
A doWhenDeletingSession() 0 8 2
A __construct() 0 37 1
A extractRecordId() 0 13 3
A removeBbbRecordingsForSession() 0 30 4
A deleteRecording() 0 11 1
A showShareLink() 0 13 4
A get_name() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like BbbPlugin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BbbPlugin, and based on these observations, apply Extract Interface, too.

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('Learner'),
57
                        STUDENT_BOSS => get_lang('Superior (n+1)'),
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
        $em = Database::getManager();
296
297
        $pluginRepo = Container::getPluginRepository();
298
        $plugin = $pluginRepo->findOneByTitle($this->get_name());
299
300
        if (!$plugin) {
301
            $plugin = new \Chamilo\CoreBundle\Entity\Plugin();
302
            $plugin->setTitle($this->get_name());
303
        } else {
304
            $plugin = $em->merge($plugin);
305
        }
306
307
        $plugin->setInstalled(true);
308
        $plugin->setInstalledVersion($this->get_version());
309
        $plugin->setSource(\Chamilo\CoreBundle\Entity\Plugin::SOURCE_OFFICIAL);
310
        $em->persist($plugin);
311
312
        $accessUrlRepo = Container::getAccessUrlRepository();
313
        $accessUrlRelPluginRepo = Container::getAccessUrlRelPluginRepository();
314
315
        $accessUrls = $accessUrlRepo->findAll();
316
317
        foreach ($accessUrls as $accessUrl) {
318
            $rel = $accessUrlRelPluginRepo->findOneBy([
319
                'plugin' => $plugin,
320
                'url' => $accessUrl,
321
            ]);
322
323
            if (!$rel) {
324
                $rel = new AccessUrlRelPlugin();
325
                $rel->setPlugin($plugin);
326
                $rel->setUrl($accessUrl);
327
                $rel->setActive(true);
328
329
                $configuration = [];
330
                foreach ($this->fields as $name => $type) {
331
                    if (is_array($type)) {
332
                        $configuration[$name] = $type['type'] === 'boolean' ? 'false' : '';
333
                    } else {
334
                        $configuration[$name] = in_array($type, ['boolean','checkbox'], true) ? 'false' : '';
335
                    }
336
                }
337
                $rel->setConfiguration($configuration);
338
339
                $em->persist($rel);
340
            }
341
        }
342
343
        $em->flush();
344
    }
345
346
    /**
347
     * Uninstalls the plugin.
348
     * - Removes AccessUrl relations
349
     * - Marks the plugin as not installed
350
     * - Keeps course-level settings/data intact (safer for future re-installs)
351
     */
352
    public function uninstall(): void
353
    {
354
        $em = Database::getManager();
355
356
        $pluginRepo = Container::getPluginRepository();
357
        $relRepo = Container::getAccessUrlRelPluginRepository();
358
359
        $plugin = $pluginRepo->findOneByTitle($this->get_name());
360
361
        if (!$plugin) {
362
            return;
363
        }
364
365
        $rels = $relRepo->findBy(['plugin' => $plugin]);
366
        foreach ($rels as $rel) {
367
            $em->remove($rel);
368
        }
369
370
        $plugin->setInstalled(false);
371
372
        $em->persist($plugin);
373
        $em->flush();
374
    }
375
376
    public function canCurrentUserSeeGlobalConferenceLink(): bool
377
    {
378
        $allowedStatuses = $this->get('global_conference_allow_roles') ?? [];
379
380
        if (empty($allowedStatuses)) {
381
            return api_is_platform_admin();
382
        }
383
384
        foreach ($allowedStatuses as $status) {
385
            switch ((int) $status) {
386
                case PLATFORM_ADMIN:
387
                    if (api_is_platform_admin()) {
388
                        return true;
389
                    }
390
                    break;
391
                case COURSEMANAGER:
392
                    if (api_is_teacher()) {
393
                        return true;
394
                    }
395
                    break;
396
                case STUDENT:
397
                    if (api_is_student()) {
398
                        return true;
399
                    }
400
                    break;
401
                case STUDENT_BOSS:
402
                    if (api_is_student_boss()) {
403
                        return true;
404
                    }
405
                    break;
406
            }
407
        }
408
409
        return false;
410
    }
411
412
    /**
413
     * Make sure the current user can see the "share" link under the join
414
     * button in the course tool page.
415
     * The method is called "show" while the setting is "hide" because we favor
416
     * minimal disruption in the introduction of new settings (so by default,
417
     * the link is shown to respect older versions' behavior).
418
     * @return bool
419
     */
420
    public function showShareLink(): bool
421
    {
422
        if (api_get_course_int_id() > 0) {
423
            // If not in a course context, we always share the link
424
            // (hiding is only for within courses)
425
            $hideLink = $this->get('hide_conference_link');
426
            if (!empty($hideLink)) {
427
                if ('true' === $hideLink) {
428
                    return false;
429
                }
430
            }
431
        }
432
        return true;
433
    }
434
435
    public function get_name(): string
436
    {
437
        return 'Bbb';
438
    }
439
}
440