From 20884e18e5a7562b2ea6ca4e33ffbdc8fcb4801a Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Mon, 12 Mar 2018 09:33:43 +0800 Subject: [PATCH] MDL-61477 admin: sitepolicy handler API - Define sitepolicy handler manager class, base class and the core handler - Allow to set a plugin as sitepolicyhandler that implements the sitepolicy API - Modify web services to return information from the 3rd party handler instead of core if needed --- admin/settings/plugins.php | 37 -- admin/settings/privacy.php | 80 +++++ admin/settings/security.php | 14 - admin/settings/top.php | 1 + admin/tool/mobile/classes/api.php | 4 +- admin/tool/mobile/tests/externallib_test.php | 1 + auth/email/classes/external.php | 8 +- .../validateagedigitalconsentmap.feature | 2 +- auth/upgrade.txt | 5 + file.php | 2 +- lang/en/admin.php | 3 + lib/adminlib.php | 43 +++ lib/moodlelib.php | 34 +- lib/upgrade.txt | 6 +- login/signup_form.php | 11 +- message/output/popup/lib.php | 3 +- .../local/sitepolicy/default_handler.php | 86 +++++ privacy/classes/local/sitepolicy/handler.php | 113 ++++++ privacy/classes/local/sitepolicy/manager.php | 137 ++++++++ .../fixtures/mock_sitepolicy_handler.php | 77 +++++ privacy/tests/sitepolicy_test.php | 324 ++++++++++++++++++ user/externallib.php | 23 +- user/policy.php | 22 +- 23 files changed, 917 insertions(+), 119 deletions(-) create mode 100644 admin/settings/privacy.php create mode 100644 privacy/classes/local/sitepolicy/default_handler.php create mode 100644 privacy/classes/local/sitepolicy/handler.php create mode 100644 privacy/classes/local/sitepolicy/manager.php create mode 100644 privacy/tests/fixtures/mock_sitepolicy_handler.php create mode 100644 privacy/tests/sitepolicy_test.php diff --git a/admin/settings/plugins.php b/admin/settings/plugins.php index 4db963b278c..e63f19071e3 100644 --- a/admin/settings/plugins.php +++ b/admin/settings/plugins.php @@ -123,43 +123,6 @@ if ($hassiteconfig) { $temp->add($setting); $ADMIN->add('authsettings', $temp); - $options = array( - 0 => get_string('no'), - 1 => get_string('yes') - ); - $url = new moodle_url('/admin/settings.php?section=supportcontact'); - $url = $url->out(); - $setting = new admin_setting_configselect('agedigitalconsentverification', - new lang_string('agedigitalconsentverification', 'admin'), - new lang_string('agedigitalconsentverification_desc', 'admin', $url), 0, $options); - $setting->set_force_ltr(true); - $temp->add($setting); - - $setting = new admin_setting_agedigitalconsentmap('agedigitalconsentmap', - new lang_string('ageofdigitalconsentmap', 'admin'), - new lang_string('ageofdigitalconsentmap_desc', 'admin'), - // See {@link https://gdpr-info.eu/art-8-gdpr/}. - implode(PHP_EOL, [ - '*, 16', - 'AT, 14', - 'CZ, 13', - 'DE, 14', - 'DK, 13', - 'ES, 13', - 'FI, 15', - 'GB, 13', - 'HU, 14', - 'IE, 13', - 'LT, 16', - 'LU, 16', - 'NL, 16', - 'PL, 13', - 'SE, 13', - ]), - PARAM_RAW - ); - $temp->add($setting); - $temp = new admin_externalpage('authtestsettings', get_string('testsettings', 'core_auth'), new moodle_url("/auth/test_settings.php"), 'moodle/site:config', true); $ADMIN->add('authsettings', $temp); diff --git a/admin/settings/privacy.php b/admin/settings/privacy.php new file mode 100644 index 00000000000..87f8c52589b --- /dev/null +++ b/admin/settings/privacy.php @@ -0,0 +1,80 @@ +. + +/** + * Adds privacy and policies links to admin tree. + * + * @package core_privacy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +if ($hassiteconfig) { + // Privacy settings. + $temp = new admin_settingpage('privacysettings', new lang_string('privacysettings', 'admin')); + + $options = array( + 0 => get_string('no'), + 1 => get_string('yes') + ); + $url = new moodle_url('/admin/settings.php?section=supportcontact'); + $url = $url->out(); + $setting = new admin_setting_configselect('agedigitalconsentverification', + new lang_string('agedigitalconsentverification', 'admin'), + new lang_string('agedigitalconsentverification_desc', 'admin', $url), 0, $options); + $setting->set_force_ltr(true); + $temp->add($setting); + + $setting = new admin_setting_agedigitalconsentmap('agedigitalconsentmap', + new lang_string('ageofdigitalconsentmap', 'admin'), + new lang_string('ageofdigitalconsentmap_desc', 'admin'), + // See {@link https://gdpr-info.eu/art-8-gdpr/}. + implode(PHP_EOL, [ + '*, 16', + 'AT, 14', + 'CZ, 13', + 'DE, 14', + 'DK, 13', + 'ES, 13', + 'FI, 15', + 'GB, 13', + 'HU, 14', + 'IE, 13', + 'LT, 16', + 'LU, 16', + 'NL, 16', + 'PL, 13', + 'SE, 13', + ]), + PARAM_RAW + ); + $temp->add($setting); + + $ADMIN->add('privacy', $temp); + + // Policy settings. + $temp = new admin_settingpage('policysettings', new lang_string('policysettings', 'admin')); + $temp->add(new admin_settings_sitepolicy_handler_select('sitepolicyhandler', new lang_string('sitepolicyhandler', 'core_admin'), + new lang_string('sitepolicyhandler_desc', 'core_admin'))); + $temp->add(new admin_setting_configtext('sitepolicy', new lang_string('sitepolicy', 'core_admin'), + new lang_string('sitepolicy_help', 'core_admin'), '', PARAM_RAW)); + $temp->add(new admin_setting_configtext('sitepolicyguest', new lang_string('sitepolicyguest', 'core_admin'), + new lang_string('sitepolicyguest_help', 'core_admin'), (isset($CFG->sitepolicy) ? $CFG->sitepolicy : ''), PARAM_RAW)); + + $ADMIN->add('privacy', $temp); +} diff --git a/admin/settings/security.php b/admin/settings/security.php index e33388442c5..432d873ce07 100644 --- a/admin/settings/security.php +++ b/admin/settings/security.php @@ -50,20 +50,6 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page $temp->add(new admin_setting_configcheckbox('extendedusernamechars', new lang_string('extendedusernamechars', 'admin'), new lang_string('configextendedusernamechars', 'admin'), 0)); - $sitepolicyhandlers = ['' => new lang_string('sitepolicyhandlercore', 'core_admin')]; - foreach (get_plugins_with_function('site_policy_handler') as $ptype => $pnames) { - foreach ($pnames as $pname => $fname) { - $sitepolicyhandlers[$ptype.'_'.$pname] = new lang_string('sitepolicyhandlerplugin', 'core_admin', - ['name' => new lang_string('pluginname', $ptype.'_'.$pname), 'component' => $ptype.'_'.$pname]); - } - } - $temp->add(new admin_setting_configselect('sitepolicyhandler', new lang_string('sitepolicyhandler', 'core_admin'), - new lang_string('sitepolicyhandler_desc', 'core_admin'), '', $sitepolicyhandlers)); - $temp->add(new admin_setting_configtext('sitepolicy', new lang_string('sitepolicy', 'core_admin'), - new lang_string('sitepolicy_help', 'core_admin'), '', PARAM_RAW)); - $temp->add(new admin_setting_configtext('sitepolicyguest', new lang_string('sitepolicyguest', 'core_admin'), - new lang_string('sitepolicyguest_help', 'core_admin'), (isset($CFG->sitepolicy) ? $CFG->sitepolicy : ''), PARAM_RAW)); - $temp->add(new admin_setting_configcheckbox('extendedusernamechars', new lang_string('extendedusernamechars', 'admin'), new lang_string('configextendedusernamechars', 'admin'), 0)); $temp->add(new admin_setting_configcheckbox('keeptagnamecase', new lang_string('keeptagnamecase','admin'),new lang_string('configkeeptagnamecase', 'admin'),'1')); diff --git a/admin/settings/top.php b/admin/settings/top.php index c78c90908f8..d751f2f520b 100644 --- a/admin/settings/top.php +++ b/admin/settings/top.php @@ -35,6 +35,7 @@ $ADMIN->add('root', new admin_category('badges', new lang_string('badges'), empt $ADMIN->add('root', new admin_category('location', new lang_string('location','admin'))); $ADMIN->add('root', new admin_category('language', new lang_string('language'))); $ADMIN->add('root', new admin_category('modules', new lang_string('plugins', 'admin'))); +$ADMIN->add('root', new admin_category('privacy', new lang_string('privacyandpolicies', 'admin'))); $ADMIN->add('root', new admin_category('security', new lang_string('security','admin'))); $ADMIN->add('root', new admin_category('appearance', new lang_string('appearance','admin'))); $ADMIN->add('root', new admin_category('frontpage', new lang_string('frontpage','admin'))); diff --git a/admin/tool/mobile/classes/api.php b/admin/tool/mobile/classes/api.php index 91dfbdaf80e..e094d3e2b21 100644 --- a/admin/tool/mobile/classes/api.php +++ b/admin/tool/mobile/classes/api.php @@ -211,7 +211,9 @@ class api { } if (empty($section) or $section == 'sitepolicies') { - $settings->sitepolicy = $CFG->sitepolicy; + $manager = new \core_privacy\local\sitepolicy\manager(); + $settings->sitepolicy = ($sitepolicy = $manager->get_embed_url()) ? $sitepolicy->out(false) : ''; + $settings->sitepolicyhandler = $CFG->sitepolicyhandler; $settings->disableuserimages = $CFG->disableuserimages; } diff --git a/admin/tool/mobile/tests/externallib_test.php b/admin/tool/mobile/tests/externallib_test.php index 3f7bac803b1..98f7a3a2896 100644 --- a/admin/tool/mobile/tests/externallib_test.php +++ b/admin/tool/mobile/tests/externallib_test.php @@ -150,6 +150,7 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase { array('name' => 'newsitems', 'value' => $SITE->newsitems), array('name' => 'commentsperpage', 'value' => $CFG->commentsperpage), array('name' => 'sitepolicy', 'value' => $mysitepolicy), + array('name' => 'sitepolicyhandler', 'value' => ''), array('name' => 'disableuserimages', 'value' => $CFG->disableuserimages), array('name' => 'mygradesurl', 'value' => user_mygrades_url()->out(false)), array('name' => 'tool_mobile_forcelogout', 'value' => 0), diff --git a/auth/email/classes/external.php b/auth/email/classes/external.php index 76bba9b466f..6a015a9bf9c 100644 --- a/auth/email/classes/external.php +++ b/auth/email/classes/external.php @@ -88,8 +88,9 @@ class auth_email_external extends external_api { if (!empty($CFG->passwordpolicy)) { $result['passwordpolicy'] = print_password_policy(); } - if (!empty($CFG->sitepolicy)) { - $result['sitepolicy'] = $CFG->sitepolicy; + $manager = new \core_privacy\local\sitepolicy\manager(); + if ($sitepolicy = $manager->get_embed_url()) { + $result['sitepolicy'] = $sitepolicy->out(false); } if (!empty($CFG->defaultcity)) { $result['defaultcity'] = $CFG->defaultcity; @@ -287,7 +288,8 @@ class auth_email_external extends external_api { $data = $params; $data['email2'] = $data['email']; // Force policy agreed if a site policy is set. The client is responsible of implementing the interface check. - if (!empty($CFG->sitepolicy)) { + $manager = new \core_privacy\local\sitepolicy\manager(); + if (!$manager->is_defined()) { $data['policyagreed'] = 1; } unset($data['recaptcharesponse']); diff --git a/auth/tests/behat/validateagedigitalconsentmap.feature b/auth/tests/behat/validateagedigitalconsentmap.feature index 00f5165f9c6..9fe1753e427 100644 --- a/auth/tests/behat/validateagedigitalconsentmap.feature +++ b/auth/tests/behat/validateagedigitalconsentmap.feature @@ -6,7 +6,7 @@ Feature: Test validation of 'Age of digital consent' setting. Background: Given I log in as "admin" - And I navigate to "Manage authentication" node in "Site administration > Plugins > Authentication" + And I navigate to "Privacy settings" node in "Site administration > Privacy and policies" Scenario: Admin provides valid value for 'Age of digital consent'. Given I set the field "s__agedigitalconsentmap" to multiline: diff --git a/auth/upgrade.txt b/auth/upgrade.txt index fff9fdd86cd..793e98a4a98 100644 --- a/auth/upgrade.txt +++ b/auth/upgrade.txt @@ -1,6 +1,11 @@ This files describes API changes in /auth/* - plugins, information provided here is intended especially for developers. +=== 3.3.5 === + +* Self registration plugins should use core_privacy\local\sitepolicy\manager instead of directly checking + $CFG->sitepolicy , especially in custom signup forms. See https://docs.moodle.org/dev/Site_policy_handler + === 3.3 === * Authentication plugins have been migrated to use the admin settings API. Plugins should use a settings.php file to diff --git a/file.php b/file.php index 61e563245f3..2799c319c89 100644 --- a/file.php +++ b/file.php @@ -68,7 +68,7 @@ if ($course->id != SITEID) { require_login($course, true, null, false); } else if ($CFG->forcelogin) { - if (!empty($CFG->sitepolicy) + if (empty($CFG->sitepolicyhandler) and !empty($CFG->sitepolicy) and ($CFG->sitepolicy == $CFG->wwwroot.'/file.php/'.$relativepath or $CFG->sitepolicy == $CFG->wwwroot.'/file.php?file=/'.$relativepath)) { //do not require login for policy file diff --git a/lang/en/admin.php b/lang/en/admin.php index d855ef77277..d39040dd4e2 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -850,6 +850,9 @@ $string['pluginscheckfailed'] = 'Dependencies check failed for {$a->pluginslist} $string['pluginschecktodo'] = 'You must solve all the plugin requirements before proceeding to install this Moodle version!'; $string['pluginsoverview'] = 'Plugins overview'; $string['pluginsoverviewsee'] = 'See plugins overview page for more details.'; +$string['policysettings'] = 'Policy settings'; +$string['privacyandpolicies'] = 'Privacy and policies'; +$string['privacysettings'] = 'Privacy settings'; $string['profilecategory'] = 'Category'; $string['profilecategoryname'] = 'Category name (must be unique)'; $string['profilecategorynamenotunique'] = 'This category name is already in use'; diff --git a/lib/adminlib.php b/lib/adminlib.php index 64acd75f84d..f2248e69813 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -10560,3 +10560,46 @@ class admin_setting_agedigitalconsentmap extends admin_setting_configtextarea { return true; } } + +/** + * Selection of plugins that can work as site policy handlers + * + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2018 Marina Glancy + */ +class admin_settings_sitepolicy_handler_select extends admin_setting_configselect { + + /** + * Constructor + * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' + * for ones in config_plugins. + * @param string $visiblename localised + * @param string $description long localised info + * @param string $defaultsetting + */ + public function __construct($name, $visiblename, $description, $defaultsetting = '') { + parent::__construct($name, $visiblename, $description, $defaultsetting, null); + } + + /** + * Lazy-load the available choices for the select box + */ + public function load_choices() { + if (during_initial_install()) { + return false; + } + if (is_array($this->choices)) { + return true; + } + + $this->choices = ['' => new lang_string('sitepolicyhandlercore', 'core_admin')]; + $manager = new \core_privacy\local\sitepolicy\manager(); + $plugins = $manager->get_all_handlers(); + foreach ($plugins as $pname => $unused) { + $this->choices[$pname] = new lang_string('sitepolicyhandlerplugin', 'core_admin', + ['name' => new lang_string('pluginname', $pname), 'component' => $pname]); + } + + return true; + } +} diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 62199170a58..ca937bf638b 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -2738,40 +2738,16 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $ // Check that the user has agreed to a site policy if there is one - do not test in case of admins. // Do not test if the script explicitly asked for skipping the site policies check. - // Also do not check during AJAX requests as they may be actually performed on pages where policies are accepted. - if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK && !AJAX_SCRIPT) { - if (!empty($CFG->sitepolicyhandler)) { - try { - $handler = component_callback($CFG->sitepolicyhandler, 'site_policy_handler'); - } catch (Exception $e) { - debugging('Error while trying to execute the site_policy_handler callback!'); - $handler = false; - } - if (!empty($handler) && (empty($PAGE->url) || !$PAGE->url->compare(new moodle_url($handler), URL_MATCH_BASE))) { - if ($preventredirect) { - throw new moodle_exception('sitepolicynotagreed', 'error', '', $handler); - } - if ($setwantsurltome) { - $SESSION->wantsurl = qualified_me(); - } - redirect($handler); - } - } else if (!empty($CFG->sitepolicy) and !isguestuser()) { + if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK) { + $manager = new \core_privacy\local\sitepolicy\manager(); + if ($policyurl = $manager->get_redirect_url(isguestuser())) { if ($preventredirect) { - throw new moodle_exception('sitepolicynotagreed', 'error', '', $CFG->sitepolicy); + throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out()); } if ($setwantsurltome) { $SESSION->wantsurl = qualified_me(); } - redirect($CFG->wwwroot .'/user/policy.php'); - } else if (!empty($CFG->sitepolicyguest) and isguestuser()) { - if ($preventredirect) { - throw new moodle_exception('sitepolicynotagreed', 'error', '', $CFG->sitepolicyguest); - } - if ($setwantsurltome) { - $SESSION->wantsurl = qualified_me(); - } - redirect($CFG->wwwroot .'/user/policy.php'); + redirect($policyurl); } } diff --git a/lib/upgrade.txt b/lib/upgrade.txt index aa3a15bc5d1..c5932a19f23 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -3,9 +3,9 @@ information provided here is intended especially for developers. === 3.3.5 === -* Plugins can provide a new callback 'site_policy_handler' if they implement an alternative mechanisms for site policies - managements and agreements. Administrators can define which component is to be used for handling site policies and - agreements. +* Plugins can define class 'PLUGINNAME\privacy\local\sitepolicy\handler' if they implement an alternative mechanisms for + site policies managements and agreements. Administrators can define which component is to be used for handling site + policies and agreements. See https://docs.moodle.org/dev/Site_policy_handler * Scripts can define a constant NO_SITEPOLICY_CHECK and set it to true before requiring the main config.php file. It will make the require_login() skipping the test for the user's policyagreed status. This is useful for plugins that act as a site policy handler. diff --git a/login/signup_form.php b/login/signup_form.php index 69c2b342d00..20419c012f0 100644 --- a/login/signup_form.php +++ b/login/signup_form.php @@ -97,13 +97,10 @@ class login_signup_form extends moodleform implements renderable, templatable { $mform->closeHeaderBefore('recaptcha_element'); } - if (!empty($CFG->sitepolicy)) { - $mform->addElement('header', 'policyagreement', get_string('policyagreement'), ''); - $mform->setExpanded('policyagreement'); - $mform->addElement('static', 'policylink', '', ''.get_String('policyagreementclick').''); - $mform->addElement('checkbox', 'policyagreed', get_string('policyaccept')); - $mform->addRule('policyagreed', get_string('policyagree'), 'required', null, 'client'); - } + // Add "Agree to sitepolicy" controls. By default it is a link to the policy text and a checkbox but + // it can be implemented differently in custom sitepolicy handlers. + $manager = new \core_privacy\local\sitepolicy\manager(); + $manager->signup_form($mform); // buttons $this->add_action_buttons(true, get_string('createaccount')); diff --git a/message/output/popup/lib.php b/message/output/popup/lib.php index 09930848b7e..4c4f910b942 100644 --- a/message/output/popup/lib.php +++ b/message/output/popup/lib.php @@ -36,7 +36,8 @@ function message_popup_render_navbar_output(\renderer_base $renderer) { // Early bail out conditions. if (!isloggedin() || isguestuser() || user_not_fully_set_up($USER) || get_user_preferences('auth_forcepasswordchange') || - ($CFG->sitepolicy && !$USER->policyagreed && !is_siteadmin())) { + (!$USER->policyagreed && !is_siteadmin() && + ($manager = new \core_privacy\local\sitepolicy\manager()) && $manager->is_defined())) { return ''; } diff --git a/privacy/classes/local/sitepolicy/default_handler.php b/privacy/classes/local/sitepolicy/default_handler.php new file mode 100644 index 00000000000..ac831fd1351 --- /dev/null +++ b/privacy/classes/local/sitepolicy/default_handler.php @@ -0,0 +1,86 @@ +. + +/** + * Default (core) handler for site policies. + * + * @package core_privacy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_privacy\local\sitepolicy; + +use moodle_url; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Default (core) handler for site policies. + * + * @package core_privacy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class default_handler extends handler { + + /** + * Checks if the site has site policy defined + * + * @param bool $forguests + * @return bool + */ + public static function is_defined($forguests = false) { + global $CFG; + if (!$forguests) { + return !empty($CFG->sitepolicy); + } else { + return !empty($CFG->sitepolicyguest); + } + } + + /** + * Returns URL to redirect user to when user needs to agree to site policy + * + * This is a regular interactive page for web users. It should have normal Moodle header/footers, it should + * allow user to view policies and accept them. + * + * @param bool $forguests + * @return moodle_url|null (returns null if site policy is not defined) + */ + public static function get_redirect_url($forguests = false) { + return static::is_defined($forguests) ? new moodle_url('/user/policy.php') : null; + } + + /** + * Returns URL of the site policy that needs to be displayed to the user (inside iframe or to use in WS such as mobile app) + * + * This page should not have any header/footer, it does not also have any buttons/checkboxes. The caller needs to implement + * the "Accept" button and call {@link self::accept()} on completion. + * + * @param bool $forguests + * @return moodle_url|null + */ + public static function get_embed_url($forguests = false) { + global $CFG; + if ($forguests && !empty($CFG->sitepolicyguest)) { + return new moodle_url($CFG->sitepolicyguest); + } else if (!$forguests && !empty($CFG->sitepolicy)) { + return new moodle_url($CFG->sitepolicy); + } + return null; + } +} diff --git a/privacy/classes/local/sitepolicy/handler.php b/privacy/classes/local/sitepolicy/handler.php new file mode 100644 index 00000000000..1a6e06734c5 --- /dev/null +++ b/privacy/classes/local/sitepolicy/handler.php @@ -0,0 +1,113 @@ +. + +/** + * Base class for site policy handlers. + * + * @package core_privacy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_privacy\local\sitepolicy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Base class for site policy handlers. + * + * If a plugin wants to act as a site policy handler it has to define class + * PLUGINNAME\privacy\sitepolicy\handler that extends \core_privacy\sitepolicy\handler + * + * @package core_privacy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class handler { + /** + * Checks if the site has site policy defined + * + * @param bool $forguests + * @return bool + */ + public static function is_defined($forguests = false) { + $url = static::get_redirect_url($forguests); + return !empty($url); + } + + /** + * Returns URL to redirect user to when user needs to agree to site policy + * + * This is a regular interactive page for web users. It should have normal Moodle header/footers, it should + * allow user to view policies and accept them. + * + * @param bool $forguests + * @return moodle_url|null (returns null if site policy is not defined) + */ + abstract public static function get_redirect_url($forguests = false); + + /** + * Returns URL of the site policy that needs to be displayed to the user (inside iframe or to use in WS such as mobile app) + * + * This page should not have any header/footer, it does not also have any buttons/checkboxes. The caller needs to implement + * the "Accept" button and call {@link self::accept()} on completion. + * + * @param bool $forguests + * @return moodle_url|null + */ + abstract public static function get_embed_url($forguests = false); + + /** + * Accept site policy for the current user + * + * @return bool - false if sitepolicy not defined, user is not logged in or user has already agreed to site policy; + * true - if we have successfully marked the user as agreed to the site policy + */ + public static function accept() { + global $USER, $DB; + if (!isloggedin()) { + return false; + } + if ($USER->policyagreed || !static::is_defined(isguestuser())) { + return false; + } + + if (!isguestuser()) { + // For the guests agreement in stored in session only, for other users - in DB. + $DB->set_field('user', 'policyagreed', 1, array('id' => $USER->id)); + } + $USER->policyagreed = 1; + return true; + } + + /** + * Adds "Agree to site policy" checkbox to the signup form. + * + * Sitepolicy handlers can override the simple checkbox with their own controls. + * + * @param \MoodleQuickForm $mform + */ + public static function signup_form($mform) { + if ($url = static::get_embed_url()) { + $mform->addElement('header', 'policyagreement', get_string('policyagreement'), ''); + $mform->setExpanded('policyagreement'); + $mform->addElement('static', 'policylink', '', '' . get_string('policyagreementclick') . ''); + $mform->addElement('checkbox', 'policyagreed', get_string('policyaccept')); + $mform->addRule('policyagreed', get_string('policyagree'), 'required', null, 'client'); + } + } +} diff --git a/privacy/classes/local/sitepolicy/manager.php b/privacy/classes/local/sitepolicy/manager.php new file mode 100644 index 00000000000..aa11c3006b6 --- /dev/null +++ b/privacy/classes/local/sitepolicy/manager.php @@ -0,0 +1,137 @@ +. + +/** + * Site policy management class. + * + * @package core_privacy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_privacy\local\sitepolicy; + +use moodle_url; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Site policy management class. + * + * @package core_privacy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class manager { + + /** + * Returns the list of plugins that can work as sitepolicy handlers (have class PLUGINNAME\privacy\sitepolicy\handler) + * @return array + */ + public function get_all_handlers() { + $sitepolicyhandlers = []; + foreach (\core_component::get_plugin_types() as $ptype => $unused) { + $plugins = \core_component::get_plugin_list_with_class($ptype, 'privacy\local\sitepolicy\handler') + + \core_component::get_plugin_list_with_class($ptype, 'privacy_local_sitepolicy_handler'); + // Allow plugins to have the class either with namespace or without (useful for unittest). + foreach ($plugins as $pname => $class) { + $sitepolicyhandlers[$pname] = $class; + } + } + return $sitepolicyhandlers; + } + + /** + * Returns the current site policy handler + * + * @return handler + */ + public function get_handler_classname() { + global $CFG; + if (!empty($CFG->sitepolicyhandler)) { + $sitepolicyhandlers = $this->get_all_handlers(); + $classname = $sitepolicyhandlers[$CFG->sitepolicyhandler]; + return $classname; + } else { + return default_handler::class; + } + } + + /** + * Checks if the site has site policy defined + * + * @param bool $forguests + * @return bool + */ + public function is_defined($forguests = false) { + return component_class_callback($this->get_handler_classname(), 'is_defined', [$forguests]); + } + + /** + * Returns URL to redirect user to when user needs to agree to site policy + * + * This is a regular interactive page for web users. It should have normal Moodle header/footers, it should + * allow user to view policies and accept them. + * + * @param bool $forguests + * @return moodle_url|null (returns null if site policy is not defined) + */ + public function get_redirect_url($forguests = false) { + $url = component_class_callback($this->get_handler_classname(), 'get_redirect_url', [$forguests]); + if ($url && !($url instanceof moodle_url)) { + $url = new moodle_url($url); + } + return $url; + } + + /** + * Returns URL of the site policy that needs to be displayed to the user (inside iframe or to use in WS such as mobile app) + * + * This page should not have any header/footer, it does not also have any buttons/checkboxes. The caller needs to implement + * the "Accept" button and call {@link self::accept()} on completion. + * + * @param bool $forguests + * @return moodle_url|null + */ + public function get_embed_url($forguests = false) { + $url = component_class_callback($this->get_handler_classname(), 'get_embed_url', [$forguests]); + if ($url && !($url instanceof moodle_url)) { + $url = new moodle_url($url); + } + return $url; + } + + /** + * Accept site policy for the current user + * + * @return bool - false if sitepolicy not defined, user is not logged in or user has already agreed to site policy; + * true - if we have successfully marked the user as agreed to the site policy + */ + public function accept() { + return component_class_callback($this->get_handler_classname(), 'accept', []); + } + + /** + * Adds "Agree to site policy" checkbox to the signup form. + * + * Sitepolicy handlers can override the simple checkbox with their own controls. + * + * @param \MoodleQuickForm $mform + */ + public function signup_form($mform) { + component_class_callback($this->get_handler_classname(), 'signup_form', [$mform]); + } +} diff --git a/privacy/tests/fixtures/mock_sitepolicy_handler.php b/privacy/tests/fixtures/mock_sitepolicy_handler.php new file mode 100644 index 00000000000..be398950619 --- /dev/null +++ b/privacy/tests/fixtures/mock_sitepolicy_handler.php @@ -0,0 +1,77 @@ +. + +/** + * Mock handler for site policies + * + * @package core_privacy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Mock handler for site policies + * + * @package core_privacy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mock_sitepolicy_handler extends \core_privacy\local\sitepolicy\handler { + + /** + * Returns URL to redirect user to when user needs to agree to site policy + * + * This is a regular interactive page for web users. It should have normal Moodle header/footers, it should + * allow user to view policies and accept them. + * + * @param bool $forguests + * @return moodle_url|null (returns null if site policy is not defined) + */ + public static function get_redirect_url($forguests = false) { + return 'http://example.com/policy.php'; + } + + /** + * Returns URL of the site policy that needs to be displayed to the user (inside iframe or to use in WS such as mobile app) + * + * This page should not have any header/footer, it does not also have any buttons/checkboxes. The caller needs to implement + * the "Accept" button and call {@link self::accept()} on completion. + * + * @param bool $forguests + * @return moodle_url|null + */ + public static function get_embed_url($forguests = false) { + return 'http://example.com/view.htm'; + } + + /** + * Accept site policy for the current user + * + * @return bool - false if sitepolicy not defined, user is not logged in or user has already agreed to site policy; + * true - if we have successfully marked the user as agreed to the site policy + */ + public static function accept() { + global $USER, $DB; + // Accepts policy on behalf of the current user. We set it to 2 here to check that this callback was called. + $USER->policyagreed = 2; + if (!isguestuser()) { + $DB->update_record('user', ['policyagreed' => 2, 'id' => $USER->id]); + } + return true; + } +} diff --git a/privacy/tests/sitepolicy_test.php b/privacy/tests/sitepolicy_test.php new file mode 100644 index 00000000000..37dd4af5666 --- /dev/null +++ b/privacy/tests/sitepolicy_test.php @@ -0,0 +1,324 @@ +. + +/** + * Unit Tests for sitepolicy manager + * + * @package core_privacy + * @category test + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +/** + * Unit Tests for sitepolicy manager + * + * @package core_privacy + * @category test + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class sitepolicy_test extends advanced_testcase { + + /** + * Tests for \core_privacy\local\sitepolicy\manager::is_defined() + */ + public function test_is_defined() { + global $CFG; + $this->resetAfterTest(true); + + $manager = new \core_privacy\local\sitepolicy\manager(); + + $this->assertFalse($manager->is_defined(true)); + $this->assertFalse($manager->is_defined(false)); + + $CFG->sitepolicy = 'http://example.com/sitepolicy.html'; + $this->assertFalse($manager->is_defined(true)); + $this->assertTrue($manager->is_defined(false)); + + $CFG->sitepolicyguest = 'http://example.com/sitepolicyguest.html'; + $this->assertTrue($manager->is_defined(true)); + $this->assertTrue($manager->is_defined(false)); + + $CFG->sitepolicy = null; + $this->assertTrue($manager->is_defined(true)); + $this->assertFalse($manager->is_defined(false)); + } + + /** + * Tests for \core_privacy\local\sitepolicy\manager::get_redirect_url() + */ + public function test_get_redirect_url() { + global $CFG; + $this->resetAfterTest(true); + + $manager = new \core_privacy\local\sitepolicy\manager(); + + $this->assertEquals(null, $manager->get_redirect_url(true)); + $this->assertEquals(null, $manager->get_redirect_url(false)); + + $CFG->sitepolicy = 'http://example.com/sitepolicy.html'; + $this->assertEquals(null, $manager->get_redirect_url(true)); + $this->assertEquals($CFG->wwwroot.'/user/policy.php', $manager->get_redirect_url(false)->out(false)); + + $CFG->sitepolicyguest = 'http://example.com/sitepolicyguest.html'; + $this->assertEquals($CFG->wwwroot.'/user/policy.php', $manager->get_redirect_url(true)->out(false)); + $this->assertEquals($CFG->wwwroot.'/user/policy.php', $manager->get_redirect_url(false)->out(false)); + + $CFG->sitepolicy = null; + $this->assertEquals($CFG->wwwroot.'/user/policy.php', $manager->get_redirect_url(true)->out(false)); + $this->assertEquals(null, $manager->get_redirect_url(false)); + } + + /** + * Tests for \core_privacy\local\sitepolicy\manager::get_redirect_url() + */ + public function test_get_embed_url() { + global $CFG; + $this->resetAfterTest(true); + + $manager = new \core_privacy\local\sitepolicy\manager(); + + $this->assertEquals(null, $manager->get_embed_url(true)); + $this->assertEquals(null, $manager->get_embed_url(false)); + + $CFG->sitepolicy = 'http://example.com/sitepolicy.html'; + $this->assertEquals(null, $manager->get_embed_url(true)); + $this->assertEquals($CFG->sitepolicy, $manager->get_embed_url(false)->out(false)); + + $CFG->sitepolicyguest = 'http://example.com/sitepolicyguest.html'; + $this->assertEquals($CFG->sitepolicyguest, $manager->get_embed_url(true)->out(false)); + $this->assertEquals($CFG->sitepolicy, $manager->get_embed_url(false)->out(false)); + + $CFG->sitepolicy = null; + $this->assertEquals($CFG->sitepolicyguest, $manager->get_embed_url(true)->out(false)); + $this->assertEquals(null, $manager->get_embed_url(false)); + } + + /** + * Tests for \core_privacy\local\sitepolicy\manager::get_redirect_url() + */ + public function test_accept() { + global $CFG, $USER, $DB; + $this->resetAfterTest(true); + + $manager = new \core_privacy\local\sitepolicy\manager(); + + // No site policy. + $user1 = $this->getDataGenerator()->create_user(); + $this->setUser($user1); + $this->assertFalse($manager->accept()); + $this->assertEquals(0, $USER->policyagreed); + + // With site policy. + $CFG->sitepolicy = 'http://example.com/sitepolicy.html'; + + $user2 = $this->getDataGenerator()->create_user(); + $this->setUser($user2); + + $this->assertEquals(0, $USER->policyagreed); + $this->assertEquals(0, $DB->get_field('user', 'policyagreed', ['id' => $USER->id])); + $this->assertTrue($manager->accept()); + $this->assertEquals(1, $USER->policyagreed); + $this->assertEquals(1, $DB->get_field('user', 'policyagreed', ['id' => $USER->id])); + } + + /** + * Tests for \core_privacy\local\sitepolicy\manager::get_redirect_url() for guests + */ + public function test_accept_guests() { + global $CFG, $USER, $DB; + $this->resetAfterTest(true); + + $manager = new \core_privacy\local\sitepolicy\manager(); + + $this->setGuestUser(); + + // No site policy. + $this->assertFalse($manager->accept()); + $this->assertEquals(0, $USER->policyagreed); + + // With site policy. + $CFG->sitepolicyguest = 'http://example.com/sitepolicy.html'; + + $this->assertEquals(0, $USER->policyagreed); + $this->assertTrue($manager->accept()); + $this->assertEquals(1, $USER->policyagreed); + $this->assertEquals(0, $DB->get_field('user', 'policyagreed', ['id' => $USER->id])); + } + + /** + * Helper to spoof the results of the internal function get_all_handlers, allowing mock handler to be tested. + * + * @return PHPUnit_Framework_MockObject_MockObject + */ + protected function get_mock_manager_with_handler() { + global $CFG; + require_once($CFG->dirroot.'/privacy/tests/fixtures/mock_sitepolicy_handler.php'); + + $mock = $this->getMockBuilder(\core_privacy\local\sitepolicy\manager::class) + ->setMethods(['get_all_handlers']) + ->getMock(); + $mock->expects($this->any()) + ->method('get_all_handlers') + ->will($this->returnValue(['testtool_testhandler' => 'mock_sitepolicy_handler'])); + return $mock; + } + + /** + * Tests for \core_privacy\local\sitepolicy\manager::is_defined() with a handler + */ + public function test_is_defined_with_handler() { + global $CFG; + $this->resetAfterTest(true); + $CFG->sitepolicyhandler = 'testtool_testhandler'; + $manager = $this->get_mock_manager_with_handler(); + $this->assertTrue($manager->is_defined(true)); + $this->assertTrue($manager->is_defined(false)); + } + + /** + * Tests for \core_privacy\local\sitepolicy\manager::get_redirect_url() with a handler + */ + public function test_get_redirect_url_with_handler() { + global $CFG; + $this->resetAfterTest(true); + + $CFG->sitepolicyhandler = 'testtool_testhandler'; + $manager = $this->get_mock_manager_with_handler(); + + $this->assertEquals('http://example.com/policy.php', $manager->get_redirect_url(true)->out(false)); + $this->assertEquals('http://example.com/policy.php', $manager->get_redirect_url(false)->out(false)); + } + + /** + * Tests for \core_privacy\local\sitepolicy\manager::get_redirect_url() with a handler + */ + public function test_get_embed_url_with_handler() { + global $CFG; + $this->resetAfterTest(true); + + $CFG->sitepolicyhandler = 'testtool_testhandler'; + $manager = $this->get_mock_manager_with_handler(); + + $this->assertEquals('http://example.com/view.htm', $manager->get_embed_url(true)->out(false)); + $this->assertEquals('http://example.com/view.htm', $manager->get_embed_url(false)->out(false)); + } + + /** + * Tests for \core_privacy\local\sitepolicy\manager::get_redirect_url() with a handler + */ + public function test_accept_with_handler() { + global $CFG, $USER, $DB; + $this->resetAfterTest(true); + + $CFG->sitepolicyhandler = 'testtool_testhandler'; + $manager = $this->get_mock_manager_with_handler(); + + $user2 = $this->getDataGenerator()->create_user(); + $this->setUser($user2); + + $this->assertEquals(0, $USER->policyagreed); + $this->assertEquals(0, $DB->get_field('user', 'policyagreed', ['id' => $USER->id])); + $this->assertTrue($manager->accept()); + $this->assertEquals(2, $USER->policyagreed); + $this->assertEquals(2, $DB->get_field('user', 'policyagreed', ['id' => $USER->id])); + } + + /** + * Tests for \core_privacy\local\sitepolicy\manager::get_redirect_url() for guests with a handler + */ + public function test_accept_guests_with_handler() { + global $CFG, $USER, $DB; + $this->resetAfterTest(true); + + $CFG->sitepolicyhandler = 'testtool_testhandler'; + $manager = $this->get_mock_manager_with_handler(); + + $this->setGuestUser(); + + $this->assertEquals(0, $USER->policyagreed); + $this->assertTrue($manager->accept()); + $this->assertEquals(2, $USER->policyagreed); + $this->assertEquals(0, $DB->get_field('user', 'policyagreed', ['id' => $USER->id])); + } +} + +/** + * Mock handler for site policies + * + * @package core_privacy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class handler extends \core_privacy\local\sitepolicy\handler { + + /** + * Checks if the site has site policy defined + * + * @param bool $forguests + * @return bool + */ + public static function is_defined($forguests = false) { + return true; + } + + /** + * Returns URL to redirect user to when user needs to agree to site policy + * + * This is a regular interactive page for web users. It should have normal Moodle header/footers, it should + * allow user to view policies and accept them. + * + * @param bool $forguests + * @return moodle_url|null (returns null if site policy is not defined) + */ + public static function get_redirect_url($forguests = false) { + return 'http://example.com/policy.php'; + } + + /** + * Returns URL of the site policy that needs to be displayed to the user (inside iframe or to use in WS such as mobile app) + * + * This page should not have any header/footer, it does not also have any buttons/checkboxes. The caller needs to implement + * the "Accept" button and call {@link self::accept()} on completion. + * + * @param bool $forguests + * @return moodle_url|null + */ + public static function get_embed_url($forguests = false) { + return 'http://example.com/view.htm'; + } + + /** + * Accept site policy for the current user + * + * @return bool - false if sitepolicy not defined, user is not logged in or user has already agreed to site policy; + * true - if we have successfully marked the user as agreed to the site policy + */ + public static function accept() { + global $USER, $DB; + // Accepts policy on behalf of the current user. We set it to 2 here to check that this callback was called. + $USER->policyagreed = 2; + if (!isguestuser()) { + $DB->update_record('user', ['policyagreed' => 2, 'id' => $USER->id]); + } + return true; + } +} diff --git a/user/externallib.php b/user/externallib.php index 3c33dfe28f1..72f6596b945 100644 --- a/user/externallib.php +++ b/user/externallib.php @@ -1831,15 +1831,8 @@ class core_user_external extends external_api { } } - if (empty($CFG->sitepolicy)) { - $status = false; - $warnings[] = array( - 'item' => 'user', - 'itemid' => $USER->id, - 'warningcode' => 'nositepolicy', - 'message' => 'The site does not have a site policy configured.' - ); - } else if (!empty($USER->policyagreed)) { + $manager = new \core_privacy\local\sitepolicy\manager(); + if (!empty($USER->policyagreed)) { $status = false; $warnings[] = array( 'item' => 'user', @@ -1847,10 +1840,16 @@ class core_user_external extends external_api { 'warningcode' => 'alreadyagreed', 'message' => 'The user already agreed the site policy.' ); + } else if (!$manager->is_defined()) { + $status = false; + $warnings[] = array( + 'item' => 'user', + 'itemid' => $USER->id, + 'warningcode' => 'nositepolicy', + 'message' => 'The site does not have a site policy configured.' + ); } else { - $DB->set_field('user', 'policyagreed', 1, array('id' => $USER->id)); - $USER->policyagreed = 1; - $status = true; + $status = $manager->accept(); } $result = array(); diff --git a/user/policy.php b/user/policy.php index d3dc4b5b9e2..c8e42d0824d 100644 --- a/user/policy.php +++ b/user/policy.php @@ -38,28 +38,30 @@ if (!isloggedin()) { require_login(); } -if (isguestuser()) { - $sitepolicy = $CFG->sitepolicyguest; -} else { - $sitepolicy = $CFG->sitepolicy; -} - if (!empty($SESSION->wantsurl)) { $return = $SESSION->wantsurl; } else { $return = $CFG->wwwroot.'/'; } +$sitepolicymanager = new \core_privacy\local\sitepolicy\manager(); +if (!empty($CFG->sitepolicyhandler)) { + // We are on the wrong page, site policies are managed by somebody else. + if ($sitepolicyurl = $sitepolicymanager->get_redirect_url(isguestuser())) { + redirect($sitepolicyurl); + } else { + redirect($return); + } +} + +$sitepolicy = $sitepolicymanager->get_embed_url(isguestuser()); if (empty($sitepolicy)) { // Nothing to agree to, sorry, hopefully we will not get to infinite loop. redirect($return); } if ($agree and confirm_sesskey()) { // User has agreed. - if (!isguestuser()) { // Don't remember guests. - $DB->set_field('user', 'policyagreed', 1, array('id' => $USER->id)); - } - $USER->policyagreed = 1; + $sitepolicymanager->accept(); unset($SESSION->wantsurl); redirect($return); }