Passed
Push — master ( ac6adc...8b1102 )
by Yannick
08:44 queued 14s
created

BbbPlugin::deleteRecording()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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

284
        /** @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...
285
    }
286
287
    public function get_name(): string
288
    {
289
        return 'Bbb';
290
    }
291
}
292