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