/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <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>
* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <p>
*/
package org.olat.admin.user.delete.service;
import java.io.File;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.olat.admin.user.delete.SelectionController;
import org.olat.basesecurity.Authentication;
import org.olat.basesecurity.IdentityImpl;
import org.olat.basesecurity.Manager;
import org.olat.basesecurity.ManagerFactory;
import org.olat.basesecurity.SecurityGroup;
import org.olat.bookmark.BookmarkManager;
import org.olat.catalog.CatalogManager;
import org.olat.commons.calendar.CalendarManagerFactory;
import org.olat.commons.lifecycle.LifeCycleManager;
import org.olat.core.commons.persistence.DBFactory;
import org.olat.core.commons.persistence.DBQuery;
import org.olat.core.gui.translator.Translator;
import org.olat.core.id.Identity;
import org.olat.core.id.User;
import org.olat.core.id.UserConstants;
import org.olat.core.logging.Tracing;
import org.olat.core.util.StringHelper;
import org.olat.core.util.Util;
import org.olat.core.util.coordinate.CoordinatorManager;
import org.olat.core.util.coordinate.SyncerExecutor;
import org.olat.core.util.i18n.I18nManager;
import org.olat.core.util.mail.MailTemplate;
import org.olat.core.util.mail.MailerResult;
import org.olat.core.util.mail.MailerWithTemplate;
import org.olat.core.util.notifications.NotificationsManager;
import org.olat.core.util.resource.OresHelper;
import org.olat.course.assessment.EfficiencyStatementManager;
import org.olat.group.BusinessGroupManagerImpl;
import org.olat.ims.qti.QTIResultManager;
import org.olat.modules.iq.IQManager;
import org.olat.note.NoteManager;
import org.olat.properties.Property;
import org.olat.properties.PropertyManager;
import org.olat.repository.delete.service.DeletionModule;
import org.olat.repository.delete.service.RepositoryDeletionManager;
import org.olat.user.DisplayPortraitManager;
import org.olat.user.HomePageConfigManagerImpl;
import org.olat.user.PersonalFolderManager;
import org.olat.user.UserDataDeletable;
import org.olat.user.UserManager;
import org.olat.user.propertyhandlers.UserPropertyHandler;
/**
* Manager for user-deletion.
*
* @author Christian Guretzki
*/
public class UserDeletionManager {
public static final String DELETED_USER_DELIMITER = "_bkp_";
/** Default value for last-login duration in month. */
private static final int DEFAULT_LAST_LOGIN_DURATION = 24;
/** Default value for delete-email duration in days. */
private static final int DEFAULT_DELETE_EMAIL_DURATION = 30;
private static final String LAST_LOGIN_DURATION_PROPERTY_NAME = "LastLoginDuration";
private static final String DELETE_EMAIL_DURATION_PROPERTY_NAME = "DeleteEmailDuration";
private static final String PROPERTY_CATEGORY = "UserDeletion";
private static final UserDeletionManager INSTANCE = new UserDeletionManager();
public static final String SEND_DELETE_EMAIL_ACTION = "sendDeleteEmail";
private static final String USER_ARCHIVE_DIR = "archive_deleted_users";
private static final String USER_DELETED_ACTION = "userdeleted";
private String emailResponseTo;
private Identity adminIdentity;
private static boolean keepUserLoginAfterDeletion;
private static boolean keepUserEmailAfterDeletion;
private Set<UserDataDeletable> userDataDeletableResources;
// Flag used in user-delete to indicate that all deletable managers are initialized
private boolean managersInitialized = false;
private String archiveRootDir;
private UserDeletionManager() {
userDataDeletableResources = new HashSet<UserDataDeletable>();
}
/**
* @return Singleton.
*/
public static UserDeletionManager getInstance() { return INSTANCE; }
/**
* Send 'delete'- emails to a list of identities. The delete email is an announcement for the user-deletion.
*
* @param selectedIdentities
* @return String with warning message (e.g. email-address not valid, could not send email).
* If there is no warning, the return String is empty ("").
*/
public String sendUserDeleteEmailTo(List<Identity> selectedIdentities, MailTemplate template,
boolean isTemplateChanged, String keyEmailSubject, String keyEmailBody, Identity sender, Translator pT ) {
StringBuilder buf = new StringBuilder();
if (template != null) {
MailerWithTemplate mailer = MailerWithTemplate.getInstance();
template.addToContext("responseTo", emailResponseTo);
for (Iterator iter = selectedIdentities.iterator(); iter.hasNext();) {
Identity identity = (Identity)iter.next();
if (!isTemplateChanged) {
// Email template has NOT changed => take translated version of subject and body text
Translator identityTranslator = Util.createPackageTranslator(SelectionController.class, I18nManager.getInstance().getLocaleOrDefault(identity.getUser().getPreferences().getLanguage()));
template.setSubjectTemplate(identityTranslator.translate(keyEmailSubject));
template.setBodyTemplate(identityTranslator.translate(keyEmailBody));
}
template.putVariablesInMailContext(template.getContext(), identity);
Tracing.logDebug(" Try to send Delete-email to identity=" + identity.getName() + " with email=" + identity.getUser().getProperty(UserConstants.EMAIL, null), this.getClass());
List<Identity> ccIdentities = new ArrayList<Identity>();
if(template.getCpfrom()) {
ccIdentities.add(sender);
} else {
ccIdentities = null;
}
MailerResult mailerResult = mailer.sendMailUsingTemplateContext(identity, ccIdentities, null, template, sender);
if (mailerResult.getReturnCode() != MailerResult.OK) {
buf.append(pT.translate("email.error.send.failed", new String[] {identity.getUser().getProperty(UserConstants.EMAIL, null), identity.getName()} )).append("\n");
}
Tracing.logAudit("User-Deletion: Delete-email send to identity=" + identity.getName() + " with email=" + identity.getUser().getProperty(UserConstants.EMAIL, null), this.getClass());
markSendEmailEvent(identity);
}
} else {
// no template => User decides to sending no delete-email, mark only in lifecycle table 'sendEmail'
for (Iterator iter = selectedIdentities.iterator(); iter.hasNext();) {
Identity identity = (Identity)iter.next();
Tracing.logAudit("User-Deletion: Move in 'Email sent' section without sending email, identity=" + identity.getName(), this.getClass());
markSendEmailEvent(identity);
}
}
return buf.toString();
}
private void markSendEmailEvent(Identity identity) {
identity = (Identity)DBFactory.getInstance().loadObject(identity);
LifeCycleManager.createInstanceFor(identity).markTimestampFor(SEND_DELETE_EMAIL_ACTION);
DBFactory.getInstance().updateObject(identity);
}
/**
* Return list of identities which have last-login older than 'lastLoginDuration' parameter.
* This user are ready to start with user-deletion process.
* @param lastLoginDuration last-login duration in month
* @return List of Identity objects
*/
public List getDeletableIdentities(int lastLoginDuration) {
Calendar lastLoginLimit = Calendar.getInstance();
lastLoginLimit.add(Calendar.MONTH, - lastLoginDuration);
Tracing.logDebug("lastLoginLimit=" + lastLoginLimit, this.getClass());
// 1. get all 'active' identities with lastlogin > x
String queryStr ="from org.olat.core.id.Identity as ident where ident.status = '"
+ Identity.STATUS_ACTIV
+ "' and (ident.lastLogin = null or ident.lastLogin < :lastLogin)";
DBQuery dbq = DBFactory.getInstance().createQuery(queryStr);
dbq.setDate("lastLogin", lastLoginLimit.getTime());
List identities = dbq.list();
// 2. get all 'active' identities in deletion process
queryStr = "select ident from org.olat.core.id.Identity as ident"
+ " , org.olat.commons.lifecycle.LifeCycleEntry as le"
+ " where ident.key = le.persistentRef "
+ " and le.persistentTypeName ='" + IdentityImpl.class.getName() + "'"
+ " and le.action ='" + SEND_DELETE_EMAIL_ACTION + "' ";
dbq = DBFactory.getInstance().createQuery(queryStr);
List identitiesInProcess = dbq.list();
// 3. Remove all identities in deletion-process from all inactive-identities
identities.removeAll(identitiesInProcess);
return identities;
}
/**
* Return list of identities which are in user-deletion-process.
* user-deletion-process means delete-announcement.email send, duration of waiting for response is not expired.
* @param deleteEmailDuration Duration of user-deletion-process in days
* @return List of Identity objects
*/
public List getIdentitiesInDeletionProcess(int deleteEmailDuration) {
Calendar deleteEmailLimit = Calendar.getInstance();
deleteEmailLimit.add(Calendar.DAY_OF_MONTH, - (deleteEmailDuration-1));
Tracing.logDebug("deleteEmailLimit=" + deleteEmailLimit, this.getClass());
String queryStr = "select ident from org.olat.core.id.Identity as ident"
+ " , org.olat.commons.lifecycle.LifeCycleEntry as le"
+ " where ident.key = le.persistentRef "
+ " and ident.status = '" + Identity.STATUS_ACTIV + "'"
+ " and le.persistentTypeName ='" + IdentityImpl.class.getName() + "'"
+ " and le.action ='" + SEND_DELETE_EMAIL_ACTION + "' and le.lcTimestamp >= :deleteEmailDate ";
DBQuery dbq = DBFactory.getInstance().createQuery(queryStr);
dbq.setDate("deleteEmailDate", deleteEmailLimit.getTime());
return dbq.list();
}
/**
* Return list of identities which are ready-to-delete in user-deletion-process.
* (delete-announcement.email send, duration of waiting for response is expired).
* @param deleteEmailDuration Duration of user-deletion-process in days
* @return List of Identity objects
*/
public List getIdentitiesReadyToDelete(int deleteEmailDuration) {
Calendar deleteEmailLimit = Calendar.getInstance();
deleteEmailLimit.add(Calendar.DAY_OF_MONTH, - (deleteEmailDuration - 1));
Tracing.logDebug("deleteEmailLimit=" + deleteEmailLimit, this.getClass());
String queryStr = "select ident from org.olat.core.id.Identity as ident"
+ " , org.olat.commons.lifecycle.LifeCycleEntry as le"
+ " where ident.key = le.persistentRef "
+ " and ident.status = '" + Identity.STATUS_ACTIV + "'"
+ " and le.persistentTypeName ='" + IdentityImpl.class.getName() + "'"
+ " and le.action ='" + SEND_DELETE_EMAIL_ACTION + "' and le.lcTimestamp < :deleteEmailDate ";
DBQuery dbq = DBFactory.getInstance().createQuery(queryStr);
dbq.setDate("deleteEmailDate", deleteEmailLimit.getTime());
return dbq.list();
}
/**
*
* @return true when user can be deleted (non deletion-process is still running)
*/
public boolean isReadyToDelete() {
return UserFileDeletionManager.isReadyToDelete();
}
/**
* Delete all user-data in registered deleteable resources.
* @param identity
* @return true
*/
public void deleteIdentity(Identity identity) {
Tracing.logInfo("Start deleteIdentity for identity=" + identity, this.getClass());
String newName = getBackupStringWithDate(identity.getName());
// TODO: chg: Workaround: instances each manager which implements UaserDataDeletable interface
// Each manager register themself as deletable
// Should be better with new config concept
if (!managersInitialized) {
HomePageConfigManagerImpl.getInstance();
DisplayPortraitManager.getInstance();
NoteManager.getInstance();
PropertyManager.getInstance();
BookmarkManager.getInstance();
NotificationsManager.getInstance();
PersonalFolderManager.getInstance();
IQManager.getInstance();
QTIResultManager.getInstance();
BusinessGroupManagerImpl.getInstance();
RepositoryDeletionManager.getInstance();
CatalogManager.getInstance();
CalendarManagerFactory.getInstance();
EfficiencyStatementManager.getInstance();
UserFileDeletionManager.getInstance();
managersInitialized = true;
}
Tracing.logInfo("Start EfficiencyStatementManager.archiveUserData for identity=" + identity, this.getClass());
EfficiencyStatementManager.getInstance().archiveUserData(identity, getArchivFilePath(identity) );
Tracing.logInfo("Start Deleting user=" + identity, this.getClass());
for (Iterator<UserDataDeletable> iter = userDataDeletableResources.iterator(); iter.hasNext();) {
UserDataDeletable element = iter.next();
Tracing.logInfo("UserDataDeletable-Loop element=" + element, this.getClass());
element.deleteUserData(identity, newName);
}
Tracing.logInfo("deleteUserProperties user=" + identity.getUser(), this.getClass());
UserManager.getInstance().deleteUserProperties(identity.getUser());
// Delete all authentications for certain identity
List authentications = ManagerFactory.getManager().getAuthentications(identity);
for (Iterator iter = authentications.iterator(); iter.hasNext();) {
Authentication auth = (Authentication) iter.next();
Tracing.logInfo("deleteAuthentication auth=" + auth, this.getClass());
ManagerFactory.getManager().deleteAuthentication(auth);
Tracing.logInfo("Delete auth=" + auth + " of identity=" + identity, this.getClass());
}
//remove identity from its security groups
Manager secMgr = ManagerFactory.getManager();
List<SecurityGroup> securityGroups = ManagerFactory.getManager().getSecurityGroupsForIdentity(identity);
for (SecurityGroup secGroup : securityGroups) {
secMgr.removeIdentityFromSecurityGroup(identity, secGroup);
Tracing.logInfo("Removing user=" + identity + " from security group=" + secGroup.toString(), this.getClass());
}
// can be used, if there is once the possibility to delete identities without db-constraints...
//if neither email nor login should be kept, REALLY DELETE Identity
/*if (!keepUserEmailAfterDeletion & !keepUserLoginAfterDeletion){
identity = (Identity)DBFactory.getInstance().loadObject(identity);
DBFactory.getInstance().deleteObject(identity.getUser());
DBFactory.getInstance().deleteObject(identity);
}
else { */
identity = (Identity)DBFactory.getInstance().loadObject(identity);
//keep login-name only -> change email
if (!keepUserEmailAfterDeletion){
List<UserPropertyHandler> userPropertyHandlers = UserManager.getInstance().getUserPropertyHandlersFor("org.olat.admin.user.UsermanagerUserSearchForm", true);
User persistedUser = identity.getUser();
String actualProperty;
for (UserPropertyHandler userPropertyHandler : userPropertyHandlers) {
actualProperty = userPropertyHandler.getName();
if (actualProperty.equals(UserConstants.EMAIL)){
String oldEmail = userPropertyHandler.getUserProperty(persistedUser, null);
String newEmail = "";
if (StringHelper.containsNonWhitespace(oldEmail)){
newEmail = getBackupStringWithDate(oldEmail);
}
Tracing.logInfo("Update user-property user=" + persistedUser , this.getClass());
userPropertyHandler.setUserProperty(persistedUser, newEmail);
}
}
}
//keep email only -> change login-name
if (!keepUserLoginAfterDeletion){
identity.setName(newName);
}
//keep everything, change identity.status to deleted
Tracing.logInfo("Change stater identity=" + identity , this.getClass());
identity.setStatus(Identity.STATUS_DELETED);
DBFactory.getInstance().updateObject(identity);
LifeCycleManager.createInstanceFor(identity).deleteTimestampFor(SEND_DELETE_EMAIL_ACTION);
LifeCycleManager.createInstanceFor(identity).markTimestampFor(USER_DELETED_ACTION, createLifeCycleLogDataFor(identity));
// }
// TODO: chg: ev. logAudit at another place
Tracing.logAudit("User-Deletion: Delete all userdata for identity=" + identity, this.getClass());
}
public String getBackupStringWithDate(String original){
DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmm");
String dateStamp = dateFormat.format(new Date());
return dateStamp + DELETED_USER_DELIMITER + original;
}
private String createLifeCycleLogDataFor(Identity identity) {
StringBuilder buf = new StringBuilder();
buf.append("<identity>");
buf.append("<username>").append(identity.getName()).append("</username>");
buf.append("<lastname>").append(identity.getName()).append("</lastname>");
buf.append("<firstname>").append(identity.getName()).append("</firstname>");
buf.append("<email>").append(identity.getName()).append("</email>");
buf.append("</identity>");
return buf.toString();
}
/**
* Re-activate an identity, lastLogin = now, reset deleteemaildate = null.
* @param identity
*/
public void setIdentityAsActiv(final Identity anIdentity) {
CoordinatorManager.getCoordinator().getSyncer().doInSync(OresHelper.createOLATResourceableInstance(anIdentity.getClass(), anIdentity.getKey()) ,
new SyncerExecutor(){
public void execute() {
//o_clusterOK by:fj : must be fast
Identity identity = (Identity)DBFactory.getInstance().loadObject(anIdentity, true);
if (Tracing.isDebugEnabled(this.getClass())) Tracing.logDebug("setIdentityAsActiv beginSingleTransaction identity=" + identity, this.getClass());
identity.setLastLogin(new Date());
LifeCycleManager lifeCycleManagerForIdenitiy = LifeCycleManager.createInstanceFor(identity);
if (lifeCycleManagerForIdenitiy.lookupLifeCycleEntry(SEND_DELETE_EMAIL_ACTION) != null) {
Tracing.logAudit("User-Deletion: Remove from delete-list identity=" + identity, this.getClass());
lifeCycleManagerForIdenitiy.deleteTimestampFor(SEND_DELETE_EMAIL_ACTION);
}
if (Tracing.isDebugEnabled(this.getClass())) Tracing.logDebug("setIdentityAsActiv updateObject identity=" + identity, this.getClass());
DBFactory.getInstance().updateObject(identity);
if (Tracing.isDebugEnabled(this.getClass())) Tracing.logDebug("setIdentityAsActiv committed identity=" + identity, this.getClass());
}
});
}
/**
* @return Return duration in days for waiting for reaction on delete-email.
*/
public int getDeleteEmailDuration() {
return getPropertyByName(DELETE_EMAIL_DURATION_PROPERTY_NAME, DEFAULT_DELETE_EMAIL_DURATION);
}
/**
* @return Return last-login duration in month for user on delete-selection list.
*/
public int getLastLoginDuration() {
return getPropertyByName(LAST_LOGIN_DURATION_PROPERTY_NAME, DEFAULT_LAST_LOGIN_DURATION);
}
private int getPropertyByName(String name, int defaultValue) {
List properties = PropertyManager.getInstance().findProperties(null, null, null, PROPERTY_CATEGORY, name);
if (properties.size() == 0) {
return defaultValue;
} else {
return ((Property)properties.get(0)).getLongValue().intValue();
}
}
public void setLastLoginDuration(int lastLoginDuration) {
setProperty(LAST_LOGIN_DURATION_PROPERTY_NAME, lastLoginDuration);
}
public void setDeleteEmailDuration(int deleteEmailDuration) {
setProperty(DELETE_EMAIL_DURATION_PROPERTY_NAME, deleteEmailDuration);
}
private void setProperty(String propertyName, int value) {
List properties = PropertyManager.getInstance().findProperties(null, null, null, PROPERTY_CATEGORY, propertyName);
Property property = null;
if (properties.size() == 0) {
property = PropertyManager.getInstance().createPropertyInstance(null, null, null, PROPERTY_CATEGORY, propertyName, null, new Long(value), null, null);
} else {
property = (Property)properties.get(0);
property.setLongValue( new Long(value) );
}
PropertyManager.getInstance().saveProperty(property);
}
public void init(DeletionModule module) {
emailResponseTo = module.getEmailResponseTo();
adminIdentity = module.getAdminUserIdentity();
archiveRootDir = module.getArchiveRootPath();
}
/**
* Return in olat_config.xml definied administrator identity.
* @return
*/
public Identity getAdminIdentity() {
return adminIdentity;
}
public void registerDeletableUserData(UserDataDeletable deletableUserDataResource) {
userDataDeletableResources.add(deletableUserDataResource);
}
private File getArchivFilePath(Identity identity) {
String archiveFilePath = archiveRootDir + File.separator + USER_ARCHIVE_DIR + File.separator + DeletionModule.getArchiveDatePath()
+ File.separator + "del_identity_" + identity.getName();
File archiveIdentityRootDir = new File(archiveFilePath);
if (!archiveIdentityRootDir.exists()) {
archiveIdentityRootDir.mkdirs();
}
return archiveIdentityRootDir;
}
/**
* Setter method used by spring
* @param keepUserLoginAfterDeletion The keepUserLoginAfterDeletion to set.
*/
public void setKeepUserLoginAfterDeletion(boolean keepUserLoginAfterDeletion) {
this.keepUserLoginAfterDeletion = keepUserLoginAfterDeletion;
}
/**
* Setter method used by spring
* @param keepUserEmailAfterDeletion The keepUserEmailAfterDeletion to set.
*/
public void setKeepUserEmailAfterDeletion(boolean keepUserEmailAfterDeletion) {
this.keepUserEmailAfterDeletion = keepUserEmailAfterDeletion;
}
public static boolean isKeepUserLoginAfterDeletion() {
return keepUserLoginAfterDeletion;
}
}