diff --git a/admin/settings/plugins.php b/admin/settings/plugins.php index c55b1f12dc330..6abbe555e7b03 100644 --- a/admin/settings/plugins.php +++ b/admin/settings/plugins.php @@ -123,6 +123,43 @@ $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/auth/classes/digital_consent.php b/auth/classes/digital_consent.php new file mode 100644 index 0000000000000..e3e7e59bd6111 --- /dev/null +++ b/auth/classes/digital_consent.php @@ -0,0 +1,106 @@ +. + +/** + * Contains helper class for digital consent. + * + * @package core_auth + * @copyright 2018 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_auth; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Helper class for digital consent. + * + * @copyright 2018 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class digital_consent { + + /** + * Returns true if age and location verification is enabled in the site. + * + * @return bool + */ + public static function is_age_digital_consent_verification_enabled() { + global $CFG; + + return !empty($CFG->agedigitalconsentverification); + } + + /** + * Checks if a user is a digital minor. + * + * @param int $age + * @param string $country The country code (ISO 3166-2) + * @return bool + */ + public static function is_minor($age, $country) { + global $CFG; + + $ageconsentmap = $CFG->agedigitalconsentmap; + $agedigitalconsentmap = self::parse_age_digital_consent_map($ageconsentmap); + + return array_key_exists($country, $agedigitalconsentmap) ? + $age < $agedigitalconsentmap[$country] : $age < $agedigitalconsentmap['*']; + } + + /** + * Parse the agedigitalconsentmap setting into an array. + * + * @param string $ageconsentmap The value of the agedigitalconsentmap setting + * @return array $ageconsentmapparsed + */ + public static function parse_age_digital_consent_map($ageconsentmap) { + + $ageconsentmapparsed = array(); + $countries = get_string_manager()->get_list_of_countries(); + $isdefaultvaluepresent = false; + $lines = preg_split('/\r|\n/', $ageconsentmap, -1, PREG_SPLIT_NO_EMPTY); + foreach ($lines as $line) { + $arr = explode(",", $line); + // Handle if there is more or less than one comma separator. + if (count($arr) != 2) { + throw new \moodle_exception('agedigitalconsentmapinvalidcomma', 'error', '', $line); + } + $country = trim($arr[0]); + $age = trim($arr[1]); + // Check if default. + if ($country == "*") { + $isdefaultvaluepresent = true; + } + // Handle if the presented value for country is not valid. + if ($country !== "*" && !array_key_exists($country, $countries)) { + throw new \moodle_exception('agedigitalconsentmapinvalidcountry', 'error', '', $country); + } + // Handle if the presented value for age is not valid. + if (!is_numeric($age)) { + throw new \moodle_exception('agedigitalconsentmapinvalidage', 'error', '', $age); + } + $ageconsentmapparsed[$country] = $age; + } + // Handle if a default value does not exist. + if (!$isdefaultvaluepresent) { + throw new \moodle_exception('agedigitalconsentmapinvaliddefault'); + } + + return $ageconsentmapparsed; + } +} diff --git a/auth/classes/external.php b/auth/classes/external.php index baa86d953ba26..bc21cbc2afdb0 100644 --- a/auth/classes/external.php +++ b/auth/classes/external.php @@ -215,4 +215,76 @@ public static function request_password_reset_returns() { ) ); } + + /** + * Describes the parameters for the digital minor check. + * + * @return external_function_parameters + * @since Moodle 3.4 + */ + public static function is_minor_parameters() { + return new external_function_parameters( + array( + 'age' => new external_value(PARAM_INT, 'Age'), + 'country' => new external_value(PARAM_ALPHA, 'Country of residence'), + ) + ); + } + + /** + * Requests a check if a user is digital minor. + * + * @param int $age User age + * @param string $country Country of residence + * @return array status (true if the user is a minor, false otherwise) + * @since Moodle 3.4 + * @throws moodle_exception + */ + public static function is_minor($age, $country) { + global $CFG, $PAGE; + require_once($CFG->dirroot . '/login/lib.php'); + + $params = self::validate_parameters( + self::is_minor_parameters(), + array( + 'age' => $age, + 'country' => $country, + ) + ); + + if (!array_key_exists($params['country'], get_string_manager()->get_list_of_countries())) { + throw new invalid_parameter_exception('Invalid value for country parameter (value: '. + $params['country'] .')'); + } + + $context = context_system::instance(); + $PAGE->set_context($context); + + // Check if verification of age and location (minor check) is enabled. + if (!\core_auth\digital_consent::is_age_digital_consent_verification_enabled()) { + throw new moodle_exception('nopermissions', 'error', '', + get_string('agelocationverificationdisabled', 'error')); + } + + $status = \core_auth\digital_consent::is_minor($params['age'], $params['country']); + + return array( + 'status' => $status + ); + } + + /** + * Describes the is_minor return value. + * + * @return external_single_structure + * @since Moodle 3.4 + */ + public static function is_minor_returns() { + return new external_single_structure( + array( + 'status' => new external_value(PARAM_BOOL, 'True if the user is considered to be a digital minor, + false if not') + ) + ); + } } diff --git a/auth/classes/form/verify_age_location_form.php b/auth/classes/form/verify_age_location_form.php new file mode 100644 index 0000000000000..d2a80e122b39c --- /dev/null +++ b/auth/classes/form/verify_age_location_form.php @@ -0,0 +1,62 @@ +. + +/** + * Age and location verification mform. + * + * @package core_auth + * @copyright 2018 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_auth\form; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/formslib.php'); + +use moodleform; + +/** + * Age and location verification mform class. + * + * @copyright 2018 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class verify_age_location_form extends moodleform { + /** + * Defines the form fields. + */ + public function definition() { + global $CFG; + + $mform = $this->_form; + + $mform->addElement('text', 'age', get_string('whatisyourage'), array('optional' => false)); + $mform->setType('age', PARAM_RAW); + $mform->addRule('age', null, 'required', null, 'client'); + $mform->addRule('age', null, 'numeric', null, 'client'); + + $countries = get_string_manager()->get_list_of_countries(); + $defaultcountry[''] = get_string('selectacountry'); + $countries = array_merge($defaultcountry, $countries); + $mform->addElement('select', 'country', get_string('wheredoyoulive'), $countries); + $mform->addRule('country', null, 'required', null, 'client'); + $mform->setDefault('country', $CFG->country); + + $this->add_action_buttons(true, get_string('proceed')); + } +} diff --git a/auth/classes/output/digital_minor_page.php b/auth/classes/output/digital_minor_page.php new file mode 100644 index 0000000000000..411893eae6428 --- /dev/null +++ b/auth/classes/output/digital_minor_page.php @@ -0,0 +1,63 @@ +. + +/** + * Digital minor renderable. + * + * @package core_auth + * @copyright 2018 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_auth\output; + +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use renderer_base; +use templatable; + +/** + * Digital minor renderable class. + * + * @copyright 2018 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class digital_minor_page implements renderable, templatable { + + /** + * Export the page data for the mustache template. + * + * @param renderer_base $output renderer to be used to render the page elements. + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + global $SITE, $CFG; + + $sitename = format_string($SITE->fullname); + $supportname = $CFG->supportname; + $supportemail = $CFG->supportemail; + + $context = [ + 'sitename' => $sitename, + 'supportname' => $supportname, + 'supportemail' => $supportemail, + 'homelink' => new \moodle_url('/') + ]; + + return $context; + } +} diff --git a/auth/classes/output/verify_age_location_page.php b/auth/classes/output/verify_age_location_page.php new file mode 100644 index 0000000000000..4d8b79072b8c2 --- /dev/null +++ b/auth/classes/output/verify_age_location_page.php @@ -0,0 +1,81 @@ +. + +/** + * Age and location verification renderable. + * + * @package core_auth + * @copyright 2018 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_auth\output; + +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use renderer_base; +use templatable; + +require_once($CFG->libdir.'/formslib.php'); + +/** + * Age and location verification renderable class. + * + * @copyright 2018 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class verify_age_location_page implements renderable, templatable { + + /** @var mform The form object */ + protected $form; + + /** @var string Error message */ + protected $errormessage; + + /** + * Constructor + * + * @param mform $form The form object + * @param string $errormessage The error message. + */ + public function __construct($form, $errormessage = null) { + $this->form = $form; + $this->errormessage = $errormessage; + } + + /** + * Export the page data for the mustache template. + * + * @param renderer_base $output renderer to be used to render the page elements. + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + global $SITE; + + $sitename = format_string($SITE->fullname); + $formhtml = $this->form->render(); + $error = $this->errormessage; + + $context = [ + 'sitename' => $sitename, + 'formhtml' => $formhtml, + 'error' => $error + ]; + + return $context; + } +} diff --git a/auth/tests/behat/validateagedigitalconsentmap.feature b/auth/tests/behat/validateagedigitalconsentmap.feature new file mode 100644 index 0000000000000..00f5165f9c61e --- /dev/null +++ b/auth/tests/behat/validateagedigitalconsentmap.feature @@ -0,0 +1,68 @@ +@core @verify_age_location +Feature: Test validation of 'Age of digital consent' setting. + In order to set the 'Age of digital consent' setting + As an admin + I need to provide valid data and valid format + + Background: + Given I log in as "admin" + And I navigate to "Manage authentication" node in "Site administration > Plugins > Authentication" + + Scenario: Admin provides valid value for 'Age of digital consent'. + Given I set the field "s__agedigitalconsentmap" to multiline: + """ + *, 16 + AT, 14 + BE, 14 + """ + When I press "Save changes" + Then I should see "Changes saved" + And I should not see "Some settings were not changed due to an error." + And I should not see "The digital age of consent is not valid:" + + Scenario: Admin provides invalid format for 'Age of digital consent'. + # Try to set a value with missing space separator + Given I set the field "s__agedigitalconsentmap" to multiline: + """ + *16 + AT, 14 + BE, 14 + """ + When I press "Save changes" + Then I should not see "Changes saved" + And I should see "Some settings were not changed due to an error." + And I should see "The digital age of consent is not valid: \"*16\" has more or less than one comma separator." + # Try to set a value with missing default age of consent + When I set the field "s__agedigitalconsentmap" to multiline: + """ + AT, 14 + BE, 14 + """ + And I press "Save changes" + Then I should not see "Changes saved" + And I should see "Some settings were not changed due to an error." + And I should see "The digital age of consent is not valid: Default (*) value is missing." + + Scenario: Admin provides invalid age of consent or country for 'Age of digital consent'. + # Try to set a value containing invalid age of consent + Given I set the field "s__agedigitalconsentmap" to multiline: + """ + *, 16 + AT, age + BE, 14 + """ + When I press "Save changes" + Then I should not see "Changes saved" + And I should see "Some settings were not changed due to an error." + And I should see "The digital age of consent is not valid: \"age\" is not a valid value for age." + # Try to set a value containing invalid country + When I set the field "s__agedigitalconsentmap" to multiline: + """ + *, 16 + COUNTRY, 14 + BE, 14 + """ + And I press "Save changes" + Then I should not see "Changes saved" + And I should see "Some settings were not changed due to an error." + And I should see "The digital age of consent is not valid: \"COUNTRY\" is not a valid value for country." diff --git a/auth/tests/behat/verifyageofconsent.feature b/auth/tests/behat/verifyageofconsent.feature new file mode 100644 index 0000000000000..9f04967367c81 --- /dev/null +++ b/auth/tests/behat/verifyageofconsent.feature @@ -0,0 +1,45 @@ +@core @verify_age_location +Feature: Test the 'Digital age of consent verification' feature works. + In order to self-register on the site + As an user + I need be to be over the age of digital consent + + Background: + Given the following config values are set as admin: + | registerauth | email | + | agedigitalconsentverification | 1 | + + Scenario: User that is not considered a digital minor attempts to self-register on the site. + # Try to access the sign up page. + Given I am on homepage + When I click on "Log in" "link" in the ".logininfo" "css_element" + And I press "Create new account" + Then I should see "Age and location verification" + When I set the field "What is your age?" to "16" + And I set the field "In which country do you live?" to "DZ" + And I press "Proceed" + Then I should see "New account" + And I should see "Choose your username and password" + # Try to access the sign up page again. + When I press "Cancel" + And I press "Create new account" + Then I should see "New account" + And I should see "Choose your username and password" + + Scenario: User that is considered a digital minor attempts to self-register on the site. + # Try to access the sign up page. + Given I am on homepage + When I click on "Log in" "link" in the ".logininfo" "css_element" + And I press "Create new account" + Then I should see "Age and location verification" + When I set the field "What is your age?" to "12" + And I set the field "In which country do you live?" to "AT" + And I press "Proceed" + Then I should see "You are considered to be a digital minor." + And I should see "To create an account on this site please have your parent/guardian contact the following person." + # Try to access the sign up page again. + When I click on "Back to the site home" "link" + And I click on "Log in" "link" in the ".logininfo" "css_element" + And I press "Create new account" + Then I should see "You are considered to be a digital minor." + And I should see "To create an account on this site please have your parent/guardian contact the following person." diff --git a/auth/tests/digital_consent_test.php b/auth/tests/digital_consent_test.php new file mode 100644 index 0000000000000..c9b2b90e10a54 --- /dev/null +++ b/auth/tests/digital_consent_test.php @@ -0,0 +1,182 @@ +. + +/** + * Unit tests for core_auth\digital_consent. + * + * @package core_auth + * @copyright 2018 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Digital consent helper testcase. + * + * @package core_auth + * @copyright 2018 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_auth_digital_consent_testcase extends advanced_testcase { + + public function test_is_age_digital_consent_verification_enabled() { + global $CFG; + $this->resetAfterTest(); + + // Age of digital consent verification is enabled. + $CFG->agedigitalconsentverification = 0; + + $isenabled = \core_auth\digital_consent::is_age_digital_consent_verification_enabled(); + $this->assertFalse($isenabled); + } + + public function test_is_minor() { + global $CFG; + $this->resetAfterTest(); + + $agedigitalconsentmap = implode(PHP_EOL, [ + '*, 16', + 'AT, 14', + 'CZ, 13', + 'DE, 14', + 'DK, 13', + ]); + $CFG->agedigitalconsentmap = $agedigitalconsentmap; + + $usercountry1 = 'DK'; + $usercountry2 = 'AU'; + $userage1 = 12; + $userage2 = 14; + $userage3 = 16; + + // Test country exists in agedigitalconsentmap and user age is below the particular digital minor age. + $isminor = \core_auth\digital_consent::is_minor($userage1, $usercountry1); + $this->assertTrue($isminor); + // Test country exists in agedigitalconsentmap and user age is above the particular digital minor age. + $isminor = \core_auth\digital_consent::is_minor($userage2, $usercountry1); + $this->assertFalse($isminor); + // Test country does not exists in agedigitalconsentmap and user age is below the particular digital minor age. + $isminor = \core_auth\digital_consent::is_minor($userage2, $usercountry2); + $this->assertTrue($isminor); + // Test country does not exists in agedigitalconsentmap and user age is above the particular digital minor age. + $isminor = \core_auth\digital_consent::is_minor($userage3, $usercountry2); + $this->assertFalse($isminor); + } + + public function test_parse_age_digital_consent_map_valid_format() { + + // Value of agedigitalconsentmap has a valid format. + $agedigitalconsentmap = implode(PHP_EOL, [ + '*, 16', + 'AT, 14', + 'BE, 13' + ]); + + $ageconsentmapparsed = \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); + + $this->assertEquals([ + '*' => 16, + 'AT' => 14, + 'BE' => 13 + ], $ageconsentmapparsed + ); + } + + public function test_parse_age_digital_consent_map_invalid_format_missing_spaces() { + + // Value of agedigitalconsentmap has an invalid format (missing space separator between values). + $agedigitalconsentmap = implode(PHP_EOL, [ + '*, 16', + 'AT14', + ]); + + $this->expectException('moodle_exception'); + $this->expectExceptionMessage(get_string('agedigitalconsentmapinvalidcomma', 'error', 'AT14')); + + \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); + } + + public function test_parse_age_digital_consent_map_invalid_format_missing_default_value() { + + // Value of agedigitalconsentmap has an invalid format (missing default value). + $agedigitalconsentmap = implode(PHP_EOL, [ + 'BE, 16', + 'AT, 14' + ]); + + $this->expectException('moodle_exception'); + $this->expectExceptionMessage(get_string('agedigitalconsentmapinvaliddefault', 'error')); + + \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); + } + + public function test_parse_age_digital_consent_map_invalid_format_invalid_country() { + + // Value of agedigitalconsentmap has an invalid format (invalid value for country). + $agedigitalconsentmap = implode(PHP_EOL, [ + '*, 16', + 'TEST, 14' + ]); + + $this->expectException('moodle_exception'); + $this->expectExceptionMessage(get_string('agedigitalconsentmapinvalidcountry', 'error', 'TEST')); + + \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); + } + + public function test_parse_age_digital_consent_map_invalid_format_invalid_age_string() { + + // Value of agedigitalconsentmap has an invalid format (string value for age). + $agedigitalconsentmap = implode(PHP_EOL, [ + '*, 16', + 'AT, ten' + ]); + + $this->expectException('moodle_exception'); + $this->expectExceptionMessage(get_string('agedigitalconsentmapinvalidage', 'error', 'ten')); + + \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); + } + + public function test_parse_age_digital_consent_map_invalid_format_missing_age() { + + // Value of agedigitalconsentmap has an invalid format (missing value for age). + $agedigitalconsentmap = implode(PHP_EOL, [ + '*, 16', + 'AT, ' + ]); + + $this->expectException('moodle_exception'); + $this->expectExceptionMessage(get_string('agedigitalconsentmapinvalidage', 'error', '')); + + \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); + } + + public function test_parse_age_digital_consent_map_invalid_format_missing_country() { + + // Value of agedigitalconsentmap has an invalid format (missing value for country). + $agedigitalconsentmap = implode(PHP_EOL, [ + '*, 16', + ', 12' + ]); + + $this->expectException('moodle_exception'); + $this->expectExceptionMessage(get_string('agedigitalconsentmapinvalidcountry', 'error', '')); + + \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); + } +} diff --git a/lang/en/admin.php b/lang/en/admin.php index eb6030da5404e..a85b3c652c9c0 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -39,6 +39,10 @@ $string['adminseesallevents'] = 'Administrators see all events'; $string['adminseesownevents'] = 'Administrators are just like other users'; $string['advancedfeatures'] = 'Advanced features'; +$string['agedigitalconsentverification'] = 'Digital age of consent verification'; +$string['agedigitalconsentverification_desc'] = 'Enables verification of the digital age of consent before displaying the sign-up page for self-registration users. This protects your site from minors signing up without parental/guardian consent. Support contact details are provided to minors for further assistance.'; +$string['ageofdigitalconsentmap'] = 'Digital age of consent'; +$string['ageofdigitalconsentmap_desc'] = 'The default digital age of consent, and the age in any country where it differs from the default, may be specified here. Enter each age on a new line with format: country code, age (separated by a comma). The default age is indicated by * in place of the country code. Country codes are as specified in ISO 3166-2.'; $string['allcountrycodes'] = 'All country codes'; $string['allowattachments'] = 'Allow attachments'; $string['allowbeforeblock'] = 'Allowed list will be processed first'; @@ -617,6 +621,7 @@ $string['installsessionerror'] = 'Can not initialise PHP session, please verify that your browser accepts cookies.'; $string['intlrecommended'] = 'Intl extension is used to improve internationalization support, such as locale aware sorting.'; $string['intlrequired'] = 'Intl extension is required to improve internationalization support, such as locale aware sorting and international domain names.'; +$string['invalidagedigitalconsent'] = 'The digital age of consent is not valid: {$a}'; $string['invalidforgottenpasswordurl'] = 'The forgotten password URL is not a valid URL.'; $string['invalidsection'] = 'Invalid section.'; $string['invaliduserchangeme'] = 'Username "changeme" is reserved -- you cannot create an account with it.'; diff --git a/lang/en/cache.php b/lang/en/cache.php index 88ecab11d44b9..44de1dc75524a 100644 --- a/lang/en/cache.php +++ b/lang/en/cache.php @@ -62,6 +62,7 @@ $string['cachedef_observers'] = 'Event observers'; $string['cachedef_plugin_functions'] = 'Plugins available callbacks'; $string['cachedef_plugin_manager'] = 'Plugin info manager'; +$string['cachedef_presignup'] = 'Pre sign-up data for particular unregistered user'; $string['cachedef_postprocessedcss'] = 'Post processed CSS'; $string['cachedef_tagindexbuilder'] = 'Search results for tagged items'; $string['cachedef_questiondata'] = 'Question definitions'; diff --git a/lang/en/error.php b/lang/en/error.php index 492de6fd62fa5..1c7de74870663 100644 --- a/lang/en/error.php +++ b/lang/en/error.php @@ -23,6 +23,11 @@ */ $string['activityisscheduledfordeletion'] = 'Activity deletion in progress...'; +$string['agedigitalconsentmapinvalidage'] = '"{$a}" is not a valid value for age.'; +$string['agedigitalconsentmapinvalidcomma'] = '"{$a}" has more or less than one comma separator.'; +$string['agedigitalconsentmapinvalidcountry'] = '"{$a}" is not a valid value for country.'; +$string['agedigitalconsentmapinvaliddefault'] = 'Default (*) value is missing.'; +$string['agelocationverificationdisabled'] = 'Age and location verification disabled'; $string['authnotexisting'] = 'The autorization plugin doesn\'t exist'; $string['backupcontainexternal'] = 'This backup file contains external Moodle Network Hosts that are not configured locally'; $string['backuptablefail'] = 'Backup tables could NOT be set up successfully!'; @@ -177,6 +182,7 @@ $string['confirmsesskeybad'] = 'Sorry, but your session key could not be confirmed to carry out this action. This security feature prevents against accidental or malicious execution of important functions in your name. Please make sure you really wanted to execute this function.'; $string['couldnotassignrole'] = 'A serious but unspecified error occurred while trying to assign a role to you'; $string['couldnotupdatenoexistinguser'] = 'Cannot update the user - user doesn\'t exist'; +$string['couldnotverifyagedigitalconsent'] = 'An error occurred while trying to verify the age of digital consent.
Please contact administrator.'; $string['countriesphpempty'] = 'Error: The file countries.php in language pack {$a} is empty or missing.'; $string['coursedoesnotbelongtocategory'] = 'The course doesn\'t belong to this category'; $string['courseformatnotfound'] = 'The course format \'{$a}\' doesn\'t exist or is not recognized'; @@ -578,6 +584,7 @@ $string['usernotupdatednotexists'] = 'User not updated - does not exist'; $string['userquotalimit'] = 'You have reached your file quota limit.'; $string['userselectortoomany'] = 'user_selector got more than one selected user, even though multiselect is false.'; +$string['verifyagedigitalconsentnotpossible'] = 'Sorry, digital age consent verification is not possible at this time.'; $string['wrongcall'] = 'This script is called wrongly'; $string['wrongcontextid'] = 'Context ID was incorrect (cannot find it)'; $string['wrongdestpath'] = 'Wrong destination path'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 207a5220c9ca1..8c8d7e810bff3 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -119,6 +119,7 @@ $string['afterresource'] = 'After resource "{$a}"'; $string['aftersection'] = 'After section "{$a}"'; $string['again'] = 'again'; +$string['agelocationverification'] = 'Age and location verification'; $string['aimid'] = 'AIM ID'; $string['ajaxuse'] = 'AJAX and Javascript'; $string['all'] = 'All'; @@ -170,6 +171,7 @@ $string['back'] = 'Back'; $string['backto'] = 'Back to {$a}'; $string['backtocourselisting'] = 'Back to course listing'; +$string['backtohome'] = 'Back to the site home'; $string['backtopageyouwereon'] = 'Back to the page you were on'; $string['backtoparticipants'] = 'Back to participants list'; $string['backup'] = 'Backup'; @@ -267,6 +269,7 @@ $string['confirmednot'] = 'Your registration has not yet been confirmed!'; $string['confirmcheckfull'] = 'Are you absolutely sure you want to confirm {$a} ?'; $string['confirmcoursemove'] = 'Are you sure you want to move this course ({$a->course}) into this category ({$a->category})?'; +$string['considereddigitalminor'] = 'You are considered to be a digital minor.'; $string['content'] = 'Content'; $string['continue'] = 'Continue'; $string['continuetocourse'] = 'Click here to enter your course'; @@ -498,6 +501,8 @@ $string['deselectall'] = 'Deselect all'; $string['detailedless'] = 'Less detailed'; $string['detailedmore'] = 'More detailed'; +$string['digitalminor'] = 'Digital minor'; +$string['digitalminor_desc'] = 'To create an account on this site please have your parent/guardian contact the following person.'; $string['directory'] = 'Directory'; $string['disable'] = 'Disable'; $string['disabledcomments'] = 'Comments are disabled'; @@ -794,6 +799,7 @@ $string['expandall'] = 'Expand all'; $string['expandcategory'] = 'Expand {$a}'; $string['explanation'] = 'Explanation'; +$string['explanationdigitalminor'] = 'This information is required to determine if your age is over the digital age of consent. This is the age when an individual can consent to terms and conditions and their data being legally stored and processed.'; $string['extendperiod'] = 'Extended period'; $string['failedloginattempts'] = '{$a->attempts} failed logins since your last login'; $string['feedback'] = 'Feedback'; @@ -1547,6 +1553,7 @@ $string['privatefiles'] = 'Private files'; $string['private_files_handler'] = 'Store attachments to an e-mail in the user\'s private files storage space.'; $string['private_files_handler_name'] = 'Email to Private files'; +$string['proceed'] = 'Proceed'; $string['profile'] = 'Profile'; $string['profilenotshown'] = 'This profile description will not be shown until this person is enrolled in at least one course.'; $string['publicprofile'] = 'Public profile'; @@ -2102,8 +2109,11 @@ {$a->profileurl}'; $string['whatforlink'] = 'What do you want to do with the link?'; $string['whatforpage'] = 'What do you want to do with the text?'; +$string['whatisyourage'] = 'What is your age?'; $string['whattocallzip'] = 'What do you want to call the zip file?'; $string['whattodo'] = 'What to do'; +$string['wheredoyoulive'] = 'In which country do you live?'; +$string['whyisthisrequired'] = 'Why is this required?'; $string['windowclosing'] = 'This window should close automatically. If not, please close it now.'; $string['withchosenfiles'] = 'With chosen files'; $string['withdisablednote'] = '{$a} (disabled)'; diff --git a/lib/adminlib.php b/lib/adminlib.php index 33f60e89843d4..0e0deff7a73a0 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -10579,3 +10579,50 @@ public function get_force_ltr() { return true; } } + +/** + * Used to validate the content and format of the age of digital consent map and ensuring it is parsable. + * + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2018 Mihail Geshoski + */ +class admin_setting_agedigitalconsentmap extends admin_setting_configtextarea { + + /** + * Constructor. + * + * @param string $name + * @param string $visiblename + * @param string $description + * @param mixed $defaultsetting string or array + * @param mixed $paramtype + * @param string $cols + * @param string $rows + */ + public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype = PARAM_RAW, + $cols = '60', $rows = '8') { + parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype, $cols, $rows); + // Pre-set force LTR to false. + $this->set_force_ltr(false); + } + + /** + * Validate the content and format of the age of digital consent map to ensure it is parsable. + * + * @param string $data The age of digital consent map from text field. + * @return mixed bool true for success or string:error on failure. + */ + public function validate($data) { + if (empty($data)) { + return true; + } + + try { + \core_auth\digital_consent::parse_age_digital_consent_map($data); + } catch (\moodle_exception $e) { + return get_string('invalidagedigitalconsent', 'admin', $e->getMessage()); + } + + return true; + } +} diff --git a/lib/db/caches.php b/lib/db/caches.php index 4f57dd71f7425..fb43616ec140a 100644 --- a/lib/db/caches.php +++ b/lib/db/caches.php @@ -362,4 +362,14 @@ 'simpledata' => true, 'staticacceleration' => true, ), + + // This is the user's pre sign-up session cache. + // This cache is used to record the user's pre sign-up data such as + // age of digital consent (minor) status, accepted policies, etc. + 'presignup' => array( + 'mode' => cache_store::MODE_SESSION, + 'simplekeys' => true, + 'simpledata' => true, + 'ttl' => 1800, + ), ); diff --git a/lib/db/services.php b/lib/db/services.php index 3a712b7772698..8cdb154783b3e 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -50,6 +50,13 @@ 'ajax' => true, 'loginrequired' => false, ), + 'core_auth_is_minor' => array( + 'classname' => 'core_auth_external', + 'methodname' => 'is_minor', + 'description' => 'Requests a check if a user is a digital minor.', + 'type' => 'read', + 'loginrequired' => false, + ), 'core_badges_get_user_badges' => array( 'classname' => 'core_badges_external', 'methodname' => 'get_user_badges', diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php index d4ad83dc010ec..6a5800d6eaca2 100644 --- a/lib/outputrenderers.php +++ b/lib/outputrenderers.php @@ -4451,6 +4451,30 @@ public function render_login_signup_form($form) { return $this->render_from_template('core/signup_form_layout', $context); } + /** + * Render the verify age and location page into a nice template for the theme. + * + * @param \core_auth\output\verify_age_location_page $page The renderable + * @return string + */ + protected function render_verify_age_location_page($page) { + $context = $page->export_for_template($this); + + return $this->render_from_template('core/auth_verify_age_location_page', $context); + } + + /** + * Render the digital minor contact information page into a nice template for the theme. + * + * @param \core_auth\output\digital_minor_page $page The renderable + * @return string + */ + protected function render_digital_minor_page($page) { + $context = $page->export_for_template($this); + + return $this->render_from_template('core/auth_digital_minor_page', $context); + } + /** * Renders a progress bar. * diff --git a/lib/templates/auth_digital_minor_page.mustache b/lib/templates/auth_digital_minor_page.mustache new file mode 100644 index 0000000000000..33b06885f8314 --- /dev/null +++ b/lib/templates/auth_digital_minor_page.mustache @@ -0,0 +1,37 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_auth/output/digital_minor_page + + Example context (json): + { + "logourl": "https://moodle.org/logo/moodle-logo.svg", + "sitename": "Site name", + "supportname": "John Doe", + "supportemail": "johndoe@example.com", + "homelink": "/" + } +}} +

{{#str}}considereddigitalminor{{/str}}

+

{{#str}}digitalminor_desc{{/str}}

+
+

{{{supportname}}}

+

{{{supportemail}}}

+
+ diff --git a/lib/templates/auth_verify_age_location_page.mustache b/lib/templates/auth_verify_age_location_page.mustache new file mode 100644 index 0000000000000..2d398f8a06704 --- /dev/null +++ b/lib/templates/auth_verify_age_location_page.mustache @@ -0,0 +1,37 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_auth/output/verify_age_location_page + + Example context (json): + { + "logourl": "https://moodle.org/logo/moodle-logo.svg", + "sitename": "Site name", + "error": "Error message", + "formhtml": "(Form html would go here)" + } +}} +{{#error}} + +{{/error}} +

{{#str}}agelocationverification{{/str}}

+{{{formhtml}}} +
+

{{#str}}whyisthisrequired{{/str}}

+

{{#str}}explanationdigitalminor{{/str}}

diff --git a/login/digital_minor.php b/login/digital_minor.php new file mode 100644 index 0000000000000..1126b43f4c63b --- /dev/null +++ b/login/digital_minor.php @@ -0,0 +1,64 @@ +. + +/** + * Display page to a digital minor. Display support contact details. + * + * @package core + * @subpackage auth + * @copyright 2018 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require('../config.php'); +require_once($CFG->libdir . '/authlib.php'); + +$authplugin = signup_is_enabled(); + +if (!$authplugin || !\core_auth\digital_consent::is_age_digital_consent_verification_enabled()) { + // Redirect user if signup or digital age of consent verification is disabled. + redirect(new moodle_url('/'), get_string('verifyagedigitalconsentnotpossible', 'error')); +} + +$PAGE->set_context(context_system::instance()); +$PAGE->set_url($CFG->wwwroot.'/login/digital_minor.php'); + +if (isloggedin() and !isguestuser()) { + // Prevent signing up when already logged in. + redirect(new moodle_url('/'), get_string('cannotsignup', 'error', fullname($USER))); +} + +$cache = cache::make('core', 'presignup'); +$isminor = $cache->get('isminor'); +if ($isminor !== 'yes') { + // Redirect when the signup session does not exists, minor check has not been done or the user is not a minor. + redirect(new moodle_url('/login/index.php')); +} + +$PAGE->navbar->add(get_string('login')); +$PAGE->navbar->add(get_string('digitalminor')); + +$PAGE->set_pagelayout('login'); +$PAGE->set_title(get_string('digitalminor')); +$sitename = format_string($SITE->fullname); +$PAGE->set_heading($sitename); + +$page = new \core_auth\output\digital_minor_page(); + +echo $OUTPUT->header(); +echo $OUTPUT->render($page); +echo $OUTPUT->footer(); + diff --git a/login/signup.php b/login/signup.php index 4e24aece1b545..dd2e2ce866982 100644 --- a/login/signup.php +++ b/login/signup.php @@ -60,6 +60,19 @@ exit; } +// If verification of age and location (digital minor check) is enabled. +if (\core_auth\digital_consent::is_age_digital_consent_verification_enabled()) { + $cache = cache::make('core', 'presignup'); + $isminor = $cache->get('isminor'); + if ($isminor === false) { + // The verification of age and location (minor) has not been done. + redirect(new moodle_url('/login/verify_age_location.php')); + } else if ($isminor === 'yes') { + // The user that attempts to sign up is a digital minor. + redirect(new moodle_url('/login/digital_minor.php')); + } +} + // Plugins can create pre sign up requests. // Can be used to force additional actions before sign up such as acceptance of policies, validations, etc. core_login_pre_signup_requests(); diff --git a/login/verify_age_location.php b/login/verify_age_location.php new file mode 100644 index 0000000000000..49644ae091259 --- /dev/null +++ b/login/verify_age_location.php @@ -0,0 +1,88 @@ +. + +/** + * Verify age and location (digital minor check). + * + * @package core + * @subpackage auth + * @copyright 2018 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require('../config.php'); +require_once($CFG->libdir . '/authlib.php'); + +$authplugin = signup_is_enabled(); + +if (!$authplugin || !\core_auth\digital_consent::is_age_digital_consent_verification_enabled()) { + // Redirect user if signup or digital age of consent verification is disabled. + redirect(new moodle_url('/'), get_string('verifyagedigitalconsentnotpossible', 'error')); +} + +$PAGE->set_context(context_system::instance()); +$PAGE->set_url(new moodle_url('/login/verify_age_location.php')); + +if (isloggedin() and !isguestuser()) { + // Prevent signing up when already logged in. + redirect(new moodle_url('/'), get_string('cannotsignup', 'error', fullname($USER))); +} + +$cache = cache::make('core', 'presignup'); +$isminor = $cache->get('isminor'); +if ($isminor === 'yes') { + // The user that attempts to sign up is a digital minor. + redirect(new moodle_url('/login/digital_minor.php')); +} else if ($isminor === 'no') { + // The user that attempts to sign up has already verified that they are not a digital minor. + redirect(new moodle_url('/login/signup.php')); +} + +$PAGE->navbar->add(get_string('login')); +$PAGE->navbar->add(get_string('agelocationverification')); + +$PAGE->set_pagelayout('login'); +$PAGE->set_title(get_string('agelocationverification')); +$sitename = format_string($SITE->fullname); +$PAGE->set_heading($sitename); + +$mform = new \core_auth\form\verify_age_location_form(); +$page = new \core_auth\output\verify_age_location_page($mform); + +if ($mform->is_cancelled()) { + redirect(new moodle_url('/login/index.php')); +} else if ($data = $mform->get_data()) { + try { + $isminor = \core_auth\digital_consent::is_minor($data->age, $data->country); + cache::make('core', 'presignup')->set('isminor', $isminor ? 'yes' : 'no'); + if ($isminor) { + redirect(new moodle_url('/login/digital_minor.php')); + } else { + redirect(new moodle_url('/login/signup.php')); + } + } catch (moodle_exception $e) { + // Display a user-friendly error message. + $errormessage = get_string('couldnotverifyagedigitalconsent', 'error'); + $page = new \core_auth\output\verify_age_location_page($mform, $errormessage); + echo $OUTPUT->header(); + echo $OUTPUT->render($page); + echo $OUTPUT->footer(); + } +} else { + echo $OUTPUT->header(); + echo $OUTPUT->render($page); + echo $OUTPUT->footer(); +} diff --git a/theme/boost/templates/core/auth_digital_minor_page.mustache b/theme/boost/templates/core/auth_digital_minor_page.mustache new file mode 100644 index 0000000000000..84a1cd2742fad --- /dev/null +++ b/theme/boost/templates/core/auth_digital_minor_page.mustache @@ -0,0 +1,58 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_auth/output/digital_minor_page + + Example context (json): + { + "logourl": "https://moodle.org/logo/moodle-logo.svg", + "sitename": "Site name", + "supportname": "John Doe", + "supportemail": "johndoe@example.com", + "homelink": "/" + } +}} +
+
+
+
+
+
+ {{#logourl}} +

{{sitename}}

+ {{/logourl}} + {{^logourl}} +

{{sitename}}

+ {{/logourl}} +
+
+
+

{{#str}}considereddigitalminor{{/str}}

+
+
+

{{#str}}digitalminor_desc{{/str}}

+

{{{supportname}}}

+

{{{supportemail}}}

+
+ +
+
+
+
+
diff --git a/theme/boost/templates/core/auth_verify_age_location_page.mustache b/theme/boost/templates/core/auth_verify_age_location_page.mustache new file mode 100644 index 0000000000000..323ecadc3edaf --- /dev/null +++ b/theme/boost/templates/core/auth_verify_age_location_page.mustache @@ -0,0 +1,64 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_auth/output/verify_age_location_page + + Example context (json): + { + "logourl": "https://moodle.org/logo/moodle-logo.svg", + "sitename": "Site name", + "error": "Error message", + "formhtml": "(Form html would go here)" + } +}} +
+
+
+
+
+
+ {{#logourl}} +

{{sitename}}

+ {{/logourl}} + {{^logourl}} +

{{sitename}}

+ {{/logourl}} +
+
+ {{#error}} + + {{/error}} +
+

{{#str}}agelocationverification{{/str}}

+
+
+ {{{formhtml}}} +
+
+
+

{{#str}}whyisthisrequired{{/str}}

+
+
+

{{#str}}explanationdigitalminor{{/str}}

+
+
+
+
+
+
diff --git a/version.php b/version.php index bdba23191beb5..da1853470ed1f 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2018022800.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2018022800.02; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.