MDL-62342 privacy: Use singular/plural form in labels
[moodle.git] / admin / tool / policy / classes / api.php
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/>.
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  */
26 namespace tool_policy;
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;
38 defined('MOODLE_INTERNAL') || die();
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 {
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) {
56         $current = [];
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         }
68         return $current;
69     }
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     }
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;
101         $versionfields = policy_version::get_sql_fields('v', 'v_');
103         $sql = "SELECT d.id, d.currentversionid, d.sortorder, $versionfields ";
105         if ($countacceptances) {
106             $sql .= ", COALESCE(ua.acceptancescount, 0) AS acceptancescount ";
107         }
109         $sql .= " FROM {tool_policy} d
110              LEFT JOIN {tool_policy_versions} v ON v.policyid = d.id ";
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         }
120         $sql .= " WHERE v.id IS NOT NULL ";
122         $params = [];
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         }
130         $sql .= " ORDER BY d.sortorder ASC, v.timecreated DESC";
132         $policies = [];
133         $versions = [];
134         $rs = $DB->get_recordset_sql($sql, $params);
136         foreach ($rs as $r) {
137             if (!isset($policies[$r->id])) {
138                 $policies[$r->id] = (object) [
139                     'id' => $r->id,
140                     'currentversionid' => $r->currentversionid,
141                     'sortorder' => $r->sortorder,
142                 ];
143             }
145             $versiondata = policy_version::extract_record($r, 'v_');
147             if ($countacceptances && $versiondata->audience != policy_version::AUDIENCE_GUESTS) {
148                 $versiondata->acceptancescount = $r->acceptancescount;
149             }
151             $versions[$r->id][$versiondata->id] = $versiondata;
152         }
154         $rs->close();
156         foreach (array_keys($policies) as $policyid) {
157             static::fix_revision_values($versions[$policyid]);
158         }
160         $return = [];
161         $context = context_system::instance();
162         $output = $PAGE->get_renderer('tool_policy');
164         foreach ($policies as $policyid => $policydata) {
165             $versionexporters = [];
166             foreach ($versions[$policyid] as $versiondata) {
167                 if ($policydata->currentversionid == $versiondata->id) {
168                     $versiondata->status = policy_version::STATUS_ACTIVE;
169                 } else if ($versiondata->archived) {
170                     $versiondata->status = policy_version::STATUS_ARCHIVED;
171                 } else {
172                     $versiondata->status = policy_version::STATUS_DRAFT;
173                 }
174                 $versionexporters[] = new policy_version_exporter($versiondata, [
175                     'context' => $context,
176                 ]);
177             }
178             $policyexporter = new policy_exporter($policydata, [
179                 'versions' => $versionexporters,
180             ]);
181             $return[] = $policyexporter->export($output);
182         }
184         return $return;
185     }
187     /**
188      * Returns total number of users who are expected to accept site policy
189      *
190      * @return int|null
191      */
192     public static function count_total_users() {
193         global $DB, $CFG;
194         static $cached = null;
195         if ($cached === null) {
196             $cached = $DB->count_records_select('user', 'deleted = 0 AND id <> ?', [$CFG->siteguest]);
197         }
198         return $cached;
199     }
201     /**
202      * Load a particular policy document version.
203      *
204      * @param int $versionid ID of the policy document version.
205      * @param array $policies cached result of self::list_policies() in case this function needs to be called in a loop
206      * @return stdClass - exported {@link tool_policy\policy_exporter} instance
207      */
208     public static function get_policy_version($versionid, $policies = null) {
209         if ($policies === null) {
210             $policies = self::list_policies();
211         }
212         foreach ($policies as $policy) {
213             if ($policy->currentversionid == $versionid) {
214                 return $policy->currentversion;
216             } else {
217                 foreach ($policy->draftversions as $draft) {
218                     if ($draft->id == $versionid) {
219                         return $draft;
220                     }
221                 }
223                 foreach ($policy->archivedversions as $archived) {
224                     if ($archived->id == $versionid) {
225                         return $archived;
226                     }
227                 }
228             }
229         }
231         throw new \moodle_exception('errorpolicyversionnotfound', 'tool_policy');
232     }
234     /**
235      * Make sure that each version has a unique revision value.
236      *
237      * Empty value are replaced with a timecreated date. Duplicates are suffixed with v1, v2, v3, ... etc.
238      *
239      * @param array $versions List of objects with id, timecreated and revision properties
240      */
241     public static function fix_revision_values(array $versions) {
243         $byrev = [];
245         foreach ($versions as $version) {
246             if ($version->revision === '') {
247                 $version->revision = userdate($version->timecreated, get_string('strftimedate', 'core_langconfig'));
248             }
249             $byrev[$version->revision][$version->id] = true;
250         }
252         foreach ($byrev as $origrevision => $versionids) {
253             $cnt = count($byrev[$origrevision]);
254             if ($cnt > 1) {
255                 foreach ($versionids as $versionid => $unused) {
256                     foreach ($versions as $version) {
257                         if ($version->id == $versionid) {
258                             $version->revision = $version->revision.' - v'.$cnt;
259                             $cnt--;
260                             break;
261                         }
262                     }
263                 }
264             }
265         }
266     }
268     /**
269      * Can the user view the given policy version document?
270      *
271      * @param stdClass $policy - exported {@link tool_policy\policy_exporter} instance
272      * @param int $behalfid The id of user on whose behalf the user is viewing the policy
273      * @param int $userid The user whom access is evaluated, defaults to the current one
274      * @return bool
275      */
276     public static function can_user_view_policy_version($policy, $behalfid = null, $userid = null) {
277         global $USER;
279         if ($policy->status == policy_version::STATUS_ACTIVE) {
280             return true;
281         }
283         if (empty($userid)) {
284             $userid = $USER->id;
285         }
287         // Check if the user is viewing the policy on someone else's behalf.
288         // Typical scenario is a parent viewing the policy on behalf of her child.
289         if ($behalfid > 0) {
290             $behalfcontext = context_user::instance($behalfid);
292             if ($behalfid != $userid && !has_capability('tool/policy:acceptbehalf', $behalfcontext, $userid)) {
293                 return false;
294             }
296             // Check that the other user (e.g. the child) has access to the policy.
297             // Pass a negative third parameter to avoid eventual endless loop.
298             // We do not support grand-parent relations.
299             return static::can_user_view_policy_version($policy, -1, $behalfid);
300         }
302         // Users who can manage policies, can see all versions.
303         if (has_capability('tool/policy:managedocs', context_system::instance(), $userid)) {
304             return true;
305         }
307         // User who can see all acceptances, must be also allowed to see what was accepted.
308         if (has_capability('tool/policy:viewacceptances', context_system::instance(), $userid)) {
309             return true;
310         }
312         // Users have access to all the policies they have ever accepted.
313         if (static::is_user_version_accepted($userid, $policy->id)) {
314             return true;
315         }
317         // Check if the user could get access through some of her minors.
318         if ($behalfid === null) {
319             foreach (static::get_user_minors($userid) as $minor) {
320                 if (static::can_user_view_policy_version($policy, $minor->id, $userid)) {
321                     return true;
322                 }
323             }
324         }
326         return false;
327     }
329     /**
330      * Return the user's minors - other users on which behalf we can accept policies.
331      *
332      * Returned objects contain all the standard user name and picture fields as well as the context instanceid.
333      *
334      * @param int $userid The id if the user with parental responsibility
335      * @param array $extrafields Extra fields to be included in result
336      * @return array of objects
337      */
338     public static function get_user_minors($userid, array $extrafields = null) {
339         global $DB;
341         $ctxfields = context_helper::get_preload_record_columns_sql('c');
342         $namefields = get_all_user_name_fields(true, 'u');
343         $pixfields = user_picture::fields('u', $extrafields);
345         $sql = "SELECT $ctxfields, $namefields, $pixfields
346                   FROM {role_assignments} ra
347                   JOIN {context} c ON c.contextlevel = ".CONTEXT_USER." AND ra.contextid = c.id
348                   JOIN {user} u ON c.instanceid = u.id
349                  WHERE ra.userid = ?
350               ORDER BY u.lastname ASC, u.firstname ASC";
352         $rs = $DB->get_recordset_sql($sql, [$userid]);
354         $minors = [];
356         foreach ($rs as $record) {
357             context_helper::preload_from_record($record);
358             $childcontext = context_user::instance($record->id);
359             if (has_capability('tool/policy:acceptbehalf', $childcontext, $userid)) {
360                 $minors[$record->id] = $record;
361             }
362         }
364         $rs->close();
366         return $minors;
367     }
369     /**
370      * Prepare data for the {@link \tool_policy\form\policydoc} form.
371      *
372      * @param \tool_policy\policy_version $version persistent representing the version.
373      * @return stdClass form data
374      */
375     public static function form_policydoc_data(policy_version $version) {
377         $data = $version->to_record();
378         $summaryfieldoptions = static::policy_summary_field_options();
379         $contentfieldoptions = static::policy_content_field_options();
381         if (empty($data->id)) {
382             // Adding a new version of a policy document.
383             $data = file_prepare_standard_editor($data, 'summary', $summaryfieldoptions, $summaryfieldoptions['context']);
384             $data = file_prepare_standard_editor($data, 'content', $contentfieldoptions, $contentfieldoptions['context']);
386         } else {
387             // Editing an existing policy document version.
388             $data = file_prepare_standard_editor($data, 'summary', $summaryfieldoptions, $summaryfieldoptions['context'],
389                 'tool_policy', 'policydocumentsummary', $data->id);
390             $data = file_prepare_standard_editor($data, 'content', $contentfieldoptions, $contentfieldoptions['context'],
391                 'tool_policy', 'policydocumentcontent', $data->id);
392         }
394         return $data;
395     }
397     /**
398      * Save the data from the policydoc form as a new policy document.
399      *
400      * @param stdClass $form data submitted from the {@link \tool_policy\form\policydoc} form.
401      * @return \tool_policy\policy_version persistent
402      */
403     public static function form_policydoc_add(stdClass $form) {
404         global $DB;
406         $form = clone($form);
408         $form->policyid = $DB->insert_record('tool_policy', (object) [
409             'sortorder' => 999,
410         ]);
412         static::distribute_policy_document_sortorder();
414         return static::form_policydoc_update_new($form);
415     }
417     /**
418      * Save the data from the policydoc form as a new policy document version.
419      *
420      * @param stdClass $form data submitted from the {@link \tool_policy\form\policydoc} form.
421      * @return \tool_policy\policy_version persistent
422      */
423     public static function form_policydoc_update_new(stdClass $form) {
424         global $DB;
426         if (empty($form->policyid)) {
427             throw new coding_exception('Invalid policy document ID');
428         }
430         $form = clone($form);
432         $form->id = $DB->insert_record('tool_policy_versions', (new policy_version(0, (object) [
433             'timecreated' => time(),
434             'policyid' => $form->policyid,
435         ]))->to_record());
437         return static::form_policydoc_update_overwrite($form);
438     }
441     /**
442      * Save the data from the policydoc form, overwriting the existing policy document version.
443      *
444      * @param stdClass $form data submitted from the {@link \tool_policy\form\policydoc} form.
445      * @return \tool_policy\policy_version persistent
446      */
447     public static function form_policydoc_update_overwrite(stdClass $form) {
449         $form = clone($form);
450         unset($form->timecreated);
452         $summaryfieldoptions = static::policy_summary_field_options();
453         $form = file_postupdate_standard_editor($form, 'summary', $summaryfieldoptions, $summaryfieldoptions['context'],
454             'tool_policy', 'policydocumentsummary', $form->id);
455         unset($form->summary_editor);
456         unset($form->summarytrust);
458         $contentfieldoptions = static::policy_content_field_options();
459         $form = file_postupdate_standard_editor($form, 'content', $contentfieldoptions, $contentfieldoptions['context'],
460             'tool_policy', 'policydocumentcontent', $form->id);
461         unset($form->content_editor);
462         unset($form->contenttrust);
464         unset($form->status);
465         unset($form->save);
466         unset($form->saveasdraft);
467         unset($form->minorchange);
469         $policyversion = new policy_version($form->id, $form);
470         $policyversion->update();
472         return $policyversion;
473     }
475     /**
476      * Make the given version the current active one.
477      *
478      * @param int $versionid
479      */
480     public static function make_current($versionid) {
481         global $DB, $USER;
483         $policyversion = new policy_version($versionid);
484         if (! $policyversion->get('id') || $policyversion->get('archived')) {
485             throw new coding_exception('Version not found or is archived');
486         }
488         // Archive current version of this policy.
489         if ($currentversionid = $DB->get_field('tool_policy', 'currentversionid', ['id' => $policyversion->get('policyid')])) {
490             if ($currentversionid == $versionid) {
491                 // Already current, do not change anything.
492                 return;
493             }
494             $DB->set_field('tool_policy_versions', 'archived', 1, ['id' => $currentversionid]);
495         }
497         // Set given version as current.
498         $DB->set_field('tool_policy', 'currentversionid', $policyversion->get('id'), ['id' => $policyversion->get('policyid')]);
500         // Reset the policyagreed flag to force everybody re-accept the policies.
501         $DB->set_field('user', 'policyagreed', 0);
503         // Make sure that the current user is not immediately redirected to the policy acceptance page.
504         if (isloggedin() && !isguestuser()) {
505             $USER->policyagreed = 1;
506         }
507     }
509     /**
510      * Inactivate the policy document - no version marked as current and the document does not apply.
511      *
512      * @param int $policyid
513      */
514     public static function inactivate($policyid) {
515         global $DB;
517         if ($currentversionid = $DB->get_field('tool_policy', 'currentversionid', ['id' => $policyid])) {
518             // Archive the current version.
519             $DB->set_field('tool_policy_versions', 'archived', 1, ['id' => $currentversionid]);
520             // Unset current version for the policy.
521             $DB->set_field('tool_policy', 'currentversionid', null, ['id' => $policyid]);
522         }
523     }
525     /**
526      * Create a new draft policy document from an archived version.
527      *
528      * @param int $versionid
529      * @return \tool_policy\policy_version persistent
530      */
531     public static function revert_to_draft($versionid) {
532         $policyversion = new policy_version($versionid);
533         if (!$policyversion->get('id') || !$policyversion->get('archived')) {
534             throw new coding_exception('Version not found or is not archived');
535         }
537         $formdata = static::form_policydoc_data($policyversion);
538         // Unarchived the new version.
539         $formdata->archived = 0;
540         return static::form_policydoc_update_new($formdata);
541     }
543     /**
544      * Can the current version be deleted
545      *
546      * @param stdClass $version object describing version, contains fields policyid, id, status, archived, audience, ...
547      */
548     public static function can_delete_version($version) {
549         // TODO MDL-61900 allow to delete not only draft versions.
550         return has_capability('tool/policy:managedocs', context_system::instance()) &&
551                 $version->status == policy_version::STATUS_DRAFT;
552     }
554     /**
555      * Delete the given version (if it is a draft). Also delete policy if this is the only version.
556      *
557      * @param int $versionid
558      */
559     public static function delete($versionid) {
560         global $DB;
562         $version = static::get_policy_version($versionid);
563         if (!self::can_delete_version($version)) {
564             // Current version can not be deleted.
565             return;
566         }
568         $DB->delete_records('tool_policy_versions', ['id' => $versionid]);
570         if (!$DB->record_exists('tool_policy_versions', ['policyid' => $version->policyid])) {
571             // This is a single version in a policy. Delete the policy.
572             $DB->delete_records('tool_policy', ['id' => $version->policyid]);
573         }
574     }
576     /**
577      * Editor field options for the policy summary text.
578      *
579      * @return array
580      */
581     public static function policy_summary_field_options() {
582         global $CFG;
583         require_once($CFG->libdir.'/formslib.php');
585         return [
586             'subdirs' => false,
587             'maxfiles' => -1,
588             'context' => context_system::instance(),
589         ];
590     }
592     /**
593      * Editor field options for the policy content text.
594      *
595      * @return array
596      */
597     public static function policy_content_field_options() {
598         global $CFG;
599         require_once($CFG->libdir.'/formslib.php');
601         return [
602             'subdirs' => false,
603             'maxfiles' => -1,
604             'context' => context_system::instance(),
605         ];
606     }
608     /**
609      * Re-sets the sortorder field of the policy documents to even values.
610      */
611     protected static function distribute_policy_document_sortorder() {
612         global $DB;
614         $sql = "SELECT p.id, p.sortorder, MAX(v.timecreated) AS timerecentcreated
615                   FROM {tool_policy} p
616              LEFT JOIN {tool_policy_versions} v ON v.policyid = p.id
617               GROUP BY p.id, p.sortorder
618               ORDER BY p.sortorder ASC, timerecentcreated ASC";
620         $rs = $DB->get_recordset_sql($sql);
621         $sortorder = 10;
623         foreach ($rs as $record) {
624             if ($record->sortorder != $sortorder) {
625                 $DB->set_field('tool_policy', 'sortorder', $sortorder, ['id' => $record->id]);
626             }
627             $sortorder = $sortorder + 2;
628         }
630         $rs->close();
631     }
633     /**
634      * Change the policy document's sortorder.
635      *
636      * @param int $policyid
637      * @param int $step
638      */
639     protected static function move_policy_document($policyid, $step) {
640         global $DB;
642         $sortorder = $DB->get_field('tool_policy', 'sortorder', ['id' => $policyid], MUST_EXIST);
643         $DB->set_field('tool_policy', 'sortorder', $sortorder + $step, ['id' => $policyid]);
644         static::distribute_policy_document_sortorder();
645     }
647     /**
648      * Move the given policy document up in the list.
649      *
650      * @param id $policyid
651      */
652     public static function move_up($policyid) {
653         static::move_policy_document($policyid, -3);
654     }
656     /**
657      * Move the given policy document down in the list.
658      *
659      * @param id $policyid
660      */
661     public static function move_down($policyid) {
662         static::move_policy_document($policyid, 3);
663     }
665     /**
666      * Returns list of acceptances for this user.
667      *
668      * @param int $userid id of a user.
669      * @param int|array $versions list of policy versions.
670      * @return array list of acceptances indexed by versionid.
671      */
672     public static function get_user_acceptances($userid, $versions = null) {
673         global $DB;
675         list($vsql, $vparams) = ['', []];
676         if (!empty($versions)) {
677             list($vsql, $vparams) = $DB->get_in_or_equal($versions, SQL_PARAMS_NAMED, 'ver');
678             $vsql = ' AND a.policyversionid ' . $vsql;
679         }
681         $userfieldsmod = get_all_user_name_fields(true, 'm', null, 'mod');
682         $sql = "SELECT u.id AS mainuserid, a.policyversionid, a.status, a.lang, a.timemodified, a.usermodified, a.note,
683                   u.policyagreed, $userfieldsmod
684                   FROM {user} u
685                   INNER JOIN {tool_policy_acceptances} a ON a.userid = u.id AND a.userid = :userid $vsql
686                   LEFT JOIN {user} m ON m.id = a.usermodified";
687         $params = ['userid' => $userid];
688         $result = $DB->get_recordset_sql($sql, $params + $vparams);
690         $acceptances = [];
691         foreach ($result as $row) {
692             if (!empty($row->policyversionid)) {
693                 $acceptances[$row->policyversionid] = $row;
694             }
695         }
696         $result->close();
698         return $acceptances;
699     }
701     /**
702      * Returns version acceptance for this user.
703      *
704      * @param int $userid User identifier.
705      * @param int $versionid Policy version identifier.
706      * @param array|null $acceptances List of policy version acceptances indexed by versionid.
707      * @return stdClass|null Acceptance object if the user has ever accepted this version or null if not.
708      */
709     public static function get_user_version_acceptance($userid, $versionid, $acceptances = null) {
710         if (empty($acceptances)) {
711             $acceptances = static::get_user_acceptances($userid, $versionid);
712         }
713         if (array_key_exists($versionid, $acceptances)) {
714             // The policy version has ever been accepted.
715             return $acceptances[$versionid];
716         }
718         return null;
719     }
721     /**
722      * Returns version acceptance for this user.
723      *
724      * @param int $userid User identifier.
725      * @param int $versionid Policy version identifier.
726      * @param array|null $acceptances Iist of policy version acceptances indexed by versionid.
727      * @return bool True if this user has accepted this policy version; false otherwise.
728      */
729     public static function is_user_version_accepted($userid, $versionid, $acceptances = null) {
730         $acceptance = static::get_user_version_acceptance($userid, $versionid, $acceptances);
731         if (!empty($acceptance)) {
732             return $acceptance->status;
733         }
735         return false;
736     }
738     /**
739      * Get the list of policies and versions that current user is able to see and the respective acceptance records for
740      * the selected user.
741      *
742      * @param int $userid
743      * @return array array with the same structure that list_policies() returns with additional attribute acceptance for versions
744      */
745     public static function get_policies_with_acceptances($userid) {
746         // Get the list of policies and versions that current user is able to see
747         // and the respective acceptance records for the selected user.
748         $policies = static::list_policies();
749         $acceptances = static::get_user_acceptances($userid);
750         $ret = [];
751         foreach ($policies as $policy) {
752             $versions = [];
753             if ($policy->currentversion && $policy->currentversion->audience != policy_version::AUDIENCE_GUESTS) {
754                 if (isset($acceptances[$policy->currentversion->id])) {
755                     $policy->currentversion->acceptance = $acceptances[$policy->currentversion->id];
756                 } else {
757                     $policy->currentversion->acceptance = 0;
758                 }
759                 $versions[] = $policy->currentversion;
760             }
761             foreach ($policy->archivedversions as $version) {
762                 if ($version->audience != policy_version::AUDIENCE_GUESTS
763                         && static::can_user_view_policy_version($version, $userid)) {
764                     $version->acceptance = isset($acceptances[$version->id]) ? $acceptances[$version->id] : 0;
765                     $versions[] = $version;
766                 }
767             }
768             if ($versions) {
769                 $ret[] = (object)['id' => $policy->id, 'versions' => $versions];
770             }
771         }
773         return $ret;
774     }
776     /**
777      * Checks if user can accept policies for themselves or on behalf of another user
778      *
779      * @param int $userid
780      * @param bool $throwexception
781      * @return bool
782      */
783     public static function can_accept_policies($userid = null, $throwexception = false) {
784         global $USER;
785         if (!isloggedin() || isguestuser()) {
786             if ($throwexception) {
787                 throw new \moodle_exception('noguest');
788             } else {
789                 return false;
790             }
791         }
792         if (!$userid) {
793             $userid = $USER->id;
794         }
796         if ($userid == $USER->id && !manager::is_loggedinas()) {
797             if ($throwexception) {
798                 require_capability('tool/policy:accept', context_system::instance());
799                 return;
800             } else {
801                 return has_capability('tool/policy:accept', context_system::instance());
802             }
803         }
805         // Check capability to accept on behalf as the real user.
806         $realuser = manager::get_realuser();
807         $usercontext = \context_user::instance($userid);
808         if ($throwexception) {
809             require_capability('tool/policy:acceptbehalf', $usercontext, $realuser);
810             return;
811         } else {
812             return has_capability('tool/policy:acceptbehalf', $usercontext, $realuser);
813         }
814     }
816     /**
817      * Checks if user can revoke policies for themselves or on behalf of another user
818      *
819      * @param int $userid
820      * @param bool $throwexception
821      * @return bool
822      */
823     public static function can_revoke_policies($userid = null, $throwexception = false) {
824         global $USER;
826         if (!isloggedin() || isguestuser()) {
827             if ($throwexception) {
828                 throw new \moodle_exception('noguest');
829             } else {
830                 return false;
831             }
832         }
833         if (!$userid) {
834             $userid = $USER->id;
835         }
837         // At the moment, current users can't revoke their own policies.
838         // Check capability to revoke on behalf as the real user.
839         $realuser = manager::get_realuser();
840         $usercontext = \context_user::instance($userid);
841         if ($throwexception) {
842             require_capability('tool/policy:acceptbehalf', $usercontext, $realuser);
843             return;
844         } else {
845             return has_capability('tool/policy:acceptbehalf', $usercontext, $realuser);
846         }
847     }
849     /**
850      * Accepts the current revisions of all policies that the user has not yet accepted
851      *
852      * @param array|int $policyversionid
853      * @param int|null $userid
854      * @param string|null $note
855      * @param string|null $lang
856      */
857     public static function accept_policies($policyversionid, $userid = null, $note = null, $lang = null) {
858         global $DB, $USER;
859         // Validate arguments and capabilities.
860         if (empty($policyversionid)) {
861             return;
862         } else if (!is_array($policyversionid)) {
863             $policyversionid = [$policyversionid];
864         }
865         if (!$userid) {
866             $userid = $USER->id;
867         }
868         self::can_accept_policies($userid, true);
870         // Retrieve the list of policy versions that need agreement (do not update existing agreements).
871         list($sql, $params) = $DB->get_in_or_equal($policyversionid, SQL_PARAMS_NAMED);
872         $sql = "SELECT v.id AS versionid, a.*
873                   FROM {tool_policy_versions} v
874                   LEFT JOIN {tool_policy_acceptances} a ON a.userid = :userid AND a.policyversionid = v.id
875                   WHERE (a.id IS NULL or a.status <> 1) AND v.id " . $sql;
876         $needacceptance = $DB->get_records_sql($sql, ['userid' => $userid] + $params);
878         $realuser = manager::get_realuser();
879         $updatedata = ['status' => 1, 'lang' => $lang ?: current_language(),
880             'timemodified' => time(), 'usermodified' => $realuser->id, 'note' => $note];
881         foreach ($needacceptance as $versionid => $currentacceptance) {
882             unset($currentacceptance->versionid);
883             if ($currentacceptance->id) {
884                 $updatedata['id'] = $currentacceptance->id;
885                 $DB->update_record('tool_policy_acceptances', $updatedata);
886                 acceptance_updated::create_from_record((object)($updatedata + (array)$currentacceptance))->trigger();
887             } else {
888                 $updatedata['timecreated'] = $updatedata['timemodified'];
889                 $updatedata['policyversionid'] = $versionid;
890                 $updatedata['userid'] = $userid;
891                 $updatedata['id'] = $DB->insert_record('tool_policy_acceptances', $updatedata);
892                 acceptance_created::create_from_record((object)($updatedata + (array)$currentacceptance))->trigger();
893             }
894         }
896         static::update_policyagreed($userid);
897     }
899     /**
900      * Make sure that $user->policyagreed matches the agreement to the policies
901      *
902      * @param int|stdClass|null $user user to check (null for current user)
903      */
904     public static function update_policyagreed($user = null) {
905         global $DB, $USER, $CFG;
906         require_once($CFG->dirroot.'/user/lib.php');
908         if (!$user || (is_numeric($user) && $user == $USER->id)) {
909             $user = $USER;
910         } else if (!is_object($user)) {
911             $user = $DB->get_record('user', ['id' => $user], 'id, policyagreed');
912         }
914         $sql = "SELECT d.id, a.status
915                   FROM {tool_policy} d
916                   INNER JOIN {tool_policy_versions} v ON v.policyid = d.id AND v.id = d.currentversionid
917                   LEFT JOIN {tool_policy_acceptances} a ON a.userid = :userid AND a.policyversionid = v.id
918                   WHERE (v.audience = :audience OR v.audience = :audienceall)";
919         $params = [
920             'audience' => policy_version::AUDIENCE_LOGGEDIN,
921             'audienceall' => policy_version::AUDIENCE_ALL,
922             'userid' => $user->id
923         ];
924         $policies = $DB->get_records_sql_menu($sql, $params);
925         $acceptedpolicies = array_filter($policies);
926         $policyagreed = (count($policies) == count($acceptedpolicies)) ? 1 : 0;
928         if ($user->policyagreed != $policyagreed) {
929             $user->policyagreed = $policyagreed;
930             $DB->set_field('user', 'policyagreed', $policyagreed, ['id' => $user->id]);
931         }
932     }
934     /**
935      * May be used to revert accidentally granted acceptance for another user
936      *
937      * @param int $policyversionid
938      * @param int $userid
939      * @param null $note
940      */
941     public static function revoke_acceptance($policyversionid, $userid, $note = null) {
942         global $DB, $USER;
943         if (!$userid) {
944             $userid = $USER->id;
945         }
946         self::can_accept_policies($userid, true);
948         if ($currentacceptance = $DB->get_record('tool_policy_acceptances',
949                 ['policyversionid' => $policyversionid, 'userid' => $userid])) {
950             $realuser = manager::get_realuser();
951             $updatedata = ['id' => $currentacceptance->id, 'status' => 0, 'timemodified' => time(),
952                 'usermodified' => $realuser->id, 'note' => $note];
953             $DB->update_record('tool_policy_acceptances', $updatedata);
954             acceptance_updated::create_from_record((object)($updatedata + (array)$currentacceptance))->trigger();
955         }
957         static::update_policyagreed($userid);
958     }
960     /**
961      * Create user policy acceptances when the user is created.
962      *
963      * @param \core\event\user_created $event
964      */
965     public static function create_acceptances_user_created(\core\event\user_created $event) {
966         global $CFG, $DB;
968         // Do nothing if not set as the site policies handler.
969         if (empty($CFG->sitepolicyhandler) || $CFG->sitepolicyhandler !== 'tool_policy') {
970             return;
971         }
973         $userid = $event->objectid;
974         $lang = current_language();
975         $user = $event->get_record_snapshot('user', $userid);
976         // Do nothing if the user has not accepted the current policies.
977         if (!$user->policyagreed) {
978             return;
979         }
980         // Remove the presignup cache after the user account is created.
981         $cache = \cache::make('core', 'presignup');
982         $cache->delete('tool_policy_userpolicyagreed');
983         $cache->delete('tool_policy_viewedpolicies');
985         // Get all active policies.
986         $currentpolicyversions = static::get_current_versions_ids(policy_version::AUDIENCE_LOGGEDIN);
987         // Save active policies as accepted by the user.
988         if (!empty($currentpolicyversions)) {
989             $acceptances = array();
990             foreach ($currentpolicyversions as $policy) {
991                 $acceptances[] = array(
992                     'policyversionid' => $policy,
993                     'userid' => $userid,
994                     'status' => 1,
995                     'lang' => $lang,
996                     'usermodified' => 0,
997                     'timecreated' => time(),
998                     'timemodified' => time()
999                 );
1000             }
1001             $DB->insert_records('tool_policy_acceptances', $acceptances);
1002         }
1003     }