See Release Notes
Long Term Support Release
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 * This file contains the core_privacy\manager class. 19 * 20 * @package core_privacy 21 * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com> 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 namespace core_privacy; 25 use core_privacy\local\metadata\collection; 26 use core_privacy\local\metadata\null_provider; 27 use core_privacy\local\request\context_aware_provider; 28 use core_privacy\local\request\contextlist_collection; 29 use core_privacy\local\request\core_user_data_provider; 30 use core_privacy\local\request\core_userlist_provider; 31 use core_privacy\local\request\data_provider; 32 use core_privacy\local\request\user_preference_provider; 33 use \core_privacy\local\metadata\provider as metadata_provider; 34 35 defined('MOODLE_INTERNAL') || die(); 36 37 /** 38 * The core_privacy\manager class, providing a facade to describe, export and delete personal data across Moodle and its components. 39 * 40 * This class is responsible for communicating with and collating privacy data from all relevant components, where relevance is 41 * determined through implementations of specific marker interfaces. These marker interfaces describe the responsibilities (in terms 42 * of personal data storage) as well as the relationship between the component and the core_privacy subsystem. 43 * 44 * The interface hierarchy is as follows: 45 * ├── local\metadata\null_provider 46 * ├── local\metadata\provider 47 * ├── local\request\data_provider 48 * └── local\request\core_data_provider 49 * └── local\request\core_user_data_provider 50 * └── local\request\plugin\provider 51 * └── local\request\subsystem\provider 52 * └── local\request\user_preference_provider 53 * └── local\request\shared_data_provider 54 * └── local\request\plugin\subsystem_provider 55 * └── local\request\plugin\subplugin_provider 56 * └── local\request\subsystem\plugin_provider 57 * 58 * Describing personal data: 59 * ------------------------- 60 * All components must state whether they store personal data (and DESCRIBE it) by implementing one of the metadata providers: 61 * - local\metadata\null_provider (indicating they don't store personal data) 62 * - local\metadata\provider (indicating they do store personal data, and describing it) 63 * 64 * The manager requests metadata for all Moodle components implementing the local\metadata\provider interface. 65 * 66 * Export and deletion of personal data: 67 * ------------------------------------- 68 * Those components storing personal data need to provide EXPORT and DELETION of this data by implementing a request provider. 69 * Which provider implementation depends on the nature of the component; whether it's a sub-component and which components it 70 * stores data for. 71 * 72 * Export and deletion for sub-components (or any component storing data on behalf of another component) is managed by the parent 73 * component. If a component contains sub-components, it must ask those sub-components to provide the relevant data. Only certain 74 * 'core provider' components are called directly from the manager and these must provide the personal data stored by both 75 * themselves, and by all sub-components. Because of this hierarchical structure, the core_privacy\manager needs to know which 76 * components are to be called directly by core: these are called core data providers. The providers implemented by sub-components 77 * are called shared data providers. 78 * 79 * The following are interfaces are not implemented directly, but are marker interfaces uses to classify components by nature: 80 * - local\request\data_provider: 81 * Not implemented directly. Used to classify components storing personal data of some kind. Includes both components storing 82 * personal data for themselves and on behalf of other components. 83 * Include: local\request\core_data_provider and local\request\shared_data_provider. 84 * - local\request\core_data_provider: 85 * Not implemented directly. Used to classify components storing personal data for themselves and which are to be called by the 86 * core_privacy subsystem directly. 87 * Includes: local\request\core_user_data_provider and local\request\user_preference_provider. 88 * - local\request\core_user_data_provider: 89 * Not implemented directly. Used to classify components storing personal data for themselves, which are either a plugin or 90 * subsystem and which are to be called by the core_privacy subsystem directly. 91 * Includes: local\request\plugin\provider and local\request\subsystem\provider. 92 * - local\request\shared_data_provider: 93 * Not implemented directly. Used to classify components storing personal data on behalf of other components and which are 94 * called by the owning component directly. 95 * Includes: local\request\plugin\subsystem_provider, local\request\plugin\subplugin_provider and local\request\subsystem\plugin_provider 96 * 97 * The manager only requests the export or deletion of personal data for components implementing the local\request\core_data_provider 98 * interface or one of its descendants; local\request\plugin\provider, local\request\subsystem\provider or local\request\user_preference_provider. 99 * Implementing one of these signals to the core_privacy subsystem that the component must be queried directly from the manager. 100 * 101 * Any component using another component to store personal data on its behalf, is responsible for making the relevant call to 102 * that component's relevant shared_data_provider class. 103 * 104 * For example: 105 * The manager calls a core_data_provider component (e.g. mod_assign) which, in turn, calls relevant subplugins or subsystems 106 * (which assign uses to store personal data) to get that data. All data for assign and its sub-components is aggregated by assign 107 * and returned to the core_privacy subsystem. 108 * 109 * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com> 110 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 111 */ 112 class manager { 113 114 /** 115 * @var manager_observer Observer. 116 */ 117 protected $observer; 118 119 /** 120 * Set the failure handler. 121 * 122 * @param manager_observer $observer 123 */ 124 public function set_observer(manager_observer $observer) { 125 $this->observer = $observer; 126 } 127 128 /** 129 * Checks whether the given component is compliant with the core_privacy API. 130 * To be considered compliant, a component must declare whether (and where) it stores personal data. 131 * 132 * Components which do store personal data must: 133 * - Have implemented the core_privacy\local\metadata\provider interface (to describe the data it stores) and; 134 * - Have implemented the core_privacy\local\request\data_provider interface (to facilitate export of personal data) 135 * - Have implemented the core_privacy\local\request\deleter interface 136 * 137 * Components which do not store personal data must: 138 * - Have implemented the core_privacy\local\metadata\null_provider interface to signal that they don't store personal data. 139 * 140 * @param string $component frankenstyle component name, e.g. 'mod_assign' 141 * @return bool true if the component is compliant, false otherwise. 142 */ 143 public function component_is_compliant(string $component) : bool { 144 // Components which don't store user data need only implement the null_provider. 145 if ($this->component_implements($component, null_provider::class)) { 146 return true; 147 } 148 149 if (static::is_empty_subsystem($component)) { 150 return true; 151 } 152 153 // Components which store user data must implement the local\metadata\provider and the local\request\data_provider. 154 if ($this->component_implements($component, metadata_provider::class) && 155 $this->component_implements($component, data_provider::class)) { 156 return true; 157 } 158 159 return false; 160 } 161 162 /** 163 * Retrieve the reason for implementing the null provider interface. 164 * 165 * @param string $component Frankenstyle component name. 166 * @return string The key to retrieve the language string for the null provider reason. 167 */ 168 public function get_null_provider_reason(string $component) : string { 169 if ($this->component_implements($component, null_provider::class)) { 170 $reason = $this->handled_component_class_callback($component, null_provider::class, 'get_reason', []); 171 return empty($reason) ? 'privacy:reason' : $reason; 172 } else { 173 throw new \coding_exception('Call to undefined method', 'Please only call this method on a null provider.'); 174 } 175 } 176 177 /** 178 * Return whether this is an 'empty' subsystem - that is, a subsystem without a directory. 179 * 180 * @param string $component Frankenstyle component name. 181 * @return string The key to retrieve the language string for the null provider reason. 182 */ 183 public static function is_empty_subsystem($component) { 184 if (strpos($component, 'core_') === 0) { 185 if (null === \core_component::get_subsystem_directory(substr($component, 5))) { 186 // This is a subsystem without a directory. 187 return true; 188 } 189 } 190 191 return false; 192 } 193 194 /** 195 * Get the privacy metadata for all components. 196 * 197 * @return collection[] The array of collection objects, indexed by frankenstyle component name. 198 */ 199 public function get_metadata_for_components() : array { 200 // Get the metadata, and put into an assoc array indexed by component name. 201 $metadata = []; 202 foreach ($this->get_component_list() as $component) { 203 $componentmetadata = $this->handled_component_class_callback($component, metadata_provider::class, 204 'get_metadata', [new collection($component)]); 205 if ($componentmetadata !== null) { 206 $metadata[$component] = $componentmetadata; 207 } 208 } 209 return $metadata; 210 } 211 212 /** 213 * Gets a collection of resultset objects for all components. 214 * 215 * 216 * @param int $userid the id of the user we're fetching contexts for. 217 * @return contextlist_collection the collection of contextlist items for the respective components. 218 */ 219 public function get_contexts_for_userid(int $userid) : contextlist_collection { 220 $progress = static::get_log_tracer(); 221 222 $components = $this->get_component_list(); 223 $a = (object) [ 224 'total' => count($components), 225 'progress' => 0, 226 'component' => '', 227 'datetime' => userdate(time()), 228 ]; 229 $clcollection = new contextlist_collection($userid); 230 231 $progress->output(get_string('trace:fetchcomponents', 'core_privacy', $a), 1); 232 foreach ($components as $component) { 233 $a->component = $component; 234 $a->progress++; 235 $a->datetime = userdate(time()); 236 $progress->output(get_string('trace:processingcomponent', 'core_privacy', $a), 2); 237 $contextlist = $this->handled_component_class_callback($component, core_user_data_provider::class, 238 'get_contexts_for_userid', [$userid]); 239 if ($contextlist === null) { 240 $contextlist = new local\request\contextlist(); 241 } 242 243 // Each contextlist is tied to its respective component. 244 $contextlist->set_component($component); 245 246 // Add contexts that the component may not know about. 247 // Example of these include activity completion which modules do not know about themselves. 248 $contextlist = local\request\helper::add_shared_contexts_to_contextlist_for($userid, $contextlist); 249 250 if (count($contextlist)) { 251 $clcollection->add_contextlist($contextlist); 252 } 253 } 254 $progress->output(get_string('trace:done', 'core_privacy'), 1); 255 256 return $clcollection; 257 } 258 259 /** 260 * Gets a collection of users for all components in the specified context. 261 * 262 * @param \context $context The context to search 263 * @return userlist_collection the collection of userlist items for the respective components. 264 */ 265 public function get_users_in_context(\context $context) : \core_privacy\local\request\userlist_collection { 266 $progress = static::get_log_tracer(); 267 268 $components = $this->get_component_list(); 269 $a = (object) [ 270 'total' => count($components), 271 'progress' => 0, 272 'component' => '', 273 'datetime' => userdate(time()), 274 ]; 275 $collection = new \core_privacy\local\request\userlist_collection($context); 276 277 $progress->output(get_string('trace:fetchcomponents', 'core_privacy', $a), 1); 278 foreach ($components as $component) { 279 $a->component = $component; 280 $a->progress++; 281 $a->datetime = userdate(time()); 282 $progress->output(get_string('trace:preprocessingcomponent', 'core_privacy', $a), 2); 283 $userlist = new local\request\userlist($context, $component); 284 285 $this->handled_component_class_callback($component, core_userlist_provider::class, 'get_users_in_context', [$userlist]); 286 287 // Add contexts that the component may not know about. 288 \core_privacy\local\request\helper::add_shared_users_to_userlist($userlist); 289 290 if (count($userlist)) { 291 $collection->add_userlist($userlist); 292 } 293 } 294 $progress->output(get_string('trace:done', 'core_privacy'), 1); 295 296 return $collection; 297 } 298 299 /** 300 * Export all user data for the specified approved_contextlist items. 301 * 302 * Note: userid and component are stored in each respective approved_contextlist. 303 * 304 * @param contextlist_collection $contextlistcollection the collection of contextlists for all components. 305 * @return string the location of the exported data. 306 * @throws \moodle_exception if the contextlist_collection does not contain all approved_contextlist items or if one of the 307 * approved_contextlists' components is not a core_data_provider. 308 */ 309 public function export_user_data(contextlist_collection $contextlistcollection) { 310 $progress = static::get_log_tracer(); 311 312 $a = (object) [ 313 'total' => count($contextlistcollection), 314 'progress' => 0, 315 'component' => '', 316 'datetime' => userdate(time()), 317 ]; 318 319 // Export for the various components/contexts. 320 $progress->output(get_string('trace:exportingapproved', 'core_privacy', $a), 1); 321 foreach ($contextlistcollection as $approvedcontextlist) { 322 323 if (!$approvedcontextlist instanceof \core_privacy\local\request\approved_contextlist) { 324 throw new \moodle_exception('Contextlist must be an approved_contextlist'); 325 } 326 327 $component = $approvedcontextlist->get_component(); 328 $a->component = $component; 329 $a->progress++; 330 $a->datetime = userdate(time()); 331 $progress->output(get_string('trace:processingcomponent', 'core_privacy', $a), 2); 332 333 // Core user data providers. 334 if ($this->component_implements($component, core_user_data_provider::class)) { 335 if (count($approvedcontextlist)) { 336 // This plugin has data it knows about. It is responsible for storing basic data about anything it is 337 // told to export. 338 $this->handled_component_class_callback($component, core_user_data_provider::class, 339 'export_user_data', [$approvedcontextlist]); 340 } 341 } else if (!$this->component_implements($component, context_aware_provider::class)) { 342 // This plugin does not know that it has data - export the shared data it doesn't know about. 343 local\request\helper::export_data_for_null_provider($approvedcontextlist); 344 } 345 } 346 $progress->output(get_string('trace:done', 'core_privacy'), 1); 347 348 // Check each component for non contextlist items too. 349 $components = $this->get_component_list(); 350 $a->total = count($components); 351 $a->progress = 0; 352 $a->datetime = userdate(time()); 353 $progress->output(get_string('trace:exportingrelated', 'core_privacy', $a), 1); 354 foreach ($components as $component) { 355 $a->component = $component; 356 $a->progress++; 357 $a->datetime = userdate(time()); 358 $progress->output(get_string('trace:processingcomponent', 'core_privacy', $a), 2); 359 // Core user preference providers. 360 $this->handled_component_class_callback($component, user_preference_provider::class, 361 'export_user_preferences', [$contextlistcollection->get_userid()]); 362 363 // Contextual information providers. Give each component a chance to include context information based on the 364 // existence of a child context in the contextlist_collection. 365 $this->handled_component_class_callback($component, context_aware_provider::class, 366 'export_context_data', [$contextlistcollection]); 367 } 368 $progress->output(get_string('trace:done', 'core_privacy'), 1); 369 370 $progress->output(get_string('trace:finalisingexport', 'core_privacy'), 1); 371 $location = local\request\writer::with_context(\context_system::instance())->finalise_content(); 372 373 $progress->output(get_string('trace:exportcomplete', 'core_privacy'), 1); 374 return $location; 375 } 376 377 /** 378 * Delete all user data for approved contexts lists provided in the collection. 379 * 380 * This call relates to the forgetting of an entire user. 381 * 382 * Note: userid and component are stored in each respective approved_contextlist. 383 * 384 * @param contextlist_collection $contextlistcollection the collections of approved_contextlist items on which to call deletion. 385 * @throws \moodle_exception if the contextlist_collection doesn't contain all approved_contextlist items, or if the component 386 * for an approved_contextlist isn't a core provider. 387 */ 388 public function delete_data_for_user(contextlist_collection $contextlistcollection) { 389 $progress = static::get_log_tracer(); 390 391 $a = (object) [ 392 'total' => count($contextlistcollection), 393 'progress' => 0, 394 'component' => '', 395 'datetime' => userdate(time()), 396 ]; 397 398 // Delete the data. 399 $progress->output(get_string('trace:deletingapproved', 'core_privacy', $a), 1); 400 foreach ($contextlistcollection as $approvedcontextlist) { 401 if (!$approvedcontextlist instanceof \core_privacy\local\request\approved_contextlist) { 402 throw new \moodle_exception('Contextlist must be an approved_contextlist'); 403 } 404 405 $component = $approvedcontextlist->get_component(); 406 $a->component = $component; 407 $a->progress++; 408 $a->datetime = userdate(time()); 409 $progress->output(get_string('trace:processingcomponent', 'core_privacy', $a), 2); 410 411 if (count($approvedcontextlist)) { 412 // The component knows about data that it has. 413 // Have it delete its own data. 414 $this->handled_component_class_callback($approvedcontextlist->get_component(), core_user_data_provider::class, 415 'delete_data_for_user', [$approvedcontextlist]); 416 } 417 418 // Delete any shared user data it doesn't know about. 419 local\request\helper::delete_data_for_user($approvedcontextlist); 420 } 421 $progress->output(get_string('trace:done', 'core_privacy'), 1); 422 } 423 424 /** 425 * Delete all user data for all specified users in a context. 426 * 427 * @param \core_privacy\local\request\userlist_collection $collection 428 */ 429 public function delete_data_for_users_in_context(\core_privacy\local\request\userlist_collection $collection) { 430 $progress = static::get_log_tracer(); 431 432 $a = (object) [ 433 'contextid' => $collection->get_context()->id, 434 'total' => count($collection), 435 'progress' => 0, 436 'component' => '', 437 'datetime' => userdate(time()), 438 ]; 439 440 // Delete the data. 441 $progress->output(get_string('trace:deletingapprovedusers', 'core_privacy', $a), 1); 442 foreach ($collection as $userlist) { 443 if (!$userlist instanceof \core_privacy\local\request\approved_userlist) { 444 throw new \moodle_exception('The supplied userlist must be an approved_userlist'); 445 } 446 447 $component = $userlist->get_component(); 448 $a->component = $component; 449 $a->progress++; 450 $a->datetime = userdate(time()); 451 452 if (empty($userlist)) { 453 // This really shouldn't happen! 454 continue; 455 } 456 457 $progress->output(get_string('trace:processingcomponent', 'core_privacy', $a), 2); 458 459 $this->handled_component_class_callback($component, core_userlist_provider::class, 460 'delete_data_for_users', [$userlist]); 461 } 462 463 $progress->output(get_string('trace:done', 'core_privacy'), 1); 464 } 465 466 /** 467 * Delete all use data which matches the specified deletion criteria. 468 * 469 * @param \context $context The specific context to delete data for. 470 */ 471 public function delete_data_for_all_users_in_context(\context $context) { 472 $progress = static::get_log_tracer(); 473 474 $components = $this->get_component_list(); 475 $a = (object) [ 476 'total' => count($components), 477 'progress' => 0, 478 'component' => '', 479 'datetime' => userdate(time()), 480 ]; 481 482 $progress->output(get_string('trace:deletingcontext', 'core_privacy', $a), 1); 483 foreach ($this->get_component_list() as $component) { 484 $a->component = $component; 485 $a->progress++; 486 $a->datetime = userdate(time()); 487 $progress->output(get_string('trace:processingcomponent', 'core_privacy', $a), 2); 488 489 // If this component knows about specific data that it owns, 490 // have it delete all of that user data for the context. 491 $this->handled_component_class_callback($component, core_user_data_provider::class, 492 'delete_data_for_all_users_in_context', [$context]); 493 494 // Delete any shared user data it doesn't know about. 495 local\request\helper::delete_data_for_all_users_in_context($component, $context); 496 } 497 $progress->output(get_string('trace:done', 'core_privacy'), 1); 498 } 499 500 /** 501 * Returns a list of frankenstyle names of core components (plugins and subsystems). 502 * 503 * @return array the array of frankenstyle component names. 504 */ 505 protected function get_component_list() { 506 $components = array_keys(array_reduce(\core_component::get_component_list(), function($carry, $item) { 507 return array_merge($carry, $item); 508 }, [])); 509 $components[] = 'core'; 510 511 return $components; 512 } 513 514 /** 515 * Return the fully qualified provider classname for the component. 516 * 517 * @param string $component the frankenstyle component name. 518 * @return string the fully qualified provider classname. 519 */ 520 protected function get_provider_classname($component) { 521 return static::get_provider_classname_for_component($component); 522 } 523 524 /** 525 * Return the fully qualified provider classname for the component. 526 * 527 * @param string $component the frankenstyle component name. 528 * @return string the fully qualified provider classname. 529 */ 530 public static function get_provider_classname_for_component(string $component) { 531 return "$component\\privacy\\provider"; 532 } 533 534 /** 535 * Checks whether the component's provider class implements the specified interface. 536 * This can either be implemented directly, or by implementing a descendant (extension) of the specified interface. 537 * 538 * @param string $component the frankenstyle component name. 539 * @param string $interface the name of the interface we want to check. 540 * @return bool True if an implementation was found, false otherwise. 541 */ 542 protected function component_implements(string $component, string $interface) : bool { 543 $providerclass = $this->get_provider_classname($component); 544 if (class_exists($providerclass)) { 545 $rc = new \ReflectionClass($providerclass); 546 return $rc->implementsInterface($interface); 547 } 548 return false; 549 } 550 551 /** 552 * Call the named method with the specified params on any plugintype implementing the relevant interface. 553 * 554 * @param string $plugintype The plugingtype to check 555 * @param string $interface The interface to implement 556 * @param string $methodname The method to call 557 * @param array $params The params to call 558 */ 559 public static function plugintype_class_callback(string $plugintype, string $interface, string $methodname, array $params) { 560 $components = \core_component::get_plugin_list($plugintype); 561 foreach (array_keys($components) as $component) { 562 static::component_class_callback("{$plugintype}_{$component}", $interface, $methodname, $params); 563 } 564 } 565 566 /** 567 * Call the named method with the specified params on the supplied component if it implements the relevant interface on its provider. 568 * 569 * @param string $component The component to call 570 * @param string $interface The interface to implement 571 * @param string $methodname The method to call 572 * @param array $params The params to call 573 * @return mixed 574 */ 575 public static function component_class_callback(string $component, string $interface, string $methodname, array $params) { 576 $classname = static::get_provider_classname_for_component($component); 577 if (class_exists($classname) && is_subclass_of($classname, $interface)) { 578 return component_class_callback($classname, $methodname, $params); 579 } 580 581 return null; 582 } 583 584 /** 585 * Get the tracer used for logging. 586 * 587 * The text tracer is used except for unit tests. 588 * 589 * @return \progress_trace 590 */ 591 protected static function get_log_tracer() { 592 if (PHPUNIT_TEST) { 593 return new \null_progress_trace(); 594 } 595 596 return new \text_progress_trace(); 597 } 598 599 /** 600 * Call the named method with the specified params on the supplied component if it implements the relevant interface 601 * on its provider. 602 * 603 * @param string $component The component to call 604 * @param string $interface The interface to implement 605 * @param string $methodname The method to call 606 * @param array $params The params to call 607 * @return mixed 608 */ 609 protected function handled_component_class_callback(string $component, string $interface, string $methodname, array $params) { 610 try { 611 return static::component_class_callback($component, $interface, $methodname, $params); 612 } catch (\Throwable $e) { 613 debugging($e->getMessage(), DEBUG_DEVELOPER, $e->getTrace()); 614 $this->component_class_callback_failed($e, $component, $interface, $methodname, $params); 615 616 return null; 617 } 618 } 619 620 /** 621 * Notifies the observer of any failure. 622 * 623 * @param \Throwable $e 624 * @param string $component 625 * @param string $interface 626 * @param string $methodname 627 * @param array $params 628 */ 629 protected function component_class_callback_failed(\Throwable $e, string $component, string $interface, 630 string $methodname, array $params) { 631 if ($this->observer) { 632 call_user_func_array([$this->observer, 'handle_component_failure'], func_get_args()); 633 } 634 } 635 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body