Search moodle.org's
Developer Documentation

  • 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 37 and 311] [Versions 38 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  }