See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body