You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openmeetings.apache.org by so...@apache.org on 2018/04/21 15:47:36 UTC

[openmeetings] branch 4.0.x updated: [OPENMEETINGS-1864] purge seems to work

This is an automated email from the ASF dual-hosted git repository.

solomax pushed a commit to branch 4.0.x
in repository https://gitbox.apache.org/repos/asf/openmeetings.git


The following commit(s) were added to refs/heads/4.0.x by this push:
     new 3133a96  [OPENMEETINGS-1864] purge seems to work
3133a96 is described below

commit 3133a96c9abf9d5de4238401adc89ce13e1e1464
Author: Maxim Solodovnik <so...@gmail.com>
AuthorDate: Sat Apr 21 22:47:26 2018 +0700

    [OPENMEETINGS-1864] purge seems to work
---
 .../apache/openmeetings/db/dao/user/UserDao.java   | 11 +++++
 .../openmeetings/service/user/UserManager.java     | 11 ++---
 .../org/apache/openmeetings/util/crypt/ICrypt.java |  2 +
 .../util/crypt/SCryptImplementation.java           | 37 ++++++++++------
 ...{AdminSavePanel.html => AdminActionsPanel.html} |  5 +--
 ...{AdminSavePanel.java => AdminActionsPanel.java} | 37 +++-------------
 .../openmeetings/web/admin/AdminBaseForm.java      |  4 +-
 .../openmeetings/web/admin/users/UserForm.java     | 49 +++++++++++----------
 .../web/common/ConfirmableAjaxBorder.java          | 51 ++++++++++++++++------
 ...SaveRefreshPanel.html => FormActionsPanel.html} |  7 ++-
 ...SaveRefreshPanel.java => FormActionsPanel.java} | 40 +++++++++++++----
 .../openmeetings/web/user/profile/ProfileForm.java | 20 ++++++++-
 12 files changed, 168 insertions(+), 106 deletions(-)

diff --git a/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/user/UserDao.java b/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/user/UserDao.java
index 990f99d..fb2211f 100644
--- a/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/user/UserDao.java
+++ b/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/user/UserDao.java
@@ -341,6 +341,8 @@ public class UserDao implements IGroupAdminDataProviderDao<User> {
 					.setParameter("email", String.format("%%%s%%", u.getAddress().getEmail()))
 					.executeUpdate();
 			}
+			u.setActivatehash(null);
+			u.setResethash(null);
 			u.setDeleted(true);
 			u.setSipUser(new AsteriskSipUser());
 			u.setAddress(new Address());
