Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]
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 $namefields = get_all_user_name_fields(true, 'u'); 347 $pixfields = user_picture::fields('u', $extrafields); 348 349 $sql = "SELECT $ctxfields, $namefields, $pixfields 350 FROM {role_assignments} ra 351 JOIN {context} c ON c.contextlevel = ".CONTEXT_USER." AND ra.contextid = c.id 352 JOIN {user} u ON c.instanceid = u.id 353 WHERE ra.userid = ? 354 ORDER BY u.lastname ASC, u.firstname ASC"; 355 356 $rs = $DB->get_recordset_sql($sql, [$userid]); 357 358 $minors = []; 359 360 foreach ($rs as $record) { 361 context_helper::preload_from_record($record); 362 $childcontext = context_user::instance($record->id); 363 if (has_capability('tool/policy:acceptbehalf', $childcontext, $userid)) { 364 $minors[$record->id] = $record; 365 } 366 } 367 368 $rs->close(); 369 370 return $minors; 371 } 372 373 /** 374 * Prepare data for the {@link \tool_policy\form\policydoc} form. 375 * 376 * @param \tool_policy\policy_version $version persistent representing the version. 377 * @return stdClass form data 378 */ 379 public static function form_policydoc_data(policy_version $version) { 380 381 $data = $version->to_record(); 382 $summaryfieldoptions = static::policy_summary_field_options(); 383 $contentfieldoptions = static::policy_content_field_options(); 384 385 if (empty($data->id)) { 386 // Adding a new version of a policy document. 387 $data = file_prepare_standard_editor($data, 'summary', $summaryfieldoptions, $summaryfieldoptions['context']); 388 $data = file_prepare_standard_editor($data, 'content', $contentfieldoptions, $contentfieldoptions['context']); 389 390 } else { 391 // Editing an existing policy document version. 392 $data = file_prepare_standard_editor($data, 'summary', $summaryfieldoptions, $summaryfieldoptions['context'], 393 'tool_policy', 'policydocumentsummary', $data->id); 394 $data = file_prepare_standard_editor($data, 'content', $contentfieldoptions, $contentfieldoptions['context'], 395 'tool_policy', 'policydocumentcontent', $data->id); 396 } 397 398 return $data; 399 } 400 401 /** 402 * Save the data from the policydoc form as a new policy document. 403 * 404 * @param stdClass $form data submitted from the {@link \tool_policy\form\policydoc} form. 405 * @return \tool_policy\policy_version persistent 406 */ 407 public static function form_policydoc_add(stdClass $form) { 408 global $DB; 409 410 $form = clone($form); 411 412 $form->policyid = $DB->insert_record('tool_policy', (object) [ 413 'sortorder' => 999, 414 ]); 415 416 static::distribute_policy_document_sortorder(); 417 418 return static::form_policydoc_update_new($form); 419 } 420 421 /** 422 * Save the data from the policydoc form as a new policy document version. 423 * 424 * @param stdClass $form data submitted from the {@link \tool_policy\form\policydoc} form. 425 * @return \tool_policy\policy_version persistent 426 */ 427 public static function form_policydoc_update_new(stdClass $form) { 428 global $DB; 429 430 if (empty($form->policyid)) { 431 throw new coding_exception('Invalid policy document ID'); 432 } 433 434 $form = clone($form); 435 436 $form->id = $DB->insert_record('tool_policy_versions', (new policy_version(0, (object) [ 437 'timecreated' => time(), 438 'policyid' => $form->policyid, 439 ]))->to_record()); 440 441 return static::form_policydoc_update_overwrite($form); 442 } 443 444 445 /** 446 * Save the data from the policydoc form, overwriting the existing policy document version. 447 * 448 * @param stdClass $form data submitted from the {@link \tool_policy\form\policydoc} form. 449 * @return \tool_policy\policy_version persistent 450 */ 451 public static function form_policydoc_update_overwrite(stdClass $form) { 452 453 $form = clone($form); 454 unset($form->timecreated); 455 456 $summaryfieldoptions = static::policy_summary_field_options(); 457 $form = file_postupdate_standard_editor($form, 'summary', $summaryfieldoptions, $summaryfieldoptions['context'], 458 'tool_policy', 'policydocumentsummary', $form->id); 459 unset($form->summary_editor); 460 unset($form->summarytrust); 461 462 $contentfieldoptions = static::policy_content_field_options(); 463 $form = file_postupdate_standard_editor($form, 'content', $contentfieldoptions, $contentfieldoptions['context'], 464 'tool_policy', 'policydocumentcontent', $form->id); 465 unset($form->content_editor); 466 unset($form->contenttrust); 467 468 unset($form->status); 469 unset($form->save); 470 unset($form->saveasdraft); 471 unset($form->minorchange); 472 473 $policyversion = new policy_version($form->id, $form); 474 $policyversion->update(); 475 476 return $policyversion; 477 } 478 479 /** 480 * Make the given version the current active one. 481 * 482 * @param int $versionid 483 */ 484 public static function make_current($versionid) { 485 global $DB, $USER; 486 487 $policyversion = new policy_version($versionid); 488 if (! $policyversion->get('id') || $policyversion->get('archived')) { 489 throw new coding_exception('Version not found or is archived'); 490 } 491 492 // Archive current version of this policy. 493 if ($currentversionid = $DB->get_field('tool_policy', 'currentversionid', ['id' => $policyversion->get('policyid')])) { 494 if ($currentversionid == $versionid) { 495 // Already current, do not change anything. 496 return; 497 } 498 $DB->set_field('tool_policy_versions', 'archived', 1, ['id' => $currentversionid]); 499 } 500 501 // Set given version as current. 502 $DB->set_field('tool_policy', 'currentversionid', $policyversion->get('id'), ['id' => $policyversion->get('policyid')]); 503 504 // Reset the policyagreed flag to force everybody re-accept the policies. 505 $DB->set_field('user', 'policyagreed', 0); 506 507 // Make sure that the current user is not immediately redirected to the policy acceptance page. 508 if (isloggedin() && !isguestuser()) { 509 $USER->policyagreed = 1; 510 } 511 } 512 513 /** 514 * Inactivate the policy document - no version marked as current and the document does not apply. 515 * 516 * @param int $policyid 517 */ 518 public static function inactivate($policyid) { 519 global $DB; 520 521 if ($currentversionid = $DB->get_field('tool_policy', 'currentversionid', ['id' => $policyid])) { 522 // Archive the current version. 523 $DB->set_field('tool_policy_versions', 'archived', 1, ['id' => $currentversionid]); 524 // Unset current version for the policy. 525 $DB->set_field('tool_policy', 'currentversionid', null, ['id' => $policyid]); 526 } 527 } 528 529 /** 530 * Create a new draft policy document from an archived version. 531 * 532 * @param int $versionid 533 * @return \tool_policy\policy_version persistent 534 */ 535 public static function revert_to_draft($versionid) { 536 $policyversion = new policy_version($versionid); 537 if (!$policyversion->get('id') || !$policyversion->get('archived')) { 538 throw new coding_exception('Version not found or is not archived'); 539 } 540 541 $formdata = static::form_policydoc_data($policyversion); 542 // Unarchived the new version. 543 $formdata->archived = 0; 544 return static::form_policydoc_update_new($formdata); 545 } 546 547 /** 548 * Can the current version be deleted 549 * 550 * @param stdClass $version object describing version, contains fields policyid, id, status, archived, audience, ... 551 */ 552 public static function can_delete_version($version) { 553 // TODO MDL-61900 allow to delete not only draft versions. 554 return has_capability('tool/policy:managedocs', context_system::instance()) && 555 $version->status == policy_version::STATUS_DRAFT; 556 } 557 558 /** 559 * Delete the given version (if it is a draft). Also delete policy if this is the only version. 560 * 561 * @param int $versionid 562 */ 563 public static function delete($versionid) { 564 global $DB; 565 566 $version = static::get_policy_version($versionid); 567 if (!self::can_delete_version($version)) { 568 // Current version can not be deleted. 569 return; 570 } 571 572 $DB->delete_records('tool_policy_versions', ['id' => $versionid]); 573 574 if (!$DB->record_exists('tool_policy_versions', ['policyid' => $version->policyid])) { 575 // This is a single version in a policy. Delete the policy. 576 $DB->delete_records('tool_policy', ['id' => $version->policyid]); 577 } 578 } 579 580 /** 581 * Editor field options for the policy summary text. 582 * 583 * @return array 584 */ 585 public static function policy_summary_field_options() { 586 global $CFG; 587 require_once($CFG->libdir.'/formslib.php'); 588 589 return [ 590 'subdirs' => false, 591 'maxfiles' => -1, 592 'context' => context_system::instance(), 593 ]; 594 } 595 596 /** 597 * Editor field options for the policy content text. 598 * 599 * @return array 600 */ 601 public static function policy_content_field_options() { 602 global $CFG; 603 require_once($CFG->libdir.'/formslib.php'); 604 605 return [ 606 'subdirs' => false, 607 'maxfiles' => -1, 608 'context' => context_system::instance(), 609 ]; 610 } 611 612 /** 613 * Re-sets the sortorder field of the policy documents to even values. 614 */ 615 protected static function distribute_policy_document_sortorder() { 616 global $DB; 617 618 $sql = "SELECT p.id, p.sortorder, MAX(v.timecreated) AS timerecentcreated 619 FROM {tool_policy} p 620 LEFT JOIN {tool_policy_versions} v ON v.policyid = p.id 621 GROUP BY p.id, p.sortorder 622 ORDER BY p.sortorder ASC, timerecentcreated ASC"; 623 624 $rs = $DB->get_recordset_sql($sql); 625 $sortorder = 10; 626 627 foreach ($rs as $record) { 628 if ($record->sortorder != $sortorder) { 629 $DB->set_field('tool_policy', 'sortorder', $sortorder, ['id' => $record->id]); 630 } 631 $sortorder = $sortorder + 2; 632 } 633 634 $rs->close(); 635 } 636 637 /** 638 * Change the policy document's sortorder. 639 * 640 * @param int $policyid 641 * @param int $step 642 */ 643 protected static function move_policy_document($policyid, $step) { 644 global $DB; 645 646 $sortorder = $DB->get_field('tool_policy', 'sortorder', ['id' => $policyid], MUST_EXIST); 647 $DB->set_field('tool_policy', 'sortorder', $sortorder + $step, ['id' => $policyid]); 648 static::distribute_policy_document_sortorder(); 649 } 650 651 /** 652 * Move the given policy document up in the list. 653 * 654 * @param id $policyid 655 */ 656 public static function move_up($policyid) { 657 static::move_policy_document($policyid, -3); 658 } 659 660 /** 661 * Move the given policy document down in the list. 662 * 663 * @param id $policyid 664 */ 665 public static function move_down($policyid) { 666 static::move_policy_document($policyid, 3); 667 } 668 669 /** 670 * Returns list of acceptances for this user. 671 * 672 * @param int $userid id of a user. 673 * @param int|array $versions list of policy versions. 674 * @return array list of acceptances indexed by versionid. 675 */ 676 public static function get_user_acceptances($userid, $versions = null) { 677 global $DB; 678 679 list($vsql, $vparams) = ['', []]; 680 if (!empty($versions)) { 681 list($vsql, $vparams) = $DB->get_in_or_equal($versions, SQL_PARAMS_NAMED, 'ver'); 682 $vsql = ' AND a.policyversionid ' . $vsql; 683 } 684 685 $userfieldsmod = get_all_user_name_fields(true, 'm', null, 'mod'); 686 $sql = "SELECT u.id AS mainuserid, a.policyversionid, a.status, a.lang, a.timemodified, a.usermodified, a.note, 687 u.policyagreed, $userfieldsmod 688 FROM {user} u 689 INNER JOIN {tool_policy_acceptances} a ON a.userid = u.id AND a.userid = :userid $vsql 690 LEFT JOIN {user} m ON m.id = a.usermodified"; 691 $params = ['userid' => $userid]; 692 $result = $DB->get_recordset_sql($sql, $params + $vparams); 693 694 $acceptances = []; 695 foreach ($result as $row) { 696 if (!empty($row->policyversionid)) { 697 $acceptances[$row->policyversionid] = $row; 698 } 699 } 700 $result->close(); 701 702 return $acceptances; 703 } 704 705 /** 706 * Returns version acceptance for this user. 707 * 708 * @param int $userid User identifier. 709 * @param int $versionid Policy version identifier. 710 * @param array|null $acceptances List of policy version acceptances indexed by versionid. 711 * @return stdClass|null Acceptance object if the user has ever accepted this version or null if not. 712 */ 713 public static function get_user_version_acceptance($userid, $versionid, $acceptances = null) { 714 if (empty($acceptances)) { 715 $acceptances = static::get_user_acceptances($userid, $versionid); 716 } 717 if (array_key_exists($versionid, $acceptances)) { 718 // The policy version has ever been accepted. 719 return $acceptances[$versionid]; 720 } 721 722 return null; 723 } 724 725 /** 726 * Did the user accept the given policy version? 727 * 728 * @param int $userid User identifier. 729 * @param int $versionid Policy version identifier. 730 * @param array|null $acceptances Pre-loaded list of policy version acceptances indexed by versionid. 731 * @return bool|null True/false if this user accepted/declined the policy; null otherwise. 732 */ 733 public static function is_user_version_accepted($userid, $versionid, $acceptances = null) { 734 735 $acceptance = static::get_user_version_acceptance($userid, $versionid, $acceptances); 736 737 if (!empty($acceptance)) { 738 return (bool) $acceptance->status; 739 } 740 741 return null; 742 } 743 744 /** 745 * Get the list of policies and versions that current user is able to see and the respective acceptance records for 746 * the selected user. 747 * 748 * @param int $userid 749 * @return array array with the same structure that list_policies() returns with additional attribute acceptance for versions 750 */ 751 public static function get_policies_with_acceptances($userid) { 752 // Get the list of policies and versions that current user is able to see 753 // and the respective acceptance records for the selected user. 754 $policies = static::list_policies(); 755 $acceptances = static::get_user_acceptances($userid); 756 $ret = []; 757 foreach ($policies as $policy) { 758 $versions = []; 759 if ($policy->currentversion && $policy->currentversion->audience != policy_version::AUDIENCE_GUESTS) { 760 if (isset($acceptances[$policy->currentversion->id])) { 761 $policy->currentversion->acceptance = $acceptances[$policy->currentversion->id]; 762 } else { 763 $policy->currentversion->acceptance = null; 764 } 765 $versions[] = $policy->currentversion; 766 } 767 foreach ($policy->archivedversions as $version) { 768 if ($version->audience != policy_version::AUDIENCE_GUESTS 769 && static::can_user_view_policy_version($version, $userid)) { 770 $version->acceptance = isset($acceptances[$version->id]) ? $acceptances[$version->id] : null; 771 $versions[] = $version; 772 } 773 } 774 if ($versions) { 775 $ret[] = (object)['id' => $policy->id, 'versions' => $versions]; 776 } 777 } 778 779 return $ret; 780 } 781 782 /** 783 * Check if given policies can be accepted by the current user (eventually on behalf of the other user) 784 * 785 * Currently, the version ids are not relevant and the check is based on permissions only. In the future, additional 786 * conditions can be added (such as policies applying to certain users only). 787 * 788 * @param array $versionids int[] List of policy version ids to check 789 * @param int $userid Accepting policies on this user's behalf (defaults to accepting on self) 790 * @param bool $throwexception Throw exception instead of returning false 791 * @return bool 792 */ 793 public static function can_accept_policies(array $versionids, $userid = null, $throwexception = false) { 794 global $USER; 795 796 if (!isloggedin() || isguestuser()) { 797 if ($throwexception) { 798 throw new \moodle_exception('noguest'); 799 } else { 800 return false; 801 } 802 } 803 804 if (!$userid) { 805 $userid = $USER->id; 806 } 807 808 if ($userid == $USER->id && !manager::is_loggedinas()) { 809 if ($throwexception) { 810 require_capability('tool/policy:accept', context_system::instance()); 811 return; 812 } else { 813 return has_capability('tool/policy:accept', context_system::instance()); 814 } 815 } 816 817 // Check capability to accept on behalf as the real user. 818 $realuser = manager::get_realuser(); 819 $usercontext = \context_user::instance($userid); 820 if ($throwexception) { 821 require_capability('tool/policy:acceptbehalf', $usercontext, $realuser); 822 return; 823 } else { 824 return has_capability('tool/policy:acceptbehalf', $usercontext, $realuser); 825 } 826 } 827 828 /** 829 * Check if given policies can be declined by the current user (eventually on behalf of the other user) 830 * 831 * Only optional policies can be declined. Otherwise, the permissions are same as for accepting policies. 832 * 833 * @param array $versionids int[] List of policy version ids to check 834 * @param int $userid Declining policies on this user's behalf (defaults to declining by self) 835 * @param bool $throwexception Throw exception instead of returning false 836 * @return bool 837 */ 838 public static function can_decline_policies(array $versionids, $userid = null, $throwexception = false) { 839 840 foreach ($versionids as $versionid) { 841 if (static::get_agreement_optional($versionid) == policy_version::AGREEMENT_COMPULSORY) { 842 // Compulsory policies can't be declined (that is what makes them compulsory). 843 if ($throwexception) { 844 throw new \moodle_exception('errorpolicyversioncompulsory', 'tool_policy'); 845 } else { 846 return false; 847 } 848 } 849 } 850 851 return static::can_accept_policies($versionids, $userid, $throwexception); 852 } 853 854 /** 855 * Check if acceptances to given policies can be revoked by the current user (eventually on behalf of the other user) 856 * 857 * Revoking optional policies is controlled by the same rules as declining them. Compulsory policies can be revoked 858 * only by users with the permission to accept policies on other's behalf. The reasoning behind this is to make sure 859 * the user communicates with the site's privacy officer and is well aware of all consequences of the decision (such 860 * as losing right to access the site). 861 * 862 * @param array $versionids int[] List of policy version ids to check 863 * @param int $userid Revoking policies on this user's behalf (defaults to revoking by self) 864 * @param bool $throwexception Throw exception instead of returning false 865 * @return bool 866 */ 867 public static function can_revoke_policies(array $versionids, $userid = null, $throwexception = false) { 868 global $USER; 869 870 // Guests' acceptance is not stored so there is nothing to revoke. 871 if (!isloggedin() || isguestuser()) { 872 if ($throwexception) { 873 throw new \moodle_exception('noguest'); 874 } else { 875 return false; 876 } 877 } 878 879 // Sort policies into two sets according the optional flag. 880 $compulsory = []; 881 $optional = []; 882 883 foreach ($versionids as $versionid) { 884 $agreementoptional = static::get_agreement_optional($versionid); 885 if ($agreementoptional == policy_version::AGREEMENT_COMPULSORY) { 886 $compulsory[] = $versionid; 887 } else if ($agreementoptional == policy_version::AGREEMENT_OPTIONAL) { 888 $optional[] = $versionid; 889 } else { 890 throw new \coding_exception('Unexpected optional flag value'); 891 } 892 } 893 894 // Check if the user can revoke the optional policies from the list. 895 if ($optional) { 896 if (!static::can_decline_policies($optional, $userid, $throwexception)) { 897 return false; 898 } 899 } 900 901 // Check if the user can revoke the compulsory policies from the list. 902 if ($compulsory) { 903 if (!$userid) { 904 $userid = $USER->id; 905 } 906 907 $realuser = manager::get_realuser(); 908 $usercontext = \context_user::instance($userid); 909 if ($throwexception) { 910 require_capability('tool/policy:acceptbehalf', $usercontext, $realuser); 911 return; 912 } else { 913 return has_capability('tool/policy:acceptbehalf', $usercontext, $realuser); 914 } 915 } 916 917 return true; 918 } 919 920 /** 921 * Mark the given policy versions as accepted by the user. 922 * 923 * @param array|int $policyversionid Policy version id(s) to set acceptance status for. 924 * @param int|null $userid Id of the user accepting the policy version, defaults to the current one. 925 * @param string|null $note Note to be recorded. 926 * @param string|null $lang Language in which the policy was shown, defaults to the current one. 927 */ 928 public static function accept_policies($policyversionid, $userid = null, $note = null, $lang = null) { 929 static::set_acceptances_status($policyversionid, $userid, $note, $lang, 1); 930 } 931 932 /** 933 * Mark the given policy versions as declined by the user. 934 * 935 * @param array|int $policyversionid Policy version id(s) to set acceptance status for. 936 * @param int|null $userid Id of the user accepting the policy version, defaults to the current one. 937 * @param string|null $note Note to be recorded. 938 * @param string|null $lang Language in which the policy was shown, defaults to the current one. 939 */ 940 public static function decline_policies($policyversionid, $userid = null, $note = null, $lang = null) { 941 static::set_acceptances_status($policyversionid, $userid, $note, $lang, 0); 942 } 943 944 /** 945 * Mark the given policy versions as accepted or declined by the user. 946 * 947 * @param array|int $policyversionid Policy version id(s) to set acceptance status for. 948 * @param int|null $userid Id of the user accepting the policy version, defaults to the current one. 949 * @param string|null $note Note to be recorded. 950 * @param string|null $lang Language in which the policy was shown, defaults to the current one. 951 * @param int $status The acceptance status, defaults to 1 = accepted 952 */ 953 protected static function set_acceptances_status($policyversionid, $userid = null, $note = null, $lang = null, $status = 1) { 954 global $DB, $USER; 955 956 // Validate arguments and capabilities. 957 if (empty($policyversionid)) { 958 return; 959 } else if (!is_array($policyversionid)) { 960 $policyversionid = [$policyversionid]; 961 } 962 if (!$userid) { 963 $userid = $USER->id; 964 } 965 self::can_accept_policies([$policyversionid], $userid, true); 966 967 // Retrieve the list of policy versions that need agreement (do not update existing agreements). 968 list($sql, $params) = $DB->get_in_or_equal($policyversionid, SQL_PARAMS_NAMED); 969 $sql = "SELECT v.id AS versionid, a.* 970 FROM {tool_policy_versions} v 971 LEFT JOIN {tool_policy_acceptances} a ON a.userid = :userid AND a.policyversionid = v.id 972 WHERE v.id $sql AND (a.id IS NULL OR a.status <> :status)"; 973 974 $needacceptance = $DB->get_records_sql($sql, $params + [ 975 'userid' => $userid, 976 'status' => $status, 977 ]); 978 979 $realuser = manager::get_realuser(); 980 $updatedata = ['status' => $status, 'lang' => $lang ?: current_language(), 981 'timemodified' => time(), 'usermodified' => $realuser->id, 'note' => $note]; 982 foreach ($needacceptance as $versionid => $currentacceptance) { 983 unset($currentacceptance->versionid); 984 if ($currentacceptance->id) { 985 $updatedata['id'] = $currentacceptance->id; 986 $DB->update_record('tool_policy_acceptances', $updatedata); 987 acceptance_updated::create_from_record((object)($updatedata + (array)$currentacceptance))->trigger(); 988 } else { 989 $updatedata['timecreated'] = $updatedata['timemodified']; 990 $updatedata['policyversionid'] = $versionid; 991 $updatedata['userid'] = $userid; 992 $updatedata['id'] = $DB->insert_record('tool_policy_acceptances', $updatedata); 993 acceptance_created::create_from_record((object)($updatedata + (array)$currentacceptance))->trigger(); 994 } 995 } 996 997 static::update_policyagreed($userid); 998 } 999 1000 /** 1001 * Make sure that $user->policyagreed matches the agreement to the policies 1002 * 1003 * @param int|stdClass|null $user user to check (null for current user) 1004 */ 1005 public static function update_policyagreed($user = null) { 1006 global $DB, $USER, $CFG; 1007 require_once($CFG->dirroot.'/user/lib.php'); 1008 1009 if (!$user || (is_numeric($user) && $user == $USER->id)) { 1010 $user = $USER; 1011 } else if (!is_object($user)) { 1012 $user = $DB->get_record('user', ['id' => $user], 'id, policyagreed'); 1013 } 1014 1015 $sql = "SELECT d.id, v.optional, a.status 1016 FROM {tool_policy} d 1017 INNER JOIN {tool_policy_versions} v ON v.policyid = d.id AND v.id = d.currentversionid 1018 LEFT JOIN {tool_policy_acceptances} a ON a.userid = :userid AND a.policyversionid = v.id 1019 WHERE (v.audience = :audience OR v.audience = :audienceall)"; 1020 1021 $params = [ 1022 'audience' => policy_version::AUDIENCE_LOGGEDIN, 1023 'audienceall' => policy_version::AUDIENCE_ALL, 1024 'userid' => $user->id 1025 ]; 1026 1027 $allresponded = true; 1028 foreach ($DB->get_records_sql($sql, $params) as $policyacceptance) { 1029 if ($policyacceptance->optional == policy_version::AGREEMENT_COMPULSORY && empty($policyacceptance->status)) { 1030 $allresponded = false; 1031 } else if ($policyacceptance->optional == policy_version::AGREEMENT_OPTIONAL && $policyacceptance->status === null) { 1032 $allresponded = false; 1033 } 1034 } 1035 1036 if ($user->policyagreed != $allresponded) { 1037 $user->policyagreed = $allresponded; 1038 $DB->set_field('user', 'policyagreed', $allresponded, ['id' => $user->id]); 1039 } 1040 } 1041 1042 /** 1043 * May be used to revert accidentally granted acceptance for another user 1044 * 1045 * @param int $policyversionid 1046 * @param int $userid 1047 * @param null $note 1048 */ 1049 public static function revoke_acceptance($policyversionid, $userid, $note = null) { 1050 global $DB, $USER; 1051 if (!$userid) { 1052 $userid = $USER->id; 1053 } 1054 self::can_accept_policies([$policyversionid], $userid, true); 1055 1056 if ($currentacceptance = $DB->get_record('tool_policy_acceptances', 1057 ['policyversionid' => $policyversionid, 'userid' => $userid])) { 1058 $realuser = manager::get_realuser(); 1059 $updatedata = ['id' => $currentacceptance->id, 'status' => 0, 'timemodified' => time(), 1060 'usermodified' => $realuser->id, 'note' => $note]; 1061 $DB->update_record('tool_policy_acceptances', $updatedata); 1062 acceptance_updated::create_from_record((object)($updatedata + (array)$currentacceptance))->trigger(); 1063 } 1064 1065 static::update_policyagreed($userid); 1066 } 1067 1068 /** 1069 * Create user policy acceptances when the user is created. 1070 * 1071 * @param \core\event\user_created $event 1072 */ 1073 public static function create_acceptances_user_created(\core\event\user_created $event) { 1074 global $USER, $CFG, $DB; 1075 1076 // Do nothing if not set as the site policies handler. 1077 if (empty($CFG->sitepolicyhandler) || $CFG->sitepolicyhandler !== 'tool_policy') { 1078 return; 1079 } 1080 1081 $userid = $event->objectid; 1082 $lang = current_language(); 1083 $user = $event->get_record_snapshot('user', $userid); 1084 // Do nothing if the user has not accepted the current policies. 1085 if (!$user->policyagreed) { 1086 return; 1087 } 1088 1089 // Cleanup our bits in the presignup cache (we can not rely on them at this stage any more anyway). 1090 $cache = \cache::make('core', 'presignup'); 1091 $cache->delete('tool_policy_userpolicyagreed'); 1092 $cache->delete('tool_policy_viewedpolicies'); 1093 $cache->delete('tool_policy_policyversionidsagreed'); 1094 1095 // Mark all compulsory policies as implicitly accepted during the signup. 1096 if ($policyversions = static::list_current_versions(policy_version::AUDIENCE_LOGGEDIN)) { 1097 $acceptances = array(); 1098 $now = time(); 1099 foreach ($policyversions as $policyversion) { 1100 if ($policyversion->optional == policy_version::AGREEMENT_OPTIONAL) { 1101 continue; 1102 } 1103 $acceptances[] = array( 1104 'policyversionid' => $policyversion->id, 1105 'userid' => $userid, 1106 'status' => 1, 1107 'lang' => $lang, 1108 'usermodified' => isset($USER->id) ? $USER->id : 0, 1109 'timecreated' => $now, 1110 'timemodified' => $now, 1111 ); 1112 } 1113 $DB->insert_records('tool_policy_acceptances', $acceptances); 1114 } 1115 1116 static::update_policyagreed($userid); 1117 } 1118 1119 /** 1120 * Returns the value of the optional flag for the given policy version. 1121 * 1122 * Optimised for being called multiple times by making use of a request cache. The cache is normally populated as a 1123 * side effect of calling {@link self::list_policies()} and in most cases should be warm enough for hits. 1124 * 1125 * @param int $versionid 1126 * @return int policy_version::AGREEMENT_COMPULSORY | policy_version::AGREEMENT_OPTIONAL 1127 */ 1128 public static function get_agreement_optional($versionid) { 1129 global $DB; 1130 1131 $optcache = \cache::make('tool_policy', 'policy_optional'); 1132 1133 $hit = $optcache->get($versionid); 1134 1135 if ($hit === false) { 1136 $flags = $DB->get_records_menu('tool_policy_versions', null, '', 'id, optional'); 1137 $optcache->set_many($flags); 1138 $hit = $flags[$versionid]; 1139 } 1140 1141 return $hit; 1142 } 1143 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body