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
26namespace tool_policy;
27
28use coding_exception;
29use context_helper;
30use context_system;
31use context_user;
32use core\session\manager;
33use stdClass;
34use tool_policy\event\acceptance_created;
35use tool_policy\event\acceptance_updated;
36use user_picture;
37
38defined('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 */
46class 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        $namefields = get_all_user_name_fields(true, 'u');
347        $pixfields = user_picture::fields('u', $extrafields);
348
349        $sql = "SELECT $ctxfields, $namefields, $pixfields
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        $userfieldsmod = get_all_user_name_fields(true, 'm', null, 'mod');
686        $sql = "SELECT u.id AS mainuserid, a.policyversionid, a.status, a.lang, a.timemodified, a.usermodified, a.note,
687                  u.policyagreed, $userfieldsmod
688                  FROM {user} u
689                  INNER JOIN {tool_policy_acceptances} a ON a.userid = u.id AND a.userid = :userid $vsql
690                  LEFT JOIN {user} m ON m.id = a.usermodified";
691        $params = ['userid' => $userid];
692        $result = $DB->get_recordset_sql($sql, $params + $vparams);
693
694        $acceptances = [];
695        foreach ($result as $row) {
696            if (!empty($row->policyversionid)) {
697                $acceptances[$row->policyversionid] = $row;
698            }
699        }
700        $result->close();
701
702        return $acceptances;
703    }
704
705    /**
706     * Returns version acceptance for this user.
707     *
708     * @param int $userid User identifier.
709     * @param int $versionid Policy version identifier.
710     * @param array|null $acceptances List of policy version acceptances indexed by versionid.
711     * @return stdClass|null Acceptance object if the user has ever accepted this version or null if not.
712     */
713    public static function get_user_version_acceptance($userid, $versionid, $acceptances = null) {
714        if (empty($acceptances)) {
715            $acceptances = static::get_user_acceptances($userid, $versionid);
716        }
717        if (array_key_exists($versionid, $acceptances)) {
718            // The policy version has ever been accepted.
719            return $acceptances[$versionid];
720        }
721
722        return null;
723    }
724
725    /**
726     * Did the user accept the given policy version?
727     *
728     * @param int $userid User identifier.
729     * @param int $versionid Policy version identifier.
730     * @param array|null $acceptances Pre-loaded list of policy version acceptances indexed by versionid.
731     * @return bool|null True/false if this user accepted/declined the policy; null otherwise.
732     */
733    public static function is_user_version_accepted($userid, $versionid, $acceptances = null) {
734
735        $acceptance = static::get_user_version_acceptance($userid, $versionid, $acceptances);
736
737        if (!empty($acceptance)) {
738            return (bool) $acceptance->status;
739        }
740
741        return null;
742    }
743
744    /**
745     * Get the list of policies and versions that current user is able to see and the respective acceptance records for
746     * the selected user.
747     *
748     * @param int $userid
749     * @return array array with the same structure that list_policies() returns with additional attribute acceptance for versions
750     */
751    public static function get_policies_with_acceptances($userid) {
752        // Get the list of policies and versions that current user is able to see
753        // and the respective acceptance records for the selected user.
754        $policies = static::list_policies();
755        $acceptances = static::get_user_acceptances($userid);
756        $ret = [];
757        foreach ($policies as $policy) {
758            $versions = [];
759            if ($policy->currentversion && $policy->currentversion->audience != policy_version::AUDIENCE_GUESTS) {
760                if (isset($acceptances[$policy->currentversion->id])) {
761                    $policy->currentversion->acceptance = $acceptances[$policy->currentversion->id];
762                } else {
763                    $policy->currentversion->acceptance = null;
764                }
765                $versions[] = $policy->currentversion;
766            }
767            foreach ($policy->archivedversions as $version) {
768                if ($version->audience != policy_version::AUDIENCE_GUESTS
769                        && static::can_user_view_policy_version($version, $userid)) {
770                    $version->acceptance = isset($acceptances[$version->id]) ? $acceptances[$version->id] : null;
771                    $versions[] = $version;
772                }
773            }
774            if ($versions) {
775                $ret[] = (object)['id' => $policy->id, 'versions' => $versions];
776            }
777        }
778
779        return $ret;
780    }
781
782    /**
783     * Check if given policies can be accepted by the current user (eventually on behalf of the other user)
784     *
785     * Currently, the version ids are not relevant and the check is based on permissions only. In the future, additional
786     * conditions can be added (such as policies applying to certain users only).
787     *
788     * @param array $versionids int[] List of policy version ids to check
789     * @param int $userid Accepting policies on this user's behalf (defaults to accepting on self)
790     * @param bool $throwexception Throw exception instead of returning false
791     * @return bool
792     */
793    public static function can_accept_policies(array $versionids, $userid = null, $throwexception = false) {
794        global $USER;
795
796        if (!isloggedin() || isguestuser()) {
797            if ($throwexception) {
798                throw new \moodle_exception('noguest');
799            } else {
800                return false;
801            }
802        }
803
804        if (!$userid) {
805            $userid = $USER->id;
806        }
807
808        if ($userid == $USER->id && !manager::is_loggedinas()) {
809            if ($throwexception) {
810                require_capability('tool/policy:accept', context_system::instance());
811                return;
812            } else {
813                return has_capability('tool/policy:accept', context_system::instance());
814            }
815        }
816
817        // Check capability to accept on behalf as the real user.
818        $realuser = manager::get_realuser();
819        $usercontext = \context_user::instance($userid);
820        if ($throwexception) {
821            require_capability('tool/policy:acceptbehalf', $usercontext, $realuser);
822            return;
823        } else {
824            return has_capability('tool/policy:acceptbehalf', $usercontext, $realuser);
825        }
826    }
827
828    /**
829     * Check if given policies can be declined by the current user (eventually on behalf of the other user)
830     *
831     * Only optional policies can be declined. Otherwise, the permissions are same as for accepting policies.
832     *
833     * @param array $versionids int[] List of policy version ids to check
834     * @param int $userid Declining policies on this user's behalf (defaults to declining by self)
835     * @param bool $throwexception Throw exception instead of returning false
836     * @return bool
837     */
838    public static function can_decline_policies(array $versionids, $userid = null, $throwexception = false) {
839
840        foreach ($versionids as $versionid) {
841            if (static::get_agreement_optional($versionid) == policy_version::AGREEMENT_COMPULSORY) {
842                // Compulsory policies can't be declined (that is what makes them compulsory).
843                if ($throwexception) {
844                    throw new \moodle_exception('errorpolicyversioncompulsory', 'tool_policy');
845                } else {
846                    return false;
847                }
848            }
849        }
850
851        return static::can_accept_policies($versionids, $userid, $throwexception);
852    }
853
854    /**
855     * Check if acceptances to given policies can be revoked by the current user (eventually on behalf of the other user)
856     *
857     * Revoking optional policies is controlled by the same rules as declining them. Compulsory policies can be revoked
858     * only by users with the permission to accept policies on other's behalf. The reasoning behind this is to make sure
859     * the user communicates with the site's privacy officer and is well aware of all consequences of the decision (such
860     * as losing right to access the site).
861     *
862     * @param array $versionids int[] List of policy version ids to check
863     * @param int $userid Revoking policies on this user's behalf (defaults to revoking by self)
864     * @param bool $throwexception Throw exception instead of returning false
865     * @return bool
866     */
867    public static function can_revoke_policies(array $versionids, $userid = null, $throwexception = false) {
868        global $USER;
869
870        // Guests' acceptance is not stored so there is nothing to revoke.
871        if (!isloggedin() || isguestuser()) {
872            if ($throwexception) {
873                throw new \moodle_exception('noguest');
874            } else {
875                return false;
876            }
877        }
878
879        // Sort policies into two sets according the optional flag.
880        $compulsory = [];
881        $optional = [];
882
883        foreach ($versionids as $versionid) {
884            $agreementoptional = static::get_agreement_optional($versionid);
885            if ($agreementoptional == policy_version::AGREEMENT_COMPULSORY) {
886                $compulsory[] = $versionid;
887            } else if ($agreementoptional == policy_version::AGREEMENT_OPTIONAL) {
888                $optional[] = $versionid;
889            } else {
890                throw new \coding_exception('Unexpected optional flag value');
891            }
892        }
893
894        // Check if the user can revoke the optional policies from the list.
895        if ($optional) {
896            if (!static::can_decline_policies($optional, $userid, $throwexception)) {
897                return false;
898            }
899        }
900
901        // Check if the user can revoke the compulsory policies from the list.
902        if ($compulsory) {
903            if (!$userid) {
904                $userid = $USER->id;
905            }
906
907            $realuser = manager::get_realuser();
908            $usercontext = \context_user::instance($userid);
909            if ($throwexception) {
910                require_capability('tool/policy:acceptbehalf', $usercontext, $realuser);
911                return;
912            } else {
913                return has_capability('tool/policy:acceptbehalf', $usercontext, $realuser);
914            }
915        }
916
917        return true;
918    }
919
920    /**
921     * Mark the given policy versions as accepted by the user.
922     *
923     * @param array|int $policyversionid Policy version id(s) to set acceptance status for.
924     * @param int|null $userid Id of the user accepting the policy version, defaults to the current one.
925     * @param string|null $note Note to be recorded.
926     * @param string|null $lang Language in which the policy was shown, defaults to the current one.
927     */
928    public static function accept_policies($policyversionid, $userid = null, $note = null, $lang = null) {
929        static::set_acceptances_status($policyversionid, $userid, $note, $lang, 1);
930    }
931
932    /**
933     * Mark the given policy versions as declined by the user.
934     *
935     * @param array|int $policyversionid Policy version id(s) to set acceptance status for.
936     * @param int|null $userid Id of the user accepting the policy version, defaults to the current one.
937     * @param string|null $note Note to be recorded.
938     * @param string|null $lang Language in which the policy was shown, defaults to the current one.
939     */
940    public static function decline_policies($policyversionid, $userid = null, $note = null, $lang = null) {
941        static::set_acceptances_status($policyversionid, $userid, $note, $lang, 0);
942    }
943
944    /**
945     * Mark the given policy versions as accepted or declined by the user.
946     *
947     * @param array|int $policyversionid Policy version id(s) to set acceptance status for.
948     * @param int|null $userid Id of the user accepting the policy version, defaults to the current one.
949     * @param string|null $note Note to be recorded.
950     * @param string|null $lang Language in which the policy was shown, defaults to the current one.
951     * @param int $status The acceptance status, defaults to 1 = accepted
952     */
953    protected static function set_acceptances_status($policyversionid, $userid = null, $note = null, $lang = null, $status = 1) {
954        global $DB, $USER;
955
956        // Validate arguments and capabilities.
957        if (empty($policyversionid)) {
958            return;
959        } else if (!is_array($policyversionid)) {
960            $policyversionid = [$policyversionid];
961        }
962        if (!$userid) {
963            $userid = $USER->id;
964        }
965        self::can_accept_policies([$policyversionid], $userid, true);
966
967        // Retrieve the list of policy versions that need agreement (do not update existing agreements).
968        list($sql, $params) = $DB->get_in_or_equal($policyversionid, SQL_PARAMS_NAMED);
969        $sql = "SELECT v.id AS versionid, a.*
970                  FROM {tool_policy_versions} v
971             LEFT JOIN {tool_policy_acceptances} a ON a.userid = :userid AND a.policyversionid = v.id
972                 WHERE v.id $sql AND (a.id IS NULL OR a.status <> :status)";
973
974        $needacceptance = $DB->get_records_sql($sql, $params + [
975            'userid' => $userid,
976            'status' => $status,
977        ]);
978
979        $realuser = manager::get_realuser();
980        $updatedata = ['status' => $status, 'lang' => $lang ?: current_language(),
981            'timemodified' => time(), 'usermodified' => $realuser->id, 'note' => $note];
982        foreach ($needacceptance as $versionid => $currentacceptance) {
983            unset($currentacceptance->versionid);
984            if ($currentacceptance->id) {
985                $updatedata['id'] = $currentacceptance->id;
986                $DB->update_record('tool_policy_acceptances', $updatedata);
987                acceptance_updated::create_from_record((object)($updatedata + (array)$currentacceptance))->trigger();
988            } else {
989                $updatedata['timecreated'] = $updatedata['timemodified'];
990                $updatedata['policyversionid'] = $versionid;
991                $updatedata['userid'] = $userid;
992                $updatedata['id'] = $DB->insert_record('tool_policy_acceptances', $updatedata);
993                acceptance_created::create_from_record((object)($updatedata + (array)$currentacceptance))->trigger();
994            }
995        }
996
997        static::update_policyagreed($userid);
998    }
999
1000    /**
1001     * Make sure that $user->policyagreed matches the agreement to the policies
1002     *
1003     * @param int|stdClass|null $user user to check (null for current user)
1004     */
1005    public static function update_policyagreed($user = null) {
1006        global $DB, $USER, $CFG;
1007        require_once($CFG->dirroot.'/user/lib.php');
1008
1009        if (!$user || (is_numeric($user) && $user == $USER->id)) {
1010            $user = $USER;
1011        } else if (!is_object($user)) {
1012            $user = $DB->get_record('user', ['id' => $user], 'id, policyagreed');
1013        }
1014
1015        $sql = "SELECT d.id, v.optional, a.status
1016                  FROM {tool_policy} d
1017            INNER JOIN {tool_policy_versions} v ON v.policyid = d.id AND v.id = d.currentversionid
1018             LEFT JOIN {tool_policy_acceptances} a ON a.userid = :userid AND a.policyversionid = v.id
1019                 WHERE (v.audience = :audience OR v.audience = :audienceall)";
1020
1021        $params = [
1022            'audience' => policy_version::AUDIENCE_LOGGEDIN,
1023            'audienceall' => policy_version::AUDIENCE_ALL,
1024            'userid' => $user->id
1025        ];
1026
1027        $allresponded = true;
1028        foreach ($DB->get_records_sql($sql, $params) as $policyacceptance) {
1029            if ($policyacceptance->optional == policy_version::AGREEMENT_COMPULSORY && empty($policyacceptance->status)) {
1030                $allresponded = false;
1031            } else if ($policyacceptance->optional == policy_version::AGREEMENT_OPTIONAL && $policyacceptance->status === null) {
1032                $allresponded = false;
1033            }
1034        }
1035
1036        if ($user->policyagreed != $allresponded) {
1037            $user->policyagreed = $allresponded;
1038            $DB->set_field('user', 'policyagreed', $allresponded, ['id' => $user->id]);
1039        }
1040    }
1041
1042    /**
1043     * May be used to revert accidentally granted acceptance for another user
1044     *
1045     * @param int $policyversionid
1046     * @param int $userid
1047     * @param null $note
1048     */
1049    public static function revoke_acceptance($policyversionid, $userid, $note = null) {
1050        global $DB, $USER;
1051        if (!$userid) {
1052            $userid = $USER->id;
1053        }
1054        self::can_accept_policies([$policyversionid], $userid, true);
1055
1056        if ($currentacceptance = $DB->get_record('tool_policy_acceptances',
1057                ['policyversionid' => $policyversionid, 'userid' => $userid])) {
1058            $realuser = manager::get_realuser();
1059            $updatedata = ['id' => $currentacceptance->id, 'status' => 0, 'timemodified' => time(),
1060                'usermodified' => $realuser->id, 'note' => $note];
1061            $DB->update_record('tool_policy_acceptances', $updatedata);
1062            acceptance_updated::create_from_record((object)($updatedata + (array)$currentacceptance))->trigger();
1063        }
1064
1065        static::update_policyagreed($userid);
1066    }
1067
1068    /**
1069     * Create user policy acceptances when the user is created.
1070     *
1071     * @param \core\event\user_created $event
1072     */
1073    public static function create_acceptances_user_created(\core\event\user_created $event) {
1074        global $USER, $CFG, $DB;
1075
1076        // Do nothing if not set as the site policies handler.
1077        if (empty($CFG->sitepolicyhandler) || $CFG->sitepolicyhandler !== 'tool_policy') {
1078            return;
1079        }
1080
1081        $userid = $event->objectid;
1082        $lang = current_language();
1083        $user = $event->get_record_snapshot('user', $userid);
1084        // Do nothing if the user has not accepted the current policies.
1085        if (!$user->policyagreed) {
1086            return;
1087        }
1088
1089        // Cleanup our bits in the presignup cache (we can not rely on them at this stage any more anyway).
1090        $cache = \cache::make('core', 'presignup');
1091        $cache->delete('tool_policy_userpolicyagreed');
1092        $cache->delete('tool_policy_viewedpolicies');
1093        $cache->delete('tool_policy_policyversionidsagreed');
1094
1095        // Mark all compulsory policies as implicitly accepted during the signup.
1096        if ($policyversions = static::list_current_versions(policy_version::AUDIENCE_LOGGEDIN)) {
1097            $acceptances = array();
1098            $now = time();
1099            foreach ($policyversions as $policyversion) {
1100                if ($policyversion->optional == policy_version::AGREEMENT_OPTIONAL) {
1101                    continue;
1102                }
1103                $acceptances[] = array(
1104                    'policyversionid' => $policyversion->id,
1105                    'userid' => $userid,
1106                    'status' => 1,
1107                    'lang' => $lang,
1108                    'usermodified' => isset($USER->id) ? $USER->id : 0,
1109                    'timecreated' => $now,
1110                    'timemodified' => $now,
1111                );
1112            }
1113            $DB->insert_records('tool_policy_acceptances', $acceptances);
1114        }
1115
1116        static::update_policyagreed($userid);
1117    }
1118
1119    /**
1120     * Returns the value of the optional flag for the given policy version.
1121     *
1122     * Optimised for being called multiple times by making use of a request cache. The cache is normally populated as a
1123     * side effect of calling {@link self::list_policies()} and in most cases should be warm enough for hits.
1124     *
1125     * @param int $versionid
1126     * @return int policy_version::AGREEMENT_COMPULSORY | policy_version::AGREEMENT_OPTIONAL
1127     */
1128    public static function get_agreement_optional($versionid) {
1129        global $DB;
1130
1131        $optcache = \cache::make('tool_policy', 'policy_optional');
1132
1133        $hit = $optcache->get($versionid);
1134
1135        if ($hit === false) {
1136            $flags = $DB->get_records_menu('tool_policy_versions', null, '', 'id, optional');
1137            $optcache->set_many($flags);
1138            $hit = $flags[$versionid];
1139        }
1140
1141        return $hit;
1142    }
1143}
1144