Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 39 and 311]

   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   * Provides {@link tool_policy\output\renderer} class.
  19   *
  20   * @package     tool_policy
  21   * @category    output
  22   * @copyright   2018 David Mudrák <david@moodle.com>
  23   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  namespace tool_policy;
  27  
  28  use coding_exception;
  29  use context_helper;
  30  use context_system;
  31  use context_user;
  32  use core\session\manager;
  33  use stdClass;
  34  use tool_policy\event\acceptance_created;
  35  use tool_policy\event\acceptance_updated;
  36  use user_picture;
  37  
  38  defined('MOODLE_INTERNAL') || die();
  39  
  40  /**
  41   * Provides the API of the policies plugin.
  42   *
  43   * @copyright 2018 David Mudrak <david@moodle.com>
  44   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  45   */
  46  class api {
  47  
  48      /**
  49       * Return current (active) policies versions.
  50       *
  51       * @param array $audience If defined, filter against the given audience (AUDIENCE_ALL always included)
  52       * @return array of stdClass - exported {@link tool_policy\policy_version_exporter} instances
  53       */
  54      public static function list_current_versions($audience = null) {
  55  
  56          $current = [];
  57  
  58          foreach (static::list_policies() as $policy) {
  59              if (empty($policy->currentversion)) {
  60                  continue;
  61              }
  62              if ($audience && !in_array($policy->currentversion->audience, [policy_version::AUDIENCE_ALL, $audience])) {
  63                  continue;
  64              }
  65              $current[] = $policy->currentversion;
  66          }
  67  
  68          return $current;
  69      }
  70  
  71      /**
  72       * Checks if there are any current policies defined and returns their ids only
  73       *
  74       * @param array $audience If defined, filter against the given audience (AUDIENCE_ALL always included)
  75       * @return array of version ids indexed by policies ids
  76       */
  77      public static function get_current_versions_ids($audience = null) {
  78          global $DB;
  79          $sql = "SELECT v.policyid, v.id
  80               FROM {tool_policy} d
  81               LEFT JOIN {tool_policy_versions} v ON v.policyid = d.id
  82               WHERE d.currentversionid = v.id";
  83          $params = [];
  84          if ($audience) {
  85              $sql .= " AND v.audience IN (?, ?)";
  86              $params = [$audience, policy_version::AUDIENCE_ALL];
  87          }
  88          return $DB->get_records_sql_menu($sql . " ORDER BY d.sortorder", $params);
  89      }
  90  
  91      /**
  92       * Returns a list of all policy documents and their versions.
  93       *
  94       * @param array|int|null $ids Load only the given policies, defaults to all.
  95       * @param int $countacceptances return number of user acceptances for each version
  96       * @return array of stdClass - exported {@link tool_policy\policy_exporter} instances
  97       */
  98      public static function list_policies($ids = null, $countacceptances = false) {
  99          global $DB, $PAGE;
 100  
 101          $versionfields = policy_version::get_sql_fields('v', 'v_');
 102  
 103          $sql = "SELECT d.id, d.currentversionid, d.sortorder, $versionfields ";
 104  
 105          if ($countacceptances) {
 106              $sql .= ", COALESCE(ua.acceptancescount, 0) AS acceptancescount ";
 107          }
 108  
 109          $sql .= " FROM {tool_policy} d
 110               LEFT JOIN {tool_policy_versions} v ON v.policyid = d.id ";
 111  
 112          if ($countacceptances) {
 113              $sql .= " LEFT JOIN (
 114                              SELECT policyversionid, COUNT(*) AS acceptancescount
 115                              FROM {tool_policy_acceptances}
 116                              GROUP BY policyversionid
 117                          ) ua ON ua.policyversionid = v.id ";
 118          }
 119  
 120          $sql .= " WHERE v.id IS NOT NULL ";
 121  
 122          $params = [];
 123  
 124          if ($ids) {
 125              list($idsql, $idparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED);
 126              $sql .= " AND d.id $idsql";
 127              $params = array_merge($params, $idparams);
 128          }
 129  
 130          $sql .= " ORDER BY d.sortorder ASC, v.timecreated DESC";
 131  
 132          $policies = [];
 133          $versions = [];
 134          $optcache = \cache::make('tool_policy', 'policy_optional');
 135  
 136          $rs = $DB->get_recordset_sql($sql, $params);
 137  
 138          foreach ($rs as $r) {
 139              if (!isset($policies[$r->id])) {
 140                  $policies[$r->id] = (object) [
 141                      'id' => $r->id,
 142                      'currentversionid' => $r->currentversionid,
 143                      'sortorder' => $r->sortorder,
 144                  ];
 145              }
 146  
 147              $versiondata = policy_version::extract_record($r, 'v_');
 148  
 149              if ($countacceptances && $versiondata->audience != policy_version::AUDIENCE_GUESTS) {
 150                  $versiondata->acceptancescount = $r->acceptancescount;
 151              }
 152  
 153              $versions[$r->id][$versiondata->id] = $versiondata;
 154  
 155              $optcache->set($versiondata->id, $versiondata->optional);
 156          }
 157  
 158          $rs->close();
 159  
 160          foreach (array_keys($policies) as $policyid) {
 161              static::fix_revision_values($versions[$policyid]);
 162          }
 163  
 164          $return = [];
 165          $context = context_system::instance();
 166          $output = $PAGE->get_renderer('tool_policy');
 167  
 168          foreach ($policies as $policyid => $policydata) {
 169              $versionexporters = [];
 170              foreach ($versions[$policyid] as $versiondata) {
 171                  if ($policydata->currentversionid == $versiondata->id) {
 172                      $versiondata->status = policy_version::STATUS_ACTIVE;
 173                  } else if ($versiondata->archived) {
 174                      $versiondata->status = policy_version::STATUS_ARCHIVED;
 175                  } else {
 176                      $versiondata->status = policy_version::STATUS_DRAFT;
 177                  }
 178                  $versionexporters[] = new policy_version_exporter($versiondata, [
 179                      'context' => $context,
 180                  ]);
 181              }
 182              $policyexporter = new policy_exporter($policydata, [
 183                  'versions' => $versionexporters,
 184              ]);
 185              $return[] = $policyexporter->export($output);
 186          }
 187  
 188          return $return;
 189      }
 190  
 191      /**
 192       * Returns total number of users who are expected to accept site policy
 193       *
 194       * @return int|null
 195       */
 196      public static function count_total_users() {
 197          global $DB, $CFG;
 198          static $cached = null;
 199          if ($cached === null) {
 200              $cached = $DB->count_records_select('user', 'deleted = 0 AND id <> ?', [$CFG->siteguest]);
 201          }
 202          return $cached;
 203      }
 204  
 205      /**
 206       * Load a particular policy document version.
 207       *
 208       * @param int $versionid ID of the policy document version.
 209       * @param array $policies cached result of self::list_policies() in case this function needs to be called in a loop
 210       * @return stdClass - exported {@link tool_policy\policy_exporter} instance
 211       */
 212      public static function get_policy_version($versionid, $policies = null) {
 213          if ($policies === null) {
 214              $policies = self::list_policies();
 215          }
 216          foreach ($policies as $policy) {
 217              if ($policy->currentversionid == $versionid) {
 218                  return $policy->currentversion;
 219  
 220              } else {
 221                  foreach ($policy->draftversions as $draft) {
 222                      if ($draft->id == $versionid) {
 223                          return $draft;
 224                      }
 225                  }
 226  
 227                  foreach ($policy->archivedversions as $archived) {
 228                      if ($archived->id == $versionid) {
 229                          return $archived;
 230                      }
 231                  }
 232              }
 233          }
 234  
 235          throw new \moodle_exception('errorpolicyversionnotfound', 'tool_policy');
 236      }
 237  
 238      /**
 239       * Make sure that each version has a unique revision value.
 240       *
 241       * Empty value are replaced with a timecreated date. Duplicates are suffixed with v1, v2, v3, ... etc.
 242       *
 243       * @param array $versions List of objects with id, timecreated and revision properties
 244       */
 245      public static function fix_revision_values(array $versions) {
 246  
 247          $byrev = [];
 248  
 249          foreach ($versions as $version) {
 250              if ($version->revision === '') {
 251                  $version->revision = userdate($version->timecreated, get_string('strftimedate', 'core_langconfig'));
 252              }
 253              $byrev[$version->revision][$version->id] = true;
 254          }
 255  
 256          foreach ($byrev as $origrevision => $versionids) {
 257              $cnt = count($byrev[$origrevision]);
 258              if ($cnt > 1) {
 259                  foreach ($versionids as $versionid => $unused) {
 260                      foreach ($versions as $version) {
 261                          if ($version->id == $versionid) {
 262                              $version->revision = $version->revision.' - v'.$cnt;
 263                              $cnt--;
 264                              break;
 265                          }
 266                      }
 267                  }
 268              }
 269          }
 270      }
 271  
 272      /**
 273       * Can the user view the given policy version document?
 274       *
 275       * @param stdClass $policy - exported {@link tool_policy\policy_exporter} instance
 276       * @param int $behalfid The id of user on whose behalf the user is viewing the policy
 277       * @param int $userid The user whom access is evaluated, defaults to the current one
 278       * @return bool
 279       */
 280      public static function can_user_view_policy_version($policy, $behalfid = null, $userid = null) {
 281          global $USER;
 282  
 283          if ($policy->status == policy_version::STATUS_ACTIVE) {
 284              return true;
 285          }
 286  
 287          if (empty($userid)) {
 288              $userid = $USER->id;
 289          }
 290  
 291          // Check if the user is viewing the policy on someone else's behalf.
 292          // Typical scenario is a parent viewing the policy on behalf of her child.
 293          if ($behalfid > 0) {
 294              $behalfcontext = context_user::instance($behalfid);
 295  
 296              if ($behalfid != $userid && !has_capability('tool/policy:acceptbehalf', $behalfcontext, $userid)) {
 297                  return false;
 298              }
 299  
 300              // Check that the other user (e.g. the child) has access to the policy.
 301              // Pass a negative third parameter to avoid eventual endless loop.
 302              // We do not support grand-parent relations.
 303              return static::can_user_view_policy_version($policy, -1, $behalfid);
 304          }
 305  
 306          // Users who can manage policies, can see all versions.
 307          if (has_capability('tool/policy:managedocs', context_system::instance(), $userid)) {
 308              return true;
 309          }
 310  
 311          // User who can see all acceptances, must be also allowed to see what was accepted.
 312          if (has_capability('tool/policy:viewacceptances', context_system::instance(), $userid)) {
 313              return true;
 314          }
 315  
 316          // Users have access to all the policies they have ever accepted/declined.
 317          if (static::is_user_version_accepted($userid, $policy->id) !== null) {
 318              return true;
 319          }
 320  
 321          // Check if the user could get access through some of her minors.
 322          if ($behalfid === null) {
 323              foreach (static::get_user_minors($userid) as $minor) {
 324                  if (static::can_user_view_policy_version($policy, $minor->id, $userid)) {
 325                      return true;
 326                  }
 327              }
 328          }
 329  
 330          return false;
 331      }
 332  
 333      /**
 334       * Return the user's minors - other users on which behalf we can accept policies.
 335       *
 336       * Returned objects contain all the standard user name and picture fields as well as the context instanceid.
 337       *
 338       * @param int $userid The id if the user with parental responsibility
 339       * @param array $extrafields Extra fields to be included in result
 340       * @return array of objects
 341       */
 342      public static function get_user_minors($userid, array $extrafields = null) {
 343          global $DB;
 344  
 345          $ctxfields = context_helper::get_preload_record_columns_sql('c');
 346          $userfieldsapi = \core_user\fields::for_name()->with_userpic()->including(...($extrafields ?? []));
 347          $userfields = $userfieldsapi->get_sql('u')->selects;
 348  
 349          $sql = "SELECT $ctxfields $userfields
 350                    FROM {role_assignments} ra
 351                    JOIN {context} c ON c.contextlevel = ".CONTEXT_USER." AND ra.contextid = c.id
 352                    JOIN {user} u ON c.instanceid = u.id
 353                   WHERE ra.userid = ?
 354                ORDER BY u.lastname ASC, u.firstname ASC";
 355  
 356          $rs = $DB->get_recordset_sql($sql, [$userid]);
 357  
 358          $minors = [];
 359  
 360          foreach ($rs as $record) {
 361              context_helper::preload_from_record($record);
 362              $childcontext = context_user::instance($record->id);
 363              if (has_capability('tool/policy:acceptbehalf', $childcontext, $userid)) {
 364                  $minors[$record->id] = $record;
 365              }
 366          }
 367  
 368          $rs->close();
 369  
 370          return $minors;
 371      }
 372  
 373      /**
 374       * Prepare data for the {@link \tool_policy\form\policydoc} form.
 375       *
 376       * @param \tool_policy\policy_version $version persistent representing the version.
 377       * @return stdClass form data
 378       */
 379      public static function form_policydoc_data(policy_version $version) {
 380  
 381          $data = $version->to_record();
 382          $summaryfieldoptions = static::policy_summary_field_options();
 383          $contentfieldoptions = static::policy_content_field_options();
 384  
 385          if (empty($data->id)) {
 386              // Adding a new version of a policy document.
 387              $data = file_prepare_standard_editor($data, 'summary', $summaryfieldoptions, $summaryfieldoptions['context']);
 388              $data = file_prepare_standard_editor($data, 'content', $contentfieldoptions, $contentfieldoptions['context']);
 389  
 390          } else {
 391              // Editing an existing policy document version.
 392              $data = file_prepare_standard_editor($data, 'summary', $summaryfieldoptions, $summaryfieldoptions['context'],
 393                  'tool_policy', 'policydocumentsummary', $data->id);
 394              $data = file_prepare_standard_editor($data, 'content', $contentfieldoptions, $contentfieldoptions['context'],
 395                  'tool_policy', 'policydocumentcontent', $data->id);
 396          }
 397  
 398          return $data;
 399      }
 400  
 401      /**
 402       * Save the data from the policydoc form as a new policy document.
 403       *
 404       * @param stdClass $form data submitted from the {@link \tool_policy\form\policydoc} form.
 405       * @return \tool_policy\policy_version persistent
 406       */
 407      public static function form_policydoc_add(stdClass $form) {
 408          global $DB;
 409  
 410          $form = clone($form);
 411  
 412          $form->policyid = $DB->insert_record('tool_policy', (object) [
 413              'sortorder' => 999,
 414          ]);
 415  
 416          static::distribute_policy_document_sortorder();
 417  
 418          return static::form_policydoc_update_new($form);
 419      }
 420  
 421      /**
 422       * Save the data from the policydoc form as a new policy document version.
 423       *
 424       * @param stdClass $form data submitted from the {@link \tool_policy\form\policydoc} form.
 425       * @return \tool_policy\policy_version persistent
 426       */
 427      public static function form_policydoc_update_new(stdClass $form) {
 428          global $DB;
 429  
 430          if (empty($form->policyid)) {
 431              throw new coding_exception('Invalid policy document ID');
 432          }
 433  
 434          $form = clone($form);
 435  
 436          $form->id = $DB->insert_record('tool_policy_versions', (new policy_version(0, (object) [
 437              'timecreated' => time(),
 438              'policyid' => $form->policyid,
 439          ]))->to_record());
 440  
 441          return static::form_policydoc_update_overwrite($form);
 442      }
 443  
 444  
 445      /**
 446       * Save the data from the policydoc form, overwriting the existing policy document version.
 447       *
 448       * @param stdClass $form data submitted from the {@link \tool_policy\form\policydoc} form.
 449       * @return \tool_policy\policy_version persistent
 450       */
 451      public static function form_policydoc_update_overwrite(stdClass $form) {
 452  
 453          $form = clone($form);
 454          unset($form->timecreated);
 455  
 456          $summaryfieldoptions = static::policy_summary_field_options();
 457          $form = file_postupdate_standard_editor($form, 'summary', $summaryfieldoptions, $summaryfieldoptions['context'],
 458              'tool_policy', 'policydocumentsummary', $form->id);
 459          unset($form->summary_editor);
 460          unset($form->summarytrust);
 461  
 462          $contentfieldoptions = static::policy_content_field_options();
 463          $form = file_postupdate_standard_editor($form, 'content', $contentfieldoptions, $contentfieldoptions['context'],
 464              'tool_policy', 'policydocumentcontent', $form->id);
 465          unset($form->content_editor);
 466          unset($form->contenttrust);
 467  
 468          unset($form->status);
 469          unset($form->save);
 470          unset($form->saveasdraft);
 471          unset($form->minorchange);
 472  
 473          $policyversion = new policy_version($form->id, $form);
 474          $policyversion->update();
 475  
 476          return $policyversion;
 477      }
 478  
 479      /**
 480       * Make the given version the current active one.
 481       *
 482       * @param int $versionid
 483       */
 484      public static function make_current($versionid) {
 485          global $DB, $USER;
 486  
 487          $policyversion = new policy_version($versionid);
 488          if (! $policyversion->get('id') || $policyversion->get('archived')) {
 489              throw new coding_exception('Version not found or is archived');
 490          }
 491  
 492          // Archive current version of this policy.
 493          if ($currentversionid = $DB->get_field('tool_policy', 'currentversionid', ['id' => $policyversion->get('policyid')])) {
 494              if ($currentversionid == $versionid) {
 495                  // Already current, do not change anything.
 496                  return;
 497              }
 498              $DB->set_field('tool_policy_versions', 'archived', 1, ['id' => $currentversionid]);
 499          }
 500  
 501          // Set given version as current.
 502          $DB->set_field('tool_policy', 'currentversionid', $policyversion->get('id'), ['id' => $policyversion->get('policyid')]);
 503  
 504          // Reset the policyagreed flag to force everybody re-accept the policies.
 505          $DB->set_field('user', 'policyagreed', 0);
 506  
 507          // Make sure that the current user is not immediately redirected to the policy acceptance page.
 508          if (isloggedin() && !isguestuser()) {
 509              $USER->policyagreed = 1;
 510          }
 511      }
 512  
 513      /**
 514       * Inactivate the policy document - no version marked as current and the document does not apply.
 515       *
 516       * @param int $policyid
 517       */
 518      public static function inactivate($policyid) {
 519          global $DB;
 520  
 521          if ($currentversionid = $DB->get_field('tool_policy', 'currentversionid', ['id' => $policyid])) {
 522              // Archive the current version.
 523              $DB->set_field('tool_policy_versions', 'archived', 1, ['id' => $currentversionid]);
 524              // Unset current version for the policy.
 525              $DB->set_field('tool_policy', 'currentversionid', null, ['id' => $policyid]);
 526          }
 527      }
 528  
 529      /**
 530       * Create a new draft policy document from an archived version.
 531       *
 532       * @param int $versionid
 533       * @return \tool_policy\policy_version persistent
 534       */
 535      public static function revert_to_draft($versionid) {
 536          $policyversion = new policy_version($versionid);
 537          if (!$policyversion->get('id') || !$policyversion->get('archived')) {
 538              throw new coding_exception('Version not found or is not archived');
 539          }
 540  
 541          $formdata = static::form_policydoc_data($policyversion);
 542          // Unarchived the new version.
 543          $formdata->archived = 0;
 544          return static::form_policydoc_update_new($formdata);
 545      }
 546  
 547      /**
 548       * Can the current version be deleted
 549       *
 550       * @param stdClass $version object describing version, contains fields policyid, id, status, archived, audience, ...
 551       */
 552      public static function can_delete_version($version) {
 553          // TODO MDL-61900 allow to delete not only draft versions.
 554          return has_capability('tool/policy:managedocs', context_system::instance()) &&
 555                  $version->status == policy_version::STATUS_DRAFT;
 556      }
 557  
 558      /**
 559       * Delete the given version (if it is a draft). Also delete policy if this is the only version.
 560       *
 561       * @param int $versionid
 562       */
 563      public static function delete($versionid) {
 564          global $DB;
 565  
 566          $version = static::get_policy_version($versionid);
 567          if (!self::can_delete_version($version)) {
 568              // Current version can not be deleted.
 569              return;
 570          }
 571  
 572          $DB->delete_records('tool_policy_versions', ['id' => $versionid]);
 573  
 574          if (!$DB->record_exists('tool_policy_versions', ['policyid' => $version->policyid])) {
 575              // This is a single version in a policy. Delete the policy.
 576              $DB->delete_records('tool_policy', ['id' => $version->policyid]);
 577          }
 578      }
 579  
 580      /**
 581       * Editor field options for the policy summary text.
 582       *
 583       * @return array
 584       */
 585      public static function policy_summary_field_options() {
 586          global $CFG;
 587          require_once($CFG->libdir.'/formslib.php');
 588  
 589          return [
 590              'subdirs' => false,
 591              'maxfiles' => -1,
 592              'context' => context_system::instance(),
 593          ];
 594      }
 595  
 596      /**
 597       * Editor field options for the policy content text.
 598       *
 599       * @return array
 600       */
 601      public static function policy_content_field_options() {
 602          global $CFG;
 603          require_once($CFG->libdir.'/formslib.php');
 604  
 605          return [
 606              'subdirs' => false,
 607              'maxfiles' => -1,
 608              'context' => context_system::instance(),
 609          ];
 610      }
 611  
 612      /**
 613       * Re-sets the sortorder field of the policy documents to even values.
 614       */
 615      protected static function distribute_policy_document_sortorder() {
 616          global $DB;
 617  
 618          $sql = "SELECT p.id, p.sortorder, MAX(v.timecreated) AS timerecentcreated
 619                    FROM {tool_policy} p
 620               LEFT JOIN {tool_policy_versions} v ON v.policyid = p.id
 621                GROUP BY p.id, p.sortorder
 622                ORDER BY p.sortorder ASC, timerecentcreated ASC";
 623  
 624          $rs = $DB->get_recordset_sql($sql);
 625          $sortorder = 10;
 626  
 627          foreach ($rs as $record) {
 628              if ($record->sortorder != $sortorder) {
 629                  $DB->set_field('tool_policy', 'sortorder', $sortorder, ['id' => $record->id]);
 630              }
 631              $sortorder = $sortorder + 2;
 632          }
 633  
 634          $rs->close();
 635      }
 636  
 637      /**
 638       * Change the policy document's sortorder.
 639       *
 640       * @param int $policyid
 641       * @param int $step
 642       */
 643      protected static function move_policy_document($policyid, $step) {
 644          global $DB;
 645  
 646          $sortorder = $DB->get_field('tool_policy', 'sortorder', ['id' => $policyid], MUST_EXIST);
 647          $DB->set_field('tool_policy', 'sortorder', $sortorder + $step, ['id' => $policyid]);
 648          static::distribute_policy_document_sortorder();
 649      }
 650  
 651      /**
 652       * Move the given policy document up in the list.
 653       *
 654       * @param id $policyid
 655       */
 656      public static function move_up($policyid) {
 657          static::move_policy_document($policyid, -3);
 658      }
 659  
 660      /**
 661       * Move the given policy document down in the list.
 662       *
 663       * @param id $policyid
 664       */
 665      public static function move_down($policyid) {
 666          static::move_policy_document($policyid, 3);
 667      }
 668  
 669      /**
 670       * Returns list of acceptances for this user.
 671       *
 672       * @param int $userid id of a user.
 673       * @param int|array $versions list of policy versions.
 674       * @return array list of acceptances indexed by versionid.
 675       */
 676      public static function get_user_acceptances($userid, $versions = null) {
 677          global $DB;
 678  
 679          list($vsql, $vparams) = ['', []];
 680          if (!empty($versions)) {
 681              list($vsql, $vparams) = $DB->get_in_or_equal($versions, SQL_PARAMS_NAMED, 'ver');
 682              $vsql = ' AND a.policyversionid ' . $vsql;
 683          }
 684  
 685          $userfieldsapi = \core_user\fields::for_name();
 686          $userfieldsmod = $userfieldsapi->get_sql('m', false, 'mod', '', false)->selects;
 687          $sql = "SELECT u.id AS mainuserid, a.policyversionid, a.status, a.lang, a.timemodified, a.usermodified, a.note,
 688                    u.policyagreed, $userfieldsmod
 689                    FROM {user} u
 690                    INNER JOIN {tool_policy_acceptances} a ON a.userid = u.id AND a.userid = :userid $vsql
 691                    LEFT JOIN {user} m ON m.id = a.usermodified";
 692          $params = ['userid' => $userid];
 693          $result = $DB->get_recordset_sql($sql, $params + $vparams);
 694  
 695          $acceptances = [];
 696          foreach ($result as $row) {
 697              if (!empty($row->policyversionid)) {
 698                  $acceptances[$row->policyversionid] = $row;
 699              }
 700          }
 701          $result->close();
 702  
 703          return $acceptances;
 704      }
 705  
 706      /**
 707       * Returns version acceptance for this user.
 708       *
 709       * @param int $userid User identifier.
 710       * @param int $versionid Policy version identifier.
 711       * @param array|null $acceptances List of policy version acceptances indexed by versionid.
 712       * @return stdClass|null Acceptance object if the user has ever accepted this version or null if not.
 713       */
 714      public static function get_user_version_acceptance($userid, $versionid, $acceptances = null) {
 715          if (empty($acceptances)) {
 716              $acceptances = static::get_user_acceptances($userid, $versionid);
 717          }
 718          if (array_key_exists($versionid, $acceptances)) {
 719              // The policy version has ever been accepted.
 720              return $acceptances[$versionid];
 721          }
 722  
 723          return null;
 724      }
 725  
 726      /**
 727       * Did the user accept the given policy version?
 728       *
 729       * @param int $userid User identifier.
 730       * @param int $versionid Policy version identifier.
 731       * @param array|null $acceptances Pre-loaded list of policy version acceptances indexed by versionid.
 732       * @return bool|null True/false if this user accepted/declined the policy; null otherwise.
 733       */
 734      public static function is_user_version_accepted($userid, $versionid, $acceptances = null) {
 735  
 736          $acceptance = static::get_user_version_acceptance($userid, $versionid, $acceptances);
 737  
 738          if (!empty($acceptance)) {
 739              return (bool) $acceptance->status;
 740          }
 741  
 742          return null;
 743      }
 744  
 745      /**
 746       * Get the list of policies and versions that current user is able to see and the respective acceptance records for
 747       * the selected user.
 748       *
 749       * @param int $userid
 750       * @return array array with the same structure that list_policies() returns with additional attribute acceptance for versions
 751       */
 752      public static function get_policies_with_acceptances($userid) {
 753          // Get the list of policies and versions that current user is able to see
 754          // and the respective acceptance records for the selected user.
 755          $policies = static::list_policies();
 756          $acceptances = static::get_user_acceptances($userid);
 757          $ret = [];
 758          foreach ($policies as $policy) {
 759              $versions = [];
 760              if ($policy->currentversion && $policy->currentversion->audience != policy_version::AUDIENCE_GUESTS) {
 761                  if (isset($acceptances[$policy->currentversion->id])) {
 762                      $policy->currentversion->acceptance = $acceptances[$policy->currentversion->id];
 763                  } else {
 764                      $policy->currentversion->acceptance = null;
 765                  }
 766                  $versions[] = $policy->currentversion;
 767              }
 768              foreach ($policy->archivedversions as $version) {
 769                  if ($version->audience != policy_version::AUDIENCE_GUESTS
 770                          && static::can_user_view_policy_version($version, $userid)) {
 771                      $version->acceptance = isset($acceptances[$version->id]) ? $acceptances[$version->id] : null;
 772                      $versions[] = $version;
 773                  }
 774              }
 775              if ($versions) {
 776                  $ret[] = (object)['id' => $policy->id, 'versions' => $versions];
 777              }
 778          }
 779  
 780          return $ret;
 781      }
 782  
 783      /**
 784       * Check if given policies can be accepted by the current user (eventually on behalf of the other user)
 785       *
 786       * Currently, the version ids are not relevant and the check is based on permissions only. In the future, additional
 787       * conditions can be added (such as policies applying to certain users only).
 788       *
 789       * @param array $versionids int[] List of policy version ids to check
 790       * @param int $userid Accepting policies on this user's behalf (defaults to accepting on self)
 791       * @param bool $throwexception Throw exception instead of returning false
 792       * @return bool
 793       */
 794      public static function can_accept_policies(array $versionids, $userid = null, $throwexception = false) {
 795          global $USER;
 796  
 797          if (!isloggedin() || isguestuser()) {
 798              if ($throwexception) {
 799                  throw new \moodle_exception('noguest');
 800              } else {
 801                  return false;
 802              }
 803          }
 804  
 805          if (!$userid) {
 806              $userid = $USER->id;
 807          }
 808  
 809          if ($userid == $USER->id && !manager::is_loggedinas()) {
 810              if ($throwexception) {
 811                  require_capability('tool/policy:accept', context_system::instance());
 812                  return;
 813              } else {
 814                  return has_capability('tool/policy:accept', context_system::instance());
 815              }
 816          }
 817  
 818          // Check capability to accept on behalf as the real user.
 819          $realuser = manager::get_realuser();
 820          $usercontext = \context_user::instance($userid);
 821          if ($throwexception) {
 822              require_capability('tool/policy:acceptbehalf', $usercontext, $realuser);
 823              return;
 824          } else {
 825              return has_capability('tool/policy:acceptbehalf', $usercontext, $realuser);
 826          }
 827      }
 828  
 829      /**
 830       * Check if given policies can be declined by the current user (eventually on behalf of the other user)
 831       *
 832       * Only optional policies can be declined. Otherwise, the permissions are same as for accepting policies.
 833       *
 834       * @param array $versionids int[] List of policy version ids to check
 835       * @param int $userid Declining policies on this user's behalf (defaults to declining by self)
 836       * @param bool $throwexception Throw exception instead of returning false
 837       * @return bool
 838       */
 839      public static function can_decline_policies(array $versionids, $userid = null, $throwexception = false) {
 840  
 841          foreach ($versionids as $versionid) {
 842              if (static::get_agreement_optional($versionid) == policy_version::AGREEMENT_COMPULSORY) {
 843                  // Compulsory policies can't be declined (that is what makes them compulsory).
 844                  if ($throwexception) {
 845                      throw new \moodle_exception('errorpolicyversioncompulsory', 'tool_policy');
 846                  } else {
 847                      return false;
 848                  }
 849              }
 850          }
 851  
 852          return static::can_accept_policies($versionids, $userid, $throwexception);
 853      }
 854  
 855      /**
 856       * Check if acceptances to given policies can be revoked by the current user (eventually on behalf of the other user)
 857       *
 858       * Revoking optional policies is controlled by the same rules as declining them. Compulsory policies can be revoked
 859       * only by users with the permission to accept policies on other's behalf. The reasoning behind this is to make sure
 860       * the user communicates with the site's privacy officer and is well aware of all consequences of the decision (such
 861       * as losing right to access the site).
 862       *
 863       * @param array $versionids int[] List of policy version ids to check
 864       * @param int $userid Revoking policies on this user's behalf (defaults to revoking by self)
 865       * @param bool $throwexception Throw exception instead of returning false
 866       * @return bool
 867       */
 868      public static function can_revoke_policies(array $versionids, $userid = null, $throwexception = false) {
 869          global $USER;
 870  
 871          // Guests' acceptance is not stored so there is nothing to revoke.
 872          if (!isloggedin() || isguestuser()) {
 873              if ($throwexception) {
 874                  throw new \moodle_exception('noguest');
 875              } else {
 876                  return false;
 877              }
 878          }
 879  
 880          // Sort policies into two sets according the optional flag.
 881          $compulsory = [];
 882          $optional = [];
 883  
 884          foreach ($versionids as $versionid) {
 885              $agreementoptional = static::get_agreement_optional($versionid);
 886              if ($agreementoptional == policy_version::AGREEMENT_COMPULSORY) {
 887                  $compulsory[] = $versionid;
 888              } else if ($agreementoptional == policy_version::AGREEMENT_OPTIONAL) {
 889                  $optional[] = $versionid;
 890              } else {
 891                  throw new \coding_exception('Unexpected optional flag value');
 892              }
 893          }
 894  
 895          // Check if the user can revoke the optional policies from the list.
 896          if ($optional) {
 897              if (!static::can_decline_policies($optional, $userid, $throwexception)) {
 898                  return false;
 899              }
 900          }
 901  
 902          // Check if the user can revoke the compulsory policies from the list.
 903          if ($compulsory) {
 904              if (!$userid) {
 905                  $userid = $USER->id;
 906              }
 907  
 908              $realuser = manager::get_realuser();
 909              $usercontext = \context_user::instance($userid);
 910              if ($throwexception) {
 911                  require_capability('tool/policy:acceptbehalf', $usercontext, $realuser);
 912                  return;
 913              } else {
 914                  return has_capability('tool/policy:acceptbehalf', $usercontext, $realuser);
 915              }
 916          }
 917  
 918          return true;
 919      }
 920  
 921      /**
 922       * Mark the given policy versions as accepted by the user.
 923       *
 924       * @param array|int $policyversionid Policy version id(s) to set acceptance status for.
 925       * @param int|null $userid Id of the user accepting the policy version, defaults to the current one.
 926       * @param string|null $note Note to be recorded.
 927       * @param string|null $lang Language in which the policy was shown, defaults to the current one.
 928       */
 929      public static function accept_policies($policyversionid, $userid = null, $note = null, $lang = null) {
 930          static::set_acceptances_status($policyversionid, $userid, $note, $lang, 1);
 931      }
 932  
 933      /**
 934       * Mark the given policy versions as declined by the user.
 935       *
 936       * @param array|int $policyversionid Policy version id(s) to set acceptance status for.
 937       * @param int|null $userid Id of the user accepting the policy version, defaults to the current one.
 938       * @param string|null $note Note to be recorded.
 939       * @param string|null $lang Language in which the policy was shown, defaults to the current one.
 940       */
 941      public static function decline_policies($policyversionid, $userid = null, $note = null, $lang = null) {
 942          static::set_acceptances_status($policyversionid, $userid, $note, $lang, 0);
 943      }
 944  
 945      /**
 946       * Mark the given policy versions as accepted or declined by the user.
 947       *
 948       * @param array|int $policyversionid Policy version id(s) to set acceptance status for.
 949       * @param int|null $userid Id of the user accepting the policy version, defaults to the current one.
 950       * @param string|null $note Note to be recorded.
 951       * @param string|null $lang Language in which the policy was shown, defaults to the current one.
 952       * @param int $status The acceptance status, defaults to 1 = accepted
 953       */
 954      protected static function set_acceptances_status($policyversionid, $userid = null, $note = null, $lang = null, $status = 1) {
 955          global $DB, $USER;
 956  
 957          // Validate arguments and capabilities.
 958          if (empty($policyversionid)) {
 959              return;
 960          } else if (!is_array($policyversionid)) {
 961              $policyversionid = [$policyversionid];
 962          }
 963          if (!$userid) {
 964              $userid = $USER->id;
 965          }
 966          self::can_accept_policies([$policyversionid], $userid, true);
 967  
 968          // Retrieve the list of policy versions that need agreement (do not update existing agreements).
 969          list($sql, $params) = $DB->get_in_or_equal($policyversionid, SQL_PARAMS_NAMED);
 970          $sql = "SELECT v.id AS versionid, a.*
 971                    FROM {tool_policy_versions} v
 972               LEFT JOIN {tool_policy_acceptances} a ON a.userid = :userid AND a.policyversionid = v.id
 973                   WHERE v.id $sql AND (a.id IS NULL OR a.status <> :status)";
 974  
 975          $needacceptance = $DB->get_records_sql($sql, $params + [
 976              'userid' => $userid,
 977              'status' => $status,
 978          ]);
 979  
 980          $realuser = manager::get_realuser();
 981          $updatedata = ['status' => $status, 'lang' => $lang ?: current_language(),
 982              'timemodified' => time(), 'usermodified' => $realuser->id, 'note' => $note];
 983          foreach ($needacceptance as $versionid => $currentacceptance) {
 984              unset($currentacceptance->versionid);
 985              if ($currentacceptance->id) {
 986                  $updatedata['id'] = $currentacceptance->id;
 987                  $DB->update_record('tool_policy_acceptances', $updatedata);
 988                  acceptance_updated::create_from_record((object)($updatedata + (array)$currentacceptance))->trigger();
 989              } else {
 990                  $updatedata['timecreated'] = $updatedata['timemodified'];
 991                  $updatedata['policyversionid'] = $versionid;
 992                  $updatedata['userid'] = $userid;
 993                  $updatedata['id'] = $DB->insert_record('tool_policy_acceptances', $updatedata);
 994                  acceptance_created::create_from_record((object)($updatedata + (array)$currentacceptance))->trigger();
 995              }
 996          }
 997  
 998          static::update_policyagreed($userid);
 999      }