@@ -350,8 +352,17 @@ public class UserDao implements IGroupAdminDataProviderDao<User> {
 			u.setFirstname(purged);
 			u.setLastname(purged);
 			u.setLogin(purged);
+			u.setGroupUsers(new ArrayList<>());
+			u.setRights(new HashSet<>());
+			u.setTimeZoneId(null);
 			File pic = OmFileHelper.getUserProfilePicture(u.getId(), u.getPictureuri(), null);
 			u.setPictureuri(null);
+			ICrypt crypt = CryptProvider.get();
+			try {
+				u.updatePassword(crypt.randomPassword(25));
+			} catch (NoSuchAlgorithmException e) {
+				log.error("Unexpected exception while updating password");
+			}
 			update(u, userId);
 			// this should be last action, so file will be deleted in case there were no errors
 			if (pic != null) {
diff --git a/openmeetings-service/src/main/java/org/apache/openmeetings/service/user/UserManager.java b/openmeetings-service/src/main/java/org/apache/openmeetings/service/user/UserManager.java
index ae14a44..6026409 100644
--- a/openmeetings-service/src/main/java/org/apache/openmeetings/service/user/UserManager.java
+++ b/openmeetings-service/src/main/java/org/apache/openmeetings/service/user/UserManager.java
@@ -32,14 +32,12 @@ import static org.apache.openmeetings.util.OpenmeetingsVariables.getWebAppRootKe
 
 import java.io.IOException;
 import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
 import java.util.UUID;
 
-import org.apache.commons.codec.binary.Base64;
 import org.apache.openmeetings.core.remote.ScopeApplicationAdapter;
 import org.apache.openmeetings.db.dao.basic.ConfigurationDao;
 import org.apache.openmeetings.db.dao.label.LabelDao;
@@ -56,6 +54,8 @@ import org.apache.openmeetings.db.entity.user.User.Type;
 import org.apache.openmeetings.db.manager.IStreamClientManager;
 import org.apache.openmeetings.service.mail.EmailManager;
 import org.apache.openmeetings.util.OmException;
+import org.apache.openmeetings.util.crypt.CryptProvider;
+import org.apache.openmeetings.util.crypt.ICrypt;
 import org.apache.wicket.util.string.Strings;
 import org.red5.logging.Red5LoggerFactory;
 import org.red5.server.api.scope.IScope;
@@ -273,10 +273,6 @@ public class UserManager implements IUserManager {
 			return null;
 		}
 		// generate random password
-		SecureRandom rnd = new SecureRandom();
-		byte[] rawPass = new byte[25];
-		rnd.nextBytes(rawPass);
-		String pass = Base64.encodeBase64String(rawPass);
 		// check if the user already exists and register new one if it's needed
 		if (u == null) {
 			u = getNewUserInstance(null);
@@ -303,7 +299,8 @@ public class UserManager implements IUserManager {
 			}
 		}
 		u.setLastlogin(new Date());
-		u = userDao.update(u, pass, Long.valueOf(-1));
+		ICrypt crypt = CryptProvider.get();
+		u = userDao.update(u, crypt.randomPassword(25), Long.valueOf(-1));
 
 		return u;
 	}
diff --git a/openmeetings-util/src/main/java/org/apache/openmeetings/util/crypt/ICrypt.java b/openmeetings-util/src/main/java/org/apache/openmeetings/util/crypt/ICrypt.java
index 88b8c7f..dcb6f12 100644
--- a/openmeetings-util/src/main/java/org/apache/openmeetings/util/crypt/ICrypt.java
+++ b/openmeetings-util/src/main/java/org/apache/openmeetings/util/crypt/ICrypt.java
@@ -58,4 +58,6 @@ public interface ICrypt {
 	 * @return <code>true</code> in case string matches hash, <code>false</code> otherwise
 	 */
 	boolean fallback(String str, String hash);
+
+	String randomPassword(int length);
 }
diff --git a/openmeetings-util/src/main/java/org/apache/openmeetings/util/crypt/SCryptImplementation.java b/openmeetings-util/src/main/java/org/apache/openmeetings/util/crypt/SCryptImplementation.java
index 94dec75..ff5efe8 100644
--- a/openmeetings-util/src/main/java/org/apache/openmeetings/util/crypt/SCryptImplementation.java
+++ b/openmeetings-util/src/main/java/org/apache/openmeetings/util/crypt/SCryptImplementation.java
@@ -31,15 +31,27 @@ import org.slf4j.Logger;
 
 public class SCryptImplementation implements ICrypt {
 	private static final Logger log = Red5LoggerFactory.getLogger(SCryptImplementation.class, getWebAppRootKey());
+	private static final ThreadLocal<SecureRandom> rnd = new ThreadLocal<SecureRandom>() {
+		@Override
+		protected SecureRandom initialValue() {
+			SecureRandom sr;
+			try {
+				sr = SecureRandom.getInstance(SECURE_RND_ALG);
+			} catch (NoSuchAlgorithmException e) {
+				log.error("Failed to get instance of SecureRandom {}", SECURE_RND_ALG);
+				sr = new SecureRandom();
+			}
+			return sr;
+		}
+	};
 	private static final String SECURE_RND_ALG = "SHA1PRNG";
 	private static final int COST = 1024 * 16;
 	private static final int KEY_LENGTH = 512;
 	private static final int SALT_LENGTH = 200;
 
-	private static byte[] getSalt() throws NoSuchAlgorithmException {
-		SecureRandom sr = SecureRandom.getInstance(SECURE_RND_ALG);
-		byte[] salt = new byte[SALT_LENGTH];
-		sr.nextBytes(salt);
+	private static byte[] getSalt(int length) {
+		byte[] salt = new byte[length];
+		rnd.get().nextBytes(salt);
 		return salt;
 	}
 
@@ -53,15 +65,9 @@ public class SCryptImplementation implements ICrypt {
 		if (str == null) {
 			return null;
 		}
-		String hash = null;
-		try {
-			byte[] salt = getSalt();
-			String h = hash(str, salt);
-			hash = String.format("%s:%s", h, Base64.encodeBase64String(salt));
-		} catch (NoSuchAlgorithmException e) {
-			log.error("Error", e);
-		}
-		return hash;
+		byte[] salt = getSalt(SALT_LENGTH);
+		String h = hash(str, salt);
+		return String.format("%s:%s", h, Base64.encodeBase64String(salt));
 	}
 
 	@Override
@@ -96,4 +102,9 @@ public class SCryptImplementation implements ICrypt {
 		}
 		return false;
 	}
+
+	@Override
+	public String randomPassword(int length) {
+		return Base64.encodeBase64String(getSalt(length));
+	}
 }
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/AdminSavePanel.html b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/AdminActionsPanel.html
similarity index 86%
rename from openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/AdminSavePanel.html
rename to openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/AdminActionsPanel.html
index a930582..635519c 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/AdminSavePanel.html
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/AdminActionsPanel.html
@@ -26,13 +26,10 @@
 		<div class="ui-button ui-widget ui-corner-all ui-button-icon-only ui-state-highlight" wicket:id="btn-delete" wicket:message="title:157">
 			<span class="ui-button-icon ui-icon ui-icon-closethick"></span>&nbsp;
 		</div>
-		<div class="ui-button ui-widget ui-corner-all ui-button-icon-only ui-state-error" wicket:id="btn-purge" wicket:message="title:admin.purge">
-			<span class="ui-button-icon ui-icon ui-icon-trash"></span>&nbsp;
-		</div>
 		<div class="ui-button ui-widget ui-corner-all ui-button-icon-only" wicket:id="btn-restore" wicket:message="title:admin.restore">
 			<span class="ui-button-icon ui-icon ui-icon-circle-check"></span>&nbsp;
 		</div>
 		<span wicket:id="newRecord"></span>
-		<form wicket:id="form"></form>
+		<form wicket:id="form" style="display: inline;"></form>
 	</wicket:extend>
 </html>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/AdminSavePanel.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/AdminActionsPanel.java
similarity index 76%
rename from openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/AdminSavePanel.java
rename to openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/AdminActionsPanel.java
index 24098c4..8c0bed9 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/AdminSavePanel.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/AdminActionsPanel.java
@@ -19,22 +19,21 @@
 package org.apache.openmeetings.web.admin;
 
 import org.apache.openmeetings.web.common.ConfirmableAjaxBorder;
-import org.apache.openmeetings.web.common.FormSaveRefreshPanel;
+import org.apache.openmeetings.web.common.FormActionsPanel;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.ajax.markup.html.form.AjaxButton;
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.form.Form;
 import org.apache.wicket.model.Model;
 
-public abstract class AdminSavePanel<T> extends FormSaveRefreshPanel<T> {
+public abstract class AdminActionsPanel<T> extends FormActionsPanel<T> {
 	private static final long serialVersionUID = 1L;
 	private final Label newRecord = new Label("newRecord", Model.of(""));
 	private final Form<T> form;
 	private ConfirmableAjaxBorder delBtn;
-	private ConfirmableAjaxBorder purgeBtn;
 	private AjaxButton restoreBtn;
 
-	public AdminSavePanel(String id, final Form<T> form) {
+	public AdminActionsPanel(String id, final Form<T> form) {
 		super(id, form);
 		this.form = form;
 	}
@@ -60,7 +59,7 @@ public abstract class AdminSavePanel<T> extends FormSaveRefreshPanel<T> {
 			protected void onError(AjaxRequestTarget target) {
 				// repaint the feedback panel so errors are shown
 				target.add(feedback);
-				AdminSavePanel.this.onError(target, form);
+				AdminActionsPanel.this.onError(target, form);
 			}
 		};
 		// add a cancel button that can be used to submit the form via ajax
@@ -75,7 +74,7 @@ public abstract class AdminSavePanel<T> extends FormSaveRefreshPanel<T> {
 				// repaint the feedback panel so errors are shown
 				target.add(feedback);
 				setNewVisible(false);
-				AdminSavePanel.this.onError(target, form);
+				AdminActionsPanel.this.onError(target, form);
 			}
 
 			@Override
@@ -86,24 +85,6 @@ public abstract class AdminSavePanel<T> extends FormSaveRefreshPanel<T> {
 				onDeleteSubmit(target, form);
 			}
 		};
-		purgeBtn = new ConfirmableAjaxBorder("btn-purge", getString("80"), getString("833"), cForm) {
-			private static final long serialVersionUID = 1L;
-
-			@Override
-			protected void onSubmit(AjaxRequestTarget target) {
-				// repaint the feedback panel so that it is hidden
-				target.add(feedback);
-				setNewVisible(false);
-				onPurgeSubmit(target, form);
-			}
-
-			@Override
-			protected void onError(AjaxRequestTarget target) {
-				// repaint the feedback panel so errors are shown
-				target.add(feedback);
-				AdminSavePanel.this.onError(target, form);
-			}
-		};
 		restoreBtn = new AjaxButton("btn-restore", form) {
 			private static final long serialVersionUID = 1L;
 
@@ -119,11 +100,10 @@ public abstract class AdminSavePanel<T> extends FormSaveRefreshPanel<T> {
 			protected void onError(AjaxRequestTarget target) {
 				// repaint the feedback panel so errors are shown
 				target.add(feedback);
-				AdminSavePanel.this.onError(target, form);
+				AdminActionsPanel.this.onError(target, form);
 			}
 		};
 		add(newBtn, delBtn
-				, purgeBtn.setOutputMarkupPlaceholderTag(true).setVisible(false)
 				, restoreBtn.setOutputMarkupPlaceholderTag(true).setVisible(false));
 		super.onInitialize();
 	}
@@ -137,16 +117,11 @@ public abstract class AdminSavePanel<T> extends FormSaveRefreshPanel<T> {
 		delBtn.setVisible(visible);
 	}
 
-	public void setPurgeVisible(boolean visible) {
-		purgeBtn.setVisible(visible);
-	}
-
 	public void setRestoreVisible(boolean visible) {
 		restoreBtn.setVisible(visible);
 	}
 
 	protected abstract void onNewSubmit(AjaxRequestTarget target, Form<?> form);
 	protected abstract void onDeleteSubmit(AjaxRequestTarget target, Form<?> form);
-	protected abstract void onPurgeSubmit(AjaxRequestTarget target, Form<?> form);
 	protected abstract void onRestoreSubmit(AjaxRequestTarget target, Form<?> form);
 }
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/AdminBaseForm.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/AdminBaseForm.java
index 9fb1dc2..25478db 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/AdminBaseForm.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/AdminBaseForm.java
@@ -35,14 +35,14 @@ import org.apache.wicket.util.time.Duration;
  */
 public abstract class AdminBaseForm<T> extends Form<T> {
 	private static final long serialVersionUID = 1L;
-	private AdminSavePanel<T> savePanel;
+	private AdminActionsPanel<T> savePanel;
 	protected final AjaxFormValidatingBehavior validationBehavior
 			= new AjaxFormValidatingBehavior("keydown", Duration.ONE_SECOND);
 
 	public AdminBaseForm(String id, IModel<T> object) {
 		super(id, object);
 
-		savePanel = new AdminSavePanel<T>("buttons", this) {
+		savePanel = new AdminActionsPanel<T>("buttons", this) {
 			private static final long serialVersionUID = 1L;
 
 			@Override
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/users/UserForm.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/users/UserForm.java
index 9c75db7..e6e65bb 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/users/UserForm.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/users/UserForm.java
@@ -29,10 +29,8 @@ import static org.apache.openmeetings.web.app.WebSession.getRights;
 import static org.apache.openmeetings.web.app.WebSession.getUserId;
 import static org.apache.wicket.validation.validator.StringValidator.minimumLength;
 
-import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -46,14 +44,11 @@ import org.apache.openmeetings.db.dao.server.OAuth2Dao;
 import org.apache.openmeetings.db.dao.user.UserDao;
 import org.apache.openmeetings.db.entity.server.LdapConfig;
 import org.apache.openmeetings.db.entity.server.OAuthServer;
-import org.apache.openmeetings.db.entity.user.Address;
-import org.apache.openmeetings.db.entity.user.AsteriskSipUser;
 import org.apache.openmeetings.db.entity.user.User;
 import org.apache.openmeetings.db.entity.user.User.Right;
 import org.apache.openmeetings.db.entity.user.User.Type;
 import org.apache.openmeetings.db.util.AuthLevelUtil;
 import org.apache.openmeetings.service.mail.EmailManager;
-import org.apache.openmeetings.util.OmFileHelper;
 import org.apache.openmeetings.web.admin.AdminBaseForm;
 import org.apache.openmeetings.web.common.ComunityUserForm;
 import org.apache.openmeetings.web.common.GeneralUserForm;
@@ -175,6 +170,12 @@ public class UserForm extends AdminBaseForm<User> {
 	}
 
 	@Override
+	protected void onConfigure() {
+		super.onConfigure();
+		onModelChanged();
+	}
+
+	@Override
 	protected void onModelChanged() {
 		super.onModelChanged();
 		boolean nd = !getModelObject().isDeleted();
@@ -197,23 +198,12 @@ public class UserForm extends AdminBaseForm<User> {
 
 	@Override
 	protected void onPurgeSubmit(AjaxRequestTarget target, Form<?> form) {
-		User u = getModelObject();
-		u.setDeleted(true);
-		u.setSipUser(new AsteriskSipUser());
-		u.setAddress(new Address());
-		u.setAge(new Date());
-		u.setExternalId(null);
-		final String purged = String.format("Purged %s", UUID.randomUUID());
-		u.setFirstname(purged);
-		u.setLastname(purged);
-		u.setLogin(purged);
-		File pic = OmFileHelper.getUserProfilePicture(u.getId(), u.getPictureuri(), null);
-		if (pic != null) {
-			pic.delete();
+		if (isAdminPassRequired()) {
+			adminPass.setAction((SerializableConsumer<AjaxRequestTarget>)t -> purgeUser(t));
+			adminPass.open(target);
+		} else {
+			purgeUser(target);
 		}
-		//u.
-		//User fields "age, externaluserid, firstname, lastname, login, pictureuri" will be replaced with "Purged_some_hash"
-		//onSaveSubmit(target, form);
 	}
 
 	@Override
@@ -237,6 +227,11 @@ public class UserForm extends AdminBaseForm<User> {
 		return checkLevel(u.getRights()) || (ou != null && checkLevel(ou.getRights()));
 	}
 
+	private void purgeUser(AjaxRequestTarget target) {
+		getBean(UserDao.class).purge(getModelObject(), getUserId());
+		updateForm(target);
+	}
+
 	private void saveUser(AjaxRequestTarget target, String pass) {
 		User u = getModelObject();
 		final UserDao dao = getBean(UserDao.class);
@@ -254,13 +249,19 @@ public class UserForm extends AdminBaseForm<User> {
 			String email = u.getAddress().getEmail();
 			getBean(EmailManager.class).sendMail(login.getValue(), email, u.getActivatehash(), false, null);
 		}
+		updateForm(target);
+		if (u.getGroupUsers().isEmpty()) {
+			warning.open(target);
+		}
+	}
+
+	private void updateForm(AjaxRequestTarget target) {
+		User u = getModelObject();
+		final UserDao dao = getBean(UserDao.class);
 		setModelObject(dao.get(u.getId()));
 		setNewVisible(false);
 		target.add(this, listContainer);
 		reinitJs(target);
-		if (u.getGroupUsers().isEmpty()) {
-			warning.open(target);
-		}
 	}
 
 	@Override
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/ConfirmableAjaxBorder.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/ConfirmableAjaxBorder.java
index c4c72cd..e7bf7b9 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/ConfirmableAjaxBorder.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/ConfirmableAjaxBorder.java
@@ -23,6 +23,7 @@ import static org.apache.openmeetings.web.common.BasePanel.EVT_CLICK;
 import org.apache.wicket.ajax.AjaxEventBehavior;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
+import org.apache.wicket.ajax.form.AjaxFormSubmitBehavior;
 import org.apache.wicket.markup.html.border.Border;
 import org.apache.wicket.markup.html.form.Form;
 import org.apache.wicket.markup.html.panel.EmptyPanel;
@@ -39,7 +40,9 @@ public abstract class ConfirmableAjaxBorder extends Border {
 	private static final long serialVersionUID = 1L;
 	private static final String DIALOG_ID = "dialog";
 	protected final Form<?> form = new Form<>("form");
+	protected final Form<?> userForm;
 	private final ConfirmableBorderDialog dialog;
+	private boolean validate = false;
 
 	public ConfirmableAjaxBorder(String id, String title, String message) {
 		this(id, title, message, null, null);
@@ -54,6 +57,10 @@ public abstract class ConfirmableAjaxBorder extends Border {
 	}
 
 	public ConfirmableAjaxBorder(String id, String title, String message, Form<?> userForm, ConfirmableBorderDialog dialog) {
+		this(id, title, message, userForm, dialog, false);
+	}
+
+	public ConfirmableAjaxBorder(String id, String title, String message, Form<?> userForm, ConfirmableBorderDialog dialog, boolean validate) {
 		super(id, Model.of(message));
 		if (dialog == null) {
 			this.dialog = new ConfirmableBorderDialog(DIALOG_ID, title, message, userForm == null ? form : userForm);
@@ -62,6 +69,8 @@ public abstract class ConfirmableAjaxBorder extends Border {
 			this.dialog = dialog;
 			form.add(new EmptyPanel(DIALOG_ID));
 		}
+		this.userForm = userForm;
+		this.validate = validate;
 		this.dialog.setSubmitHandler((SerializableConsumer<AjaxRequestTarget>)t->onSubmit(t));
 		this.dialog.setErrorHandler((SerializableConsumer<AjaxRequestTarget>)t->onError(t));
 		setOutputMarkupId(true);
@@ -74,22 +83,38 @@ public abstract class ConfirmableAjaxBorder extends Border {
 	@Override
 	protected void onInitialize() {
 		super.onInitialize();
-		add(new AjaxEventBehavior(EVT_CLICK) {
-			private static final long serialVersionUID = 1L;
-
-			@Override
-			protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
-				super.updateAjaxAttributes(attributes);
-				ConfirmableAjaxBorder.this.updateAjaxAttributes(attributes);
-			}
+		if (validate) {
+			add(new AjaxFormSubmitBehavior(EVT_CLICK) {
+				private static final long serialVersionUID = 1L;
 
-			@Override
-			protected void onEvent(AjaxRequestTarget target) {
-				if (isClickable()) {
+				@Override
+				protected void onSubmit(AjaxRequestTarget target) {
 					dialog.open(target);
 				}
-			}
-		});
+
+				@Override
+				protected void onError(AjaxRequestTarget target) {
+					ConfirmableAjaxBorder.this.onError(target);
+				}
+			});
+		} else {
+			add(new AjaxEventBehavior(EVT_CLICK) {
+				private static final long serialVersionUID = 1L;
+
+				@Override
+				protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
+					super.updateAjaxAttributes(attributes);
+					ConfirmableAjaxBorder.this.updateAjaxAttributes(attributes);
+				}
+
+				@Override
+				protected void onEvent(AjaxRequestTarget target) {
+					if (isClickable()) {
+						dialog.open(target);
+					}
+				}
+			});
+		}
 		addToBorder(form);
 	}
 
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/FormSaveRefreshPanel.html b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/FormActionsPanel.html
similarity index 79%
rename from openmeetings-web/src/main/java/org/apache/openmeetings/web/common/FormSaveRefreshPanel.html
rename to openmeetings-web/src/main/java/org/apache/openmeetings/web/common/FormActionsPanel.html
index 6790cbb..6578d94 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/FormSaveRefreshPanel.html
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/FormActionsPanel.html
@@ -21,13 +21,16 @@
 <html xmlns:wicket="http://wicket.apache.org">
 <wicket:panel>
 	<div class="formSaveBar ui-widget-header">
-		<div class="ui-button ui-widget ui-corner-all ui-button-icon-only" wicket:id="ajax-save-button" wicket:message="title:144">
+		<div class="ui-button ui-widget ui-corner-all ui-button-icon-only" wicket:id="btn-save" wicket:message="title:144">
 			<span class="ui-button-icon ui-icon ui-icon-disk"></span>&nbsp;
 		</div>
-		<div class="ui-button ui-widget ui-corner-all ui-button-icon-only" wicket:id="ajax-refresh-button" wicket:message="title:lbl.refresh">
+		<div class="ui-button ui-widget ui-corner-all ui-button-icon-only" wicket:id="btn-refresh" wicket:message="title:lbl.refresh">
 			<span class="ui-button-icon ui-icon ui-icon-refresh"></span>&nbsp;
 		</div>
 		<wicket:child/>
+		<div class="ui-button ui-widget ui-corner-all ui-button-icon-only ui-state-error" wicket:id="btn-purge" wicket:message="title:admin.purge">
+			<span class="ui-button-icon ui-icon ui-icon-trash"></span>&nbsp;
+		</div>
 	</div>
 	<div wicket:id="feedback" class="error adminFormsFeedbackPanel"></div>
 </wicket:panel>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/FormSaveRefreshPanel.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/FormActionsPanel.java
similarity index 73%
rename from openmeetings-web/src/main/java/org/apache/openmeetings/web/common/FormSaveRefreshPanel.java
rename to openmeetings-web/src/main/java/org/apache/openmeetings/web/common/FormActionsPanel.java
index cca36cf..142cb99 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/FormSaveRefreshPanel.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/FormActionsPanel.java
@@ -26,13 +26,14 @@ import org.apache.wicket.markup.html.panel.Panel;
 import com.googlecode.wicket.jquery.core.Options;
 import com.googlecode.wicket.kendo.ui.panel.KendoFeedbackPanel;
 
-public abstract class FormSaveRefreshPanel<T> extends Panel {
+public abstract class FormActionsPanel<T> extends Panel {
 	private static final long serialVersionUID = 1L;
 	private final Form<T> form;
 	protected final KendoFeedbackPanel feedback = new KendoFeedbackPanel("feedback", new Options("button", true));
 	private AjaxButton saveBtn;
+	private ConfirmableAjaxBorder purgeBtn;
 
-	public FormSaveRefreshPanel(String id, Form<T> form) {
+	public FormActionsPanel(String id, Form<T> form) {
 		super(id);
 		this.form = form;
 		setOutputMarkupId(true);
@@ -43,7 +44,7 @@ public abstract class FormSaveRefreshPanel<T> extends Panel {
 		add(feedback.setOutputMarkupId(true));
 
 		// add a save button that can be used to submit the form via ajax
-		add(saveBtn = new AjaxButton("ajax-save-button", form) {
+		add(saveBtn = new AjaxButton("btn-save", form) {
 			private static final long serialVersionUID = 1L;
 
 			@Override
@@ -57,12 +58,12 @@ public abstract class FormSaveRefreshPanel<T> extends Panel {
 			protected void onError(AjaxRequestTarget target) {
 				// repaint the feedback panel so errors are shown
 				target.add(feedback);
-				FormSaveRefreshPanel.this.onError(target, form);
+				FormActionsPanel.this.onError(target, form);
 			}
 		});
 
 		// add a refresh button that can be used to submit the form via ajax
-		add(new AjaxButton("ajax-refresh-button", form) {
+		add(new AjaxButton("btn-refresh", form) {
 			private static final long serialVersionUID = 1L;
 
 			@Override
@@ -78,9 +79,28 @@ public abstract class FormSaveRefreshPanel<T> extends Panel {
 				// repaint the feedback panel so errors are shown
 				target.add(feedback);
 				setNewVisible(false);
-				FormSaveRefreshPanel.this.onError(target, form);
+				FormActionsPanel.this.onError(target, form);
 			}
 		});
+		purgeBtn = new ConfirmableAjaxBorder("btn-purge", getString("80"), getString("833"), form, null, true) {
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			protected void onSubmit(AjaxRequestTarget target) {
+				// repaint the feedback panel so that it is hidden
+				target.add(feedback);
+				setNewVisible(false);
+				onPurgeSubmit(target, form);
+			}
+
+			@Override
+			protected void onError(AjaxRequestTarget target) {
+				// repaint the feedback panel so errors are shown
+				target.add(feedback);
+				FormActionsPanel.this.onError(target, form);
+			}
+		};
+		add(purgeBtn.setOutputMarkupPlaceholderTag(true).setVisible(false));
 		super.onInitialize();
 	}
 
@@ -97,7 +117,13 @@ public abstract class FormSaveRefreshPanel<T> extends Panel {
 		// for admin only, will be implemented in admin
 	}
 
+	public void setPurgeVisible(boolean visible) {
+		purgeBtn.setVisible(visible);
+	}
+
 	protected abstract void onSaveSubmit(AjaxRequestTarget target, Form<?> form);
+	protected abstract void onRefreshSubmit(AjaxRequestTarget target, Form<?> form);
+	protected abstract void onPurgeSubmit(AjaxRequestTarget target, Form<?> form);
 
 	/**
 	 * Save error handler
@@ -108,6 +134,4 @@ public abstract class FormSaveRefreshPanel<T> extends Panel {
 	protected void onError(AjaxRequestTarget target, Form<?> form) {
 		//no-op
 	}
-
-	protected abstract void onRefreshSubmit(AjaxRequestTarget target, Form<?> form);
 }
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ProfileForm.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ProfileForm.java
index b25e900..e29b2eb 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ProfileForm.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ProfileForm.java
@@ -25,8 +25,10 @@ import static org.apache.openmeetings.web.common.BasePanel.EVT_CLICK;
 
 import org.apache.openmeetings.db.dao.user.UserDao;
 import org.apache.openmeetings.db.entity.user.User;
+import org.apache.openmeetings.web.app.Application;
+import org.apache.openmeetings.web.app.WebSession;
 import org.apache.openmeetings.web.common.ComunityUserForm;
-import org.apache.openmeetings.web.common.FormSaveRefreshPanel;
+import org.apache.openmeetings.web.common.FormActionsPanel;
 import org.apache.openmeetings.web.common.GeneralUserForm;
 import org.apache.openmeetings.web.common.UploadableProfileImagePanel;
 import org.apache.wicket.ajax.AjaxEventBehavior;
@@ -52,6 +54,7 @@ public class ProfileForm extends Form<User> {
 	private final PasswordTextField passwd = new PasswordTextField("passwd", new Model<String>());
 	private final GeneralUserForm userForm;
 	private final ChangePasswordDialog chPwdDlg;
+	private FormActionsPanel<User> actions;
 
 	public ProfileForm(String id, final ChangePasswordDialog chPwdDlg) {
 		super(id, new CompoundPropertyModel<>(getBean(UserDao.class).get(getUserId())));
@@ -64,7 +67,7 @@ public class ProfileForm extends Form<User> {
 		super.onInitialize();
 		add(passwd.setLabel(Model.of(getString("current.password"))).setRequired(true));
 
-		add(new FormSaveRefreshPanel<User>("buttons", this) {
+		add(actions = new FormActionsPanel<User>("buttons", this) {
 			private static final long serialVersionUID = 1L;
 
 			private void refreshUser() {
@@ -93,6 +96,13 @@ public class ProfileForm extends Form<User> {
 				refreshUser();
 				target.add(ProfileForm.this);
 			}
+
+			@Override
+			protected void onPurgeSubmit(AjaxRequestTarget target, Form<?> form) {
+				getBean(UserDao.class).purge(getModelObject(), getUserId());
+				WebSession.get().invalidateNow();
+				setResponsePage(Application.get().getSignInPageClass());
+			}
 		});
 		add(new WebMarkupContainer("changePwd").add(new ButtonBehavior("#changePwd"), new AjaxEventBehavior(EVT_CLICK) {
 			private static final long serialVersionUID = 1L;
@@ -112,6 +122,12 @@ public class ProfileForm extends Form<User> {
 	}
 
 	@Override
+	protected void onConfigure() {
+		super.onConfigure();
+		actions.setPurgeVisible(true);
+	}
+
+	@Override
 	protected void onValidate() {
 		String p = passwd.getConvertedInput();
 		if (!Strings.isEmpty(p) && !getBean(UserDao.class).verifyPassword(getModelObject().getId(), p)) {

-- 
To stop receiving notification emails like this one, please contact
solomax@apache.org.