provider::_export_user_data()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17
/**
18
 * Privacy class for requesting user data.
19
 *
20
 * @package   mod_bigbluebuttonbn
21
 * @copyright 2018 - present, Blindside Networks Inc
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 * @author    Jesus Federico  (jesus [at] blindsidenetworks [dt] com)
24
 */
25
26
namespace mod_bigbluebuttonbn\privacy;
27
28
use \core_privacy\local\metadata\collection;
29
use \core_privacy\local\request\approved_contextlist;
30
use core_privacy\local\request\approved_userlist;
31
use \core_privacy\local\request\contextlist;
32
use \core_privacy\local\request\helper;
33
use \core_privacy\local\request\transform;
34
use core_privacy\local\request\userlist;
35
use \core_privacy\local\request\writer;
36
37
defined('MOODLE_INTERNAL') || die();
38
39
global $CFG;
40
require_once($CFG->dirroot . '/mod/bigbluebuttonbn/locallib.php');
41
42
/*
43
 * This part is to be eliminated as soon as possible but allows the phpunit test to pass Ok on MOODLE_33 and below WHILST allowing
44
 * also the privacy/tests/provider_test.php tests to pass
45
 * (vendor/bin/phpunit --fail-on-risky --disallow-test-output -v privacy/tests/provider_test.php).
46
 * Downside we add a new warning to the code checker. This is not ideal but will be ok until we stop supporting MOODLE_33 or we
47
 * change the test in provider_test.php so to cater for classes which are implementing the right method but not necessarily
48
 * inheriting from the new interface setup in MOODLE_34 (\core_privacy\local\request\core_userlist_provider).
49
 * This is linked to CONTRIB-7983
50
 */
51
if (!interface_exists("\\core_privacy\\local\\request\\core_userlist_provider")) {
52
    interface core_userlist_provider {
53
        /**
54
         * Get the list of users who have data within a context.
55
         *
56
         * @param   userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
57
         */
58
        public static function get_users_in_context(userlist $userlist);
59
60
        /**
61
         * Delete multiple users within a single context.
62
         *
63
         * @param   approved_userlist       $userlist The approved context and user information to delete information for.
64
         */
65
        public static function delete_data_for_users(approved_userlist $userlist);
66
    }
67
} else {
68
    interface core_userlist_provider extends \core_privacy\local\request\core_userlist_provider {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The type mod_bigbluebuttonbn\privacy\core_userlist_provider has been defined more than once; this definition is ignored, only the first definition in this file (L52-66) is considered.

This check looks for classes that have been defined more than once in the same file.

If you can, we would recommend to use standard object-oriented programming techniques. For example, to avoid multiple types, it might make sense to create a common interface, and then multiple, different implementations for that interface.

This also has the side-effect of providing you with better IDE auto-completion, static analysis and also better OPCode caching from PHP.

Loading history...
69
70
    }
71
}
72
73
/**
74
 * Privacy class for requesting user data.
75
 *
76
 * @package   mod_bigbluebuttonbn
77
 * @copyright 2018 - present, Blindside Networks Inc
78
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
79
 * @author    Jesus Federico  (jesus [at] blindsidenetworks [dt] com)
80
 */
81
class provider implements
82
    // This plugin has data.
83
    \core_privacy\local\metadata\provider,
84
85
    // This plugin currently implements the original plugin\provider interface.
86
    \core_privacy\local\request\plugin\provider,
87
88
    // This plugin is capable of determining which users have data within it.
