Commit | Line | Data |
---|---|---|
c49f3092 AG |
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 | * Privacy tests for core_user. | |
18 | * | |
19 | * @package core_user | |
20 | * @category test | |
21 | * @copyright 2018 Adrian Greeve <adrian@moodle.com> | |
22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
23 | */ | |
24 | ||
25 | defined('MOODLE_INTERNAL') || die(); | |
26 | global $CFG; | |
27 | ||
28 | use \core_privacy\tests\provider_testcase; | |
29 | ||
30 | require_once($CFG->dirroot . "/user/lib.php"); | |
31 | ||
32 | /** | |
33 | * Unit tests for core_user. | |
34 | * | |
35 | * @copyright 2018 Adrian Greeve <adrian@moodle.com> | |
36 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
37 | */ | |
38 | class core_user_privacy_testcase extends provider_testcase { | |
39 | ||
40 | /** | |
41 | * Check that context information is returned correctly. | |
42 | */ | |
43 | public function test_get_contexts_for_userid() { | |
44 | $this->resetAfterTest(); | |
45 | $user = $this->getDataGenerator()->create_user(); | |
46 | // Create some other users as well. | |
47 | $user2 = $this->getDataGenerator()->create_user(); | |
48 | $user3 = $this->getDataGenerator()->create_user(); | |
49 | ||
50 | $context = context_user::instance($user->id); | |
51 | $contextlist = \core_user\privacy\provider::get_contexts_for_userid($user->id); | |
52 | $this->assertSame($context, $contextlist->current()); | |
53 | } | |
54 | ||
55 | /** | |
56 | * Test that data is exported as expected for a user. | |
57 | */ | |
58 | public function test_export_user_data() { | |
59 | $this->resetAfterTest(); | |
60 | $user = $this->getDataGenerator()->create_user(); | |
61 | $course = $this->getDataGenerator()->create_course(); | |
62 | $context = \context_user::instance($user->id); | |
63 | ||
64 | $this->create_data_for_user($user, $course); | |
65 | ||
66 | $approvedlist = new \core_privacy\local\request\approved_contextlist($user, 'core_user', [$context->id]); | |
67 | ||
68 | $writer = \core_privacy\local\request\writer::with_context($context); | |
69 | \core_user\privacy\provider::export_user_data($approvedlist); | |
70 | ||
71 | // Make sure that the password history only returns a count. | |
72 | $history = $writer->get_data([get_string('privacy:passwordhistorypath', 'user')]); | |
73 | $objectcount = new ArrayObject($history); | |
74 | // This object should only have one property. | |
75 | $this->assertCount(1, $objectcount); | |
76 | $this->assertEquals(1, $history->password_history_count); | |
77 | ||
78 | // Password resets should have two fields - timerequested and timererequested. | |
79 | $resetarray = (array) $writer->get_data([get_string('privacy:passwordresetpath', 'user')]); | |
80 | $detail = array_shift($resetarray); | |
81 | $this->assertTrue(array_key_exists('timerequested', $detail)); | |
82 | $this->assertTrue(array_key_exists('timererequested', $detail)); | |
83 | ||
84 | // Last access to course. | |
85 | $lastcourseaccess = (array) $writer->get_data([get_string('privacy:lastaccesspath', 'user')]); | |
86 | $entry = array_shift($lastcourseaccess); | |
87 | $this->assertEquals($course->fullname, $entry['course_name']); | |
88 | $this->assertTrue(array_key_exists('timeaccess', $entry)); | |
89 | ||
90 | // User devices. | |
91 | $userdevices = (array) $writer->get_data([get_string('privacy:devicespath', 'user')]); | |
92 | $entry = array_shift($userdevices); | |
93 | $this->assertEquals('com.moodle.moodlemobile', $entry['appid']); | |
94 | // Make sure these fields are not exported. | |
95 | $this->assertFalse(array_key_exists('pushid', $entry)); | |
96 | $this->assertFalse(array_key_exists('uuid', $entry)); | |
97 | ||
98 | // Session data. | |
99 | $sessiondata = (array) $writer->get_data([get_string('privacy:sessionpath', 'user')]); | |
100 | $entry = array_shift($sessiondata); | |
101 | // Make sure that the sid is not exported. | |
102 | $this->assertFalse(array_key_exists('sid', $entry)); | |
103 | // Check that some of the other fields are present. | |
104 | $this->assertTrue(array_key_exists('state', $entry)); | |
105 | $this->assertTrue(array_key_exists('sessdata', $entry)); | |
106 | $this->assertTrue(array_key_exists('timecreated', $entry)); | |
107 | ||
108 | // Course requests | |
109 | $courserequestdata = (array) $writer->get_data([get_string('privacy:courserequestpath', 'user')]); | |
110 | $entry = array_shift($courserequestdata); | |
111 | // Make sure that the password is not exported. | |
112 | $this->assertFalse(array_key_exists('password', $entry)); | |
113 | // Check that some of the other fields are present. | |
114 | $this->assertTrue(array_key_exists('fullname', $entry)); | |
115 | $this->assertTrue(array_key_exists('shortname', $entry)); | |
116 | $this->assertTrue(array_key_exists('summary', $entry)); | |
117 | ||
118 | // User details. | |
119 | $userdata = (array) $writer->get_data([]); | |
120 | // Check that the password is not exported. | |
121 | $this->assertFalse(array_key_exists('password', $userdata)); | |
122 | // Check that some critical fields exist. | |
123 | $this->assertTrue(array_key_exists('firstname', $userdata)); | |
124 | $this->assertTrue(array_key_exists('lastname', $userdata)); | |
125 | $this->assertTrue(array_key_exists('email', $userdata)); | |
126 | } | |
127 | ||
128 | /** | |
129 | * Test that user data is deleted for one user. | |
130 | */ | |
131 | public function test_delete_data_for_all_users_in_context() { | |
132 | global $DB; | |
133 | $this->resetAfterTest(); | |
134 | $user = $this->getDataGenerator()->create_user([ | |
135 | 'idnumber' => 'A0023', | |
136 | 'emailstop' => 1, | |
137 | 'icq' => 'aksdjf98', | |
138 | 'phone1' => '555 3257', | |
139 | 'institution' => 'test', | |
140 | 'department' => 'Science', | |
141 | 'city' => 'Perth', | |
142 | 'country' => 'au' | |
143 | ]); | |
144 | $user2 = $this->getDataGenerator()->create_user(); | |
145 | $course = $this->getDataGenerator()->create_course(); | |
146 | ||
147 | $this->create_data_for_user($user, $course); | |
148 | $this->create_data_for_user($user2, $course); | |
149 | ||
150 | \core_user\privacy\provider::delete_data_for_all_users_in_context(context_user::instance($user->id)); | |
151 | ||
152 | // These tables should not have any user data for $user. Only for $user2. | |
153 | $records = $DB->get_records('user_password_history'); | |
154 | $this->assertCount(1, $records); | |
155 | $data = array_shift($records); | |
156 | $this->assertNotEquals($user->id, $data->userid); | |
157 | $this->assertEquals($user2->id, $data->userid); | |
158 | $records = $DB->get_records('user_password_resets'); | |
159 | $this->assertCount(1, $records); | |
160 | $data = array_shift($records); | |
161 | $this->assertNotEquals($user->id, $data->userid); | |
162 | $this->assertEquals($user2->id, $data->userid); | |
163 | $records = $DB->get_records('user_lastaccess'); | |
164 | $this->assertCount(1, $records); | |
165 | $data = array_shift($records); | |
166 | $this->assertNotEquals($user->id, $data->userid); | |
167 | $this->assertEquals($user2->id, $data->userid); | |
168 | $records = $DB->get_records('user_devices'); | |
169 | $this->assertCount(1, $records); | |
170 | $data = array_shift($records); | |
171 | $this->assertNotEquals($user->id, $data->userid); | |
172 | $this->assertEquals($user2->id, $data->userid); | |
173 | ||
174 | // Now check that there is still a record for the deleted user, but that non-critical information is removed. | |
175 | $record = $DB->get_record('user', ['id' => $user->id]); | |
176 | $this->assertEmpty($record->idnumber); | |
177 | $this->assertEmpty($record->emailstop); | |
178 | $this->assertEmpty($record->icq); | |
179 | $this->assertEmpty($record->phone1); | |
180 | $this->assertEmpty($record->institution); | |
181 | $this->assertEmpty($record->department); | |
182 | $this->assertEmpty($record->city); | |
183 | $this->assertEmpty($record->country); | |
184 | $this->assertEmpty($record->timezone); | |
185 | $this->assertEmpty($record->timecreated); | |
186 | $this->assertEmpty($record->timemodified); | |
187 | $this->assertEmpty($record->firstnamephonetic); | |
188 | // Check for critical fields. | |
189 | // Deleted should now be 1. | |
190 | $this->assertEquals(1, $record->deleted); | |
191 | $this->assertEquals($user->id, $record->id); | |
192 | $this->assertEquals($user->username, $record->username); | |
193 | $this->assertEquals($user->password, $record->password); | |
194 | $this->assertEquals($user->firstname, $record->firstname); | |
195 | $this->assertEquals($user->lastname, $record->lastname); | |
196 | $this->assertEquals($user->email, $record->email); | |
197 | } | |
198 | ||
199 | /** | |
200 | * Test that user data is deleted for one user. | |
201 | */ | |
202 | public function test_delete_data_for_user() { | |
203 | global $DB; | |
204 | $this->resetAfterTest(); | |
205 | $user = $this->getDataGenerator()->create_user([ | |
206 | 'idnumber' => 'A0023', | |
207 | 'emailstop' => 1, | |
208 | 'icq' => 'aksdjf98', | |
209 | 'phone1' => '555 3257', | |
210 | 'institution' => 'test', | |
211 | 'department' => 'Science', | |
212 | 'city' => 'Perth', | |
213 | 'country' => 'au' | |
214 | ]); | |
215 | $user2 = $this->getDataGenerator()->create_user(); | |
216 | $course = $this->getDataGenerator()->create_course(); | |
217 | ||
218 | $this->create_data_for_user($user, $course); | |
219 | $this->create_data_for_user($user2, $course); | |
220 | ||
221 | // Provide multiple different context to check that only the correct user is deleted. | |
222 | $contexts = [context_user::instance($user->id)->id, context_user::instance($user2->id)->id, context_system::instance()->id]; | |
223 | $approvedlist = new \core_privacy\local\request\approved_contextlist($user, 'core_user', $contexts); | |
224 | ||
225 | \core_user\privacy\provider::delete_data_for_user($approvedlist); | |
226 | ||
227 | // These tables should not have any user data for $user. Only for $user2. | |
228 | $records = $DB->get_records('user_password_history'); | |
229 | $this->assertCount(1, $records); | |
230 | $data = array_shift($records); | |
231 | $this->assertNotEquals($user->id, $data->userid); | |
232 | $this->assertEquals($user2->id, $data->userid); | |
233 | $records = $DB->get_records('user_password_resets'); | |
234 | $this->assertCount(1, $records); | |
235 | $data = array_shift($records); | |
236 | $this->assertNotEquals($user->id, $data->userid); | |
237 | $this->assertEquals($user2->id, $data->userid); | |
238 | $records = $DB->get_records('user_lastaccess'); | |
239 | $this->assertCount(1, $records); | |
240 | $data = array_shift($records); | |
241 | $this->assertNotEquals($user->id, $data->userid); | |
242 | $this->assertEquals($user2->id, $data->userid); | |
243 | $records = $DB->get_records('user_devices'); | |
244 | $this->assertCount(1, $records); | |
245 | $data = array_shift($records); | |
246 | $this->assertNotEquals($user->id, $data->userid); | |
247 | $this->assertEquals($user2->id, $data->userid); | |
248 | ||
249 | // Now check that there is still a record for the deleted user, but that non-critical information is removed. | |
250 | $record = $DB->get_record('user', ['id' => $user->id]); | |
251 | $this->assertEmpty($record->idnumber); | |
252 | $this->assertEmpty($record->emailstop); | |
253 | $this->assertEmpty($record->icq); | |
254 | $this->assertEmpty($record->phone1); | |
255 | $this->assertEmpty($record->institution); | |
256 | $this->assertEmpty($record->department); | |
257 | $this->assertEmpty($record->city); | |
258 | $this->assertEmpty($record->country); | |
259 | $this->assertEmpty($record->timezone); | |
260 | $this->assertEmpty($record->timecreated); | |
261 | $this->assertEmpty($record->timemodified); | |
262 | $this->assertEmpty($record->firstnamephonetic); | |
263 | // Check for critical fields. | |
264 | // Deleted should now be 1. | |
265 | $this->assertEquals(1, $record->deleted); | |
266 | $this->assertEquals($user->id, $record->id); | |
267 | $this->assertEquals($user->username, $record->username); | |
268 | $this->assertEquals($user->password, $record->password); | |
269 | $this->assertEquals($user->firstname, $record->firstname); | |
270 | $this->assertEquals($user->lastname, $record->lastname); | |
271 | $this->assertEquals($user->email, $record->email); | |
272 | } | |
273 | ||
274 | /** | |
275 | * Create user data for a user. | |
276 | * | |
277 | * @param stdClass $user A user object. | |
278 | * @param stdClass $course A course. | |
279 | */ | |
280 | protected function create_data_for_user($user, $course) { | |
281 | global $DB; | |
282 | $this->resetAfterTest(); | |
283 | // Last course access. | |
284 | $lastaccess = (object) [ | |
285 | 'userid' => $user->id, | |
286 | 'courseid' => $course->id, | |
287 | 'timeaccess' => time() - DAYSECS | |
288 | ]; | |
289 | $DB->insert_record('user_lastaccess', $lastaccess); | |
290 | ||
291 | // Password history. | |
292 | $history = (object) [ | |
293 | 'userid' => $user->id, | |
294 | 'hash' => 'HID098djJUU', | |
295 | 'timecreated' => time() | |
296 | ]; | |
297 | $DB->insert_record('user_password_history', $history); | |
298 | ||
299 | // Password resets. | |
300 | $passwordreset = (object) [ | |
301 | 'userid' => $user->id, | |
302 | 'timerequested' => time(), | |
303 | 'timererequested' => time(), | |
304 | 'token' => $this->generate_random_string() | |
305 | ]; | |
306 | $DB->insert_record('user_password_resets', $passwordreset); | |
307 | ||
308 | // User mobile devices. | |
309 | $userdevices = (object) [ | |
310 | 'userid' => $user->id, | |
311 | 'appid' => 'com.moodle.moodlemobile', | |
312 | 'name' => 'occam', | |
313 | 'model' => 'Nexus 4', | |
314 | 'platform' => 'Android', | |
315 | 'version' => '4.2.2', | |
316 | 'pushid' => 'kishUhd', | |
317 | 'uuid' => 'KIhud7s', | |
318 | 'timecreated' => time(), | |
319 | 'timemodified' => time() | |
320 | ]; | |
321 | $DB->insert_record('user_devices', $userdevices); | |
322 | ||
323 | // Course request. | |
324 | $courserequest = (object) [ | |
325 | 'fullname' => 'Test Course', | |
326 | 'shortname' => 'TC', | |
327 | 'summary' => 'Summary of course', | |
328 | 'summaryformat' => 1, | |
329 | 'category' => 1, | |
330 | 'reason' => 'Because it would be nice.', | |
331 | 'requester' => $user->id, | |
332 | 'password' => '' | |
333 | ]; | |
334 | $DB->insert_record('course_request', $courserequest); | |
335 | ||
336 | // User session table data. | |
337 | $usersessions = (object) [ | |
338 | 'state' => 0, | |
339 | 'sid' => $this->generate_random_string(), // Needs a unique id. | |
340 | 'userid' => $user->id, | |
341 | 'sessdata' => 'Nothing', | |
342 | 'timecreated' => time(), | |
343 | 'timemodified' => time(), | |
344 | 'firstip' => '0.0.0.0', | |
345 | 'lastip' => '0.0.0.0' | |
346 | ]; | |
347 | $DB->insert_record('sessions', $usersessions); | |
348 | } | |
349 | ||
350 | /** | |
351 | * Create a random string. | |
352 | * | |
353 | * @param integer $length length of the string to generate. | |
354 | * @return string A random string. | |
355 | */ | |
356 | protected function generate_random_string($length = 6) { | |
357 | $response = ''; | |
358 | $source = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; | |
359 | ||
360 | if ($length > 0) { | |
361 | ||
362 | $response = ''; | |
363 | $source = str_split($source, 1); | |
364 | ||
365 | for ($i = 1; $i <= $length; $i++) { | |
366 | $num = mt_rand(1, count($source)); | |
367 | $response .= $source[$num - 1]; | |
368 | } | |
369 | } | |
370 | ||
371 | return $response; | |
372 | } | |
373 | } |