From 9810e36e19ba664ae84e7fdd2878654d51e892dc Mon Sep 17 00:00:00 2001
From: srosse <stephane.rosse@frentix.com>
Date: Tue, 5 Feb 2019 10:00:58 +0100
Subject: [PATCH] OO-3519, OO-3520: allow account deletion requests by users

Configuration and implementation to allow the users to request the
deletion of their account if they don't accept the disclaimer or in
their settings under the disclaimers.
---
 .../basesecurity/BaseSecurityManager.java     |   1 -
 .../core/dispatcher/DispatcherModule.java     |   4 -
 .../ldap/ui/LDAPAuthenticationController.java |   2 +-
 .../login/OLATAuthenticationController.java   |   5 +-
 .../oauth/ui/OAuthDisclaimerController.java   |   2 +-
 .../oauth/ui/OAuthRegistrationController.java |   2 +-
 .../registration/DisclaimerController.java    | 171 +++++++++++++++---
 .../registration/RegistrationController.java  |   2 +-
 .../registration/RegistrationManager.java     |   7 +
 ...equestAccountDataDeletetionController.java |  84 +++++++++
 .../RequestAccountDeletionController.java     | 157 ++++++++++++++++
 .../registration/_content/disclaimer.html     |   3 +
 .../registration/_content/request_delete.html |   9 +
 .../_content/request_delete_data.html         |  11 ++
 .../_i18n/LocalStrings_de.properties          |  11 ++
 .../_i18n/LocalStrings_en.properties          |  11 ++
 .../shibboleth/ShibDisclaimerController.java  |   2 +-
 .../ShibbolethRegistrationController.java     |   9 +-
 src/main/java/org/olat/user/UserModule.java   |  77 ++++++--
 .../org/olat/user/UserSettingsController.java |   2 +-
 .../org/olat/user/_spring/userContext.xml     |  20 ++
 ...UserAccountDeletionSettingsController.java | 124 +++++++++++++
 .../ui/admin/_i18n/LocalStrings_de.properties |   8 +
 .../ui/admin/_i18n/LocalStrings_en.properties |   8 +
 .../resources/serviceconfig/olat.properties   |   5 +
 .../basesecurity/BaseSecurityManagerTest.java |  12 +-
 26 files changed, 681 insertions(+), 68 deletions(-)
 create mode 100644 src/main/java/org/olat/registration/RequestAccountDataDeletetionController.java
 create mode 100644 src/main/java/org/olat/registration/RequestAccountDeletionController.java
 create mode 100644 src/main/java/org/olat/registration/_content/request_delete.html
 create mode 100644 src/main/java/org/olat/registration/_content/request_delete_data.html
 create mode 100644 src/main/java/org/olat/user/ui/admin/UserAccountDeletionSettingsController.java

