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