89
    core_userlist_provider {
90
91
    // This trait must be included.
92
    use \core_privacy\local\legacy_polyfill;
93
94
    /**
95
     * Returns metadata.
96
     *
97
     * @param collection $collection The initialised collection to add items to.
98
     * @return collection A listing of user data stored through this system.
99
     */
100
    public static function _get_metadata(collection $collection) {
101
102
         // The table bigbluebuttonbn stores only the room properties.
103
         // However, there is a chance that some personal information is stored as metadata.
104
         // This would be done in the column 'participants' where rules can be set to define BBB roles.
105
         // It is fair to say that only the userid is stored, which is useless if user is removed.
106
         // But if this is a concern a refactoring on the way the rules are stored will be required.
107
        $collection->add_database_table('bigbluebuttonbn', [
108
            'participants' => 'privacy:metadata:bigbluebuttonbn:participants',
109
        ], 'privacy:metadata:bigbluebuttonbn');
110
111
        // The table bigbluebuttonbn_logs stores events triggered by users when using the plugin.
112
        // Some personal information along with the resource accessed is stored.
113
        $collection->add_database_table('bigbluebuttonbn_logs', [
114
            'userid' => 'privacy:metadata:bigbluebuttonbn_logs:userid',
115
            'timecreated' => 'privacy:metadata:bigbluebuttonbn_logs:timecreated',
116
            'meetingid' => 'privacy:metadata:bigbluebuttonbn_logs:meetingid',
117
            'log' => 'privacy:metadata:bigbluebuttonbn_logs:log',
118
            'meta' => 'privacy:metadata:bigbluebuttonbn_logs:meta',
119
        ], 'privacy:metadata:bigbluebuttonbn_logs');
120
121
        // Personal information has to be passed to BigBlueButton.
122
        // This includes the user ID and fullname.
123
        $collection->add_external_location_link('bigbluebutton', [
124
                'userid' => 'privacy:metadata:bigbluebutton:userid',
125
                'fullname' => 'privacy:metadata:bigbluebutton:fullname',
126
            ], 'privacy:metadata:bigbluebutton');
127
128
        return $collection;
129
    }
130
131
    /**
132
     * Get the list of contexts that contain user information for the specified user.
133
     *
134
     * @param   int           $userid       The user to search.
135
     * @return  contextlist   $contextlist  The list of contexts used in this plugin.
136
     */
137
    public static function _get_contexts_for_userid(int $userid) {
138
        // If user was already deleted, do nothing.
139
        if (!\core_user::get_user($userid)) {
140
            return;
141
        }
142
        // Fetch all bigbluebuttonbn logs.
143
        $sql = "SELECT c.id
144
                  FROM {context} c
145
            INNER JOIN {course_modules} cm
146
                    ON cm.id = c.instanceid
147
                   AND c.contextlevel = :contextlevel
148
            INNER JOIN {modules} m
149
                    ON m.id = cm.module
150
                   AND m.name = :modname
151
            INNER JOIN {bigbluebuttonbn} bigbluebuttonbn
152
                    ON bigbluebuttonbn.id = cm.instance
153
            INNER JOIN {bigbluebuttonbn_logs} bigbluebuttonbnlogs
154
                    ON bigbluebuttonbnlogs.bigbluebuttonbnid = bigbluebuttonbn.id
155
                 WHERE bigbluebuttonbnlogs.userid = :userid";
156
157
        $params = [
158
            'modname' => 'bigbluebuttonbn',
159
            'contextlevel' => CONTEXT_MODULE,
160
            'userid' => $userid,
161
        ];
162
        $contextlist = new contextlist();
163
        $contextlist->add_from_sql($sql, $params);
164
        return $contextlist;
165
    }
166
167
    /**
168
     * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist.
169
     *
170
     * @param approved_contextlist $contextlist a list of contexts approved for export.
171
     */
172
    public static function _export_user_data(approved_contextlist $contextlist) {
173
        self::_export_user_data_bigbliebuttonbn_logs($contextlist);
174
    }
175
176
    /**
177
     * Delete all data for all users in the specified context.
178
     *
179
     * @param \context $context the context to delete in.
180
     */
181
    public static function _delete_data_for_all_users_in_context(\context $context) {
182
        global $DB;
183
184
        if (!$context instanceof \context_module) {
0 ignored issues
show
Bug introduced by
The class context_module does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
185
            return;
186
        }
187
188
        $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST);
189
        $DB->delete_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $instanceid]);
190
    }
191
192
    /**
193
     * Delete all user data for the specified user, in the specified contexts.
194
     *
195
     * @param approved_contextlist $contextlist a list of contexts approved for deletion.
196
     */
197
    public static function _delete_data_for_user(approved_contextlist $contextlist) {
198
        global $DB;
199
        $count = $contextlist->count();
200
        if (empty($count)) {
201
            return;
202
        }
203
        $userid = $contextlist->get_user()->id;
204
        foreach ($contextlist->get_contexts() as $context) {
205
            if (!$context instanceof \context_module) {
0 ignored issues
show
Bug introduced by
The class context_module does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
206
                return;
207
            }
208
            $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST);
209
            $DB->delete_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $instanceid, 'userid' => $userid]);
210
        }
211
    }
212
213
    /**
214
     * Export personal data for the given approved_contextlist related to bigbluebuttonbn logs.
215
     *
216
     * @param approved_contextlist $contextlist a list of contexts approved for export.
217
     */
218
    protected static function _export_user_data_bigbliebuttonbn_logs(approved_contextlist $contextlist) {
219
        global $DB;
220
221
        // Filter out any contexts that are not related to modules.
222
        $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
223
            if ($context->contextlevel == CONTEXT_MODULE) {
224
                $carry[] = $context->instanceid;
225
            }
226
            return $carry;
227
        }, []);
228
229
        if (empty($cmids)) {
230
            return;
231
        }
232
233
        $user = $contextlist->get_user();
234
235
        // Get all the bigbluebuttonbn activities associated with the above course modules.
236
        $instanceidstocmids = self::get_instance_ids_to_cmids_from_cmids($cmids);
237
        $instanceids = array_keys($instanceidstocmids);
238
239
        list($insql, $inparams) = $DB->get_in_or_equal($instanceids, SQL_PARAMS_NAMED);
