Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   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   * The abstract custom fields handler
  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;
  26  
  27  use core_customfield\output\field_data;
  28  use stdClass;
  29  
  30  defined('MOODLE_INTERNAL') || die;
  31  
  32  /**
  33   * Base class for custom fields handlers
  34   *
  35   * This handler provides callbacks for field configuration form and also allows to add the fields to the instance editing form
  36   *
  37   * Every plugin that wants to use custom fields must define a handler class:
  38   * <COMPONENT_OR_PLUGIN>\customfield\<AREA>_handler extends \core_customfield\handler
  39   *
  40   * To initiate a class use an appropriate static method:
  41   * - <handlerclass>::create - to create an instance of a known handler
  42   * - \core_customfield\handler::get_handler - to create an instance of a handler for given component/area/itemid
  43   *
  44   * Also handler is automatically created when the following methods are called:
  45   * - \core_customfield\api::get_field($fieldid)
  46   * - \core_customfield\api::get_category($categoryid)
  47   *
  48   * @package core_customfield
  49   * @copyright 2018 David Matamoros <davidmc@moodle.com>
  50   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  51   */
  52  abstract class handler {
  53  
  54      /**
  55       * The component this handler handles
  56       *
  57       * @var string $component
  58       */
  59      private $component;
  60  
  61      /**
  62       * The area within the component
  63       *
  64       * @var string $area
  65       */
  66      private $area;
  67  
  68      /**
  69       * The id of the item within the area and component
  70  
  71       * @var int $itemid
  72       */
  73      private $itemid;
  74  
  75      /**
  76       * @var category_controller[]
  77       */
  78      protected $categories = null;
  79  
  80      /**
  81       * Handler constructor.
  82       *
  83       * @param int $itemid
  84       */
  85      protected final function __construct(int $itemid = 0) {
  86          if (!preg_match('|^(\w+_[\w_]+)\\\\customfield\\\\([\w_]+)_handler$|', static::class, $matches)) {
  87              throw new \coding_exception('Handler class name must have format: <PLUGIN>\\customfield\\<AREA>_handler');
  88          }
  89          $this->component = $matches[1];
  90          $this->area = $matches[2];
  91          $this->itemid = $itemid;
  92      }
  93  
  94      /**
  95       * Returns an instance of the handler
  96       *
  97       * Some areas may choose to use singleton/caching here
  98       *
  99       * @param int $itemid
 100       * @return handler
 101       */
 102      public static function create(int $itemid = 0) : handler {
 103          return new static($itemid);
 104      }
 105  
 106      /**
 107       * Returns an instance of handler by component/area/itemid
 108       *
 109       * @param string $component component name of full frankenstyle plugin name
 110       * @param string $area name of the area (each component/plugin may define handlers for multiple areas)
 111       * @param int $itemid item id if the area uses them (usually not used)
 112       * @return handler
 113       */
 114      public static function get_handler(string $component, string $area, int $itemid = 0) : handler {
 115          $classname = $component . '\\customfield\\' . $area . '_handler';
 116          if (class_exists($classname) && is_subclass_of($classname, self::class)) {
 117              return $classname::create($itemid);
 118          }
 119          $a = ['component' => s($component), 'area' => s($area)];
 120          throw new \moodle_exception('unknownhandler', 'core_customfield', (object)$a);
 121      }
 122  
 123      /**
 124       * Get component
 125       *
 126       * @return string
 127       */
 128      public function get_component() : string {
 129          return $this->component;
 130      }
 131  
 132      /**
 133       * Get area
 134       *
 135       * @return string
 136       */
 137      public function get_area() : string {
 138          return $this->area;
 139      }
 140  
 141      /**
 142       * Context that should be used for new categories created by this handler
 143       *
 144       * @return \context
 145       */
 146      abstract public function get_configuration_context() : \context;
 147  
 148      /**
 149       * URL for configuration of the fields on this handler.
 150       *
 151       * @return \moodle_url
 152       */
 153      abstract public function get_configuration_url() : \moodle_url;
 154  
 155      /**
 156       * Context that should be used for data stored for the given record
 157       *
 158       * @param int $instanceid id of the instance or 0 if the instance is being created
 159       * @return \context
 160       */
 161      abstract public function get_instance_context(int $instanceid = 0) : \context;
 162  
 163      /**
 164       * Get itemid
 165       *
 166       * @return int|null
 167       */
 168      public function get_itemid() : int {
 169          return $this->itemid;
 170      }
 171  
 172      /**
 173       * Uses categories
 174       *
 175       * @return bool
 176       */
 177      public function uses_categories() : bool {
 178          return true;
 179      }
 180  
 181      /**
 182       * The form to create or edit a field
 183       *
 184       * @param field_controller $field
 185       * @return field_config_form
 186       */
 187      public function get_field_config_form(field_controller $field) : field_config_form {
 188           $form = new field_config_form(null, ['field' => $field]);
 189           $form->set_data(api::prepare_field_for_config_form($field));
 190           return $form;
 191      }
 192  
 193      /**
 194       * Generates a name for the new category
 195       *
 196       * @param int $suffix
 197       * @return string
 198       */
 199      protected function generate_category_name($suffix = 0) : string {
 200          if ($suffix) {
 201              return get_string('otherfieldsn', 'core_customfield', $suffix);
 202          } else {
 203              return get_string('otherfields', 'core_customfield');
 204          }
 205      }
 206  
 207      /**
 208       * Creates a new category and inserts it to the database
 209       *
 210       * @param string $name name of the category, null to generate automatically
 211       * @return int id of the new category
 212       */
 213      public function create_category(string $name = null) : int {
 214          global $DB;
 215          $params = ['component' => $this->get_component(), 'area' => $this->get_area(), 'itemid' => $this->get_itemid()];
 216  
 217          if (empty($name)) {
 218              for ($suffix = 0; $suffix < 100; $suffix++) {
 219                  $name = $this->generate_category_name($suffix);
 220                  if (!$DB->record_exists(category::TABLE, $params + ['name' => $name])) {
 221                      break;
 222                  }
 223              }
 224          }
 225  
 226          $category = category_controller::create(0, (object)['name' => $name], $this);
 227          api::save_category($category);
 228          $this->clear_configuration_cache();
 229          return $category->get('id');
 230      }
 231  
 232      /**
 233       * Validate that the given category belongs to this handler
 234       *
 235       * @param category_controller $category
 236       * @return category_controller
 237       * @throws \moodle_exception
 238       */
 239      protected function validate_category(category_controller $category) : category_controller {
 240          $categories = $this->get_categories_with_fields();
 241          if (!array_key_exists($category->get('id'), $categories)) {
 242              throw new \moodle_exception('categorynotfound', 'core_customfield');
 243          }
 244          return $categories[$category->get('id')];
 245      }
 246  
 247      /**
 248       * Validate that the given field belongs to this handler
 249       *
 250       * @param field_controller $field
 251       * @return field_controller
 252       * @throws \moodle_exception
 253       */
 254      protected function validate_field(field_controller $field) : field_controller {
 255          if (!array_key_exists($field->get('categoryid'), $this->get_categories_with_fields())) {
 256              throw new \moodle_exception('fieldnotfound', 'core_customfield');
 257          }
 258          $category = $this->get_categories_with_fields()[$field->get('categoryid')];
 259          if (!array_key_exists($field->get('id'), $category->get_fields())) {
 260              throw new \moodle_exception('fieldnotfound', 'core_customfield');
 261          }
 262          return $category->get_fields()[$field->get('id')];
 263      }
 264  
 265      /**
 266       * Change name for a field category
 267       *
 268       * @param category_controller $category
 269       * @param string $name
 270       */
 271      public function rename_category(category_controller $category, string $name) {
 272          $this->validate_category($category);
 273          $category->set('name', $name);
 274          api::save_category($category);
 275          $this->clear_configuration_cache();
 276      }
 277  
 278      /**
 279       * Change sort order of the categories
 280       *
 281       * @param category_controller $category category that needs to be moved
 282       * @param int $beforeid id of the category this category needs to be moved before, 0 to move to the end
 283       */
 284      public function move_category(category_controller $category, int $beforeid = 0) {
 285          $category = $this->validate_category($category);
 286          api::move_category($category, $beforeid);
 287          $this->clear_configuration_cache();
 288      }
 289  
 290      /**
 291       * Permanently delete category, all fields in it and all associated data
 292       *
 293       * @param category_controller $category
 294       * @return bool
 295       */
 296      public function delete_category(category_controller $category) : bool {
 297          $category = $this->validate_category($category);
 298          $result = api::delete_category($category);
 299          $this->clear_configuration_cache();
 300          return $result;
 301      }
 302  
 303      /**
 304       * Deletes all data and all fields and categories defined in this handler
 305       */
 306      public function delete_all() {
 307          $categories = $this->get_categories_with_fields();
 308          foreach ($categories as $category) {
 309              api::delete_category($category);
 310          }
 311          $this->clear_configuration_cache();
 312      }
 313  
 314      /**
 315       * Permanently delete a custom field configuration and all associated data
 316       *
 317       * @param field_controller $field
 318       * @return bool
 319       */
 320      public function delete_field_configuration(field_controller $field) : bool {
 321          $field = $this->validate_field($field);
 322          $result = api::delete_field_configuration($field);
 323          $this->clear_configuration_cache();
 324          return $result;
 325      }
 326  
 327      /**
 328       * Change fields sort order, move field to another category
 329       *
 330       * @param field_controller $field field that needs to be moved
 331       * @param int $categoryid category that needs to be moved
 332       * @param int $beforeid id of the category this category needs to be moved before, 0 to move to the end
 333       */
 334      public function move_field(field_controller $field, int $categoryid, int $beforeid = 0) {
 335          $field = $this->validate_field($field);
 336          api::move_field($field, $categoryid, $beforeid);
 337          $this->clear_configuration_cache();
 338      }
 339  
 340      /**
 341       * The current user can configure custom fields on this component.
 342       *
 343       * @return bool
 344       */
 345      abstract public function can_configure() : bool;
 346  
 347      /**
 348       * The current user can edit given custom fields on the given instance
 349       *
 350       * Called to filter list of fields displayed on the instance edit form
 351       *
 352       * Capability to edit/create instance is checked separately
 353       *
 354       * @param field_controller $field
 355       * @param int $instanceid id of the instance or 0 if the instance is being created
 356       * @return bool
 357       */
 358      abstract public function can_edit(field_controller $field, int $instanceid = 0) : bool;
 359  
 360      /**
 361       * The current user can view the value of the custom field for a given custom field and instance
 362       *
 363       * Called to filter list of fields returned by methods get_instance_data(), get_instances_data(),
 364       * export_instance_data(), export_instance_data_object()
 365       *
 366       * Access to the instance itself is checked by handler before calling these methods
 367       *
 368       * @param field_controller $field
 369       * @param int $instanceid
 370       * @return bool
 371       */
 372      abstract public function can_view(field_controller $field, int $instanceid) : bool;
 373  
 374      /**
 375       * Returns the custom field values for an individual instance
 376       *
 377       * The caller must check access to the instance itself before invoking this method
 378       *
 379       * The result is an array of data_controller objects
 380       *
 381       * @param int $instanceid
 382       * @param bool $returnall return data for all fields (by default only visible fields)
 383       * @return data_controller[] array of data_controller objects indexed by fieldid. All fields are present,
 384       *     some data_controller objects may have 'id', some not
 385       *     In the last case data_controller::get_value() and export_value() functions will return default values.
 386       */
 387      public function get_instance_data(int $instanceid, bool $returnall = false) : array {
 388          $fields = $returnall ? $this->get_fields() : $this->get_visible_fields($instanceid);
 389          return api::get_instance_fields_data($fields, $instanceid);
 390      }
 391  
 392      /**
 393       * Returns the custom fields values for multiple instances
 394       *
 395       * The caller must check access to the instance itself before invoking this method
 396       *
 397       * The result is an array of data_controller objects
 398       *
 399       * @param int[] $instanceids
 400       * @param bool $returnall return data for all fields (by default only visible fields)
 401       * @return data_controller[][] 2-dimension array, first index is instanceid, second index is fieldid.
 402       *     All instanceids and all fieldids are present, some data_controller objects may have 'id', some not.
 403       *     In the last case data_controller::get_value() and export_value() functions will return default values.
 404       */
 405      public function get_instances_data(array $instanceids, bool $returnall = false) : array {
 406          $result = api::get_instances_fields_data($this->get_fields(), $instanceids);
 407  
 408          if (!$returnall) {
 409              // Filter only by visible fields (list of visible fields may be different for each instance).
 410              $handler = $this;
 411              foreach ($instanceids as $instanceid) {
 412                  $result[$instanceid] = array_filter($result[$instanceid], function(data_controller $d) use ($handler) {
 413                      return $handler->can_view($d->get_field(), $d->get('instanceid'));
 414                  });
 415              }
 416          }
 417          return $result;
 418      }
 419  
 420      /**
 421       * Returns the custom field values for an individual instance ready to be displayed
 422       *
 423       * The caller must check access to the instance itself before invoking this method
 424       *
 425       * The result is an array of \core_customfield\output\field_data objects
 426       *
 427       * @param int $instanceid
 428       * @param bool $returnall
 429       * @return \core_customfield\output\field_data[]
 430       */
 431      public function export_instance_data(int $instanceid, bool $returnall = false) : array {
 432          return array_map(function($d) {
 433              return new field_data($d);
 434          }, $this->get_instance_data($instanceid, $returnall));
 435      }
 436  
 437      /**
 438       * Returns the custom field values for an individual instance ready to be displayed
 439       *
 440       * The caller must check access to the instance itself before invoking this method
 441       *
 442       * The result is a class where properties are fields short names and the values their export values for this instance
 443       *
 444       * @param int $instanceid
 445       * @param bool $returnall
 446       * @return stdClass
 447       */
 448      public function export_instance_data_object(int $instanceid, bool $returnall = false) : stdClass {
 449          $rv = new stdClass();
 450          foreach ($this->export_instance_data($instanceid, $returnall) as $d) {
 451              $rv->{$d->get_shortname()} = $d->get_value();
 452          }
 453          return $rv;
 454      }
 455  
 456      /**
 457       * Display visible custom fields.
 458       * This is a sample implementation that can be overridden in each handler.
 459       *
 460       * @param data_controller[] $fieldsdata
 461       * @return string
 462       */
 463      public function display_custom_fields_data(array $fieldsdata) : string {
 464          global $PAGE;
 465          $output = $PAGE->get_renderer('core_customfield');
 466          $content = '';
 467          foreach ($fieldsdata as $data) {
 468              $fd = new field_data($data);
 469              $content .= $output->render($fd);
 470          }
 471  
 472          return $content;
 473      }
 474  
 475      /**
 476       * Returns array of categories, each of them contains a list of fields definitions.
 477       *
 478       * @return category_controller[]
 479       */
 480      public function get_categories_with_fields() : array {
 481          if ($this->categories === null) {
 482              $this->categories = api::get_categories_with_fields($this->get_component(), $this->get_area(), $this->get_itemid());
 483          }
 484          $handler = $this;
 485          array_walk($this->categories, function(category_controller $c) use ($handler) {
 486              $c->set_handler($handler);
 487          });
 488          return $this->categories;
 489      }
 490  
 491      /**
 492       * Clears a list of categories with corresponding fields definitions.
 493       */
 494      protected function clear_configuration_cache() {
 495          $this->categories = null;
 496      }
 497  
 498      /**
 499       * Checks if current user can backup a given field
 500       *
 501       * Capability to backup the instance does not need to be checked here
 502       *
 503       * @param field_controller $field
 504       * @param int $instanceid
 505       * @return bool
 506       */
 507      protected function can_backup(field_controller $field, int $instanceid) : bool {
 508          return $this->can_view($field, $instanceid) || $this->can_edit($field, $instanceid);
 509      }
 510  
 511      /**
 512       * Get raw data associated with all fields current user can view or edit
 513       *
 514       * @param int $instanceid
 515       * @return array
 516       */
 517      public function get_instance_data_for_backup(int $instanceid) : array {
 518          $finalfields = [];
 519          $data = $this->get_instance_data($instanceid, true);
 520          foreach ($data as $d) {
 521              if ($d->get('id') && $this->can_backup($d->get_field(), $instanceid)) {
 522                  $finalfields[] = [
 523                      'id' => $d->get('id'),
 524                      'shortname' => $d->get_field()->get('shortname'),
 525                      'type' => $d->get_field()->get('type'),
 526                      'value' => $d->get_value(),
 527                      'valueformat' => $d->get('valueformat')];
 528              }
 529          }
 530          return $finalfields;
 531      }
 532  
 533      /**
 534       * Form data definition callback.
 535       *
 536       * This method is called from moodleform::definition_after_data and allows to tweak
 537       * mform with some data coming directly from the field plugin data controller.
 538       *
 539       * @param \MoodleQuickForm $mform
 540       * @param int $instanceid
 541       */
 542      public function instance_form_definition_after_data(\MoodleQuickForm $mform, int $instanceid = 0) {
 543          $editablefields = $this->get_editable_fields($instanceid);
 544          $fields = api::get_instance_fields_data($editablefields, $instanceid);
 545  
 546          foreach ($fields as $formfield) {
 547              $formfield->instance_form_definition_after_data($mform);
 548          }
 549      }
 550  
 551      /**
 552       * Prepares the custom fields data related to the instance to pass to mform->set_data()
 553       *
 554       * Example:
 555       *   $instance = $DB->get_record(...);
 556       *   // .... prepare editor, filemanager, add tags, etc.
 557       *   $handler->instance_form_before_set_data($instance);
 558       *   $form->set_data($instance);
 559       *
 560       * @param stdClass $instance the instance that has custom fields, if 'id' attribute is present the custom
 561       *    fields for this instance will be added, otherwise the default values will be added.
 562       */
 563      public function instance_form_before_set_data(stdClass $instance) {
 564          $instanceid = !empty($instance->id) ? $instance->id : 0;
 565          $fields = api::get_instance_fields_data($this->get_editable_fields($instanceid), $instanceid);
 566  
 567          foreach ($fields as $formfield) {
 568              $formfield->instance_form_before_set_data($instance);
 569          }
 570      }
 571  
 572      /**
 573       * Saves the given data for custom fields, must be called after the instance is saved and id is present
 574       *
 575       * Example:
 576       *   if ($data = $form->get_data()) {
 577       *     // ... save main instance, set $data->id if instance was created.
 578       *     $handler->instance_form_save($data);
 579       *     redirect(...);
 580       *   }
 581       *
 582       * @param stdClass $instance data received from a form
 583       * @param bool $isnewinstance if this is call is made during instance creation
 584       */
 585      public function instance_form_save(stdClass $instance, bool $isnewinstance = false) {
 586          if (empty($instance->id)) {
 587              throw new \coding_exception('Caller must ensure that id is already set in data before calling this method');
 588          }
 589          if (!preg_grep('/^customfield_/', array_keys((array)$instance))) {
 590              // For performance.
 591              return;
 592          }
 593          $editablefields = $this->get_editable_fields($isnewinstance ? 0 : $instance->id);
 594          $fields = api::get_instance_fields_data($editablefields, $instance->id);
 595          foreach ($fields as $data) {
 596              if (!$data->get('id')) {
 597                  $data->set('contextid', $this->get_instance_context($instance->id)->id);
 598              }
 599              $data->instance_form_save($instance);
 600          }
 601      }
 602  
 603      /**
 604       * Validates the given data for custom fields, used in moodleform validation() function
 605       *
 606       * Example:
 607       *   public function validation($data, $files) {
 608       *     $errors = [];
 609       *     // .... check other fields.
 610       *     $errors = array_merge($errors, $handler->instance_form_validation($data, $files));
 611       *     return $errors;
 612       *   }
 613       *
 614       * @param array $data
 615       * @param array $files
 616       * @return array validation errors
 617       */
 618      public function instance_form_validation(array $data, array $files) {
 619          $instanceid = empty($data['id']) ? 0 : $data['id'];
 620          $editablefields = $this->get_editable_fields($instanceid);
 621          $fields = api::get_instance_fields_data($editablefields, $instanceid);
 622          $errors = [];
 623          foreach ($fields as $formfield) {
 624              $errors += $formfield->instance_form_validation($data, $files);
 625          }
 626          return $errors;
 627      }
 628  
 629      /**
 630       * Adds custom fields to instance editing form
 631       *
 632       * Example:
 633       *   public function definition() {
 634       *     // ... normal instance definition, including hidden 'id' field.
 635       *     $handler->instance_form_definition($this->_form, $instanceid);
 636       *     $this->add_action_buttons();
 637       *   }
 638       *
 639       * @param \MoodleQuickForm $mform
 640       * @param int $instanceid id of the instance, can be null when instance is being created
 641       * @param string $headerlangidentifier If specified, a lang string will be used for field category headings
 642       * @param string $headerlangcomponent
 643       */
 644      public function instance_form_definition(\MoodleQuickForm $mform, int $instanceid = 0,
 645              ?string $headerlangidentifier = null, ?string $headerlangcomponent = null) {
 646  
 647          $editablefields = $this->get_editable_fields($instanceid);
 648          $fieldswithdata = api::get_instance_fields_data($editablefields, $instanceid);
 649          $lastcategoryid = null;
 650          foreach ($fieldswithdata as $data) {
 651              $categoryid = $data->get_field()->get_category()->get('id');
 652              if ($categoryid != $lastcategoryid) {
 653                  $categoryname = format_string($data->get_field()->get_category()->get('name'));
 654  
 655                  // Load category header lang string if specified.
 656                  if (!empty($headerlangidentifier)) {
 657                      $categoryname = get_string($headerlangidentifier, $headerlangcomponent, $categoryname);
 658                  }
 659  
 660                  $mform->addElement('header', 'category_' . $categoryid, $categoryname);
 661                  $lastcategoryid = $categoryid;
 662              }
 663              $data->instance_form_definition($mform);
 664              $field = $data->get_field()->to_record();
 665              if (strlen($field->description)) {
 666                  // Add field description.
 667                  $context = $this->get_configuration_context();
 668                  $value = file_rewrite_pluginfile_urls($field->description, 'pluginfile.php',
 669                      $context->id, 'core_customfield', 'description', $field->id);
 670                  $value = format_text($value, $field->descriptionformat, ['context' => $context]);
 671                  $mform->addElement('static', 'customfield_' . $field->shortname . '_static', '', $value);
 672              }
 673          }
 674      }
 675  
 676      /**
 677       * Get field types array
 678       *
 679       * @return array
 680       */
 681      public function get_available_field_types() :array {
 682          return api::get_available_field_types();
 683      }
 684  
 685      /**
 686       * Options for processing embedded files in the field description.
 687       *
 688       * Handlers may want to extend it to disable files support and/or specify 'noclean'=>true
 689       * Context is not necessary here
 690       *
 691       * @return array
 692       */
 693      public function get_description_text_options() : array {
 694          global $CFG;
 695          require_once($CFG->libdir.'/formslib.php');
 696          return [
 697              'maxfiles' => EDITOR_UNLIMITED_FILES,
 698              'maxbytes' => $CFG->maxbytes,
 699              'context' => $this->get_configuration_context()
 700          ];
 701      }
 702  
 703      /**
 704       * Save the field configuration with the data from the form
 705       *
 706       * @param field_controller $field
 707       * @param stdClass $data data from the form
 708       */
 709      public function save_field_configuration(field_controller $field, stdClass $data) {
 710          if ($field->get('id')) {
 711              $field = $this->validate_field($field);
 712          } else {
 713              $this->validate_category($field->get_category());
 714          }
 715          api::save_field_configuration($field, $data);
 716          $this->clear_configuration_cache();
 717      }
 718  
 719      /**
 720       * Creates or updates custom field data for a instanceid from backup data.
 721       *
 722       * The handlers have to override it if they support backup
 723       *
 724       * @param \restore_task $task
 725       * @param array $data
 726       */
 727      public function restore_instance_data_from_backup(\restore_task $task, array $data) {
 728          throw new \coding_exception('Must be implemented in the handler');
 729      }
 730  
 731      /**
 732       * Returns list of fields defined for this instance as an array (not groupped by categories)
 733       *
 734       * Fields are sorted in the same order they would appear on the instance edit form
 735       *
 736       * Note that this function returns all fields in all categories regardless of whether the current user
 737       * can view or edit data associated with them
 738       *
 739       * @return field_controller[]
 740       */
 741      public function get_fields() : array {
 742          $categories = $this->get_categories_with_fields();
 743          $fields = [];
 744          foreach ($categories as $category) {
 745              foreach ($category->get_fields() as $field) {
 746                  $fields[$field->get('id')] = $field;
 747              }
 748          }
 749          return $fields;
 750      }
 751  
 752      /**
 753       * Get visible fields
 754       *
 755       * @param int $instanceid
 756       * @return field_controller[]
 757       */
 758      protected function get_visible_fields(int $instanceid) : array {
 759          $handler = $this;
 760          return array_filter($this->get_fields(),
 761              function($field) use($handler, $instanceid) {
 762                  return $handler->can_view($field, $instanceid);
 763              }
 764          );
 765      }
 766  
 767      /**
 768       * Get editable fields
 769       *
 770       * @param int $instanceid
 771       * @return field_controller[]
 772       */
 773      public function get_editable_fields(int $instanceid) : array {
 774          $handler = $this;
 775          return array_filter($this->get_fields(),
 776              function($field) use($handler, $instanceid) {
 777                  return $handler->can_edit($field, $instanceid);
 778              }
 779          );
 780      }
 781  
 782      /**
 783       * Allows to add custom controls to the field configuration form that will be saved in configdata
 784       *
 785       * @param \MoodleQuickForm $mform
 786       */
 787      public function config_form_definition(\MoodleQuickForm $mform) {
 788      }
 789  
 790      /**
 791       * Deletes all data related to all fields of an instance.
 792       *
 793       * @param int $instanceid
 794       */
 795      public function delete_instance(int $instanceid) {
 796          $fielddata = api::get_instance_fields_data($this->get_fields(), $instanceid, false);
 797          foreach ($fielddata as $data) {
 798              $data->delete();
 799          }
 800      }
 801  
 802      /**
 803       * Set up page customfield/edit.php
 804       *
 805       * Handler should override this method and set page context
 806       *
 807       * @param field_controller $field
 808       * @return string page heading
 809       */
 810      public function setup_edit_page(field_controller $field) : string {
 811          global $PAGE;
 812  
 813          // Page context.
 814          $context = $this->get_configuration_context();
 815          if ($context->contextlevel == CONTEXT_MODULE) {
 816              list($course, $cm) = get_course_and_cm_from_cmid($context->instanceid, '', $context->get_course_context()->instanceid);
 817              require_login($course, false, $cm);
 818          } else if ($context->contextlevel == CONTEXT_COURSE) {
 819              require_login($context->instanceid, false);
 820          } else {
 821              $PAGE->set_context(null); // This will set to system context only if the context was not set before.
 822              if ($PAGE->context->id != $context->id) {
 823                  // In case of user or block context level this method must be overridden.
 824                  debugging('Handler must override setup_edit_page() and set the page context before calling parent method.',
 825                      DEBUG_DEVELOPER);
 826              }
 827          }
 828  
 829          // Set up url and title.
 830          if ($field->get('id')) {
 831              $field = $this->validate_field($field);
 832          } else {
 833              $this->validate_category($field->get_category());
 834          }
 835          $url = new \moodle_url('/customfield/edit.php',
 836              ['id' => $field->get('id'), 'type' => $field->get('type'), 'categoryid' => $field->get('categoryid')]);
 837  
 838          $PAGE->set_url($url);
 839          $typestr = get_string('pluginname', 'customfield_' . $field->get('type'));
 840          if ($field->get('id')) {
 841              $title = get_string('editingfield', 'core_customfield',
 842                  $field->get_formatted_name());
 843          } else {
 844              $title = get_string('addingnewcustomfield', 'core_customfield', $typestr);
 845          }
 846          $PAGE->set_title($title);
 847          return $title;
 848      }
 849  }