diff --git a/src/main/java/org/olat/basesecurity/BaseSecurityManager.java b/src/main/java/org/olat/basesecurity/BaseSecurityManager.java
index c3a897b7637..862547f78e8 100644
--- a/src/main/java/org/olat/basesecurity/BaseSecurityManager.java
+++ b/src/main/java/org/olat/basesecurity/BaseSecurityManager.java
@@ -359,7 +359,6 @@ public class BaseSecurityManager implements BaseSecurity, UserDataDeletable {
 		IdentityImpl iimpl = new IdentityImpl();
 		iimpl.setUser(user);
 		iimpl.setName(username);
-		iimpl.setLastLogin(new Date());
 		iimpl.setExternalId(externalId);
 		iimpl.setStatus(Identity.STATUS_ACTIV);
 		((UserImpl)user).setIdentity(iimpl);
diff --git a/src/main/java/org/olat/core/dispatcher/DispatcherModule.java b/src/main/java/org/olat/core/dispatcher/DispatcherModule.java
index e5a9f3530db..4a7b19393dc 100644
--- a/src/main/java/org/olat/core/dispatcher/DispatcherModule.java
+++ b/src/main/java/org/olat/core/dispatcher/DispatcherModule.java
@@ -136,10 +136,6 @@ public class DispatcherModule {
 	public static final void redirectToDefaultDispatcher(HttpServletResponse response) {
 		redirectTo(response, WebappHelper.getServletContextPath() + PATH_DEFAULT);
 	}
-	
-	public static final void redirectToMobile(HttpServletResponse response) {
-		redirectTo(response, WebappHelper.getServletContextPath() + WebappHelper.getMobileContext());
-	}
 
 	/**
 	 * Generic redirect method.
diff --git a/src/main/java/org/olat/ldap/ui/LDAPAuthenticationController.java b/src/main/java/org/olat/ldap/ui/LDAPAuthenticationController.java
index 3611692916e..2217ebe4ea5 100644
--- a/src/main/java/org/olat/ldap/ui/LDAPAuthenticationController.java
+++ b/src/main/java/org/olat/ldap/ui/LDAPAuthenticationController.java
@@ -227,7 +227,7 @@ public class LDAPAuthenticationController extends AuthenticationController imple
 				// accept disclaimer first
 				
 				removeAsListenerAndDispose(disclaimerCtr);
-				disclaimerCtr = new DisclaimerController(ureq, getWindowControl());
+				disclaimerCtr = new DisclaimerController(ureq, getWindowControl(), authenticatedIdentity, false);
 				listenTo(disclaimerCtr);
 				
 				removeAsListenerAndDispose(cmc);
diff --git a/src/main/java/org/olat/login/OLATAuthenticationController.java b/src/main/java/org/olat/login/OLATAuthenticationController.java
index 8279840daa8..240b85a7bc0 100644
--- a/src/main/java/org/olat/login/OLATAuthenticationController.java
+++ b/src/main/java/org/olat/login/OLATAuthenticationController.java
@@ -175,9 +175,6 @@ public class OLATAuthenticationController extends AuthenticationController imple
 		cmc.activate();
 	}
 
-	/**
-	 * @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest, org.olat.core.gui.control.Controller, org.olat.core.gui.control.Event)
-	 */
 	@Override
 	public void event(UserRequest ureq, Controller source, Event event) {
 		if (source == loginForm && event == Event.DONE_EVENT) {
@@ -218,7 +215,7 @@ public class OLATAuthenticationController extends AuthenticationController imple
 				// accept disclaimer first
 				
 				removeAsListenerAndDispose(disclaimerCtr);
-				disclaimerCtr = new DisclaimerController(ureq, getWindowControl());
+				disclaimerCtr = new DisclaimerController(ureq, getWindowControl(), authenticatedIdentity, false);
 				listenTo(disclaimerCtr);
 				
 				removeAsListenerAndDispose(cmc);
diff --git a/src/main/java/org/olat/login/oauth/ui/OAuthDisclaimerController.java b/src/main/java/org/olat/login/oauth/ui/OAuthDisclaimerController.java
index 56462460103..835ade37a29 100644
--- a/src/main/java/org/olat/login/oauth/ui/OAuthDisclaimerController.java
+++ b/src/main/java/org/olat/login/oauth/ui/OAuthDisclaimerController.java
@@ -80,7 +80,7 @@ public class OAuthDisclaimerController extends FormBasicController implements Ac
 
 	@Override
 	public void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) {
-		disclaimerController = new DisclaimerController(ureq, getWindowControl());
+		disclaimerController = new DisclaimerController(ureq, getWindowControl(), null, false);
 		listenTo(disclaimerController);
 		
 		cmc = new CloseableModalController(getWindowControl(), translate("close"), disclaimerController.getInitialComponent(),
diff --git a/src/main/java/org/olat/login/oauth/ui/OAuthRegistrationController.java b/src/main/java/org/olat/login/oauth/ui/OAuthRegistrationController.java
index 0df609b858e..7ec37f240c8 100644
--- a/src/main/java/org/olat/login/oauth/ui/OAuthRegistrationController.java
+++ b/src/main/java/org/olat/login/oauth/ui/OAuthRegistrationController.java
@@ -228,7 +228,7 @@ public class OAuthRegistrationController extends FormBasicController {
 		
 		//open disclaimer
 		removeAsListenerAndDispose(disclaimerController);
-		disclaimerController = new DisclaimerController(ureq, getWindowControl());
+		disclaimerController = new DisclaimerController(ureq, getWindowControl(), authenticatedIdentity, false);
 		listenTo(disclaimerController);
 		
 		cmc = new CloseableModalController(getWindowControl(), translate("close"), disclaimerController.getInitialComponent(),
diff --git a/src/main/java/org/olat/registration/DisclaimerController.java b/src/main/java/org/olat/registration/DisclaimerController.java
index 5b474cfdc30..826425fbb2d 100644
--- a/src/main/java/org/olat/registration/DisclaimerController.java
+++ b/src/main/java/org/olat/registration/DisclaimerController.java
@@ -28,6 +28,7 @@ package org.olat.registration;
 import java.io.File;
 import java.util.Locale;
 
+import org.olat.admin.user.delete.service.UserDeletionManager;
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.Component;
 import org.olat.core.gui.components.link.Link;
@@ -37,11 +38,18 @@ import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.controller.BasicController;
+import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController;
+import org.olat.core.id.Identity;
+import org.olat.core.id.UserConstants;
 import org.olat.core.util.WebappHelper;
+import org.olat.core.util.mail.ContactList;
+import org.olat.core.util.mail.ContactMessage;
 import org.olat.core.util.vfs.LocalFolderImpl;
 import org.olat.core.util.vfs.VFSContainer;
 import org.olat.core.util.vfs.VFSLeaf;
 import org.olat.core.util.vfs.VFSMediaResource;
+import org.olat.modules.co.ContactFormController;
+import org.olat.user.UserModule;
 import org.springframework.beans.factory.annotation.Autowired;
 
 /**
@@ -69,31 +77,52 @@ public class DisclaimerController extends BasicController {
 	private static final String SR_ERROR_DISCLAIMER_CHECKBOX = "sr.error.disclaimer.checkbox";
 	private static final String SR_ERROR_DISCLAIMER_CHECKBOXES = "sr.error.disclaimer.checkboxes";
 
-	private VelocityContainer main;
-	private DisclaimerFormController disclaimerFormController;
 	private Link downloadLink;
 	private VFSLeaf downloadFile;
+	private VelocityContainer main;
+
+	private CloseableModalController cmc;
+	private ContactFormController contactCtrl;
+	private DisclaimerFormController disclaimerFormController;
+	private RequestAccountDeletionController requestAccountDeletetionCtrl;
+	private RequestAccountDataDeletetionController requestAccountDataDeletetionCtrl;
+	
+	private Identity identity;
 
+	@Autowired
+	private UserModule userModule;
 	@Autowired
 	private RegistrationModule registrationModule;
-
+	@Autowired
+	private UserDeletionManager userDeletionManager;
+	
 	/**
-	 * Display a disclaimer which can be accepted or denied.
+	 * Display the disclaimer in a read only view to the current user.
+	 * 
 	 * @param ureq
 	 * @param wControl
 	 */
 	public DisclaimerController(UserRequest ureq, WindowControl wControl) {
-		this(ureq, wControl, false);
+		this(ureq, wControl, ureq.getIdentity(), true);
+		
+		if(userModule.isAllowRequestToDeleteAccount() && ureq.getIdentity() != null) {
+			requestAccountDeletetionCtrl = new RequestAccountDeletionController(ureq, getWindowControl());
+			listenTo(requestAccountDeletetionCtrl);
+			main.put("radform", requestAccountDeletetionCtrl.getInitialComponent());
+		}
 	}
 
 	/**
 	 * Display a disclaimer which can be accepted or denied or in a read only manner
-	 * @param ureq
-	 * @param wControl
+	 * @param ureq The user request
+	 * @param wControl The window control
+	 * @param identity The identity which need to accept the disclaimer (or null if it doesn't exist now)
 	 * @param readOnly true: show only read only; false: allow user to accept
 	 */
-	public DisclaimerController(UserRequest ureq, WindowControl wControl, boolean readOnly) {
+	public DisclaimerController(UserRequest ureq, WindowControl wControl, Identity identity, boolean readOnly) {
 		super(ureq, wControl);
+		
+		this.identity = identity;
 	
 		disclaimerFormController = new DisclaimerFormController(ureq, wControl, readOnly);
 		listenTo(disclaimerFormController);
@@ -122,6 +151,7 @@ public class DisclaimerController extends BasicController {
 				}
 			}
 		}
+
 		putInitialPanel(main);
 	}
 
@@ -138,30 +168,64 @@ public class DisclaimerController extends BasicController {
 	protected void event(UserRequest ureq, Controller source, Event event) {
 		if (source == disclaimerFormController) {
 			if (event == Event.CANCELLED_EVENT) {
-				fireEvent(ureq, Event.CANCELLED_EVENT);
+				doCancel(ureq);
 			} else if (event == Event.DONE_EVENT) {
-				// Verify that, if the additional checkbox is configured to be visible, it is checked as well
-				boolean accepted = (disclaimerFormController.acceptCheckbox != null) ? (disclaimerFormController.acceptCheckbox.isSelected(0)) : false;
-				// configure additional checkbox, see class comments in DisclaimerFormController
-				if (accepted && registrationModule.isDisclaimerAdditionalCheckbox()) {
-					accepted = (disclaimerFormController.additionalCheckbox != null) ? (disclaimerFormController.additionalCheckbox.isSelected(0)) : false;
-					if (accepted && registrationModule.isDisclaimerAdditionalCheckbox2()) {
-						accepted = (disclaimerFormController.additionalCheckbox2 != null) ? (disclaimerFormController.additionalCheckbox2.isSelected(0)) : false;
-					}
-				}
-				if (accepted) {
-					fireEvent(ureq, Event.DONE_EVENT);
-				} else if (registrationModule.isDisclaimerAdditionalCheckbox()) {
-					// error handling case multiple checkboxes enabled
-					showError(SR_ERROR_DISCLAIMER_CHECKBOXES);									
-				} else {
-					// error handling case single checkboxe enabled
-					showError(SR_ERROR_DISCLAIMER_CHECKBOX);
-				}
+				acceptDisclaimer(ureq);
+			}
+		} else if(requestAccountDeletetionCtrl == source) {
+			if (event == Event.DONE_EVENT) {
+				fireEvent(ureq, Event.CANCELLED_EVENT);
+			}
+		} else if(requestAccountDataDeletetionCtrl == source) {
+			if(event == Event.CANCELLED_EVENT) {
+				fireEvent(ureq, Event.CANCELLED_EVENT);
+			}
+			cmc.deactivate();
+			cleanUp();
+			if(event == Event.DONE_EVENT) {
+				doDeleteData(ureq);
 			}
+		} else if(contactCtrl == source) {
+			cmc.deactivate();
+			cleanUp();
+			fireEvent(ureq, Event.CANCELLED_EVENT);
+			if(event == Event.DONE_EVENT) {
+				showInfo("request.delete.account.sent");
+			}
+		} else if(cmc == source) {
+			cleanUp();
+		}
+	}
+	
+	private void cleanUp() {
+		removeAsListenerAndDispose(requestAccountDataDeletetionCtrl);
+		removeAsListenerAndDispose(contactCtrl);
+		removeAsListenerAndDispose(cmc);
+		requestAccountDataDeletetionCtrl = null;
+		contactCtrl = null;
+		cmc = null;
+	}
+	
+	private void acceptDisclaimer(UserRequest ureq) {
+		// Verify that, if the additional checkbox is configured to be visible, it is checked as well
+		boolean accepted = (disclaimerFormController.acceptCheckbox != null) ? (disclaimerFormController.acceptCheckbox.isSelected(0)) : false;
+		// configure additional checkbox, see class comments in DisclaimerFormController
+		if (accepted && registrationModule.isDisclaimerAdditionalCheckbox()) {
+			accepted = (disclaimerFormController.additionalCheckbox != null) ? (disclaimerFormController.additionalCheckbox.isSelected(0)) : false;
+			if (accepted && registrationModule.isDisclaimerAdditionalCheckbox2()) {
+				accepted = (disclaimerFormController.additionalCheckbox2 != null) ? (disclaimerFormController.additionalCheckbox2.isSelected(0)) : false;
+			}
+		}
+		if (accepted) {
+			fireEvent(ureq, Event.DONE_EVENT);
+		} else if (registrationModule.isDisclaimerAdditionalCheckbox()) {
+			// error handling case multiple checkboxes enabled
+			showError(SR_ERROR_DISCLAIMER_CHECKBOXES);									
+		} else {
+			// error handling case single checkboxe enabled
+			showError(SR_ERROR_DISCLAIMER_CHECKBOX);
 		}
 	}
-
 	
 	/**
 	 * Change the locale of this controller.
@@ -172,6 +236,57 @@ public class DisclaimerController extends BasicController {
 		main.put("dclform", this.disclaimerFormController.getInitialComponent());
 	}
 	
+	private void doDeleteData(UserRequest ureq) {
+		if(identity.getLastLogin() == null) {
+			userDeletionManager.deleteIdentity(identity, identity);
+			fireEvent(ureq, Event.CANCELLED_EVENT);
+		} else {
+			doOpenContactForm(ureq);
+		}
+	}
+	
+	private void doOpenContactForm(UserRequest ureq) {
+		if(contactCtrl != null) return;
+		
+		String[] args = new String[] {
+			identity.getKey().toString(),											// 0
+			identity.getName(),														// 1
+			identity.getUser().getProperty(UserConstants.FIRSTNAME, getLocale()),	// 2
+			identity.getUser().getProperty(UserConstants.LASTNAME, getLocale())		// 3
+		};
+		ContactMessage contactMessage = new ContactMessage(identity);
+		contactMessage.setSubject(translate("request.delete.account.subject", args));
+		contactMessage.setBodyText(translate("request.delete.account.body", args));
+		
+		String mailAddress = userModule.getMailToRequestAccountDeletion();
+		ContactList contact = new ContactList(mailAddress);
+		contact.add(mailAddress);
+		contactMessage.addEmailTo(contact);
+
+		contactCtrl = new ContactFormController(ureq, getWindowControl(), true, false, false, contactMessage);
+		listenTo(contactCtrl);
+		
+		String title = translate("request.delete.account");
+		cmc = new CloseableModalController(getWindowControl(), "c", contactCtrl.getInitialComponent(), true, title);
+		listenTo(cmc);
+		cmc.activate();
+	}
+	
+	private void doCancel(UserRequest ureq) {
+		if(identity == null || !userModule.isAllowRequestToDeleteAccountDisclaimer()) {
+			fireEvent(ureq, Event.CANCELLED_EVENT);
+			return;
+		}
+		
+		requestAccountDataDeletetionCtrl = new RequestAccountDataDeletetionController(ureq, getWindowControl());
+		listenTo(requestAccountDataDeletetionCtrl);
+		
+		String title = translate("request.data.deletion.title");
+		cmc = new CloseableModalController(getWindowControl(), "c", requestAccountDataDeletetionCtrl.getInitialComponent(), true, title);
+		listenTo(cmc);
+		cmc.activate();
+	}
+	
 	@Override
 	protected void doDispose() {
 		//
diff --git a/src/main/java/org/olat/registration/RegistrationController.java b/src/main/java/org/olat/registration/RegistrationController.java
index bc4d5fd1f37..11b0e7b1661 100644
--- a/src/main/java/org/olat/registration/RegistrationController.java
+++ b/src/main/java/org/olat/registration/RegistrationController.java
@@ -308,7 +308,7 @@ public class RegistrationController extends BasicController implements Activatea
 			myContent.contextPut("text", translate("step4.reg.text"));
 			
 			removeAsListenerAndDispose(disclaimerController);
-			disclaimerController = new DisclaimerController(ureq, getWindowControl());
+			disclaimerController = new DisclaimerController(ureq, getWindowControl(), null, false);
 			listenTo(disclaimerController);
 			
 			regarea.setContent(disclaimerController.getInitialComponent());
diff --git a/src/main/java/org/olat/registration/RegistrationManager.java b/src/main/java/org/olat/registration/RegistrationManager.java
index e3960797af6..e3ebd4120e3 100644
--- a/src/main/java/org/olat/registration/RegistrationManager.java
+++ b/src/main/java/org/olat/registration/RegistrationManager.java
@@ -538,6 +538,13 @@ public class RegistrationManager implements UserDataDeletable, UserDataExportabl
 		Property disclaimerProperty = propertyManager.createUserPropertyInstance(identity, "user", "dislaimer_accepted", null, 1l, null, null);
 		propertyManager.saveProperty(disclaimerProperty);
 	}
+	
+	public Date getDisclaimerConfirmationDate(Identity identity) {
+		if(identity == null) return null;
+		
+		List<Property> disclaimerProperties = propertyManager.listProperties(identity, null, null, "user", "dislaimer_accepted");
+		return disclaimerProperties.isEmpty() ? null : disclaimerProperties.get(0).getLastModified();
+	}
 
 	/**
 	 * Remove all disclaimer confirmations. This means that every user on the
diff --git a/src/main/java/org/olat/registration/RequestAccountDataDeletetionController.java b/src/main/java/org/olat/registration/RequestAccountDataDeletetionController.java
new file mode 100644
index 00000000000..7ed5dbb6926
--- /dev/null
+++ b/src/main/java/org/olat/registration/RequestAccountDataDeletetionController.java
@@ -0,0 +1,84 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.registration;
+
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.form.flexible.FormItemContainer;
+import org.olat.core.gui.components.form.flexible.elements.MultipleSelectionElement;
+import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
+import org.olat.core.gui.components.form.flexible.impl.elements.FormCancel;
+import org.olat.core.gui.control.Controller;
+import org.olat.core.gui.control.Event;
+import org.olat.core.gui.control.WindowControl;
+
+/**
+ * 
+ * Initial date: 4 févr. 2019<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class RequestAccountDataDeletetionController extends FormBasicController {
+	
+	private MultipleSelectionElement confirmEl;
+	
+	public RequestAccountDataDeletetionController(UserRequest ureq, WindowControl wControl) {
+		super(ureq, wControl, "request_delete_data");
+		initForm(ureq);
+	}
+
+	@Override
+	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
+		String[] keys = new String[] { "on" };
+		String[] values = new String[] { translate("request.data.deletion.confirm.text") };
+ 		confirmEl = uifactory.addCheckboxesHorizontal("request.data.deletion.confirm", formLayout, keys, values);
+
+		FormCancel cancel = uifactory.addFormCancelButton("no", formLayout, ureq, getWindowControl());
+		cancel.setI18nKey("no");
+		uifactory.addFormSubmitButton("yes", formLayout);
+	}
+
+	@Override
+	protected void doDispose() {
+		//
+	}
+
+	@Override
+	protected boolean validateFormLogic(UserRequest ureq) {
+		boolean allOk = super.validateFormLogic(ureq);
+		
+		confirmEl.clearError();
+		if(!confirmEl.isAtLeastSelected(1)) {
+			confirmEl.setErrorKey("request.data.deletion.confirm.error", null);
+			allOk &= false;
+		}
+		
+		return allOk;
+	}
+
+	@Override
+	protected void formOK(UserRequest ureq) {
+		fireEvent(ureq, Event.DONE_EVENT);
+	}
+
+	@Override
+	protected void formCancelled(UserRequest ureq) {
+		fireEvent(ureq, Event.CANCELLED_EVENT);
+	}
+}
diff --git a/src/main/java/org/olat/registration/RequestAccountDeletionController.java b/src/main/java/org/olat/registration/RequestAccountDeletionController.java
new file mode 100644
index 00000000000..da3dbc0a82d
--- /dev/null
+++ b/src/main/java/org/olat/registration/RequestAccountDeletionController.java
@@ -0,0 +1,157 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.registration;
+
+import java.util.Date;
+
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.form.flexible.FormItem;
+import org.olat.core.gui.components.form.flexible.FormItemContainer;
+import org.olat.core.gui.components.form.flexible.elements.FormLink;
+import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
+import org.olat.core.gui.components.form.flexible.impl.FormEvent;
+import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer;
+import org.olat.core.gui.components.link.Link;
+import org.olat.core.gui.control.Controller;
+import org.olat.core.gui.control.Event;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController;
+import org.olat.core.id.Identity;
+import org.olat.core.id.UserConstants;
+import org.olat.core.util.Formatter;
+import org.olat.core.util.mail.ContactList;
+import org.olat.core.util.mail.ContactMessage;
+import org.olat.modules.co.ContactFormController;
+import org.olat.user.UserModule;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * The panel to request per E-mail the deletion of it's account.
+ * 
+ * 
+ * Initial date: 4 févr. 2019<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class RequestAccountDeletionController extends FormBasicController {
+	
+	private FormLink requestButton;
+	
+	private CloseableModalController cmc;
+	private ContactFormController contactCtrl;
+	
+	@Autowired
+	private UserModule userModule;
+	@Autowired
+	private RegistrationManager registrationManager;
+	
+	public RequestAccountDeletionController(UserRequest ureq, WindowControl wControl) {
+		super(ureq, wControl, "request_delete");
+		initForm(ureq);
+	}
+
+	@Override
+	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
+		if(formLayout instanceof FormLayoutContainer) {
+			FormLayoutContainer layoutCont = (FormLayoutContainer)formLayout;
+			Date confirmationDate = registrationManager.getDisclaimerConfirmationDate(getIdentity());
+			if(confirmationDate != null) {
+				String date = Formatter.getInstance(getLocale()).formatDate(confirmationDate);
+				String time = Formatter.getInstance(getLocale()).formatTimeShort(confirmationDate);
+				layoutCont.contextPut("title", translate("request.delete.account.title.date", new String[] { date, time }));
+			}
+			layoutCont.contextPut("text", translate("request.delete.account.text"));
+		}
+		
+		requestButton = uifactory.addFormLink("request.delete.account", formLayout, Link.BUTTON);
+	}
+
+	@Override
+	protected void doDispose() {
+		//
+	}
+
+	@Override
+	protected void event(UserRequest ureq, Controller source, Event event) {
+		if(contactCtrl == source) {
+			cmc.deactivate();
+			cleanUp();
+			if(event == Event.DONE_EVENT) {
+				fireEvent(ureq, Event.DONE_EVENT);
+				showInfo("request.delete.account.sent");
+			}
+		} else if(cmc == source) {
+			cleanUp();
+		}
+		super.event(ureq, source, event);
+	}
+	
+	private void cleanUp() {
+		removeAsListenerAndDispose(contactCtrl);
+		removeAsListenerAndDispose(cmc);
+		contactCtrl = null;
+		cmc = null;
+	}
+
+	@Override
+	protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
+		if(requestButton == source) {
+			doRequestDeletion(ureq);
+		}
+		super.formInnerEvent(ureq, source, event);
+	}
+
+	@Override
+	protected void formOK(UserRequest ureq) {
+		//
+	}
+	
+	private void doRequestDeletion(UserRequest ureq) {
+		doOpenContactForm(ureq);
+	}
+	
+	private void doOpenContactForm(UserRequest ureq) {
+		if(contactCtrl != null) return;
+		
+		Identity identity = getIdentity();
+		String[] args = new String[] {
+			identity.getKey().toString(),											// 0
+			identity.getName(),														// 1
+			identity.getUser().getProperty(UserConstants.FIRSTNAME, getLocale()),	// 2
+			identity.getUser().getProperty(UserConstants.LASTNAME, getLocale())		// 3
+		};
+		ContactMessage contactMessage = new ContactMessage(identity);
+		contactMessage.setSubject(translate("request.delete.account.subject", args));
+		contactMessage.setBodyText(translate("request.delete.account.body", args));
+		
+		String mailAddress = userModule.getMailToRequestAccountDeletion();
+		ContactList contact = new ContactList(mailAddress);
+		contact.add(mailAddress);
+		contactMessage.addEmailTo(contact);
+
+		contactCtrl = new ContactFormController(ureq, getWindowControl(), true, false, false, contactMessage);
+		listenTo(contactCtrl);
+		
+		String title = translate("request.delete.account");
+		cmc = new CloseableModalController(getWindowControl(), "c", contactCtrl.getInitialComponent(), true, title);
+		listenTo(cmc);
+		cmc.activate();
+	}
+}
diff --git a/src/main/java/org/olat/registration/_content/disclaimer.html b/src/main/java/org/olat/registration/_content/disclaimer.html
index dc5e47dd516..5202edaf286 100644
--- a/src/main/java/org/olat/registration/_content/disclaimer.html
+++ b/src/main/java/org/olat/registration/_content/disclaimer.html
@@ -11,4 +11,7 @@
 		<p>$r.render("disclaimer.additionallinktext")</p>
 	#end
 	$r.render("dclform")
+	#if($r.available("radform")) 
+		$r.render("radform")
+	#end
 </fieldset>
diff --git a/src/main/java/org/olat/registration/_content/request_delete.html b/src/main/java/org/olat/registration/_content/request_delete.html
new file mode 100644
index 00000000000..d8384b45662
--- /dev/null
+++ b/src/main/java/org/olat/registration/_content/request_delete.html
@@ -0,0 +1,9 @@
+<div class="o_info">
+	#if($r.isNotEmpty($title))
+	<h5>$title</h5>
+	#end
+	<p>$text</p>
+	<div class="o_button_group">
+		$r.render("request.delete.account")
+	</div>
+</div>
\ No newline at end of file
diff --git a/src/main/java/org/olat/registration/_content/request_delete_data.html b/src/main/java/org/olat/registration/_content/request_delete_data.html
new file mode 100644
index 00000000000..3e666aadaac
--- /dev/null
+++ b/src/main/java/org/olat/registration/_content/request_delete_data.html
@@ -0,0 +1,11 @@
+<div class="o_info">
+	$r.translate("request.data.deletion.text")
+	$r.render("request.data.deletion.confirm")
+	#if($f.hasError("request.data.deletion.confirm"))
+	<div class="">$r.render("request.data.deletion.confirm_ERROR")</div>
+	#end
+</div>
+<div class="o_button_group">
+	$r.render("no")
+	$r.render("yes")
+</div>
\ No newline at end of file
diff --git a/src/main/java/org/olat/registration/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/registration/_i18n/LocalStrings_de.properties
index ad014c705e4..e68e642b76d 100644
--- a/src/main/java/org/olat/registration/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/registration/_i18n/LocalStrings_de.properties
@@ -110,6 +110,17 @@ registration.pending.status.pending.props=H\u00E4ngig, wenn eines der folgenden
 regkey.missing=Der Registrierungsschl\u00FCssel fehlt. Fordern Sie bitte einen neuen an.
 regkey.missingentry=Dieser Registrierungsschl\u00FCssel existiert nicht. Bitte fordern Sie einen neuen an.
 remote.login.title=Loginformular in externe Webseite/CMS einbinden
+request.data.deletion.title=Daten l\u00F6schen
+request.data.deletion.text=Wollen Sie dass ihre Daten <strong>komplett</strong> und <strong>definitve</strong> von dem OpenOLAT System?
+request.data.deletion.confirm=Best\u00E4tigung
+request.data.deletion.confirm.error=Sie m\u00FCssen best\u00E4gigen dass Sie die Konzequence verstehen.
+request.data.deletion.confirm.text=Ich verstehe dass alle meine Daten werden definitive gel\u00F6scht und k\u00F6nnen nicht mehr zur\u00FCcksetzt werden.
+request.delete.account=Konto l\u00F6schen beantragen
+request.delete.account.subject=Konto "{0}" l\u00F6schen beantragen
+request.delete.account.body=Ich m\u00F6chte meinem Benutzerkonto l\u00F6schen.<br>Konto ID: {0}<br>Benutzername: {1}<br>Name: {2} {3}<br><br>Mit freundlichen Gr\u00FCsse
+request.delete.account.title.date=Die Zustimmung erflogte am {0} um {1}
+request.delete.account.text=Falls sie den Nutzungsbedingungen nicht mehr zustimmen k\u00F6nnen Sie die L\u00F6schung Ihres Benutzerkonto beantragen.
+request.delete.account.sent=Die Anfrage wurde erfolgreich abgeschickt.
 select.language=Sprache
 select.language.description=W\u00E4hlen Sie die Sprache f\u00FCr die OpenOLAT Registrierung und Ihr Benutzerkonto. Sie k\u00F6nnen die Sprache sp\u00E4ter in Ihrem Benutzerprofil jederzeit anpassen. Anschliessend werden Sie durch den Registrationprozess gef\u00FChrt.
 sr.error.disclaimer.checkbox=Sie m\u00FCssen durch Anklicken des K\u00E4stchens best\u00E4tigen, dass Sie die Nutzungsbedingungen gelesen und verstanden haben und diese akzeptieren.
diff --git a/src/main/java/org/olat/registration/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/registration/_i18n/LocalStrings_en.properties
index 00ab19974a1..473b8a0ff7b 100644
--- a/src/main/java/org/olat/registration/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/registration/_i18n/LocalStrings_en.properties
@@ -109,6 +109,17 @@ registration.pending.status.pending=Pending
 registration.pending.status.pending.props=Pending if one of the following user properties matches
 regkey.missing=Registration key missing. Please ask for a new one.
 regkey.missingentry=This registration key does not exist. Please ask for another one.
+request.data.deletion.title=Delete data
+request.data.deletion.text=Do you want that your data be <strong>completely</strong> and <strong>definitively</strong> deleted?
+request.data.deletion.confirm=Confirmation
+request.data.deletion.confirm.error=You must confirm that you understand the consequences.
+request.data.deletion.confirm.text=I understand that my data will be definitively deleted and that they cannot be restored.
+request.delete.account=Ask to delete your account
+request.delete.account.subject=Ask to delete account "{0}"
+request.delete.account.body=I want to delete my user account.<br>Account ID: {0}<br>Username: {1}<br>Name: {2} {3}<br><br>Best regards
+request.delete.account.title.date=The consent came at {0} {1}
+request.delete.account.text=If you no longer agree to the terms of use, you can request the deletion of your user account.
+request.delete.account.sent=The request was sent successfully.
 remote.login.title=Embed login form into external website/CMS
 select.language=Language
 select.language.description=Please select a language for your OpenOLAT registration and user account. Later on you can adapt your choice in your user profile at any time. You will then be guided through the registration process.
diff --git a/src/main/java/org/olat/shibboleth/ShibDisclaimerController.java b/src/main/java/org/olat/shibboleth/ShibDisclaimerController.java
index fe451d2bb42..f301086a265 100644
--- a/src/main/java/org/olat/shibboleth/ShibDisclaimerController.java
+++ b/src/main/java/org/olat/shibboleth/ShibDisclaimerController.java
@@ -69,7 +69,7 @@ public class ShibDisclaimerController extends FormBasicController implements Act
 
 	@Override
 	public void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) {
-		disclaimerController = new DisclaimerController(ureq, getWindowControl());
+		disclaimerController = new DisclaimerController(ureq, getWindowControl(), null, false);
 		listenTo(disclaimerController);
 		
 		cmc = new CloseableModalController(getWindowControl(), translate("close"), disclaimerController.getInitialComponent(),
diff --git a/src/main/java/org/olat/shibboleth/ShibbolethRegistrationController.java b/src/main/java/org/olat/shibboleth/ShibbolethRegistrationController.java
index 40ad6666a52..cee7eed5746 100644
--- a/src/main/java/org/olat/shibboleth/ShibbolethRegistrationController.java
+++ b/src/main/java/org/olat/shibboleth/ShibbolethRegistrationController.java
@@ -194,7 +194,7 @@ public class ShibbolethRegistrationController extends DefaultController implemen
 			setRegistrationForm(ureq, wControl, null);
 		}
 
-		dclController = new DisclaimerController(ureq, getWindowControl());
+		dclController = new DisclaimerController(ureq, getWindowControl(), null, false);
 		dclController.addControllerListener(this);
 		mainContainer.put("dclComp", dclController.getInitialComponent());
 
@@ -368,15 +368,10 @@ public class ShibbolethRegistrationController extends DefaultController implemen
 	private void doLogin(Identity identity, UserRequest ureq) {
 		int loginStatus = AuthHelper.doLogin(identity, ShibbolethDispatcher.PROVIDER_SHIB, ureq);
 		if (loginStatus != AuthHelper.LOGIN_OK) {
-			//REVIEW:2010-01-11:revisited:pb: do not redirect if already MediaResource is set before
-			//ureq.getDispatchResult().setResultingMediaResource(resultingMediaResource);
-			//instead set the media resource accordingly
-			//pb -> provide a DispatcherAction.getDefaultDispatcherRedirectMediaresource();
-			//to be used here. (and some more places like CatalogController.
 			DispatcherModule.redirectToDefaultDispatcher(ureq.getHttpResp()); // error, redirect to login screen
 			return;
 		}
-		// successfull login
+		// successful login
 		ureq.getUserSession().getIdentityEnvironment().addAttributes(
 				shibbolethModule.getAttributeTranslator().translateAttributesMap(shibbolethAttributes.toMap()));
 	}
diff --git a/src/main/java/org/olat/user/UserModule.java b/src/main/java/org/olat/user/UserModule.java
index 0e45fe6a9c5..f9bcf4bcb1b 100644
--- a/src/main/java/org/olat/user/UserModule.java
+++ b/src/main/java/org/olat/user/UserModule.java
@@ -67,6 +67,9 @@ public class UserModule extends AbstractSpringModule {
 	
 	private static final String USER_EMAIL_MANDATORY = "userEmailMandatory";
 	private static final String USER_EMAIL_UNIQUE = "userEmailUnique";
+	private static final String ALLOW_REQUEST_DELETE_ACCOUNT = "allow.request.delete.account";
+	private static final String ALLOW_REQUEST_DELETE_ACCOUNT_DISCLAIMER = "allow.request.delete.account.disclaimer";
+	private static final String MAIL_REQUEST_DELETE_ACCOUNT = "request.delete.account.mail";
 	
 	@Autowired @Qualifier("loginBlacklist")
 	private ArrayList<String> loginBlacklist;
@@ -76,6 +79,13 @@ public class UserModule extends AbstractSpringModule {
 	private boolean pwdchangeallowed;
 	@Value("${password.change.allowed.without.authentications:false}")
 	private boolean pwdChangeWithoutAuthenticationAllowed;
+	
+	@Value("${allow.request.delete.account:false}")
+	private boolean allowRequestToDeleteAccount;
+	@Value("${allow.request.delete.account.disclaimer:false}")
+	private boolean allowRequestToDeleteAccountDisclaimer;
+	@Value("${request.delete.account.mail}")
+	private String mailToRequestAccountDeletion;
 
 	private String adminUserName = "administrator";
 	@Value("${user.logoByProfile:disabled}")
@@ -108,17 +118,8 @@ public class UserModule extends AbstractSpringModule {
 		}
 		
 		log.info("Successfully added " + count + " entries to login blacklist.");
-		
-		String userEmailOptionalValue = getStringPropertyValue(USER_EMAIL_MANDATORY, false);
-		if(StringHelper.containsNonWhitespace(userEmailOptionalValue)) {
-			isEmailMandatory = "true".equalsIgnoreCase(userEmailOptionalValue);
-		}
-		
-		String userEmailUniquenessOptionalValue = getStringPropertyValue(USER_EMAIL_UNIQUE, false);
-		if(StringHelper.containsNonWhitespace(userEmailUniquenessOptionalValue)) {
-			isEmailUnique = "true".equalsIgnoreCase(userEmailUniquenessOptionalValue);
-		}
-		
+		updateProperties();
+
 		// Check if user manager is configured properly and has user property
 		// handlers for the mandatory user properties used in OLAT
 		checkMandatoryUserProperty(UserConstants.FIRSTNAME);
@@ -142,7 +143,31 @@ public class UserModule extends AbstractSpringModule {
 
 	@Override
 	protected void initFromChangedProperties() {
-		//
+		updateProperties();
+	}
+	
+	private void updateProperties() {
+		String userEmailOptionalValue = getStringPropertyValue(USER_EMAIL_MANDATORY, false);
+		if(StringHelper.containsNonWhitespace(userEmailOptionalValue)) {
+			isEmailMandatory = "true".equalsIgnoreCase(userEmailOptionalValue);
+		}
+		String userEmailUniquenessOptionalValue = getStringPropertyValue(USER_EMAIL_UNIQUE, false);
+		if(StringHelper.containsNonWhitespace(userEmailUniquenessOptionalValue)) {
+			isEmailUnique = "true".equalsIgnoreCase(userEmailUniquenessOptionalValue);
+		}
+		
+		String allowRequestDeleteObj = getStringPropertyValue(ALLOW_REQUEST_DELETE_ACCOUNT, false);
+		if(StringHelper.containsNonWhitespace(allowRequestDeleteObj)) {
+			allowRequestToDeleteAccount = "true".equalsIgnoreCase(allowRequestDeleteObj);
+		}
+		String allowRequestDeleteDisclaimerObj = getStringPropertyValue(ALLOW_REQUEST_DELETE_ACCOUNT_DISCLAIMER, false);
+		if(StringHelper.containsNonWhitespace(allowRequestDeleteDisclaimerObj)) {
+			allowRequestToDeleteAccountDisclaimer = "true".equalsIgnoreCase(allowRequestDeleteDisclaimerObj);
+		}
+		String mailRequestDeleteObj = getStringPropertyValue(MAIL_REQUEST_DELETE_ACCOUNT, false);
+		if(StringHelper.containsNonWhitespace(mailRequestDeleteObj)) {
+			mailToRequestAccountDeletion = mailRequestDeleteObj;
+		}
 	}
 
 	private void checkMandatoryUserProperty(String userPropertyIdentifyer) {
@@ -262,4 +287,32 @@ public class UserModule extends AbstractSpringModule {
 		setStringProperty(USER_EMAIL_UNIQUE, isEmailUniqueStr, true);
 	}
 
+	public boolean isAllowRequestToDeleteAccount() {
+		return allowRequestToDeleteAccount;
+	}
+
+	public void setAllowRequestToDeleteAccount(boolean allowRequestToDeleteAccount) {
+		this.allowRequestToDeleteAccount = allowRequestToDeleteAccount;
+		String allowed = allowRequestToDeleteAccount ? "true" : "false";
+		setStringProperty(ALLOW_REQUEST_DELETE_ACCOUNT, allowed, true);
+	}
+
+	public boolean isAllowRequestToDeleteAccountDisclaimer() {
+		return allowRequestToDeleteAccountDisclaimer;
+	}
+
+	public void setAllowRequestToDeleteAccountDisclaimer(boolean allowRequestToDeleteAccountDisclaimer) {
+		this.allowRequestToDeleteAccountDisclaimer = allowRequestToDeleteAccountDisclaimer;
+		String allowed = allowRequestToDeleteAccountDisclaimer ? "true" : "false";
+		setStringProperty(ALLOW_REQUEST_DELETE_ACCOUNT_DISCLAIMER, allowed, true);
+	}
+
+	public String getMailToRequestAccountDeletion() {
+		return mailToRequestAccountDeletion;
+	}
+
+	public void setMailToRequestAccountDeletion(String mailToRequestAccountDeletion) {
+		this.mailToRequestAccountDeletion = mailToRequestAccountDeletion;
+		setStringProperty(MAIL_REQUEST_DELETE_ACCOUNT, mailToRequestAccountDeletion, true);
+	}
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/user/UserSettingsController.java b/src/main/java/org/olat/user/UserSettingsController.java
index 51f49e17a27..0b33b8ec76e 100644
--- a/src/main/java/org/olat/user/UserSettingsController.java
+++ b/src/main/java/org/olat/user/UserSettingsController.java
@@ -202,7 +202,7 @@ public class UserSettingsController extends BasicController implements Activatea
 		if(disclaimerCtrl == null) {
 			OLATResourceable ores = OresHelper.createOLATResourceableInstance("Disclaimer", 0l);
 			WindowControl bwControl = BusinessControlFactory.getInstance().createBusinessWindowControl(ores, null, getWindowControl());
-			disclaimerCtrl = new DisclaimerController(ureq, bwControl, true);
+			disclaimerCtrl = new DisclaimerController(ureq, bwControl);
 			listenTo(disclaimerCtrl);
 		}
 		mainVC.put("segmentCmp", disclaimerCtrl.getInitialComponent());
diff --git a/src/main/java/org/olat/user/_spring/userContext.xml b/src/main/java/org/olat/user/_spring/userContext.xml
index f7a3b5bcf73..ea993cc035b 100644
--- a/src/main/java/org/olat/user/_spring/userContext.xml
+++ b/src/main/java/org/olat/user/_spring/userContext.xml
@@ -331,6 +331,26 @@
 		<property name="order" value="7411" />
 	</bean>
 	
+	<!-- User request to delete account -->
+	<bean class="org.olat.core.extensions.action.GenericActionExtension" init-method="initExtensionPoints">
+		<property name="order" value="9014" />
+		<property name="actionController">	
+			<bean class="org.olat.core.gui.control.creator.AutoCreator" scope="prototype">
+				<property name="className" value="org.olat.user.ui.admin.UserAccountDeletionSettingsController"/>
+			</bean>
+		</property>
+		<property name="navigationKey" value="requestdeleteaccount" />
+		<property name="parentTreeNodeIdentifier" value="modulesParent" /> 
+		<property name="i18nActionKey" value="admin.menu.title.request"/>
+		<property name="i18nDescriptionKey" value="admin.menu.title.request.alt"/>
+		<property name="translationPackage" value="org.olat.user.ui.admin"/>
+		<property name="extensionPoints">
+			<list>	
+				<value>org.olat.admin.SystemAdminMainController</value>		
+			</list>
+		</property>
+	</bean>
+	
 	<!-- Delete old user data export job -->
 	<bean id="deleteUserDataExportTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
 		<property name="jobDetail" ref="deleteUserDataExportJob.${cluster.singleton.services}" />
diff --git a/src/main/java/org/olat/user/ui/admin/UserAccountDeletionSettingsController.java b/src/main/java/org/olat/user/ui/admin/UserAccountDeletionSettingsController.java
new file mode 100644
index 00000000000..e951ea144cd
--- /dev/null
+++ b/src/main/java/org/olat/user/ui/admin/UserAccountDeletionSettingsController.java
@@ -0,0 +1,124 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.user.ui.admin;
+
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.form.flexible.FormItem;
+import org.olat.core.gui.components.form.flexible.FormItemContainer;
+import org.olat.core.gui.components.form.flexible.elements.MultipleSelectionElement;
+import org.olat.core.gui.components.form.flexible.elements.TextElement;
+import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
+import org.olat.core.gui.components.form.flexible.impl.FormEvent;
+import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer;
+import org.olat.core.gui.control.Controller;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.util.StringHelper;
+import org.olat.core.util.mail.MailHelper;
+import org.olat.user.UserModule;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * Initial date: 4 févr. 2019<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class UserAccountDeletionSettingsController extends FormBasicController {
+	
+	private MultipleSelectionElement requestDeleteEl;
+	private TextElement emailEl;
+	
+	@Autowired
+	private UserModule userModule;
+	
+	public UserAccountDeletionSettingsController(UserRequest ureq, WindowControl wControl) {
+		super(ureq, wControl);
+		
+		initForm(ureq);
+	}
+
+	@Override
+	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
+		setFormTitle("allow.request.delete.account.time");
+		
+		String[] deleteKeys = new String[] { "anytime", "disclaimer" };
+		String[] deleteValues = new String[] { translate("allow.request.delete.account.anytime"), translate("allow.request.delete.account.disclaimer") };
+		
+		requestDeleteEl = uifactory.addCheckboxesVertical("allow.request.delete.account", formLayout, deleteKeys, deleteValues, 1);
+		requestDeleteEl.addActionListener(FormEvent.ONCHANGE);
+		if(userModule.isAllowRequestToDeleteAccount()) {
+			requestDeleteEl.select(deleteKeys[0], true);
+		}
+		if(userModule.isAllowRequestToDeleteAccountDisclaimer()) {
+			requestDeleteEl.select(deleteKeys[1], true);
+		}
+		
+		String email = userModule.getMailToRequestAccountDeletion();
+		emailEl = uifactory.addTextElement("allow.request.delete.account.mail", 256, email, formLayout);
+		emailEl.setVisible(requestDeleteEl.isAtLeastSelected(1));
+		
+		FormLayoutContainer layoutCont = FormLayoutContainer.createButtonLayout("buttons", getTranslator());
+		formLayout.add(layoutCont);
+		uifactory.addFormSubmitButton("save", layoutCont);
+	}
+
+	@Override
+	protected boolean validateFormLogic(UserRequest ureq) {
+		boolean allOk = super.validateFormLogic(ureq);
+		
+		emailEl.clearError();
+		requestDeleteEl.clearError();
+		if(requestDeleteEl.isAtLeastSelected(1)) {
+			if(!StringHelper.containsNonWhitespace(emailEl.getValue())) {
+				emailEl.setErrorKey("form.legende.mandatory", null);
+				allOk &= false;
+			} else if(!MailHelper.isValidEmailAddress(emailEl.getValue())) {
+				emailEl.setErrorKey("error.mail.not.valid", null);
+				allOk &= false;
+			}
+		}
+		
+		return allOk;
+	}
+
+	@Override
+	protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
+		if(requestDeleteEl == source) {
+			emailEl.setVisible(requestDeleteEl.isAtLeastSelected(1));
+		}
+		super.formInnerEvent(ureq, source, event);
+	}
+
+	@Override
+	protected void formOK(UserRequest ureq) {
+		userModule.setAllowRequestToDeleteAccount(requestDeleteEl.isSelected(0));
+		userModule.setAllowRequestToDeleteAccountDisclaimer(requestDeleteEl.isSelected(1));
+		if(requestDeleteEl.isAtLeastSelected(1)) {
+			userModule.setMailToRequestAccountDeletion(emailEl.getValue());
+		} else {
+			userModule.setMailToRequestAccountDeletion("");
+		}
+	}
+
+	@Override
+	protected void doDispose() {
+		//
+	}
+}
diff --git a/src/main/java/org/olat/user/ui/admin/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/user/ui/admin/_i18n/LocalStrings_de.properties
index 79232c02137..26480f5ee4e 100644
--- a/src/main/java/org/olat/user/ui/admin/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/user/ui/admin/_i18n/LocalStrings_de.properties
@@ -1,6 +1,14 @@
 #Fri Mar 23 15:13:55 CET 2018
+admin.menu.title.request=Anfrage Konto l\u00F6schen
+admin.menu.title.request.alt=Anfrage Benutzerkonto l\u00F6schen
+allow.request.delete.account=Erlaubt anfragen Benutzerkonto zu l\u00F6schen
+allow.request.delete.account.anytime=Jederzeit
+allow.request.delete.account.disclaimer=Wenn der Benutzer den Nutzungbedingungen nicht akkzeptiert
+allow.request.delete.account.mail=Email f\u00FCr Anfrage
+allow.request.delete.account.time=Benutzerkonto l\u00F6schen anfragen
 command.next=Weiter zur n\u00E4chsten Benutzer
 command.previous=Zur\u00FCck zur letzten Benutzer
+error.mail.not.valid=Der Email Adresse ist nicht g\u00FCltig.
 menu.admingroup=Administratoren
 menu.admingroup.alt=Administratoren verwalten
 menu.anonymousgroup=Anonyme Benutzer / G\u00E4ste
diff --git a/src/main/java/org/olat/user/ui/admin/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/user/ui/admin/_i18n/LocalStrings_en.properties
index c28c40b57bd..d47e409e60d 100644
--- a/src/main/java/org/olat/user/ui/admin/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/user/ui/admin/_i18n/LocalStrings_en.properties
@@ -1,4 +1,12 @@
 #Fri Mar 23 15:13:55 CET 2018
+admin.menu.title.request=Request account deletion
+admin.menu.title.request.alt=Request user account deletion
+allow.request.delete.account=Allow request to delete user account
+allow.request.delete.account.anytime=Anytime
+allow.request.delete.account.disclaimer=If the user doesn't accept the disclaimers
+allow.request.delete.account.mail=E-mail for requests
+allow.request.delete.account.time=Request user account deletion
+error.mail.not.valid=The E-mail addresse is not valid.
 command.next=Go to next user
 command.previous=Go to previous user
 menu.admingroup=Administrators
diff --git a/src/main/resources/serviceconfig/olat.properties b/src/main/resources/serviceconfig/olat.properties
index a0be8ed84e9..8e708154083 100644
--- a/src/main/resources/serviceconfig/olat.properties
+++ b/src/main/resources/serviceconfig/olat.properties
@@ -250,6 +250,11 @@ notification.interval.default.values=never,monthly,weekly,daily,half-daily,four-
 #notification cron job
 notification.cronjob.expression=0 10 */2 * * ?
 
+# Request to delete account
+allow.request.delete.account=false
+allow.request.delete.account.disclaimer=false
+request.delete.account.mail=
+
 ####################################################
 # Groups
 ####################################################
diff --git a/src/test/java/org/olat/basesecurity/BaseSecurityManagerTest.java b/src/test/java/org/olat/basesecurity/BaseSecurityManagerTest.java
index e4f4cab25f0..a03ee79e295 100644
--- a/src/test/java/org/olat/basesecurity/BaseSecurityManagerTest.java
+++ b/src/test/java/org/olat/basesecurity/BaseSecurityManagerTest.java
@@ -270,7 +270,7 @@ public class BaseSecurityManagerTest extends OlatTestCase {
 	@Test
 	public void loadIdentityShortByKey() {
 		//create a user it
-		String idName = "find-me-short-1-" + UUID.randomUUID().toString();
+		String idName = "find-me-short-1-" + UUID.randomUUID();
 		Identity id = JunitTestHelper.createAndPersistIdentityAsUser(idName);
 		dbInstance.commitAndCloseSession();
 		
@@ -282,19 +282,19 @@ public class BaseSecurityManagerTest extends OlatTestCase {
 		Assert.assertEquals(id.getUser().getEmail(), foundId.getEmail());
 		Assert.assertEquals(id.getUser().getFirstName(), foundId.getFirstName());
 		Assert.assertEquals(id.getUser().getLastName(), foundId.getLastName());
-		Assert.assertNotNull(foundId.getLastLogin());
+		Assert.assertNull(foundId.getLastLogin());// no login, no last login date
 		Assert.assertEquals(id.getUser().getKey(), foundId.getUserKey());
 		Assert.assertTrue(foundId.getStatus() < Identity.STATUS_VISIBLE_LIMIT);
 	}
 
 	@Test
 	public void testLoadIdentityByKeys() {
-		//create a security group with 2 identites
-		Identity id1 = JunitTestHelper.createAndPersistIdentityAsUser( "load-1-sec-" + UUID.randomUUID().toString());
-		Identity id2 = JunitTestHelper.createAndPersistIdentityAsUser( "load-2-sec-" + UUID.randomUUID().toString());
+		//create a security group with 2 identities
+		Identity id1 = JunitTestHelper.createAndPersistIdentityAsRndUser( "load-1-sec-");
+		Identity id2 = JunitTestHelper.createAndPersistIdentityAsRndUser( "load-2-sec-");
 		dbInstance.commitAndCloseSession();
 		
-		List<Long> keys = new ArrayList<Long>(2);
+		List<Long> keys = new ArrayList<>(2);
 		keys.add(id1.getKey());
 		keys.add(id2.getKey());
 		List<Identity> identities = securityManager.loadIdentityByKeys(keys);
-- 
GitLab