Commit d46ebb31 authored by Cecilia Vela Gurovic's avatar Cecilia Vela Gurovic Committed by Gerrit Code Review
Browse files

Merge changes from topic 'GDPR'

* changes:
  Bug 1734169: Allow the user to say why he refuses the privacy
  Bug 1734169: Send message to admin when user rejects the privacy
  Bug 1734171: Revoke privacy consent
  Bug 1734169: Add privacy statement to the register form
parents e04aa87c 35a117ac
......@@ -22,26 +22,33 @@ if (!is_logged_in()) {
throw new AccessDeniedException();
}
// Get all institutions of a user.
$userinstitutions = array_keys($USER->get('institutions'));
// Include the 'mahara' institution so that we may show the site privacy statement as well.
array_push($userinstitutions, 'mahara');
// Get all the latest privacy statement (institution and site) the user has agreed to.
$data = get_latest_privacy_versions($userinstitutions);
$form = privacy_form();
// JQuery logic for panel hide/show.
// Needed here because there are multiple dropdown panels on this page.
$js = <<< EOF
function showPanel(el) {
elementid = $(el).attr('id');
$("#dropdown" + elementid).toggleClass("collapse");
$( document ).ready(function() {
$(".state-label").click(function() {
$(this).siblings( ".switch-inner" ).toggleClass("redraw-consent");
showSubmitButton();
});
});
function showSubmitButton() {
if ($('body').find(".redraw-consent").length == 0) {
$('#agreetoprivacy_submit_container').addClass('js-hidden');
$('#agreetoprivacy_submit').addClass('js-hidden');
}
else {
$('#agreetoprivacy_submit_container').removeClass('js-hidden');
$('#agreetoprivacy_submit').removeClass('js-hidden');
}
}
EOF;
$smarty = smarty();
setpageicon($smarty, 'icon-umbrella');
$smarty->assign('results', $data);
$smarty->assign('form', $form);
$smarty->assign('INLINEJAVASCRIPT', $js);
$smarty->assign('description', get_string('userprivacypagedescription', 'admin'));
$smarty->display('account/userprivacy.tpl');
......@@ -742,6 +742,79 @@ function auth_get_available_auth_types($institution=null) {
return $result;
}
/**
* Build the agree with or withdraw consent to privacy statement
*
* @param ignoreagreevalue true when a new privacy statement needs to be accepted,
* false when the form will be displayed to allow the consent withdraw.
* @return form
*/
function privacy_form($ignoreagreevalue = false) {
global $USER;
// Get all institutions of a user.
$userinstitutions = array_keys($USER->get('institutions'));
// Include the 'mahara' institution so that we may show the site privacy statement as well.
array_push($userinstitutions, 'mahara');
// Check if there are new privacies that need to be accepted.
$latestversions = get_latest_privacy_versions($userinstitutions, $ignoreagreevalue);
if (empty($latestversions)) {
// We may be masquerading as user
return '<div>' . get_string('noprivacystatementsaccepted', 'account') . '</div>';
}
foreach ($latestversions as $privacy) {
$privacytitle = $privacy->institution == 'mahara' ? get_string('siteprivacystatement', 'admin') : get_string('institutionprivacystatement', 'admin');
$smarty = smarty_core();
$smarty->assign('privacy', $privacy);
$smarty->assign('privacytitle', $privacytitle);
$smarty->assign('privacytime', format_date(strtotime($privacy->ctime)));
$smarty->assign('ignoreagreevalue', $ignoreagreevalue);
$htmlbegin = $smarty->fetch('privacy_panel_begin.tpl');
//Build form elements.
$elements[$privacy->institution . 'text'] = array(
'type' => 'markup',
'value' => $htmlbegin,
);
$elements[$privacy->institution . 'id'] = array(
'type' => 'hidden',
'value' => $privacy->id,
);
$elements[$privacy->institution] = array(
'type' => 'switchbox',
'title' => get_string('privacyagreement', 'admin'),
'description' => $privacy->agreed ? get_string('privacyagreedto', 'admin', format_date(strtotime($privacy->agreedtime))) : '',
'defaultvalue' => $privacy->agreed ? true : false,
'disabled' => ($privacy->agreed && $ignoreagreevalue) ? true : false,
'required' => true,
);
$elements[$privacy->institution . 'switch'] = array(
'type' => 'hidden',
'value' => ($privacy->agreed && $ignoreagreevalue) ? 'disabled' : 'enabled',
);
$smarty = smarty_core();
$smarty->assign('ignoreagreevalue', $ignoreagreevalue);
$htmlend = $smarty->fetch('privacy_panel_end.tpl');
$elements[$privacy->institution . 'text2'] = array(
'type' => 'markup',
'value' => $htmlend,
);
}
$classhidden = $ignoreagreevalue ? '' : 'js-hidden';
$elements['submit'] = array(
'class' => 'btn-primary ' . $classhidden,
'type' => 'submit',
'value' => get_string('savechanges', 'admin')
);
$form = pieform(array(
'name' => 'agreetoprivacy',
'elements' => $elements,
));
return $form;
}
/**
* Checks that all the required fields are set, and handles setting them if required.
*
......@@ -762,45 +835,9 @@ function auth_check_required_fields() {
}
// Privacy statement.
if (get_config('institutionstrictprivacy') && !$USER->has_latest_agreement() && !$restoreadmin && !$loginanyway) {
// Get all institutions of a user.
$userinstitutions = array_keys($USER->get('institutions'));
// Include the 'mahara' institution so that we may show the site privacy statement as well.
array_push($userinstitutions, 'mahara');
// Check if there are new privacies that need to be accepted.
$latestversions = get_latest_privacy_versions($userinstitutions, true);
foreach ($latestversions as $privacy) {
$elements[$privacy->institution . 'text'] = array(
'type' => 'markup',
'value' => '<h2>' . ($privacy->institution == 'mahara' ? get_string('siteprivacystatement', 'admin') : get_string('institutionprivacystatement', 'admin')) . '</h2>' . $privacy->content,
);
$elements[$privacy->institution . 'id'] = array(
'type' => 'hidden',
'value' => $privacy->id,
);
$elements[$privacy->institution] = array(
'type' => 'switchbox',
'title' => get_string('privacyagreement', 'admin'),
'description' => $privacy->agreed ? get_string('privacyagreedto', 'admin', format_date(strtotime($privacy->agreedtime))) : '',
'defaultvalue' => $privacy->agreed ? true : false,
'disabled' => $privacy->agreed ? true : false,
'required' => true,
);
$elements[$privacy->institution . 'switch'] = array(
'type' => 'hidden',
'value' => $privacy->agreed ? 'disabled' : 'enabled',
);
}
$elements['submit'] = array(
'class' => 'btn-primary',
'type' => 'submit',
'value' => get_string('savechanges', 'admin')
);
$form = pieform(array(
'name' => 'agreetoprivacy',
'elements' => $elements,
));
// Build the agree with privacy statement form.
$form = privacy_form(true);
define('TITLE', get_string('privacy', 'admin'));
$smarty = smarty();
setpageicon($smarty, 'icon-umbrella');
......@@ -810,7 +847,8 @@ function auth_check_required_fields() {
'<strong><a class="" href="' . get_config('wwwroot') . '?loginanyway">', '</a></strong>'));
}
$smarty->assign('form', $form);
$smarty->display('account/useracceptprivacy.tpl');
$smarty->assign('description', get_string('newprivacy', 'admin'));
$smarty->display('account/userprivacy.tpl');
exit;
}
......@@ -1206,6 +1244,7 @@ function agreetoprivacy_submit(Pieform $form, $values) {
array_push($userinstitutions, 'mahara');
$hasrefused = param_integer('hasrefused', 0);
$reason = param_variable('reason', '');
foreach ($userinstitutions as $institution) {
// check if the institution has a privacy statement
......@@ -1219,6 +1258,10 @@ function agreetoprivacy_submit(Pieform $form, $values) {
save_user_reply_to_agreement($USER->get('id'), $values[$institution . 'id'], $agreed);
$SESSION->add_ok_msg(get_string('agreementsaved', 'admin'));
if ($hasrefused) {
// Send a message to the institution/site admin informing that the user has refused the privacy statement.
$institution = new Institution($institution);
$institution->send_admin_institution_refused_privacy_message($USER->get('id'), $reason);
suspend_user($USER->get('id'), 'privacyrefusal');
$SESSION->add_ok_msg(get_string('usersuspended', 'admin'));
$USER->logout();
......@@ -2330,6 +2373,50 @@ function auth_generate_registration_form($formname, $authname='internal', $goto)
)
);
}
// Add site privacy statement and T&C to the register form.
$siteprivacy = get_latest_privacy_versions(array('mahara'));
$elements['privacy'] = array(
'type' => 'markup',
'value' => '<div id ="siteprivacy">' .
'<h2>' . get_string('siteprivacystatement', 'admin') . '</h2>' .
'<p class="text-midtone">' . get_string('registerprivacy1') . '</p>' .
'<div id ="siteprivacytext">' . $siteprivacy[0]->content . '</div>' .
'</div>',
);
$elements['privacyswitch'] = array(
'type' => 'switchbox',
'title' => get_string('privacyagreement', 'admin'),
'description' => get_string('registerprivacydetails', 'admin'),
'required' => true,
);
$elements['privacyid'] = array(
'type' => 'hidden',
'value' => $siteprivacy[0]->id,
);
// Add institution privacy if an institution has been selected.
$elements['instprivacy'] = array(
'type' => 'markup',
'value' => '<div id ="instprivacy" class ="js-hidden">' .
'<h2>' . get_string('institutionprivacystatement', 'admin') . '</h2>' .
'<p class="text-midtone">' . get_string('registerprivacy1') . '</p>' .
'<div id ="instprivacytext"></div>' .
'</div>',
);
$elements['instprivacyswitch'] = array(
'type' => 'switchbox',
'title' => get_string('privacyagreement', 'admin'),
'description' => get_string('registerprivacydetails', 'admin'),
'class' => 'instprivacyswitch js-hidden',
);
$elements['instprivacyid'] = array(
'type' => 'text',
'class' => 'js-hidden',
);
// Add the terms and conditions.
$elements['terms'] = array(
'type' => 'markup',
'value' => "<h2>Terms and condititions</h2>" . get_site_page_content('termsandconditions'),
);
$registerterms = get_config('registerterms');
if ($registerterms) {
......@@ -2413,17 +2500,19 @@ function auth_generate_registration_form_js($aform, $registerconfirm) {
});
';
}
else {
$url = get_config('wwwroot') . 'json/termsandconditions.php';
$js = '
// Display the institution privacy statement, if it exist.
$url = get_config('wwwroot') . 'json/privacystatement.php';
$js = '
var registerconfirm = ' . json_encode($registerconfirm) . ';
jQuery(function($) {
function show_reason(reasonid, value) {
if (value) {
$("#" + reasonid + "_container").removeClass("js-hidden");
$("#" + reasonid + "_container textarea").removeClass("js-hidden");
$("#" + reasonid + "_container").next("tr.textarea").removeClass("js-hidden");
// need to fetch the correct terms and conditions for the institution
function show_privacy(institutionid, value) {
$("#register_instprivacyid").attr("value", "");
$("#instprivacy").addClass("js-hidden");
$("#instprivacytext").html("");
$(".instprivacyswitch").addClass("js-hidden");
if (value !== "0" && value !== "mahara") {
// Fetch the institution privacy statement.
$.ajax({
type: "POST",
dataType: "json",
......@@ -2432,11 +2521,21 @@ function auth_generate_registration_form_js($aform, $registerconfirm) {
"institution": value,
}
}).done(function (data) {
if (data.content) {
$("#termscontainer").html(data.content);
if (data && data.content) {
$("#instprivacy").removeClass("js-hidden");
$("#instprivacytext").html(data.content);
$(".instprivacyswitch").removeClass("js-hidden");
$("#register_instprivacyid").attr("value", data.id);
}
});
}
}
function show_reason(reasonid, value) {
if (value) {
$("#" + reasonid + "_container").removeClass("js-hidden");
$("#" + reasonid + "_container textarea").removeClass("js-hidden");
$("#" + reasonid + "_container").next("tr.textarea").removeClass("js-hidden");
}
else {
$("#" + reasonid + "_container").addClass("js-hidden");
$("#" + reasonid + "_container textarea").addClass("js-hidden");
......@@ -2446,22 +2545,28 @@ function auth_generate_registration_form_js($aform, $registerconfirm) {
// For when page loads after error found on form completion
var defaultselect = $j("#' . $institutionid . '").val();
var reasonid = "' . $reasonid . '";
if (defaultselect != 0 && registerconfirm[defaultselect] == 1) {
show_reason(reasonid, defaultselect);
if (defaultselect != 0) {
if (registerconfirm[defaultselect] == 1) {
show_reason(reasonid, defaultselect);
}
show_privacy("' . $institutionid . '", defaultselect);
}
// For when select changes
$("#' . $institutionid . '").change(function() {
if (this.value && registerconfirm[this.value] == 1) {
show_reason(reasonid, this.value);
}
else {
show_reason(reasonid, null);
if (this.value) {
if (registerconfirm[this.value] == 1) {
show_reason(reasonid, this.value);
}
else {
show_reason(reasonid, null);
}
show_privacy("' . $institutionid . '", this.value);
}
});
});
';
}
';
return array($formhtml, $js);
}
......@@ -2505,6 +2610,16 @@ function auth_register_validate(Pieform $form, $values) {
$institution = $values['institution'];
safe_require('auth', 'internal');
// Privacy statements must have been accepted by the user.
if (!$values['instprivacyswitch'] && $values['instprivacyid'] != '') {
$SESSION->add_error_msg(get_string('registerprivacyrefusal', 'admin'));
$form->set_error('instprivacyswitch', get_string('registerprivacyrefusal', 'admin'));
}
if (!$values['privacyswitch']) {
$SESSION->add_error_msg(get_string('registerprivacyrefusal', 'admin'));
$form->set_error('privacyswitch', get_string('registerprivacyrefusal', 'admin'));
}
// First name and last name must contain at least one non whitespace
// character, so that there's something to read
if (!$form->get_error('firstname') && !preg_match('/\S/', $values['firstname'])) {
......@@ -2590,7 +2705,9 @@ function auth_register_submit(Pieform $form, $values) {
if (function_exists('local_register_submit')) {
local_register_submit($values);
}
$extra = new StdClass;
$extra->privacy = array($values['privacyid'], $values['instprivacyid']);
$values['extra'] = serialize($extra);
try {
if (!record_exists('usr_registration', 'email', $values['email'])) {
insert_record('usr_registration', $values);
......
<?php
/**
*
* @package mahara
* @subpackage core
* @author Catalyst IT Ltd
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
* @copyright For copyright information on Mahara, please see the README file distributed with this software.
*
*/
define('INTERNAL', 1);
define('PUBLIC', 1);
define('JSON', 1);
define('NOSESSKEY', 1);
require(dirname(dirname(__FILE__)) . '/init.php');
$institution = param_alphanum('institution', null);
// Get the institution privacy statement.
$privacy = get_latest_privacy_versions(array($institution));
json_headers();
print json_encode($privacy[0]);
......@@ -84,3 +84,4 @@ $string['resizeonuploaduserdefaultdescription2'] = '"Automatic resizing of image
$string['devicedetection'] = 'Device detection';
$string['devicedetectiondescription'] = 'Enable mobile device detection when browsing this site.';
$string['noprivacystatementsaccepted'] = 'This account has not accepted any current privacy statements.';
\ No newline at end of file
......@@ -1356,3 +1356,7 @@ $string['refuseprivacy'] = 'Refuse privacy statement';
$string['confirmprivacyrefusal'] = 'Are you really sure you wish to continue?';
$string['privacyrefusaldetails'] = 'If you do not consent to the privacy statement, your account will be suspended.';
$string['privacyrefusal'] = 'Refused to consent to the privacy statement.';
$string['registerprivacyrefusal'] = 'Your account will not be created when you do not consent to the privacy statement.';
$string['registerprivacydetails'] = 'Please read the privacy statement. If you do not consent to it, you cannot create an account on the site.';
$string['enterreason'] = 'Please enter the reason of refusal here...';
$string['hasrefused'] = 'has refused the privacy statement';
......@@ -483,7 +483,7 @@ $string['displayname'] = 'Display name';
$string['fullname'] = 'Full name';
$string['registerwelcome'] = 'Welcome! To use this site you must register first.';
$string['registeragreeterms'] = 'You must also agree to the <a href="terms.php">terms and conditions</a>.';
$string['registerprivacy'] = 'The data we collect here will be stored according to our <a href="privacy.php">privacy statement</a>.';
$string['registerprivacy1'] = 'The data we collect here will be stored according to our privacy statement.';
$string['registerstep3fieldsoptional'] = '<h3>Choose an optional profile picture</h3><p>You have now successfully registered with %s. You may now choose an optional profile picture to be displayed as your avatar.</p>';
$string['registerstep3fieldsmandatory'] = '<h3>Fill out mandatory profile fields</h3><p>The following fields are required. You must fill them out before your registration is complete.</p>';
$string['registeringdisallowed'] = 'Sorry, you cannot register for this system at this time.';
......@@ -767,6 +767,16 @@ Please clean up existing user accounts or ask to have the maximum number of allo
Regards,
The %s Team';
$string['institutionmemberrefusedprivacy'] = 'Hello %s,
The user %s, with the username %s, has refused the privacy statement. Their user account was suspended.
%s %s
Please contact the user via email at %s if you wish to discuss the refusal.
Regards,
The %s Team';
$string['thereasonis'] = 'The user\'s reason is:';
$string['config'] = 'Configuration';
$string['sendmessage'] = 'Send message';
......
......@@ -464,6 +464,50 @@ class Institution {
}
}
}
/**
* Send a message to the site admin or to the institution admin when a user refuses the privacy statement.
*
* If the user is part of an institution and the institution has admin(s), send the message just to the inst. admin(s).
* Else send the messege to the site admin(s).
*
* @param integer $studentid The id of the user who has refused the privacy statement.
* @param string $reason The reson why the user refused the privacy statement.
*/
public function send_admin_institution_refused_privacy_message($studentid, $reason) {
$student = new User();
$student->find_by_id($studentid);
$studentname = display_name($student, null, true);
// Get the institution admin user records.
$admins = $this->admins();
// If the user is not part of an institution OR his institution has no admin, send the message to the site admin.
if (empty($admins)) {
$admins = $this->institution_and_site_admins();
}
$thereasonis = '';
if ($reason != '') {
$thereasonis = get_string('thereasonis', 'mahara');
$reason = '"' . urldecode($reason) . '"';
}
// check if there are admins - otherwise there are no site admins?!?!?
if (count($admins) > 0) {
require_once('activity.php');
// send an email/message to each amdininistrator based on their specific language.
foreach ($admins as $index => $id) {
$lang = get_user_language($id);
$user = new User();
$user->find_by_id($id);
$message = (object) array(
'users' => array($id),
'subject' => $studentname . ' ' . get_string('hasrefused', 'admin'),
'message' => get_string_from_language($lang, 'institutionmemberrefusedprivacy', 'mahara',
$user->firstname, $studentname, $student->username,
$thereasonis, $reason, $student->email, get_config('sitename')),
);
activity_occurred('maharamessage', $message);
}
}
}
public function declineRequestFromUser($userid) {
$lang = get_user_language($userid);
......
......@@ -3235,18 +3235,26 @@ function get_site_admins() {
function get_latest_privacy_versions($institutions = array(), $ignoreagreevalue = false) {
global $USER;
$joinsql = $ignoreagreevalue ? 'LEFT JOIN' : 'JOIN';
$userdetails = '';
$useragreementsql = '';
$params = array();
if ($USER->is_logged_in()) {
$userdetails = ' u.agreed, u.ctime AS agreedtime,';
$joinsql = $ignoreagreevalue ? 'LEFT JOIN' : 'JOIN';
$useragreementsql = $joinsql . " {usr_agreement} u ON s2.current = u.sitecontentid AND u.usr = ? AND u.agreed = 1";
$params = array($USER->get('id'));
}
$latestversions = get_records_sql_assoc("
SELECT s.id, s.version, s.content, s.ctime, s.institution, u.agreed, u.ctime AS agreedtime,
$latestversions = get_records_sql_array("
SELECT s.id, s.version, s.content, s.ctime, s.institution, " . $userdetails . "
CASE s.institution WHEN 'mahara' THEN 1 ELSE 2 END as type
FROM {site_content_version} s
INNER JOIN (SELECT MAX(id) as current, institution
FROM {site_content_version}
GROUP BY institution) s2 ON s.institution = s2.institution AND s.id = s2.current
{$joinsql} {usr_agreement} u ON s2.current = u.sitecontentid AND u.usr = ? AND u.agreed = 1
WHERE s.institution IN (" . join(',',array_map('db_quote',$institutions)) . ")
ORDER BY type", array($USER->get('id')));
" . $useragreementsql . "
WHERE s.institution IN (" . join(',',array_map('db_quote', $institutions)) . ")
ORDER BY type", $params);
return $latestversions;
}
......
......@@ -172,7 +172,12 @@ if (isset($key)) {
set_field('usr_institution', 'staff', 1, 'usr', $user->id, 'institution', $registration->institution);
}
}
// Save in DB the privacy statement(s) the user has accepted while registering.
if (!empty($extrafields->privacy)) {
foreach ($extrafields->privacy as $privacyid) {
save_user_reply_to_agreement($user->id, $privacyid, 1);
}
}
if (!empty($registration->lang) && $registration->lang != 'default') {
set_account_preference($user->id, 'lang', $registration->lang);
......@@ -207,16 +212,9 @@ if (!$form) {
list($formhtml, $js) = auth_generate_registration_form_js($form, $registerconfirm);
$registerdescription = get_string('registerwelcome');
if ($registerterms = get_config('registerterms')) {
$registerdescription .= ' ' . get_string('registeragreeterms');
}
$registerdescription .= ' ' . get_string('registerprivacy');
$smarty = smarty();
$smarty->assign('register_form', $formhtml);
$smarty->assign('registerdescription', $registerdescription);
if ($registerterms) {
$smarty->assign('termsandconditions', '<a name="user_acceptterms"></a>' . get_site_page_content('termsandconditions'));
}
$smarty->assign('INLINEJAVASCRIPT', $js);
$smarty->display('register.tpl');
{include file="header.tpl"}
<div class="lead">{str tag="userprivacypagedescription" section="admin"}</div>
{foreach from=$results item=result key=key}
<div class="panel panel-default" id="{$result->id}" onclick="showPanel(this)">
<div class="last form-group collapsible-group">
<fieldset class="pieform-fieldset last collapsible">
<legend>
<h4>
<a href="#dropdown" data-toggle="collapse" aria-expanded="false" aria-controls="dropdown" class="collapsed">
{if $result->institution == 'mahara'}
{str tag="siteprivacystatement" section="admin"}
{else}
{str tag="institutionprivacystatement" section="admin"}
{/if}
<span class="icon icon-chevron-down collapse-indicator right pull-right"> </span>
</a>
</h4>
</legend>
<div class="fieldset-body collapse" id="dropdown{$result->id}">
<span class="text-midtone pull-right">{str tag="lastupdated" section="admin"} {$result->ctime|date_format:'%d %B %Y %H:%M %p'}</span>
<br>
{$result->content|safe}
</div>
</fieldset>
</div>
</div>
{/foreach}
{if $loginanyway}
<p class="lead alert alert-warning">
{$loginanyway|safe}
</p>
{/if}
<div class="lead">{$description}</div>
<div>{$form|safe}</div>
{include file="privacy_modal.tpl"}
{include file="footer.tpl"}
{include file="header.tpl"}
{if $loginanyway}
<p class="lead">
{$loginanyway|safe}
</p>
{/if}
<div class="lead">{str tag="newprivacy" section="admin"}</div>
<div>{$form|safe}</div>