Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 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.
   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * This file contains the form add/update a data purpose.
  19   *
  20   * @package   tool_dataprivacy
  21   * @copyright 2018 David Monllao
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace tool_dataprivacy\form;
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  use core\form\persistent;
  29  
  30  /**
  31   * Data purpose form.
  32   *
  33   * @package   tool_dataprivacy
  34   * @copyright 2018 David Monllao
  35   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  class purpose extends persistent {
  38  
  39      /**
  40       * @var string The persistent class.
  41       */
  42      protected static $persistentclass = 'tool_dataprivacy\\purpose';
  43  
  44      /**
  45       * @var array The list of current overrides.
  46       */
  47      protected $existingoverrides = [];
  48  
  49      /**
  50       * Define the form - called by parent constructor
  51       */
  52      public function definition() {
  53          $mform = $this->_form;
  54  
  55          $mform->addElement('text', 'name', get_string('name'), 'maxlength="100"');
  56          $mform->setType('name', PARAM_TEXT);
  57          $mform->addRule('name', get_string('required'), 'required', null, 'server');
  58          $mform->addRule('name', get_string('maximumchars', '', 100), 'maxlength', 100, 'server');
  59  
  60          $mform->addElement('editor', 'description', get_string('description'), null, ['autosave' => false]);
  61          $mform->setType('description', PARAM_CLEANHTML);
  62  
  63          // Field for selecting lawful bases (from GDPR Article 6.1).
  64          $this->add_field($this->get_lawful_base_field());
  65          $mform->addRule('lawfulbases', get_string('required'), 'required', null, 'server');
  66  
  67          // Optional field for selecting reasons for collecting sensitive personal data (from GDPR Article 9.2).
  68          $this->add_field($this->get_sensitive_base_field());
  69  
  70          $this->add_field($this->get_retention_period_fields());
  71          $this->add_field($this->get_protected_field());
  72  
  73          $this->add_override_fields();
  74  
  75          if (!empty($this->_customdata['showbuttons'])) {
  76              if (!$this->get_persistent()->get('id')) {
  77                  $savetext = get_string('add');
  78              } else {
  79                  $savetext = get_string('savechanges');
  80              }
  81              $this->add_action_buttons(true, $savetext);
  82          }
  83      }
  84  
  85      /**
  86       * Add a fieldset to the current form.
  87       *
  88       * @param   \stdClass   $data
  89       */
  90      protected function add_field(\stdClass $data) {
  91          foreach ($data->fields as $field) {
  92              $this->_form->addElement($field);
  93          }
  94  
  95          if (!empty($data->helps)) {
  96              foreach ($data->helps as $fieldname => $helpdata) {
  97                  $help = array_merge([$fieldname], $helpdata);
  98                  call_user_func_array([$this->_form, 'addHelpButton'], $help);
  99              }
 100          }
 101  
 102          if (!empty($data->types)) {
 103              foreach ($data->types as $fieldname => $type) {
 104                  $this->_form->setType($fieldname, $type);
 105              }
 106          }
 107  
 108          if (!empty($data->rules)) {
 109              foreach ($data->rules as $fieldname => $ruledata) {
 110                  $rule = array_merge([$fieldname], $ruledata);
 111                  call_user_func_array([$this->_form, 'addRule'], $rule);
 112              }
 113          }
 114  
 115          if (!empty($data->defaults)) {
 116              foreach ($data->defaults as $fieldname => $default) {
 117                  $this->_form($fieldname, $default);
 118              }
 119          }
 120      }
 121  
 122      /**
 123       * Handle addition of relevant repeated element fields for role overrides.
 124       */
 125      protected function add_override_fields() {
 126          $purpose = $this->get_persistent();
 127  
 128          if (empty($purpose->get('id'))) {
 129              // It is not possible to use repeated elements in a modal form yet.
 130              return;
 131          }
 132  
 133          $fields = [
 134              $this->get_role_override_id('roleoverride_'),
 135              $this->get_role_field('roleoverride_'),
 136              $this->get_retention_period_fields('roleoverride_'),
 137              $this->get_protected_field('roleoverride_'),
 138              $this->get_lawful_base_field('roleoverride_'),
 139              $this->get_sensitive_base_field('roleoverride_'),
 140          ];
 141  
 142          $options = [
 143              'type' => [],
 144              'helpbutton' => [],
 145          ];
 146  
 147          // Start by adding the title.
 148          $overrideelements = [
 149              $this->_form->createElement('header', 'roleoverride', get_string('roleoverride', 'tool_dataprivacy')),
 150              $this->_form->createElement(
 151                  'static',
 152                  'roleoverrideoverview',
 153                  '',
 154                  get_string('roleoverrideoverview', 'tool_dataprivacy')
 155              ),
 156          ];
 157  
 158          foreach ($fields as $fielddata) {
 159              foreach ($fielddata->fields as $field) {
 160                  $overrideelements[] = $field;
 161              }
 162  
 163              if (!empty($fielddata->helps)) {
 164                  foreach ($fielddata->helps as $name => $help) {
 165                      if (!isset($options[$name])) {
 166                          $options[$name] = [];
 167                      }
 168                      $options[$name]['helpbutton'] = $help;
 169                  }
 170              }
 171  
 172              if (!empty($fielddata->types)) {
 173                  foreach ($fielddata->types as $name => $type) {
 174                      if (!isset($options[$name])) {
 175                          $options[$name] = [];
 176                      }
 177                      $options[$name]['type'] = $type;
 178                  }
 179              }
 180  
 181              if (!empty($fielddata->rules)) {
 182                  foreach ($fielddata->rules as $name => $rule) {
 183                      if (!isset($options[$name])) {
 184                          $options[$name] = [];
 185                      }
 186                      $options[$name]['rule'] = $rule;
 187                  }
 188              }
 189  
 190              if (!empty($fielddata->defaults)) {
 191                  foreach ($fielddata->defaults as $name => $default) {
 192                      if (!isset($options[$name])) {
 193                          $options[$name] = [];
 194                      }
 195                      $options[$name]['default'] = $default;
 196                  }
 197              }
 198  
 199              if (!empty($fielddata->advanceds)) {
 200                  foreach ($fielddata->advanceds as $name => $advanced) {
 201                      if (!isset($options[$name])) {
 202                          $options[$name] = [];
 203                      }
 204                      $options[$name]['advanced'] = $advanced;
 205                  }
 206              }
 207          }
 208  
 209          $this->existingoverrides = $purpose->get_purpose_overrides();
 210          $existingoverridecount = count($this->existingoverrides);
 211  
 212          $this->repeat_elements(
 213                  $overrideelements,
 214                  $existingoverridecount,
 215                  $options,
 216                  'overrides',
 217                  'addoverride',
 218                  1,
 219                  get_string('addroleoverride', 'tool_dataprivacy')
 220              );
 221      }
 222  
 223      /**
 224       * Converts fields.
 225       *
 226       * @param \stdClass $data
 227       * @return \stdClass
 228       */
 229      public function filter_data_for_persistent($data) {
 230          $data = parent::filter_data_for_persistent($data);
 231  
 232          $classname = static::$persistentclass;
 233          $properties = $classname::properties_definition();
 234  
 235          $data = (object) array_filter((array) $data, function($value, $key) use ($properties) {
 236              return isset($properties[$key]);
 237          }, ARRAY_FILTER_USE_BOTH);
 238  
 239          return $data;
 240      }
 241  
 242      /**
 243       * Get the field for the role name.
 244       *
 245       * @param   string  $prefix The prefix to apply to the field
 246       * @return  \stdClass
 247       */
 248      protected function get_role_override_id(string $prefix = '') : \stdClass {
 249          $fieldname = "{$prefix}id";
 250  
 251          $fielddata = (object) [
 252              'fields' => [],
 253          ];
 254  
 255          $fielddata->fields[] = $this->_form->createElement('hidden', $fieldname);
 256          $fielddata->types[$fieldname] = PARAM_INT;
 257  
 258          return $fielddata;
 259      }
 260  
 261      /**
 262       * Get the field for the role name.
 263       *
 264       * @param   string  $prefix The prefix to apply to the field
 265       * @return  \stdClass
 266       */
 267      protected function get_role_field(string $prefix = '') : \stdClass {
 268          $fieldname = "{$prefix}roleid";
 269  
 270          $fielddata = (object) [
 271              'fields' => [],
 272              'helps' => [],
 273          ];
 274  
 275          $roles = [
 276              '' => get_string('none'),
 277          ];
 278          foreach (role_get_names() as $roleid => $role) {
 279              $roles[$roleid] = $role->localname;
 280          }
 281  
 282          $fielddata->fields[] = $this->_form->createElement('select', $fieldname, get_string('role'),
 283              $roles,
 284              [
 285                  'multiple' => false,
 286              ]
 287          );
 288          $fielddata->helps[$fieldname] = ['role', 'tool_dataprivacy'];
 289          $fielddata->defaults[$fieldname] = null;
 290  
 291          return $fielddata;
 292      }
 293  
 294      /**
 295       * Get the mform field for lawful bases.
 296       *
 297       * @param   string  $prefix The prefix to apply to the field
 298       * @return  \stdClass
 299       */
 300      protected function get_lawful_base_field(string $prefix = '') : \stdClass {
 301          $fieldname = "{$prefix}lawfulbases";
 302  
 303          $data = (object) [
 304              'fields' => [],
 305          ];
 306  
 307          $bases = [];
 308          foreach (\tool_dataprivacy\purpose::GDPR_ART_6_1_ITEMS as $article) {
 309              $key = 'gdpr_art_6_1_' . $article;
 310              $bases[$key] = get_string("{$key}_name", 'tool_dataprivacy');
 311          }
 312  
 313          $data->fields[] = $this->_form->createElement('autocomplete', $fieldname, get_string('lawfulbases', 'tool_dataprivacy'),
 314              $bases,
 315              [
 316                  'multiple' => true,
 317              ]
 318          );
 319  
 320          $data->helps = [
 321              $fieldname => ['lawfulbases', 'tool_dataprivacy'],
 322          ];
 323  
 324          $data->advanceds = [
 325              $fieldname => true,
 326          ];
 327  
 328          return $data;
 329      }
 330  
 331      /**
 332       * Get the mform field for sensitive bases.
 333       *
 334       * @param   string  $prefix The prefix to apply to the field
 335       * @return  \stdClass
 336       */
 337      protected function get_sensitive_base_field(string $prefix = '') : \stdClass {
 338          $fieldname = "{$prefix}sensitivedatareasons";
 339  
 340          $data = (object) [
 341              'fields' => [],
 342          ];
 343  
 344          $bases = [];
 345          foreach (\tool_dataprivacy\purpose::GDPR_ART_9_2_ITEMS as $article) {
 346              $key = 'gdpr_art_9_2_' . $article;
 347              $bases[$key] = get_string("{$key}_name", 'tool_dataprivacy');
 348          }
 349  
 350          $data->fields[] = $this->_form->createElement(
 351              'autocomplete',
 352              $fieldname,
 353              get_string('sensitivedatareasons', 'tool_dataprivacy'),
 354              $bases,
 355              [
 356                  'multiple' => true,
 357              ]
 358          );
 359          $data->helps = [
 360              $fieldname => ['sensitivedatareasons', 'tool_dataprivacy'],
 361          ];
 362  
 363          $data->advanceds = [
 364              $fieldname => true,
 365          ];
 366  
 367          return $data;
 368      }
 369  
 370      /**
 371       * Get the retention period fields.
 372       *
 373       * @param   string  $prefix The name of the main field, and prefix for the subfields.
 374       * @return  \stdClass
 375       */
 376      protected function get_retention_period_fields(string $prefix = '') : \stdClass {
 377          $prefix = "{$prefix}retentionperiod";
 378          $data = (object) [
 379              'fields' => [],
 380              'types' => [],
 381          ];
 382  
 383          $number = $this->_form->createElement('text', "{$prefix}number", null, ['size' => 8]);
 384          $data->types["{$prefix}number"] = PARAM_INT;
 385  
 386          $unitoptions = [
 387              'Y' => get_string('years'),
 388              'M' => strtolower(get_string('months')),
 389              'D' => strtolower(get_string('days'))
 390          ];
 391          $unit = $this->_form->createElement('select', "{$prefix}unit", '', $unitoptions);
 392  
 393          $data->fields[] = $this->_form->createElement(
 394                  'group',
 395                  $prefix,
 396                  get_string('retentionperiod', 'tool_dataprivacy'),
 397                  [
 398                      'number' => $number,
 399                      'unit' => $unit,
 400                  ],
 401                  null,
 402                  false
 403              );
 404  
 405          return $data;
 406      }
 407  
 408      /**
 409       * Get the mform field for the protected flag.
 410       *
 411       * @param   string  $prefix The prefix to apply to the field
 412       * @return  \stdClass
 413       */
 414      protected function get_protected_field(string $prefix = '') : \stdClass {
 415          $fieldname = "{$prefix}protected";
 416  
 417          return (object) [
 418              'fields' => [
 419                  $this->_form->createElement(
 420                          'advcheckbox',
 421                          $fieldname,
 422                          get_string('protected', 'tool_dataprivacy'),
 423                          get_string('protectedlabel', 'tool_dataprivacy')
 424                      ),
 425              ],
 426          ];
 427      }
 428  
 429      /**
 430       * Converts data to data suitable for storage.
 431       *
 432       * @param \stdClass $data
 433       * @return \stdClass
 434       */
 435      protected static function convert_fields(\stdClass $data) {
 436          $data = parent::convert_fields($data);
 437  
 438          if (!empty($data->lawfulbases) && is_array($data->lawfulbases)) {
 439              $data->lawfulbases = implode(',', $data->lawfulbases);
 440          }
 441          if (!empty($data->sensitivedatareasons) && is_array($data->sensitivedatareasons)) {
 442              $data->sensitivedatareasons = implode(',', $data->sensitivedatareasons);
 443          } else {
 444              // Nothing selected. Set default value of null.
 445              $data->sensitivedatareasons = null;
 446          }
 447  
 448          // A single value.
 449          $data->retentionperiod = 'P' . $data->retentionperiodnumber . $data->retentionperiodunit;
 450          unset($data->retentionperiodnumber);
 451          unset($data->retentionperiodunit);
 452  
 453          return $data;
 454      }
 455  
 456      /**
 457       * Get the default data.
 458       *
 459       * @return \stdClass
 460       */
 461      protected function get_default_data() {
 462          $data = parent::get_default_data();
 463  
 464          return $this->convert_existing_data_to_values($data);
 465      }
 466  
 467      /**
 468       * Normalise any values stored in existing data.
 469       *
 470       * @param   \stdClass $data
 471       * @return  \stdClass
 472       */
 473      protected function convert_existing_data_to_values(\stdClass $data) : \stdClass {
 474          $data->lawfulbases = explode(',', $data->lawfulbases);
 475          if (!empty($data->sensitivedatareasons)) {
 476              $data->sensitivedatareasons = explode(',', $data->sensitivedatareasons);
 477          }
 478  
 479          // Convert the single properties into number and unit.
 480          $strlen = strlen($data->retentionperiod);
 481          $data->retentionperiodnumber = substr($data->retentionperiod, 1, $strlen - 2);
 482          $data->retentionperiodunit = substr($data->retentionperiod, $strlen - 1);
 483          unset($data->retentionperiod);
 484  
 485          return $data;
 486      }
 487  
 488      /**
 489       * Fetch the role override data from the list of submitted data.
 490       *
 491       * @param   \stdClass   $data The complete set of processed data
 492       * @return  \stdClass[] The list of overrides
 493       */
 494      public function get_role_overrides_from_data(\stdClass $data) {
 495          $overrides = [];
 496          if (!empty($data->overrides)) {
 497              $searchkey = 'roleoverride_';
 498  
 499              for ($i = 0; $i < $data->overrides; $i++) {
 500                  $overridedata = (object) [];
 501                  foreach ((array) $data as $fieldname => $value) {
 502                      if (strpos($fieldname, $searchkey) !== 0) {
 503                          continue;
 504                      }
 505  
 506                      $overridefieldname = substr($fieldname, strlen($searchkey));
 507                      $overridedata->$overridefieldname = $value[$i];
 508                  }
 509  
 510                  if (empty($overridedata->roleid) || empty($overridedata->retentionperiodnumber)) {
 511                      // Skip this one.
 512                      // There is no value and it will be delete.
 513                      continue;
 514                  }
 515  
 516                  $override = static::convert_fields($overridedata);
 517  
 518                  $overrides[$i] = $override;
 519              }
 520          }
 521  
 522          return $overrides;
 523      }
 524  
 525      /**
 526       * Define extra validation mechanims.
 527       *
 528       * @param  stdClass $data Data to validate.
 529       * @param  array $files Array of files.
 530       * @param  array $errors Currently reported errors.
 531       * @return array of additional errors, or overridden errors.
 532       */
 533      protected function extra_validation($data, $files, array &$errors) {
 534          $overrides = $this->get_role_overrides_from_data($data);
 535  
 536          // Check role overrides to ensure that:
 537          // - roles are unique; and
 538          // - specifeid retention periods are numeric.
 539          $seenroleids = [];
 540          foreach ($overrides as $id => $override) {
 541              $override->purposeid = 0;
 542              $persistent = new \tool_dataprivacy\purpose_override($override->id, $override);
 543  
 544              if (isset($seenroleids[$persistent->get('roleid')])) {
 545                  $errors["roleoverride_roleid[{$id}]"] = get_string('duplicaterole');
 546              }
 547              $seenroleids[$persistent->get('roleid')] = true;
 548  
 549              $errors = array_merge($errors, $persistent->get_errors());
 550          }
 551  
 552          return $errors;
 553      }
 554  
 555      /**
 556       * Load in existing data as form defaults. Usually new entry defaults are stored directly in
 557       * form definition (new entry form); this function is used to load in data where values
 558       * already exist and data is being edited (edit entry form).
 559       *
 560       * @param stdClass $data
 561       */
 562      public function set_data($data) {
 563          $purpose = $this->get_persistent();
 564  
 565          $count = 0;
 566          foreach ($this->existingoverrides as $override) {
 567              $overridedata = $this->convert_existing_data_to_values($override->to_record());
 568              foreach ($overridedata as $key => $value) {
 569                  $keyname = "roleoverride_{$key}[{$count}]";
 570                  $data->$keyname = $value;
 571              }
 572              $count++;
 573          }
 574  
 575          parent::set_data($data);
 576      }
 577  }