240
        $params = array_merge($inparams, ['userid' => $user->id]);
241
        $recordset = $DB->get_recordset_select(
242
            'bigbluebuttonbn_logs', "bigbluebuttonbnid $insql AND userid = :userid", $params, 'timecreated, id');
243
        self::recordset_loop_and_export($recordset, 'bigbluebuttonbnid', [],
244
            function($carry, $record) use ($user, $instanceidstocmids) {
245
                $carry[] = [
246
                    'timecreated' => transform::datetime($record->timecreated),
247
                    'meetingid' => $record->meetingid,
248
                    'log' => $record->log,
249
                    'meta' => $record->meta,
250
                  ];
251
                return $carry;
252
            },
253
            function($instanceid, $data) use ($user, $instanceidstocmids) {
254
                $context = \context_module::instance($instanceidstocmids[$instanceid]);
255
                $contextdata = helper::get_context_data($context, $user);
256
                $finaldata = (object) array_merge((array) $contextdata, ['logs' => $data]);
257
                helper::export_context_files($context, $user);
258
                writer::with_context($context)->export_data([], $finaldata);
259
            }
260
        );
261
    }
262
263
    /**
264
     * Return a dict of bigbluebuttonbn IDs mapped to their course module ID.
265
     *
266
     * @param array $cmids The course module IDs.
267
     * @return array In the form of [$bigbluebuttonbnid => $cmid].
268
     */
269
    protected static function get_instance_ids_to_cmids_from_cmids(array $cmids) {
270
        global $DB;
271
272
        list($insql, $inparams) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED);
273
        $sql = "SELECT bigbluebuttonbn.id, cm.id AS cmid
274
                 FROM {bigbluebuttonbn} bigbluebuttonbn
275
                 JOIN {modules} m
276
                   ON m.name = :bigbluebuttonbn
277
                 JOIN {course_modules} cm
278
                   ON cm.instance = bigbluebuttonbn.id
279
                  AND cm.module = m.id
280
                WHERE cm.id $insql";
281
        $params = array_merge($inparams, ['bigbluebuttonbn' => 'bigbluebuttonbn']);
282
283
        return $DB->get_records_sql_menu($sql, $params);
284
    }
285
286
    /**
287
     * Loop and export from a recordset.
288
     *
289
     * @param \moodle_recordset $recordset The recordset.
290
     * @param string $splitkey The record key to determine when to export.
291
     * @param mixed $initial The initial data to reduce from.
292
     * @param callable $reducer The function to return the dataset, receives current dataset, and the current record.
293
     * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset.
294
     * @return void
295
     */
296
    protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial,
297
                                                        callable $reducer, callable $export) {
298
        $data = $initial;
299
        $lastid = null;
300
301
        foreach ($recordset as $record) {
302
            if ($lastid && $record->{$splitkey} != $lastid) {
303
                $export($lastid, $data);
304
                $data = $initial;
305
            }
306
            $data = $reducer($data, $record);
307
            $lastid = $record->{$splitkey};
308
        }
309
        $recordset->close();
310
311
        if (!empty($lastid)) {
312
            $export($lastid, $data);
313
        }
314
    }
315
316
    /**
317
     * Get the list of users who have data within a context.
318
     *
319
     * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
320
     */
321
    public static function get_users_in_context(\core_privacy\local\request\userlist $userlist) {
322
323
        $context = $userlist->get_context();
324
325
        if (!$context instanceof \context_module) {
0 ignored issues
show
Bug introduced by
The class context_module does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
326
            return;
327
        }
328
329
        $params = [
330
            'instanceid'    => $context->instanceid,
331
            'modulename'    => 'bigbluebuttonbn',
332
        ];
333
334
        $sql = "SELECT bnl.userid
335
                  FROM {course_modules} cm
336
                  JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
337
                  JOIN {bigbluebuttonbn} bn ON bn.id = cm.instance
338
                  JOIN {bigbluebuttonbn_logs} bnl ON bnl.bigbluebuttonbnid = bn.id
339
                 WHERE cm.id = :instanceid";
340
341
        $userlist->add_from_sql('userid', $sql, $params);
342
    }
343
344
    /**
345
     * Delete multiple users within a single context.
346
     *
347
     * @param   approved_userlist       $userlist The approved context and user information to delete information for.
348
     */
349
    public static function delete_data_for_users(\core_privacy\local\request\approved_userlist $userlist) {
350
        global $DB;
351
352
        $context = $userlist->get_context();
353
        $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]);
354
355
        list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
356
        $params = array_merge(['bigbluebuttonbnid' => $cm->instance], $userinparams);
357
        $sql = "bigbluebuttonbnid = :bigbluebuttonbnid AND userid {$userinsql}";
358
359
        $DB->delete_records_select('bigbluebuttonbn_logs', $sql, $params);
360
    }
361
}
362