1000  
1001      /**
1002       * Make sure that $user->policyagreed matches the agreement to the policies
1003       *
1004       * @param int|stdClass|null $user user to check (null for current user)
1005       */
1006      public static function update_policyagreed($user = null) {
1007          global $DB, $USER, $CFG;
1008          require_once($CFG->dirroot.'/user/lib.php');
1009  
1010          if (!$user || (is_numeric($user) && $user == $USER->id)) {
1011              $user = $USER;
1012          } else if (!is_object($user)) {
1013              $user = $DB->get_record('user', ['id' => $user], 'id, policyagreed');
1014          }
1015  
1016          $sql = "SELECT d.id, v.optional, a.status
1017                    FROM {tool_policy} d
1018              INNER JOIN {tool_policy_versions} v ON v.policyid = d.id AND v.id = d.currentversionid
1019               LEFT JOIN {tool_policy_acceptances} a ON a.userid = :userid AND a.policyversionid = v.id
1020                   WHERE (v.audience = :audience OR v.audience = :audienceall)";
1021  
1022          $params = [
1023              'audience' => policy_version::AUDIENCE_LOGGEDIN,
1024              'audienceall' => policy_version::AUDIENCE_ALL,
1025              'userid' => $user->id
1026          ];
1027  
1028          $allresponded = true;
1029          foreach ($DB->get_records_sql($sql, $params) as $policyacceptance) {
1030              if ($policyacceptance->optional == policy_version::AGREEMENT_COMPULSORY && empty($policyacceptance->status)) {
1031                  $allresponded = false;
1032              } else if ($policyacceptance->optional == policy_version::AGREEMENT_OPTIONAL && $policyacceptance->status === null) {
1033                  $allresponded = false;
1034              }
1035          }
1036  
1037          if ($user->policyagreed != $allresponded) {
1038              $user->policyagreed = $allresponded;
1039              $DB->set_field('user', 'policyagreed', $allresponded, ['id' => $user->id]);
1040          }
1041      }
1042  
1043      /**
1044       * May be used to revert accidentally granted acceptance for another user
1045       *
1046       * @param int $policyversionid
1047       * @param int $userid
1048       * @param null $note
1049       */
1050      public static function revoke_acceptance($policyversionid, $userid, $note = null) {
1051          global $DB, $USER;
1052          if (!$userid) {
1053              $userid = $USER->id;
1054          }
1055          self::can_accept_policies([$policyversionid], $userid, true);
1056  
1057          if ($currentacceptance = $DB->get_record('tool_policy_acceptances',
1058                  ['policyversionid' => $policyversionid, 'userid' => $userid])) {
1059              $realuser = manager::get_realuser();
1060              $updatedata = ['id' => $currentacceptance->id, 'status' => 0, 'timemodified' => time(),
1061                  'usermodified' => $realuser->id, 'note' => $note];
1062              $DB->update_record('tool_policy_acceptances', $updatedata);
1063              acceptance_updated::create_from_record((object)($updatedata + (array)$currentacceptance))->trigger();
1064          }
1065  
1066          static::update_policyagreed($userid);
1067      }
1068  
1069      /**
1070       * Create user policy acceptances when the user is created.
1071       *
1072       * @param \core\event\user_created $event
1073       */
1074      public static function create_acceptances_user_created(\core\event\user_created $event) {
1075          global $USER, $CFG, $DB;
1076  
1077          // Do nothing if not set as the site policies handler.
1078          if (empty($CFG->sitepolicyhandler) || $CFG->sitepolicyhandler !== 'tool_policy') {
1079              return;
1080          }
1081  
1082          $userid = $event->objectid;
1083          $lang = current_language();
1084          $user = $event->get_record_snapshot('user', $userid);
1085          // Do nothing if the user has not accepted the current policies.
1086          if (!$user->policyagreed) {
1087              return;
1088          }
1089  
1090          // Cleanup our bits in the presignup cache (we can not rely on them at this stage any more anyway).
1091          $cache = \cache::make('core', 'presignup');
1092          $cache->delete('tool_policy_userpolicyagreed');
1093          $cache->delete('tool_policy_viewedpolicies');
1094          $cache->delete('tool_policy_policyversionidsagreed');
1095  
1096          // Mark all compulsory policies as implicitly accepted during the signup.
1097          if ($policyversions = static::list_current_versions(policy_version::AUDIENCE_LOGGEDIN)) {
1098              $acceptances = array();
1099              $now = time();
1100              foreach ($policyversions as $policyversion) {
1101                  if ($policyversion->optional == policy_version::AGREEMENT_OPTIONAL) {
1102                      continue;
1103                  }
1104                  $acceptances[] = array(
1105                      'policyversionid' => $policyversion->id,
1106                      'userid' => $userid,
1107                      'status' => 1,
1108                      'lang' => $lang,
1109                      'usermodified' => isset($USER->id) ? $USER->id : 0,
1110                      'timecreated' => $now,
1111                      'timemodified' => $now,
1112                  );
1113              }
1114              $DB->insert_records('tool_policy_acceptances', $acceptances);
1115          }
1116  
1117          static::update_policyagreed($userid);
1118      }
1119  
1120      /**
1121       * Returns the value of the optional flag for the given policy version.
1122       *
1123       * Optimised for being called multiple times by making use of a request cache. The cache is normally populated as a
1124       * side effect of calling {@link self::list_policies()} and in most cases should be warm enough for hits.
1125       *
1126       * @param int $versionid
1127       * @return int policy_version::AGREEMENT_COMPULSORY | policy_version::AGREEMENT_OPTIONAL
1128       */
1129      public static function get_agreement_optional($versionid) {
1130          global $DB;
1131  
1132          $optcache = \cache::make('tool_policy', 'policy_optional');
1133  
1134          $hit = $optcache->get($versionid);
1135  
1136          if ($hit === false) {
1137              $flags = $DB->get_records_menu('tool_policy_versions', null, '', 'id, optional');
1138              $optcache->set_many($flags);
1139              $hit = $flags[$versionid];
1140          }
1141  
1142          return $hit;
1143      }
1144  }