Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 310 and 400]

   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   * Customfield component provider class
  19   *
  20   * @package   core_customfield
  21   * @copyright 2018 David Matamoros <davidmc@moodle.com>
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace core_customfield\privacy;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  use core_customfield\data_controller;
  30  use core_customfield\handler;
  31  use core_privacy\local\metadata\collection;
  32  use core_privacy\local\request\approved_contextlist;
  33  use core_privacy\local\request\contextlist;
  34  use core_privacy\local\request\writer;
  35  use core_privacy\manager;
  36  
  37  /**
  38   * Class provider
  39   *
  40   * Customfields API does not directly store userid and does not perform any export or delete functionality by itself
  41   *
  42   * However this class defines several functions that can be utilized by components that use customfields API to
  43   * export/delete user data.
  44   *
  45   * @package core_customfield
  46   * @copyright 2018 David Matamoros <davidmc@moodle.com>
  47   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  48   */
  49  class provider implements
  50          // Customfield store data.
  51          \core_privacy\local\metadata\provider,
  52  
  53          // The customfield subsystem stores data on behalf of other components.
  54          \core_privacy\local\request\subsystem\plugin_provider,
  55          \core_privacy\local\request\shared_userlist_provider  {
  56  
  57      /**
  58       * Return the fields which contain personal data.
  59       *
  60       * @param collection $collection a reference to the collection to use to store the metadata.
  61       * @return collection the updated collection of metadata items.
  62       */
  63      public static function get_metadata(collection $collection) : collection {
  64          $collection->add_database_table(
  65              'customfield_data',
  66              [
  67                  'fieldid' => 'privacy:metadata:customfield_data:fieldid',
  68                  'instanceid' => 'privacy:metadata:customfield_data:instanceid',
  69                  'intvalue' => 'privacy:metadata:customfield_data:intvalue',
  70                  'decvalue' => 'privacy:metadata:customfield_data:decvalue',
  71                  'shortcharvalue' => 'privacy:metadata:customfield_data:shortcharvalue',
  72                  'charvalue' => 'privacy:metadata:customfield_data:charvalue',
  73                  'value' => 'privacy:metadata:customfield_data:value',
  74                  'valueformat' => 'privacy:metadata:customfield_data:valueformat',
  75                  'timecreated' => 'privacy:metadata:customfield_data:timecreated',
  76                  'timemodified' => 'privacy:metadata:customfield_data:timemodified',
  77                  'contextid' => 'privacy:metadata:customfield_data:contextid',
  78              ],
  79              'privacy:metadata:customfield_data'
  80          );
  81  
  82          // Link to subplugins.
  83          $collection->add_plugintype_link('customfield', [], 'privacy:metadata:customfieldpluginsummary');
  84  
  85          $collection->link_subsystem('core_files', 'privacy:metadata:filepurpose');
  86  
  87          return $collection;
  88      }
  89  
  90      /**
  91       * Returns contexts that have customfields data
  92       *
  93       * To be used in implementations of core_user_data_provider::get_contexts_for_userid
  94       * Caller needs to transfer the $userid to the select subqueries for
  95       * customfield_category->itemid and/or customfield_data->instanceid
  96       *
  97       * @param string $component
  98       * @param string $area
  99       * @param string $itemidstest subquery for selecting customfield_category->itemid
 100       * @param string $instanceidstest subquery for selecting customfield_data->instanceid
 101       * @param array $params array of named parameters
 102       * @return contextlist
 103       */
 104      public static function get_customfields_data_contexts(string $component, string $area,
 105              string $itemidstest = 'IS NOT NULL', string $instanceidstest = 'IS NOT NULL', array $params = []) : contextlist {
 106  
 107          $sql = "SELECT d.contextid FROM {customfield_category} c
 108              JOIN {customfield_field} f ON f.categoryid = c.id
 109              JOIN {customfield_data} d ON d.fieldid = f.id AND d.instanceid $instanceidstest
 110              WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest";
 111  
 112          $contextlist = new contextlist();
 113          $contextlist->add_from_sql($sql, self::get_params($component, $area, $params));
 114  
 115          return $contextlist;
 116      }
 117  
 118      /**
 119       * Returns contexts that have customfields configuration (categories and fields)
 120       *
 121       * To be used in implementations of core_user_data_provider::get_contexts_for_userid in cases when user is
 122       * an owner of the fields configuration
 123       * Caller needs to transfer the $userid to the select subquery for customfield_category->itemid
 124       *
 125       * @param string $component
 126       * @param string $area
 127       * @param string $itemidstest subquery for selecting customfield_category->itemid
 128       * @param array $params array of named parameters for itemidstest subquery
 129       * @return contextlist
 130       */
 131      public static function get_customfields_configuration_contexts(string $component, string $area,
 132              string $itemidstest = 'IS NOT NULL', array $params = []) : contextlist {
 133  
 134          $sql = "SELECT c.contextid FROM {customfield_category} c
 135              WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest";
 136          $params['component'] = $component;
 137          $params['area'] = $area;
 138  
 139          $contextlist = new contextlist();
 140          $contextlist->add_from_sql($sql, self::get_params($component, $area, $params));
 141  
 142          return $contextlist;
 143  
 144      }
 145  
 146      /**
 147       * Exports customfields data
 148       *
 149       * To be used in implementations of core_user_data_provider::export_user_data
 150       * Caller needs to transfer the $userid to the select subqueries for
 151       * customfield_category->itemid and/or customfield_data->instanceid
 152       *
 153       * @param approved_contextlist $contextlist
 154       * @param string $component
 155       * @param string $area
 156       * @param string $itemidstest subquery for selecting customfield_category->itemid
 157       * @param string $instanceidstest subquery for selecting customfield_data->instanceid
 158       * @param array $params array of named parameters for itemidstest and instanceidstest subqueries
 159       * @param array $subcontext subcontext to use in context_writer::export_data, if null (default) the
 160       *     "Custom fields data" will be used;
 161       *     the data id will be appended to the subcontext array.
 162       */
 163      public static function export_customfields_data(approved_contextlist $contextlist, string $component, string $area,
 164                  string $itemidstest = 'IS NOT NULL', string $instanceidstest = 'IS NOT NULL', array $params = [],
 165                  array $subcontext = null) {
 166          global $DB;
 167  
 168          // This query is very similar to api::get_instances_fields_data() but also works for multiple itemids
 169          // and has a context filter.
 170          list($contextidstest, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED, 'cfctx');
 171          $sql = "SELECT d.*, f.type AS fieldtype, f.name as fieldname, f.shortname as fieldshortname, c.itemid
 172              FROM {customfield_category} c
 173              JOIN {customfield_field} f ON f.categoryid = c.id
 174              JOIN {customfield_data} d ON d.fieldid = f.id AND d.instanceid $instanceidstest AND d.contextid $contextidstest
 175              WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest
 176              ORDER BY c.itemid, c.sortorder, f.sortorder";
 177          $params = self::get_params($component, $area, $params) + $contextparams;
 178          $records = $DB->get_recordset_sql($sql, $params);
 179  
 180          if ($subcontext === null) {
 181              $subcontext = [get_string('customfielddata', 'core_customfield')];
 182          }
 183  
 184          /** @var handler $handler */
 185          $handler = null;
 186          $fields = null;
 187          foreach ($records as $record) {
 188              if (!$handler || $handler->get_itemid() != $record->itemid) {
 189                  $handler = handler::get_handler($component, $area, $record->itemid);
 190                  $fields = $handler->get_fields();
 191              }
 192              $field = (object)['type' => $record->fieldtype, 'shortname' => $record->fieldshortname, 'name' => $record->fieldname];
 193              unset($record->itemid, $record->fieldtype, $record->fieldshortname, $record->fieldname);
 194              try {
 195                  $field = array_key_exists($record->fieldid, $fields) ? $fields[$record->fieldid] : null;
 196                  $data = data_controller::create(0, $record, $field);
 197                  self::export_customfield_data($data, array_merge($subcontext, [$record->id]));
 198              } catch (\Exception $e) {
 199                  // We store some data that we can not initialise controller for. We still need to export it.
 200                  self::export_customfield_data_unknown($record, $field, array_merge($subcontext, [$record->id]));
 201              }
 202          }
 203          $records->close();
 204      }
 205  
 206      /**
 207       * Deletes customfields data
 208       *
 209       * To be used in implementations of core_user_data_provider::delete_data_for_user
 210       * Caller needs to transfer the $userid to the select subqueries for
 211       * customfield_category->itemid and/or customfield_data->instanceid
 212       *
 213       * @param approved_contextlist $contextlist
 214       * @param string $component
 215       * @param string $area
 216       * @param string $itemidstest subquery for selecting customfield_category->itemid
 217       * @param string $instanceidstest subquery for selecting customfield_data->instanceid
 218       * @param array $params array of named parameters for itemidstest and instanceidstest subqueries
 219       */
 220      public static function delete_customfields_data(approved_contextlist $contextlist, string $component, string $area,
 221              string $itemidstest = 'IS NOT NULL', string $instanceidstest = 'IS NOT NULL', array $params = []) {
 222          global $DB;
 223  
 224          list($contextidstest, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED, 'cfctx');
 225          $sql = "SELECT d.id
 226              FROM {customfield_category} c
 227              JOIN {customfield_field} f ON f.categoryid = c.id
 228              JOIN {customfield_data} d ON d.fieldid = f.id AND d.instanceid $instanceidstest AND d.contextid $contextidstest
 229              WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest";
 230          $params = self::get_params($component, $area, $params) + $contextparams;
 231  
 232          self::before_delete_data('IN (' . $sql . ') ', $params);
 233  
 234          $DB->execute("DELETE FROM {customfield_data}
 235              WHERE instanceid $instanceidstest
 236              AND contextid $contextidstest
 237              AND fieldid IN (SELECT f.id
 238                  FROM {customfield_category} c
 239                  JOIN {customfield_field} f ON f.categoryid = c.id
 240                  WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest)", $params);
 241      }
 242  
 243      /**
 244       * Deletes customfields configuration (categories and fields) and all relevant data
 245       *
 246       * To be used in implementations of core_user_data_provider::delete_data_for_user in cases when user is
 247       * an owner of the fields configuration and it is considered user information (quite unlikely situtation but we never
 248       * know what customfields API can be used for)
 249       *
 250       * Caller needs to transfer the $userid to the select subquery for customfield_category->itemid
 251       *
 252       * @param approved_contextlist $contextlist
 253       * @param string $component
 254       * @param string $area
 255       * @param string $itemidstest subquery for selecting customfield_category->itemid
 256       * @param array $params array of named parameters for itemidstest subquery
 257       */
 258      public static function delete_customfields_configuration(approved_contextlist $contextlist, string $component, string $area,
 259              string $itemidstest = 'IS NOT NULL', array $params = []) {
 260          global $DB;
 261  
 262          list($contextidstest, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED, 'cfctx');
 263          $params = self::get_params($component, $area, $params) + $contextparams;
 264  
 265          $categoriesids = $DB->get_fieldset_sql("SELECT c.id
 266              FROM {customfield_category} c
 267              WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest AND c.contextid $contextidstest",
 268              $params);
 269  
 270          self::delete_categories($contextlist->get_contextids(), $categoriesids);
 271      }
 272  
 273      /**
 274       * Deletes all customfields configuration (categories and fields) and all relevant data for the given category context
 275       *
 276       * To be used in implementations of core_user_data_provider::delete_data_for_all_users_in_context
 277       *
 278       * @param string $component
 279       * @param string $area
 280       * @param \context $context
 281       */
 282      public static function delete_customfields_configuration_for_context(string $component, string $area, \context $context) {
 283          global $DB;
 284          $categoriesids = $DB->get_fieldset_sql("SELECT c.id
 285              FROM {customfield_category} c
 286              JOIN {context} ctx ON ctx.id = c.contextid AND ctx.path LIKE :ctxpath
 287              WHERE c.component = :cfcomponent AND c.area = :cfarea",
 288              self::get_params($component, $area, ['ctxpath' => $context->path]));
 289  
 290          self::delete_categories([$context->id], $categoriesids);
 291      }
 292  
 293      /**
 294       * Deletes all customfields data for the given context
 295       *
 296       * To be used in implementations of core_user_data_provider::delete_data_for_all_users_in_context
 297       *
 298       * @param string $component
 299       * @param string $area
 300       * @param \context $context
 301       */
 302      public static function delete_customfields_data_for_context(string $component, string $area, \context $context) {
 303          global $DB;
 304  
 305          $sql = "SELECT d.id
 306              FROM {customfield_category} c
 307              JOIN {customfield_field} f ON f.categoryid = c.id
 308              JOIN {customfield_data} d ON d.fieldid = f.id
 309              JOIN {context} ctx ON ctx.id = d.contextid AND ctx.path LIKE :ctxpath
 310              WHERE c.component = :cfcomponent AND c.area = :cfarea";
 311          $params = self::get_params($component, $area, ['ctxpath' => $context->path . '%']);
 312  
 313          self::before_delete_data('IN (' . $sql . ') ', $params);
 314  
 315          $DB->execute("DELETE FROM {customfield_data}
 316              WHERE fieldid IN (SELECT f.id
 317                  FROM {customfield_category} c
 318                  JOIN {customfield_field} f ON f.categoryid = c.id
 319                  WHERE c.component = :cfcomponent AND c.area = :cfarea)
 320              AND contextid IN (SELECT id FROM {context} WHERE path LIKE :ctxpath)",
 321              $params);
 322      }
 323  
 324      /**
 325       * Checks that $params is an associative array and adds parameters for component and area
 326       *
 327       * @param string $component
 328       * @param string $area
 329       * @param array $params
 330       * @return array
 331       * @throws \coding_exception
 332       */
 333      protected static function get_params(string $component, string $area, array $params) : array {
 334          if (!empty($params) && (array_keys($params) === range(0, count($params) - 1))) {
 335              // Argument $params is not an associative array.
 336              throw new \coding_exception('Argument $params must be an associative array!');
 337          }
 338          return $params + ['cfcomponent' => $component, 'cfarea' => $area];
 339      }
 340  
 341      /**
 342       * Delete custom fields categories configurations, all their fields and data
 343       *
 344       * @param array $contextids
 345       * @param array $categoriesids
 346       */
 347      protected static function delete_categories(array $contextids, array $categoriesids) {
 348          global $DB;
 349  
 350          if (!$categoriesids) {
 351              return;
 352          }
 353  
 354          list($categoryidstest, $catparams) = $DB->get_in_or_equal($categoriesids, SQL_PARAMS_NAMED, 'cfcat');
 355          $datasql = "SELECT d.id FROM {customfield_data} d JOIN {customfield_field} f ON f.id = d.fieldid " .
 356              "WHERE f.categoryid $categoryidstest";
 357          $fieldsql = "SELECT f.id AS fieldid FROM {customfield_field} f WHERE f.categoryid $categoryidstest";
 358  
 359          self::before_delete_data("IN ($datasql)", $catparams);
 360          self::before_delete_fields($categoryidstest, $catparams);
 361  
 362          $DB->execute('DELETE FROM {customfield_data} WHERE fieldid IN (' . $fieldsql . ')', $catparams);
 363          $DB->execute("DELETE FROM {customfield_field} WHERE categoryid $categoryidstest", $catparams);
 364          $DB->execute("DELETE FROM {customfield_category} WHERE id $categoryidstest", $catparams);
 365  
 366      }
 367  
 368      /**
 369       * Executes callbacks from the customfield plugins to delete anything related to the data records (usually files)
 370       *
 371       * @param string $dataidstest
 372       * @param array $params
 373       */
 374      protected static function before_delete_data(string $dataidstest, array $params) {
 375          global $DB;
 376          // Find all field types and all contexts for each field type.
 377          $records = $DB->get_recordset_sql("SELECT ff.type, dd.contextid
 378              FROM {customfield_data} dd
 379              JOIN {customfield_field} ff ON ff.id = dd.fieldid
 380              WHERE dd.id $dataidstest
 381              GROUP BY ff.type, dd.contextid",
 382              $params);
 383  
 384          $fieldtypes = [];
 385          foreach ($records as $record) {
 386              $fieldtypes += [$record->type => []];
 387              $fieldtypes[$record->type][] = $record->contextid;
 388          }
 389          $records->close();
 390  
 391          // Call plugin callbacks to delete data customfield_provider::before_delete_data().
 392          foreach ($fieldtypes as $fieldtype => $contextids) {
 393              $classname = manager::get_provider_classname_for_component('customfield_' . $fieldtype);
 394              if (class_exists($classname) && is_subclass_of($classname, customfield_provider::class)) {
 395                  component_class_callback($classname, 'before_delete_data', [$dataidstest, $params, $contextids]);
 396              }
 397          }
 398      }
 399  
 400      /**
 401       * Executes callbacks from the plugins to delete anything related to the fields (usually files)
 402       *
 403       * Also deletes description files
 404       *
 405       * @param string $categoryidstest
 406       * @param array $params
 407       */
 408      protected static function before_delete_fields(string $categoryidstest, array $params) {
 409          global $DB;
 410          // Find all field types and contexts.
 411          $fieldsql = "SELECT f.id AS fieldid FROM {customfield_field} f WHERE f.categoryid $categoryidstest";
 412          $records = $DB->get_recordset_sql("SELECT f.type, c.contextid
 413              FROM {customfield_field} f
 414              JOIN {customfield_category} c ON c.id = f.categoryid
 415              WHERE c.id $categoryidstest",
 416              $params);
 417  
 418          $contexts = [];
 419          $fieldtypes = [];
 420          foreach ($records as $record) {
 421              $contexts[$record->contextid] = $record->contextid;
 422              $fieldtypes += [$record->type => []];
 423              $fieldtypes[$record->type][] = $record->contextid;
 424          }
 425          $records->close();
 426  
 427          // Delete description files.
 428          foreach ($contexts as $contextid) {
 429              get_file_storage()->delete_area_files_select($contextid, 'core_customfield', 'description',
 430                  " IN ($fieldsql) ", $params);
 431          }
 432  
 433          // Call plugin callbacks to delete fields customfield_provider::before_delete_fields().
 434          foreach ($fieldtypes as $type => $contextids) {
 435              $classname = manager::get_provider_classname_for_component('customfield_' . $type);
 436              if (class_exists($classname) && is_subclass_of($classname, customfield_provider::class)) {
 437                  component_class_callback($classname, 'before_delete_fields',
 438                      [" IN ($fieldsql) ", $params, $contextids]);
 439              }
 440          }
 441          $records->close();
 442      }
 443  
 444      /**
 445       * Exports one instance of custom field data
 446       *
 447       * @param data_controller $data
 448       * @param array $subcontext subcontext to pass to content_writer::export_data
 449       */
 450      public static function export_customfield_data(data_controller $data, array $subcontext) {
 451          $context = $data->get_context();
 452  
 453          $exportdata = $data->to_record();
 454          $exportdata->fieldtype = $data->get_field()->get('type');
 455          $exportdata->fieldshortname = $data->get_field()->get('shortname');
 456          $exportdata->fieldname = $data->get_field()->get_formatted_name();
 457          $exportdata->timecreated = \core_privacy\local\request\transform::datetime($exportdata->timecreated);
 458          $exportdata->timemodified = \core_privacy\local\request\transform::datetime($exportdata->timemodified);
 459          unset($exportdata->contextid);
 460          // Use the "export_value" by default for the 'value' attribute, however the plugins may override it in their callback.
 461          $exportdata->value = $data->export_value();
 462  
 463          $classname = manager::get_provider_classname_for_component('customfield_' . $data->get_field()->get('type'));
 464          if (class_exists($classname) && is_subclass_of($classname, customfield_provider::class)) {
 465              component_class_callback($classname, 'export_customfield_data', [$data, $exportdata, $subcontext]);
 466          } else {
 467              // Custom field plugin does not implement customfield_provider, just export default value.
 468              writer::with_context($context)->export_data($subcontext, $exportdata);
 469          }
 470      }
 471  
 472      /**
 473       * Export data record of unknown type when we were not able to create instance of data_controller
 474       *
 475       * @param \stdClass $record record from db table {customfield_data}
 476       * @param \stdClass $field field record with at least fields type, shortname, name
 477       * @param array $subcontext
 478       */
 479      protected static function export_customfield_data_unknown(\stdClass $record, \stdClass $field, array $subcontext) {
 480          $context = \context::instance_by_id($record->contextid);
 481  
 482          $record->fieldtype = $field->type;
 483          $record->fieldshortname = $field->shortname;
 484          $record->fieldname = format_string($field->name);
 485          $record->timecreated = \core_privacy\local\request\transform::datetime($record->timecreated);
 486          $record->timemodified = \core_privacy\local\request\transform::datetime($record->timemodified);
 487          unset($record->contextid);
 488          $record->value = format_text($record->value, $record->valueformat, ['context' => $context]);
 489          writer::with_context($context)->export_data($subcontext, $record);
 490      }
 491  }