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 * Data provider.
19 *
20 * @package    core_webservice
21 * @copyright  2018 Frédéric Massart
22 * @author     Frédéric Massart <fred@branchup.tech>
23 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26namespace core_webservice\privacy;
27defined('MOODLE_INTERNAL') || die();
28
29use context;
30use context_user;
31use core_privacy\local\metadata\collection;
32use core_privacy\local\request\approved_contextlist;
33use core_privacy\local\request\transform;
34use core_privacy\local\request\writer;
35use core_privacy\local\request\userlist;
36use core_privacy\local\request\approved_userlist;
37
38/**
39 * Data provider class.
40 *
41 * @package    core_webservice
42 * @copyright  2018 Frédéric Massart
43 * @author     Frédéric Massart <fred@branchup.tech>
44 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
45 */
46class provider implements
47    \core_privacy\local\metadata\provider,
48    \core_privacy\local\request\core_userlist_provider,
49    \core_privacy\local\request\subsystem\provider {
50
51    /**
52     * Returns metadata.
53     *
54     * @param collection $collection The initialised collection to add items to.
55     * @return collection A listing of user data stored through this system.
56     */
57    public static function get_metadata(collection $collection) : collection {
58
59        $collection->add_database_table('external_tokens', [
60            'token' => 'privacy:metadata:tokens:token',
61            'privatetoken' => 'privacy:metadata:tokens:privatetoken',
62            'tokentype' => 'privacy:metadata:tokens:tokentype',
63            'userid' => 'privacy:metadata:tokens:userid',
64            'creatorid' => 'privacy:metadata:tokens:creatorid',
65            'iprestriction' => 'privacy:metadata:tokens:iprestriction',
66            'validuntil' => 'privacy:metadata:tokens:validuntil',
67            'timecreated' => 'privacy:metadata:tokens:timecreated',
68            'lastaccess' => 'privacy:metadata:tokens:lastaccess',
69        ], 'privacy:metadata:tokens');
70
71        $collection->add_database_table('external_services_users', [
72            'userid' => 'privacy:metadata:serviceusers:userid',
73            'iprestriction' => 'privacy:metadata:serviceusers:iprestriction',
74            'validuntil' => 'privacy:metadata:serviceusers:validuntil',
75            'timecreated' => 'privacy:metadata:serviceusers:timecreated',
76        ], 'privacy:metadata:serviceusers');
77
78        return $collection;
79    }
80
81    /**
82     * Get the list of contexts that contain user information for the specified user.
83     *
84     * @param int $userid The user to search.
85     * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
86     */
87    public static function get_contexts_for_userid(int $userid) : \core_privacy\local\request\contextlist {
88        $contextlist = new \core_privacy\local\request\contextlist();
89
90        $sql = "
91            SELECT ctx.id
92              FROM {external_tokens} t
93              JOIN {context} ctx
94                ON ctx.instanceid = t.userid
95               AND ctx.contextlevel = :userlevel
96             WHERE t.userid = :userid1
97                OR t.creatorid = :userid2";
98        $contextlist->add_from_sql($sql, ['userlevel' => CONTEXT_USER, 'userid1' => $userid, 'userid2' => $userid]);
99
100        $sql = "
101            SELECT ctx.id
102              FROM {external_services_users} su
103              JOIN {context} ctx
104                ON ctx.instanceid = su.userid
105               AND ctx.contextlevel = :userlevel
106             WHERE su.userid = :userid";
107        $contextlist->add_from_sql($sql, ['userlevel' => CONTEXT_USER, 'userid' => $userid]);
108
109        return $contextlist;
110    }
111
112    /**
113     * Get the list of users within a specific context.
114     *
115     * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
116     */
117    public static function get_users_in_context(userlist $userlist) {
118        global $DB;
119
120        $context = $userlist->get_context();
121
122        if (!$context instanceof \context_user) {
123            return;
124        }
125
126        $userid = $context->instanceid;
127
128        $hasdata = false;
129        $hasdata = $hasdata || $DB->record_exists_select('external_tokens', 'userid = ? OR creatorid = ?', [$userid, $userid]);
130        $hasdata = $hasdata || $DB->record_exists('external_services_users', ['userid' => $userid]);
131
132        if ($hasdata) {
133            $userlist->add_user($userid);
134        }
135    }
136
137    /**
138     * Export all user data for the specified user, in the specified contexts.
139     *
140     * @param approved_contextlist $contextlist The approved contexts to export information for.
141     */
142    public static function export_user_data(approved_contextlist $contextlist) {
143        global $DB;
144
145        $userid = $contextlist->get_user()->id;
146        $contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) use ($userid) {
147            if ($context->contextlevel == CONTEXT_USER) {
148                if ($context->instanceid == $userid) {
149                    $carry['has_mine'] = true;
150                } else {
151                    $carry['others'][] = $context->instanceid;
152                }
153            }
154            return $carry;
155        }, [
156            'has_mine' => false,
157            'others' => []
158        ]);
159
160        $path = [get_string('webservices', 'core_webservice')];
161
162        // Exporting my stuff.
163        if ($contexts['has_mine']) {
164
165            $data = [];
166
167            // Exporting my tokens.
168            $sql = "
169                SELECT t.*, s.name as externalservicename
170                  FROM {external_tokens} t
171                  JOIN {external_services} s
172                    ON s.id = t.externalserviceid
173                 WHERE t.userid = :userid
174              ORDER BY t.id";
175            $recordset = $DB->get_recordset_sql($sql, ['userid' => $userid]);
176            foreach ($recordset as $record) {
177                if (!isset($data['tokens'])) {
178                    $data['tokens'] = [];
179                }
180                $data['tokens'][] = static::transform_token($record);
181            }
182            $recordset->close();
183
184            // Exporting the services I have access to.
185            $sql = "
186                SELECT su.*, s.name as externalservicename
187                  FROM {external_services_users} su
188                  JOIN {external_services} s
189                    ON s.id = su.externalserviceid
190                 WHERE su.userid = :userid
191              ORDER BY su.id";
192            $recordset = $DB->get_recordset_sql($sql, ['userid' => $userid]);
193            foreach ($recordset as $record) {
194                if (!isset($data['services_user'])) {
195                    $data['services_user'] = [];
196                }
197                $data['services_user'][] = [
198                    'external_service' => $record->externalservicename,
199                    'ip_restriction' => $record->iprestriction,
200                    'valid_until' => $record->validuntil ? transform::datetime($record->validuntil) : null,
201                    'created_on' => transform::datetime($record->timecreated),
202                ];
203            }
204            $recordset->close();
205
206            if (!empty($data)) {
207                writer::with_context(context_user::instance($userid))->export_data($path, (object) $data);
208            };
209        }
210
211        // Exporting the tokens I created.
212        if (!empty($contexts['others'])) {
213            list($insql, $inparams) = $DB->get_in_or_equal($contexts['others'], SQL_PARAMS_NAMED);
214            $sql = "
215                SELECT t.*, s.name as externalservicename
216                  FROM {external_tokens} t
217                  JOIN {external_services} s
218                    ON s.id = t.externalserviceid
219                 WHERE t.userid $insql
220                   AND t.creatorid = :userid1
221                   AND t.userid <> :userid2
222              ORDER BY t.userid, t.id";
223            $params = array_merge($inparams, ['userid1' => $userid, 'userid2' => $userid]);
224            $recordset = $DB->get_recordset_sql($sql, $params);
225            static::recordset_loop_and_export($recordset, 'userid', [], function($carry, $record) {
226                $carry[] = static::transform_token($record);
227                return $carry;
228            }, function($userid, $data) use ($path) {
229                writer::with_context(context_user::instance($userid))->export_related_data($path, 'created_by_you', (object) [
230                    'tokens' => $data
231                ]);
232            });
233        }
234    }
235
236    /**
237     * Delete all data for all users in the specified context.
238     *
239     * @param context $context The specific context to delete data for.
240     */
241    public static function delete_data_for_all_users_in_context(context $context) {
242        if ($context->contextlevel != CONTEXT_USER) {
243            return;
244        }
245        static::delete_user_data($context->instanceid);
246    }
247
248    /**
249     * Delete multiple users within a single context.
250     *
251     * @param approved_userlist $userlist The approved context and user information to delete information for.
252     */
253    public static function delete_data_for_users(approved_userlist $userlist) {
254
255        $context = $userlist->get_context();
256
257        if ($context instanceof \context_user) {
258            static::delete_user_data($context->instanceid);
259        }
260    }
261
262    /**
263     * Delete all user data for the specified user, in the specified contexts.
264     *
265     * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
266     */
267    public static function delete_data_for_user(approved_contextlist $contextlist) {
268        $userid = $contextlist->get_user()->id;
269        foreach ($contextlist as $context) {
270            if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $userid) {
271                static::delete_user_data($context->instanceid);
272                break;
273            }
274        }
275    }
276
277    /**
278     * Delete user data.
279     *
280     * @param int $userid The user ID.
281     * @return void
282     */
283    protected static function delete_user_data($userid) {
284        global $DB;
285        $DB->delete_records('external_tokens', ['userid' => $userid]);
286        $DB->delete_records('external_services_users', ['userid' => $userid]);
287    }
288
289    /**
290     * Transform a token entry.
291     *
292     * @param object $record The token record.
293     * @return array
294     */
295    protected static function transform_token($record) {
296        $notexportedstr = get_string('privacy:request:notexportedsecurity', 'core_webservice');
297        return [
298            'external_service' => $record->externalservicename,
299            'token' => $notexportedstr,
300            'private_token' => $record->privatetoken ? $notexportedstr : null,
301            'ip_restriction' => $record->iprestriction,
302            'valid_until' => $record->validuntil ? transform::datetime($record->validuntil) : null,
303            'created_on' => transform::datetime($record->timecreated),
304            'last_access' => $record->lastaccess ? transform::datetime($record->lastaccess) : null,
305        ];
306    }
307
308    /**
309     * Loop and export from a recordset.
310     *
311     * @param \moodle_recordset $recordset The recordset.
312     * @param string $splitkey The record key to determine when to export.
313     * @param mixed $initial The initial data to reduce from.
314     * @param callable $reducer The function to return the dataset, receives current dataset, and the current record.
315     * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset.
316     * @return void
317     */
318    protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial,
319            callable $reducer, callable $export) {
320
321        $data = $initial;
322        $lastid = null;
323
324        foreach ($recordset as $record) {
325            if ($lastid && $record->{$splitkey} != $lastid) {
326                $export($lastid, $data);
327                $data = $initial;
328            }
329            $data = $reducer($data, $record);
330            $lastid = $record->{$splitkey};
331        }
332        $recordset->close();
333
334        if (!empty($lastid)) {
335            $export($lastid, $data);
336        }
337    }
338}
339