Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
   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  }