Proyectos de Subversion Moodle

Rev

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