Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
        // MDL-80973: At this point, the policyagreed value in DB could be 0 but $user->policyagreed could be 1 (as it was copied from $USER).
1038
        // So we need to ensure that the value in DB is set true if all policies were responded.
1039
        if ($user->policyagreed != $allresponded || $allresponded) {
1040
            $user->policyagreed = $allresponded;
1041
            $DB->set_field('user', 'policyagreed', $allresponded, ['id' => $user->id]);
1042
        }
1043
    }
1044
 
1045
    /**
1046
     * May be used to revert accidentally granted acceptance for another user
1047
     *
1048
     * @param int $policyversionid
1049
     * @param int $userid
1050
     * @param null $note
1051
     */
1052
    public static function revoke_acceptance($policyversionid, $userid, $note = null) {
1053
        global $DB, $USER;
1054
        if (!$userid) {
1055
            $userid = $USER->id;
1056
        }
1057
        self::can_accept_policies([$policyversionid], $userid, true);
1058
 
1059
        if ($currentacceptance = $DB->get_record('tool_policy_acceptances',
1060
                ['policyversionid' => $policyversionid, 'userid' => $userid])) {
1061
            $realuser = manager::get_realuser();
1062
            $updatedata = ['id' => $currentacceptance->id, 'status' => 0, 'timemodified' => time(),
1063
                'usermodified' => $realuser->id, 'note' => $note];
1064
            $DB->update_record('tool_policy_acceptances', $updatedata);
1065
            acceptance_updated::create_from_record((object)($updatedata + (array)$currentacceptance))->trigger();
1066
        }
1067
 
1068
        static::update_policyagreed($userid);
1069
    }
1070
 
1071
    /**
1072
     * Create user policy acceptances when the user is created.
1073
     *
1074
     * @param \core\event\user_created $event
1075
     */
1076
    public static function create_acceptances_user_created(\core\event\user_created $event) {
1077
        global $USER, $CFG, $DB;
1078
 
1079
        // Do nothing if not set as the site policies handler.
1080
        if (empty($CFG->sitepolicyhandler) || $CFG->sitepolicyhandler !== 'tool_policy') {
1081
            return;
1082
        }
1083
 
1084
        $userid = $event->objectid;
1085
        $lang = current_language();
1086
        $user = $event->get_record_snapshot('user', $userid);
1087
        // Do nothing if the user has not accepted the current policies.
1088
        if (!$user->policyagreed) {
1089
            return;
1090
        }
1091
 
1092
        // Cleanup our bits in the presignup cache (we can not rely on them at this stage any more anyway).
1093
        $cache = \cache::make('core', 'presignup');
1094
        $cache->delete('tool_policy_userpolicyagreed');
1095
        $cache->delete('tool_policy_viewedpolicies');
1096
        $cache->delete('tool_policy_policyversionidsagreed');
1097
 
1098
        // Mark all compulsory policies as implicitly accepted during the signup.
1099
        if ($policyversions = static::list_current_versions(policy_version::AUDIENCE_LOGGEDIN)) {
1100
            $acceptances = array();
1101
            $now = time();
1102
            foreach ($policyversions as $policyversion) {
1103
                if ($policyversion->optional == policy_version::AGREEMENT_OPTIONAL) {
1104
                    continue;
1105
                }
1106
                $acceptances[] = array(
1107
                    'policyversionid' => $policyversion->id,
1108
                    'userid' => $userid,
1109
                    'status' => 1,
1110
                    'lang' => $lang,
1111
                    'usermodified' => isset($USER->id) ? $USER->id : 0,
1112
                    'timecreated' => $now,
1113
                    'timemodified' => $now,
1114
                );
1115
            }
1116
            $DB->insert_records('tool_policy_acceptances', $acceptances);
1117
        }
1118
 
1119
        static::update_policyagreed($userid);
1120
    }
1121
 
1122
    /**
1123
     * Returns the value of the optional flag for the given policy version.
1124
     *
1125
     * Optimised for being called multiple times by making use of a request cache. The cache is normally populated as a
1126
     * side effect of calling {@link self::list_policies()} and in most cases should be warm enough for hits.
1127
     *
1128
     * @param int $versionid
1129
     * @return int policy_version::AGREEMENT_COMPULSORY | policy_version::AGREEMENT_OPTIONAL
1130
     */
1131
    public static function get_agreement_optional($versionid) {
1132
        global $DB;
1133
 
1134
        $optcache = \cache::make('tool_policy', 'policy_optional');
1135
 
1136
        $hit = $optcache->get($versionid);
1137
 
1138
        if ($hit === false) {
1139
            $flags = $DB->get_records_menu('tool_policy_versions', null, '', 'id, optional');
1140
            $optcache->set_many($flags);
1141
            $hit = $flags[$versionid];
1142
        }
1143
 
1144
        return $hit;
1145
    }
1146
}