/**********************************************************************************
 * $URL$
 * $Id$
 ***********************************************************************************
 *
 * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 The Sakai Foundation
 *
 * Licensed under the Educational Community License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.opensource.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 **********************************************************************************/

package org.sakaiproject.user.tool;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.any23.encoding.TikaEncodingDetector;
import org.apache.commons.io.input.BOMInputStream;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.validator.routines.EmailValidator;
import org.sakaiproject.accountvalidator.logic.ValidationLogic;
import org.sakaiproject.accountvalidator.model.ValidationAccount;
import org.sakaiproject.authz.api.AuthzGroup;
import org.sakaiproject.authz.api.AuthzGroupService;
import org.sakaiproject.authz.api.SecurityService;
import org.sakaiproject.cheftool.Context;
import org.sakaiproject.cheftool.ControllerState;
import org.sakaiproject.cheftool.JetspeedRunData;
import org.sakaiproject.cheftool.PagedResourceActionII;
import org.sakaiproject.cheftool.PortletConfig;
import org.sakaiproject.cheftool.RunData;
import org.sakaiproject.cheftool.VelocityPortlet;
import org.sakaiproject.cheftool.api.Menu;
import org.sakaiproject.cheftool.api.MenuItem;
import org.sakaiproject.cheftool.menu.MenuEntry;
import org.sakaiproject.cheftool.menu.MenuImpl;
import org.sakaiproject.component.cover.ComponentManager;
import org.sakaiproject.component.cover.ServerConfigurationService;
import org.sakaiproject.content.api.ContentHostingService;
import org.sakaiproject.content.api.ContentResource;
import org.sakaiproject.content.api.FilePickerHelper;
import org.sakaiproject.entity.api.Reference;
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.entity.api.ResourcePropertiesEdit;
import org.sakaiproject.event.api.SessionState;
import org.sakaiproject.event.api.UsageSessionService;
import org.sakaiproject.portal.util.PortalUtils;
import org.sakaiproject.site.api.Site;
import org.sakaiproject.site.api.SiteService;
import org.sakaiproject.thread_local.api.ThreadLocalManager;
import org.sakaiproject.time.api.UserTimeService;
import org.sakaiproject.tool.api.Session;
import org.sakaiproject.tool.api.SessionManager;
import org.sakaiproject.tool.api.ToolSession;
import org.sakaiproject.user.api.Authentication;
import org.sakaiproject.user.api.AuthenticationException;
import org.sakaiproject.user.api.AuthenticationManager;
import org.sakaiproject.user.api.Evidence;
import org.sakaiproject.user.api.User;
import org.sakaiproject.user.api.UserAlreadyDefinedException;
import org.sakaiproject.user.api.UserDirectoryService;
import org.sakaiproject.user.api.UserDirectoryService.PasswordRating;
import org.sakaiproject.user.api.UserEdit;
import org.sakaiproject.user.api.UserIdInvalidException;
import org.sakaiproject.user.api.UserLockedException;
import org.sakaiproject.user.api.UserNotDefinedException;
import org.sakaiproject.user.api.UserPermissionException;
import org.sakaiproject.user.tool.PasswordPolicyHelper.TempUser;
import org.sakaiproject.util.BaseResourcePropertiesEdit;
import org.sakaiproject.util.ExternalTrustedEvidence;
import org.sakaiproject.util.RequestFilter;
import org.sakaiproject.util.ResourceLoader;
import org.sakaiproject.util.StringUtil;
import org.sakaiproject.util.api.PasswordFactory;

import com.opencsv.CSVReader;

import lombok.extern.slf4j.Slf4j;
import net.tanesha.recaptcha.ReCaptcha;
import net.tanesha.recaptcha.ReCaptchaFactory;
import net.tanesha.recaptcha.ReCaptchaResponse;

/**
 * <p>
 * UsersAction is the Sakai users editor.
 * </p>
 */
@Slf4j
public class UsersAction extends PagedResourceActionII
{

	private static final long serialVersionUID = 1L;

	private static ResourceLoader rb = new ResourceLoader("users");
		
	//private static final String XLS_MIME_TYPE="application/vnd.ms-excel";
	private static final String CSV_MIME_TYPE="text/csv";
	
	//the column headings in the imported file, which will be used as the primary user attributes
	private static final String IMPORT_USER_ID="user id";
	private static final String IMPORT_FIRST_NAME="first name";
	private static final String IMPORT_LAST_NAME="last name";
	private static final String IMPORT_EMAIL="email";
	private static final String IMPORT_PASSWORD="password";
	private static final String IMPORT_TYPE="type";

	// SAK-23568
	private static final PasswordPolicyHelper pwHelper = new PasswordPolicyHelper();
	private static final String MSG_KEY_PASSWORD_WEAK = "pw.weak";
	private static final String MSG_KEY_PW_STRENGTH_INFO = "pw.strengthInfo";

	private static final String SAK_PROP_UNENROLL_BEFORE_DELETE = "user.unenroll.before.delete";

	private static final String CONFIG_CREATE_BLURB = "create-blurb";
	private static final String CONFIG_FORCE_EID_EQUALS_EMAIL = "force-eid-equals-email";
	private static final String CONFIG_VALIDATE_THROUGH_EMAIL = "validate-through-email";
	private static final String STATE_SUCCESS_MESSAGE = "successMessage";

	// SAK-29182
	private static final String SAK_PROP_INVALID_EMAIL_DOMAINS = "invalidEmailInIdAccountString";
	private static final String SAK_PROP_INVALID_EMAIL_DOMAINS_CUSTOM_MESSAGE = "user.email.invalid.domain.message";

	private static final String USER_TEMPLATE_PREFIX = "!user.template.";

	/*Kernel api */
	private AuthzGroupService authzGroupService;

	private UserDirectoryService userDirectoryService;
	
	private AuthenticationManager authenticationManager;
	
	private SecurityService securityService;

	private UsageSessionService usageSessionService;
	
	private ContentHostingService contentHostingService;
	
	private SessionManager sessionManager;
	
	private ThreadLocalManager threadLocalManager;
	private UserTimeService userTimeService;
	private PasswordFactory passwordFactory;
	
	public UsersAction() {
		super();
		authzGroupService = ComponentManager.get(AuthzGroupService.class);
		userDirectoryService = ComponentManager.get(UserDirectoryService.class);
		authenticationManager = ComponentManager.get(AuthenticationManager.class);
		securityService = ComponentManager.get(SecurityService.class);
		contentHostingService = ComponentManager.get(ContentHostingService.class);
		usageSessionService =  ComponentManager.get(UsageSessionService.class);
		sessionManager =  ComponentManager.get(SessionManager.class);
		threadLocalManager = ComponentManager.get(ThreadLocalManager.class);
		userTimeService = (UserTimeService)ComponentManager.get(UserTimeService.class);
		passwordFactory = ComponentManager.get(PasswordFactory.class);
	}

	/**
	 * {@inheritDoc}
	 */
	protected List<User> readResourcesPage(SessionState state, int first, int last)
	{
		// search?
		String search = StringUtils.trimToNull((String) state.getAttribute(STATE_SEARCH));

		if (search != null)
		{
			return userDirectoryService.searchUsers(search, first, last);
		}

		return userDirectoryService.getUsers(first, last);
	}

	/**
	 * {@inheritDoc}
	 */
	protected int sizeResources(SessionState state)
	{
		// search?
		String search = StringUtils.trimToNull((String) state.getAttribute(STATE_SEARCH));

		if (search != null)
		{
			return userDirectoryService.countSearchUsers(search);
		}

		return userDirectoryService.countUsers();
	}

	/**
	 * Populate the state object, if needed.
	 */
	protected void initState(SessionState state, VelocityPortlet portlet, JetspeedRunData rundata)
	{
		super.initState(state, portlet, rundata);

		PortletConfig config = portlet.getPortletConfig();

		if (state.getAttribute("single-user") == null)
		{
			state.setAttribute("single-user", new Boolean(config.getInitParameter("single-user", "false")));
			state.setAttribute("include-password", new Boolean(config.getInitParameter("include-password", "true")));
		}

		if (state.getAttribute("create-user") == null)
		{
			state.setAttribute("create-user", new Boolean(config.getInitParameter("create-user", "false")));
			state.setAttribute("create-login", new Boolean(config.getInitParameter("create-login", "false")));
		}

		if (state.getAttribute("create-type") == null)
		{
			state.setAttribute("create-type", config.getInitParameter("create-type", ""));
		}

		if (state.getAttribute(CONFIG_VALIDATE_THROUGH_EMAIL) == null)
		{
			state.setAttribute(CONFIG_VALIDATE_THROUGH_EMAIL, new Boolean(config.getInitParameter(CONFIG_VALIDATE_THROUGH_EMAIL, "false")));
		}

		if (state.getAttribute(CONFIG_FORCE_EID_EQUALS_EMAIL) == null)
		{
			state.setAttribute(CONFIG_FORCE_EID_EQUALS_EMAIL, new Boolean(config.getInitParameter(CONFIG_FORCE_EID_EQUALS_EMAIL, "false")));
		}

		if (state.getAttribute(CONFIG_CREATE_BLURB) == null)
		{
			state.setAttribute(CONFIG_CREATE_BLURB, config.getInitParameter(CONFIG_CREATE_BLURB, ""));
		}
		
		if (state.getAttribute("user.recaptcha-enabled") == null)
		{
			String publicKey = ServerConfigurationService.getString("user.recaptcha.public-key", "");
			String privateKey = ServerConfigurationService.getString("user.recaptcha.private-key", "");
			Boolean systemEnabled = ServerConfigurationService.getBoolean("user.recaptcha.enabled", false);
			Boolean toolEnabled = Boolean.parseBoolean(config.getInitParameter("user.recaptcha-enabled", "false"));
			Boolean enabled = systemEnabled && toolEnabled;
			if (enabled)
			{
				if (publicKey == null || publicKey.length() == 0)
				{
				 	log.warn("recaptcha is enabled but no public key is found.");
					enabled = Boolean.FALSE;
				}
				if (privateKey == null || privateKey.length() == 0)
				{
				 	log.warn("recaptcha is enabled but no private key is found.");
					enabled = Boolean.FALSE;
				}
			}
			state.setAttribute("user.recaptcha-public-key", publicKey);
			state.setAttribute("user.recaptcha-private-key", privateKey);
			state.setAttribute("user.recaptcha-enabled", enabled);
		}

	} // initState

	/**
	 * build the context
	 */
	public String buildMainPanelContext(VelocityPortlet portlet, Context context, RunData rundata, SessionState state)
	{
		context.put("tlang", rb);
		context.put("userTimeService", userTimeService);
		context.put("includeLatestJQuery", PortalUtils.includeLatestJQuery("UsersAction"));
		boolean singleUser = ((Boolean) state.getAttribute("single-user")).booleanValue();
		boolean createUser = ((Boolean) state.getAttribute("create-user")).booleanValue();
		
		UsersActionState sstate = (UsersActionState)getState(context, rundata, UsersActionState.class);
		String status = sstate.getStatus();

		String[] userTypes = ServerConfigurationService.getStrings("user.type.selector");
		if (userTypes != null && userTypes.length > 0)
		{
			context.put("userTypes", userTypes);
		} else {
			context.put("userTypes", getUserTypes());
		}



		// if not logged in as the super user, we won't do anything
		if ((!singleUser) && (!createUser) && (!securityService.isSuperUser()))
		{
			context.put("tlang",rb);
			return (String) getContext(rundata).get("template") + "_noaccess";
		}

		String template = null;

		// for the create-user create-login case, we set this in the do so we can process the redirect here
		if (state.getAttribute("redirect") != null)
		{
			state.removeAttribute("redirect");
			Session s = sessionManager.getCurrentSession();
			// TODO: Decide if this should be in "getPortalUrl"
			// I don't think so but could be convinced - /chuck
			String controllingPortal = (String) s.getAttribute("sakai-controlling-portal");
			String portalUrl = ServerConfigurationService.getPortalUrl();
			if ( controllingPortal != null ) {
				portalUrl = portalUrl + "/" + controllingPortal;
			}
 
			sendParentRedirect((HttpServletResponse) threadLocalManager.get(RequestFilter.CURRENT_HTTP_RESPONSE),
					portalUrl);
			return template;
		}

		// put $action into context for menus, forms and links
		context.put(Menu.CONTEXT_ACTION, state.getAttribute(STATE_ACTION));

		//put successMessage into context and remove from state
		context.put("successMessage", state.getAttribute(STATE_SUCCESS_MESSAGE));
		state.removeAttribute(STATE_SUCCESS_MESSAGE);

		// SAK-23568
		pwHelper.addJavaScriptParamsToContext(context);

		// check mode and dispatch
		String mode = (String) state.getAttribute("mode");

		if ((singleUser) && (mode != null) && (mode.equals("edit")))
		{
			template = buildEditContext(state, context);
		}
		else if (singleUser)
		{
			String id = sessionManager.getCurrentSessionUserId();
			state.setAttribute("user-id", id);
			template = buildViewContext(state, context);
		}
		else if (createUser)
		{
			template = buildCreateContext(state, context);
		}
		else if (mode == null)
		{
			template = buildListContext(state, context);
		}
		else if (mode.equals("new"))
		{
			template = buildNewContext(state, context);
		}
		else if (mode.equals("edit"))
		{
			template = buildEditContext(state, context);
		}
		else if (mode.equals("confirm"))
		{
			template = buildConfirmRemoveContext(state, context);
		}
		else if (mode.equals("import"))
		{
			template = buildImportContext(state, context);
		}
		else if (mode.equals("mode_helper") && StringUtils.equals(status, "processImport")) {
			//returning from helper after uploading file
			template = buildProcessImportContext(state, rundata, context);
		}
		else
		{
		 	log.warn("UsersAction: mode: {}", mode);
			template = buildListContext(state, context);
		}

		String prefix = (String) getContext(rundata).get("template");
		return prefix + template;

	} // buildNormalContext

	/**
	 * Build the context for the main list mode.
	 */
	private String buildListContext(SessionState state, Context context)
	{
		// put the service in the context
		context.put("service", userDirectoryService);

		// put all (internal) users into the context
		context.put("users", prepPage(state));

		// build the menu
		Menu bar = new MenuImpl();
		if (userDirectoryService.allowAddUser())
		{
			bar.add(new MenuEntry(rb.getString("useact.newuse"), null, true, MenuItem.CHECKED_NA, "doNew"));
			bar.add(new MenuEntry(rb.getString("import.user.file"), null, true, MenuItem.CHECKED_NA, "doImport"));
		}

		// add the paging commands
		int pageSize = Integer.valueOf(state.getAttribute(STATE_PAGESIZE).toString()).intValue();
		int currentPageNumber = Integer.valueOf(state.getAttribute(STATE_CURRENT_PAGE).toString()).intValue();
		int startNumber = state.getAttribute(STATE_TOP_PAGE_MESSAGE) != null ? ((Integer) state.getAttribute(STATE_TOP_PAGE_MESSAGE)).intValue() + 1 : pageSize * (currentPageNumber - 1);
		int endNumber = pageSize * currentPageNumber;

		int totalNumber = 0;
		Object[] params;
		ArrayList<Integer[]> list = new ArrayList<>();
		list.add(new Integer[]{Integer.valueOf(5)});
		list.add(new Integer[]{Integer.valueOf(10)});
		list.add(new Integer[]{Integer.valueOf(20)});
		list.add(new Integer[]{Integer.valueOf(50)});
		list.add(new Integer[]{Integer.valueOf(100)});
		list.add(new Integer[]{Integer.valueOf(200)});

		try
		{
			totalNumber = Integer.valueOf(state.getAttribute(STATE_NUM_MESSAGES).toString()).intValue();
		}
		catch (java.lang.NullPointerException ignore) {}
		catch (java.lang.NumberFormatException ignore) {}

		if (totalNumber < endNumber) endNumber = totalNumber;

		params = new Object[]{startNumber, endNumber, totalNumber};

		context.put("startNumber", Integer.valueOf(startNumber));
		context.put("endNumber", Integer.valueOf(endNumber));
		context.put("totalNumber", Integer.valueOf(totalNumber));
		context.put("params", params);
		context.put("list", list);
		pagingInfoToContext(state, context);

		// add the search commands
		addSearchMenus(bar, state, rb.getString("useact.search"));

		if (bar.size() > 0)
		{
			context.put(Menu.CONTEXT_MENU, bar);
		}

		return "_list";

	} // buildListContext

	/**
	 * @author bjones86 - SAK-29182
	 * @return a list of strings contained in the invalidEmailInIdAccountString sakai.property, or an empty list if not set
	 */
	private List<String> getInvalidEmailDomains()
	{
		return Arrays.asList( ArrayUtils.nullToEmpty( ServerConfigurationService.getStrings( SAK_PROP_INVALID_EMAIL_DOMAINS ) ) );
	}

	/**
	 * Build the context for the new user mode.
	 */
	private String buildNewContext(SessionState state, Context context)
	{
		// put the service in the context
		context.put("service", userDirectoryService);

		// name the html form for user edit fields
		context.put("form-name", "user-form");
		
		// include the password fields?
		context.put("incPw", state.getAttribute("include-password"));

		context.put("incType", Boolean.valueOf(true));

    context.put("superUser", Boolean.valueOf(securityService.isSuperUser()));

		String value = (String) state.getAttribute("valueEid");
		if (value != null) context.put("valueEid", value);

		value = (String) state.getAttribute("valueFirstName");
		if (value != null) context.put("valueFirstName", value);

		value = (String) state.getAttribute("valueLastName");
		if (value != null) context.put("valueLastName", value);

		value = (String) state.getAttribute("valueEmail");
		if (value != null) context.put("valueEmail", value);

		value = (String) state.getAttribute("valueType");
		if (value != null) context.put("valueType", value);
		
		//optional attributes list
		context.put("optionalAttributes", getOptionalAttributes());
		
		return "_edit";

	} // buildNewContext

	/**
	 * Build the context for the create user mode.
	 */
	private String buildCreateContext(SessionState state, Context context)
	{
		// put the service in the context
		context.put("service", userDirectoryService);

		String blurb = (String) state.getAttribute(CONFIG_CREATE_BLURB);
		if (!StringUtils.isEmpty(blurb))
		{
			context.put("createBlurb", blurb);
		}

		// is the type to be pre-set
		context.put("type", state.getAttribute("create-type"));

		boolean isValidatedWithAccountValidator = isValidatedWithAccountValidator(state);
		boolean isEidEditable = isEidEditable(state);

		// if the tool is configured to validate through email, we will use AccountValidator to set name fields, etc. So we indicate this in the context to hide fields that are redundant
		context.put("isValidatedWithAccountValidator", isValidatedWithAccountValidator);

		// If we're using account validator, an email needs to be sent
		// If the eid is not editable, the email will be used as the eid
		context.put("emailRequired", isValidatedWithAccountValidator || !isEidEditable);

		// password is required when using Gateway New Account tool
		// attribute "create-user" is true only for New Account tool
		context.put("pwRequired", state.getAttribute("create-user"));

		context.put("displayEid", isEidEditable);
		String value = (String) state.getAttribute("valueEid");
		if (value != null) context.put("valueEid", value);

		value = (String) state.getAttribute("valueFirstName");
		if (value != null) context.put("valueFirstName", value);

		value = (String) state.getAttribute("valueLastName");
		if (value != null) context.put("valueLastName", value);

		value = (String) state.getAttribute("valueEmail");
		if (value != null) context.put("valueEmail", value);
				
		if ((Boolean)state.getAttribute("user.recaptcha-enabled"))
		{
			ReCaptcha captcha = ReCaptchaFactory.newReCaptcha((String)state.getAttribute("user.recaptcha-public-key"), (String)state.getAttribute("user.recaptcha-private-key"), false);
	        String captchaScript = captcha.createRecaptchaHtml((String)state.getAttribute("recaptcha-error"), null);
	        state.removeAttribute("recaptcha-error");
	        context.put("recaptchaScript", captchaScript);
		}

		return "_create";

	} // buildCreateContext

	/**
	 * Build the context for the new user mode.
	 */
	private String buildEditContext(SessionState state, Context context)
	{
		
		// put the service in the context
		context.put("service", userDirectoryService);
		
		// name the html form for user edit fields
		context.put("form-name", "user-form");

		// get the user to edit
		UserEdit user = (UserEdit) state.getAttribute("user");
		context.put("user", user);
		
		// is super user/admin user?
		context.put("superUser", Boolean.valueOf(securityService.isSuperUser()));

		// include the password fields?
		context.put("incPw", state.getAttribute("include-password"));

		context.put("providedUserType", isProvidedType(user.getType()));
		
		// include type fields (not if single user)
		boolean singleUser = ((Boolean) state.getAttribute("single-user")).booleanValue();
		context.put("incType", Boolean.valueOf(!singleUser));

		// build the menu
		// we need the form fields for the remove...
		boolean menuPopulated = false;
		Menu bar = new MenuImpl();
		if ((!singleUser) && (userDirectoryService.allowRemoveUser(user.getId())))
		{
			bar.add(new MenuEntry(rb.getString("useact.remuse"), null, true, MenuItem.CHECKED_NA, "doRemove", "user-form"));
			menuPopulated = true;
		}

		if (menuPopulated)
		{
			context.put(Menu.CONTEXT_MENU, bar);
		}

		String value = (String) state.getAttribute("valueEid");
		if (value != null) context.put("valueEid", value);

		value = (String) state.getAttribute("valueFirstName");
		if (value != null) context.put("valueFirstName", value);

		value = (String) state.getAttribute("valueLastName");
		if (value != null) context.put("valueLastName", value);

		value = (String) state.getAttribute("valueEmail");
		if (value != null) context.put("valueEmail", value);

		value = (String) state.getAttribute("valueType");
		if (value != null) context.put("valueType", value);
		
		//Users date
		DateFormat dsf = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, rb.getLocale());
		dsf.setTimeZone(userTimeService.getLocalTimeZone());
		String userCreated = dsf.format(user.getCreatedDate());
		String userModifiedDate = dsf.format(user.getModifiedDate());
		context.put("userCreated", userCreated);
		context.put("userModifiedDate", userModifiedDate);
		
		//optional attributes lists
		context.put("optionalAttributes", getOptionalAttributes());
		context.put("currentAttributes", getCurrentAttributes((UserEdit) state.getAttribute("user")));


		return "_edit";

	} // buildEditContext

	/**
	 * Build the context for the view user mode.
	 */
	private String buildViewContext(SessionState state, Context context)
	{
		log.debug("buildViewContext start");

		// get current user's id
		String id = (String) state.getAttribute("user-id");

		// get the user and put in state as "user"
		try
		{
			User user = userDirectoryService.getUser(id);
			context.put("user", user);

			// name the html form for user edit fields
			context.put("form-name", "user-form");

			state.setAttribute("mode", "view");

			// make sure we can do an edit
			try
			{
				UserEdit edit = userDirectoryService.editUser(id);
				userDirectoryService.cancelEdit(edit);
				context.put("enableEdit", "true");
			}
			catch (UserNotDefinedException e)
			{
			}
			catch (UserPermissionException e)
			{
			}
			catch (UserLockedException e)
			{
			}
		}
		catch (UserNotDefinedException e)
		{
		 	log.warn("UsersAction.doEdit: user not found: {}", id);

			Object[] params = new Object[]{id};
			addAlert(state, rb.getFormattedMessage("useact.use_notfou", params));
			state.removeAttribute("mode");
		}

		return "_view";

	} // buildViewContext

	/**
	 * @author bbailla2
	 * @return the sakai property "user.unenroll.before.delete" (default is true)
	 */
	private boolean isUnenrollBeforeDeleteEnabled()
	{
		return ServerConfigurationService.getBoolean(SAK_PROP_UNENROLL_BEFORE_DELETE, true);
	}

	/**
	 * Build the context for the new user mode.
	 */
	private String buildConfirmRemoveContext(SessionState state, Context context)
	{
		// get the user to edit
		UserEdit user = (UserEdit) state.getAttribute("user");
		context.put("user", user);

		// get list of memberships; populate the UI
		// determines whether we need to unenroll the user from sites before we delete them
		boolean unenrollFirst = isUnenrollBeforeDeleteEnabled();

		String permDelWarning = "";
		if (unenrollFirst)
		{
			SiteService siteService = (SiteService)ComponentManager.get(SiteService.class);
			List<Site> sites = siteService.getUserSites(false, user.getId(), true);
			if (sites != null && !sites.isEmpty())
			{
				// there are sites to unenroll from, present this to the user
				int siteLen = sites.size();
				String siteMsg = siteLen == 1 ? rb.getString("useconrem.site") : rb.getFormattedMessage("useconrem.sites", Integer.valueOf(siteLen));
				permDelWarning = rb.getFormattedMessage("useconrem.unenrol", user.getEid(), siteMsg);
			}
			else
			{
				// nothing to unenroll from
				unenrollFirst = false;
			}
		}

		if (!unenrollFirst)
		{
			// we don't need to unenroll the user from anything, so just indicate that this user will be permanently deleted
			permDelWarning = rb.getFormattedMessage("useconrem.permdel", user.getEid());
		}
		context.put("permDelWarning", permDelWarning);

		return "_confirm_remove";

	} // buildConfirmRemoveContext

	/**
	 * Build the context for the import mode.
	 */
	private String buildImportContext(SessionState state, Context context) {
		
		//render the template		
		return "_import";

	} // buildImportContext
	
	/**
	 * Build the context for processing the files
	 */
	private String buildProcessImportContext(SessionState state, RunData data, Context context) {
		
		//process the attachments (there will be only one)
		UsersActionState sstate = (UsersActionState)getState(context, data, UsersActionState.class);
		
		try {
			Reference attachment = (Reference)sstate.getAttachments().get(0);
			processImportedUserFile(state, context, attachment);
		} catch (IndexOutOfBoundsException e) {
			//no attachment, carry on, will render correctly
		}
				
		//render the template		
		return "_import";

	} // buildProcessImportContext
	
	/**
	 * doNew called when "eventSubmit_doNew" is in the request parameters to add a new user
	 */
	public void doNew(RunData data, Context context)
	{
		SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid());
		state.setAttribute("mode", "new");

		// mark the user as new, so on cancel it can be deleted
		state.setAttribute("new", "true");

	} // doNew

	/**
	 * doImport called when "eventSubmit_doImport" is clicked. This actuall imports the users that were uploaded.
	 */
	public void doImport(RunData data, Context context)
	{
		SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid());
		UsersActionState sstate = (UsersActionState)getState(context, data, UsersActionState.class);
		
		state.setAttribute("mode", "import");
				
	 	log.debug("doImport");
			
		List<ImportedUser> users = (List<ImportedUser>)state.getAttribute("importedUsers");
		if(users !=null && users.size() > 0) {
			//Check if the email is duplicated
			boolean allowEmailDuplicates = ServerConfigurationService.getBoolean("user.email.allowduplicates",true);
			
			
			for(ImportedUser user: users) {
				try {
					
					TempUser tempUser = new TempUser(user.getEid(), user.getEmail(), null, null, user.getEid(), user.getPassword(), null);
					
					if (!allowEmailDuplicates && userDirectoryService.checkDuplicatedEmail(tempUser)){
						addAlert(state, rb.getString("useact.theuseemail1") + ":" + tempUser.getEmail());
						
						//Try to import the rest
						continue;
					}
					String userPass = user.getPassword();
					// If the password form the CSV is null, set an empty string.
					// Else, trim possible spaces surrounding the password, to avoid login problems.
					userPass = userPass == null ? "" : userPass.trim();

					userDirectoryService.addUser(null, user.getEid(), user.getFirstName(), user.getLastName(), user.getEmail(), userPass, user.getType(), user.getProperties());
				}
				catch (UserAlreadyDefinedException e){
					//ok, just skip
					continue;
				}
				catch (UserIdInvalidException e) {
					addAlert(state, rb.getString("useact.theuseid2") + ": " + user.getEid());
				 	log.error("Import user error: {}", e.getMessage(), e);
					//try to import the rest
					continue;
				}
				catch (UserPermissionException e){
					addAlert(state, rb.getString("useact.youdonot3"));
				 	log.error("Import user error: {}", e.getMessage(), e);
					//this is bad so return
					return;
				} 
			}
			
			//set a message to show it was successful
			state.setAttribute(STATE_SUCCESS_MESSAGE, rb.getString("import.success"));
			
			//cleanup
			state.removeAttribute("importedUsers");
			state.removeAttribute("mode");
		}
		
	} // doImport
	
	
	
	/**
	 * doEdit called when "eventSubmit_doEdit" is in the request parameters to edit a user
	 */
	public void doEdit(RunData data, Context context)
	{
		SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid());
		String id = data.getParameters().getString("id");
		state.removeAttribute("user");
		state.removeAttribute("newuser");

		// get the user
		try
		{
			UserEdit user = userDirectoryService.editUser(id);
			state.setAttribute("user", user);
			state.setAttribute("mode", "edit");
		}
		catch (UserNotDefinedException e)
		{
		 	log.warn("UsersAction.doEdit: user not found: {}", id);

			Object[] params = new Object[]{id};
			addAlert(state, rb.getFormattedMessage("useact.use_notfou", params));
			state.removeAttribute("mode");
		}
		catch (UserPermissionException e)
		{
			addAlert(state, rb.getFormattedMessage("useact.youdonot1", new Object[]{id}));
			state.removeAttribute("mode");
		}
		catch (UserLockedException e)
		{
			addAlert(state, rb.getFormattedMessage("useact.somels", new Object[]{id}));
			state.removeAttribute("mode");
		}

	} // doEdit

	/**
	 * doModify called when "eventSubmit_doModify" is in the request parameters to edit a user
	 */
	public void doModify(RunData data, Context context)
	{
		log.debug("doModify start");

		SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid());
		String id = data.getParameters().getString("id");
		state.removeAttribute("user");
		state.removeAttribute("newuser");

		// get the user
		try
		{
			UserEdit user = userDirectoryService.editUser(id);
			state.setAttribute("user", user);
			state.setAttribute("mode", "edit");
		}
		catch (UserNotDefinedException e)
		{
		 	log.warn("UsersAction.doEdit: user not found: {}", id);

			Object[] params = new Object[]{id};
			addAlert(state, rb.getFormattedMessage("useact.use_notfou", params));
			state.removeAttribute("mode");
		}
		catch (UserPermissionException e)
		{
			addAlert(state, rb.getFormattedMessage("useact.youdonot1", new Object[]{id}));
			state.removeAttribute("mode");
		}
		catch (UserLockedException e)
		{
			addAlert(state, rb.getFormattedMessage("useact.somels", new Object[]{id}));
			state.removeAttribute("mode");
		}

	} // doModify

	/**
	 * doSave called when "eventSubmit_doSave" is in the request parameters to save user edits
	 */
	public void doSave(RunData data, Context context)
	{
		SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid());
		
		if (!"POST".equals(data.getRequest().getMethod())) {
			return;
		}

		// read the form - if rejected, leave things as they are
		if (!readUserForm(data, state)) return;


		
		// commit the change
		UserEdit edit = (UserEdit) state.getAttribute("user");
		if (edit != null)
		{
			
			//Check if the email is duplicated
			boolean allowEmailDuplicates = ServerConfigurationService.getBoolean("user.email.allowduplicates",true);
			
			if (!allowEmailDuplicates && userDirectoryService.checkDuplicatedEmail(edit)){
				addAlert(state, rb.getString("useact.theuseemail1"));
				return;
			}
			
			try
			{
				userDirectoryService.commitEdit(edit);
			}
			catch (UserAlreadyDefinedException e)
			{
				// TODO: this means the EID value is not unique... when we implement EID fully, we need to check this and send it back to the user
			 	log.warn("UsersAction.doSave() {}", e.getMessage());
				addAlert(state, rb.getString("useact.theuseid1"));
				return;
			}
		}

		User user = edit;
		if (user == null)
		{
			user = (User) state.getAttribute("newuser");
		}

		// cleanup
		state.removeAttribute("user");
		state.removeAttribute("newuser");
		state.removeAttribute("new");
		state.removeAttribute("valueEid");
		state.removeAttribute("valueFirstName");
		state.removeAttribute("valueLastName");
		state.removeAttribute("valueEmail");
		state.removeAttribute("valueType");

		// return to main mode
		state.removeAttribute("mode");

		if ((user != null) && ((Boolean) state.getAttribute("create-login")).booleanValue())
		{
			if (isValidatedWithAccountValidator(state))
			{
				// Don't log the user in, their account is not activated yet.
				// inform them that an email has been sent
				state.setAttribute(STATE_SUCCESS_MESSAGE, rb.getFormattedMessage("email.validation.success", user.getEmail()));
			}
			else
			{
				try
				{
					// login - use the fact that we just created the account as external evidence
					Evidence e = new ExternalTrustedEvidence(user.getEid());
					Authentication a = authenticationManager.authenticate(e);
					if (!usageSessionService.login(a, (HttpServletRequest) threadLocalManager.get(RequestFilter.CURRENT_HTTP_REQUEST)))
					{
						addAlert(state, rb.getString("useact.tryloginagain"));
					}
				}
				catch (AuthenticationException ex)
				{
				 	log.warn("UsersAction.doSave: authentication failure: {}", ex.getMessage());
				}

				// redirect to home (on next build)
				state.setAttribute("redirect", "");
			}
		}

	} // doSave

	/**
	 * doCancel called when "eventSubmit_doCancel" is in the request parameters to cancel user edits
	 */
	public void doCancel(RunData data, Context context)
	{
		SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid());
		
		if (!"POST".equals(data.getRequest().getMethod())) {
			return;
		}

		// get the user
		UserEdit user = (UserEdit) state.getAttribute("user");
		if (user != null)
		{
			// if this was a new, delete the user
			if ("true".equals(state.getAttribute("new")))
			{
				// remove
				try
				{
					userDirectoryService.removeUser(user);
				}
				catch (UserPermissionException e)
				{
					addAlert(state, rb.getFormattedMessage("useact.youdonot2", new Object[]{user.getId()}));
				}
			}
			else
			{
				userDirectoryService.cancelEdit(user);
			}
		}

		// cleanup
		state.removeAttribute("user");
		state.removeAttribute("newuser");
		state.removeAttribute("new");
		state.removeAttribute("valueEid");
		state.removeAttribute("valueFirstName");
		state.removeAttribute("valueLastName");
		state.removeAttribute("valueEmail");
		state.removeAttribute("valueType");

		// return to main mode
		state.removeAttribute("mode");

	} // doCancel
	
	/**
	 * doCancelImport called when "eventSubmit_doCancelImport" is in the request parameters to cancel user imports
	 */
	public void doCancelImport(RunData data, Context context)
	{
		SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid());
		
		if (!"POST".equals(data.getRequest().getMethod())) {
			return;
		}
		
		//cleanup session
		state.removeAttribute("importedUsers");
		
		//also cleanup our state handler (I think this should be combined into SessionState)
		UsersActionState sstate = (UsersActionState)getState(context, data, UsersActionState.class);
		sstate.setAttachments(new ArrayList());
		sstate.setStatus(null);

		// return to main mode
		state.removeAttribute("mode");

	} // doCancelImport

	/**
	 * doRemove called when "eventSubmit_doRemove" is in the request par ameters to confirm removal of the user
	 */
	public void doRemove(RunData data, Context context)
	{
		SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid());
		
		// set mode so we can skip some checks in readUserForm
		state.setAttribute("mode", "remove");

		// read the form - if rejected, leave things as they are
		if (!readUserForm(data, state)) return;

		// go to remove confirm mode
		state.setAttribute("mode", "confirm");

	} // doRemove

	/**
	 * doRemove_confirmed called when "eventSubmit_doRemove_confirmed" is in the request parameters to remove the user
	 */
	public void doRemove_confirmed(RunData data, Context context)
	{
		SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid());
		
		if (!"POST".equals(data.getRequest().getMethod())) {
			return;
		}

		// get the user
		UserEdit user = (UserEdit) state.getAttribute("user");

		// unenroll the user from all AuthzGroups (if enabled)
		String userId = user.getId();
		String userEid = user.getEid();
		if (isUnenrollBeforeDeleteEnabled())
		{
			Map<String, String> userRoles = authzGroupService.getUserRoles(userId, null);
			for (String realm : userRoles.keySet())
			{
				try
				{
					AuthzGroup realmEdit = authzGroupService.getAuthzGroup(realm);
					realmEdit.removeMember(userId);
					authzGroupService.save(realmEdit);
				 	log.info("User {} removed from realm {}", userEid, realm);
				}
				catch (Exception e)
				{
				 	log.error("Could not remove user {} from realm {}", userEid, realm);
					addAlert(state, rb.getFormattedMessage("useact.couldnot", user.getEid(), realm));
				}
			}
		}

		// remove the user
		try
		{
			userDirectoryService.removeUser(user);

			// tracking information
		 	log.info("User {} has been deleted by {}. The internal ID was {}", userEid, userDirectoryService.getCurrentUser().getEid(), userId);
		}
		catch (UserPermissionException e)
		{
			addAlert(state, rb.getFormattedMessage("useact.youdonot2", new Object[]{user.getId()}));
		}

		// cleanup
		state.removeAttribute("user");
		state.removeAttribute("newuser");
		state.removeAttribute("new");
                state.removeAttribute("valueEid");
                state.removeAttribute("valueFirstName");
                state.removeAttribute("valueLastName");
                state.removeAttribute("valueEmail");
                state.removeAttribute("valueType");

		// go to main mode
		state.removeAttribute("mode");

	} // doRemove_confirmed

	/**
	 * doCancel_remove called when "eventSubmit_doCancel_remove" is in the request parameters to cancel user removal
	 */
	public void doCancel_remove(RunData data, Context context)
	{
		SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid());
		
		if (!"POST".equals(data.getRequest().getMethod())) {
			return;
		}

		// return to edit mode
		state.setAttribute("mode", "edit");

	} // doCancel_remove

	/**
	 * Check to see if password meets requirements set in password policy.
	 * If current user is admin, ignores password policy.
	 *
	 * @author plukasew, bjones86 - SAK-23568
	 *
	 * @param pw the password
	 * @param user the user
	 * @param state the session state
	 * @return true if password is valid or if current user is admin
	 */
	private boolean validatePassword(String pw, User user, SessionState state) {
		if (pw != null && !securityService.isSuperUser() && pwHelper.validatePassword(pw, user) == PasswordRating.FAILED) {
			addAlert(state, rb.getString(MSG_KEY_PASSWORD_WEAK) + " " + rb.getString(MSG_KEY_PW_STRENGTH_INFO));
			return false;
		}
		return true;
	}

	/**
	 * Read the user form and update the user in state.
	 * 
	 * @return true if the form is accepted, false if there's a validation error (an alertMessage will be set)
	 */
	private boolean readUserForm(RunData data, SessionState state)
	{
		// boolean parameters and values
		// --------------Mode----singleUser-createUser-typeEnable
		// Admin New-----new-----false------false------true
		// Admin Update--edit----false------false------true
		// Admin Delete--remove--false------false------false
		// Gateway New---null----false------true-------false
		// Account Edit--edit----true-------false------false

		// read the form
		String id = StringUtils.trimToNull(data.getParameters().getString("id"));
		String eid = StringUtils.trimToNull(data.getParameters().getString("eid"));
		state.setAttribute("valueEid", eid);
		String firstName = StringUtils.trimToNull(data.getParameters().getString("first-name"));
		state.setAttribute("valueFirstName", firstName);
		String lastName = StringUtils.trimToNull(data.getParameters().getString("last-name"));
		state.setAttribute("valueLastName", lastName);
		String email = StringUtils.trimToNull(data.getParameters().getString("email"));
		state.setAttribute("valueEmail", email);
		String pw = StringUtils.trimToNull(data.getParameters().getString("pw"));
        String pwConfirm = StringUtils.trimToNull(data.getParameters().getString("pw0"));

        String pwcur = StringUtils.trimToNull(data.getParameters().getString("pwcur"));
        
        Integer disabled = Integer.valueOf(StringUtils.trimToNull(data.getParameters().getString("disabled")) != null ? "1" : "0" );
        
        String mode = (String) state.getAttribute("mode");
		boolean singleUser = ((Boolean) state.getAttribute("single-user")).booleanValue();
		boolean createUser = ((Boolean) state.getAttribute("create-user")).booleanValue();

		// SAK-29182 - enforce invalid domains when creating a user through Gateway -> New Account
		boolean isEidEditable = isEidEditable( state );
		if( createUser && !isEidEditable )
		{
			for( String domain : getInvalidEmailDomains() )
			{
				if( email.toLowerCase().endsWith( domain.toLowerCase() ) )
				{
					String defaultMsg = rb.getFormattedMessage( "email.invalid.domain", new Object[] { domain } );
					String customMsg = ServerConfigurationService.getString( SAK_PROP_INVALID_EMAIL_DOMAINS_CUSTOM_MESSAGE, "" );
					if( !customMsg.isEmpty() )
					{
						String institution = ServerConfigurationService.getString( "ui.institution", "" );
						customMsg = new MessageFormat( customMsg, rb.getLocale() ).format( new Object[] { institution, domain }, new StringBuffer(), null ).toString();
					}

					addAlert( state, customMsg.isEmpty() ? defaultMsg : customMsg );
					return false;
				}
			}
		}

		boolean typeEnable = false;
		String type = null;
		if ((mode != null) && (mode.equalsIgnoreCase("new")))
		{
			typeEnable = true;
		}
		else if ((mode != null) && (mode.equalsIgnoreCase("edit")) && (!singleUser))
		{
			typeEnable = true;
		}

		if (typeEnable)
		{
			// for the case of Admin User tool creating new user
			type = StringUtils.trimToNull(data.getParameters().getString("type"));
			state.setAttribute("valueType", type);
		}
		else
		{
			if (createUser)
			{
				// for the case of Gateway Account tool creating new user
				type = (String) state.getAttribute("create-type");
			}
		}
		
		if ((Boolean)state.getAttribute("user.recaptcha-enabled"))
		{
			String challengeField = data.getParameters().getString("recaptcha_challenge_field");
			String responseField = data.getParameters().getString("recaptcha_response_field");
			if (challengeField == null) challengeField = "";
			if (responseField == null) responseField = "";
			ReCaptcha captcha = ReCaptchaFactory.newReCaptcha((String)state.getAttribute("user.recaptcha-public-key"), (String)state.getAttribute("user.recaptcha-private-key"), false);
			ReCaptchaResponse response = captcha.checkAnswer(data.getRequest().getRemoteAddr(), challengeField, responseField);
			if (!response.isValid())
			{
				addAlert(state, rb.getString("useact.capterr"));
		        state.setAttribute("recaptcha-error", response.getErrorMessage());
				return false;
			}
		}
		
		
		//Ensure valid email address. Empty emails are invalid iff email validation is required. For non-empty email Strings, use EmailValidator.
		//email.matches(".+@.+\\..+")
		boolean validateWithAccountValidator = isValidatedWithAccountValidator(state);
		boolean emailInvalid = StringUtils.isEmpty(email) ? validateWithAccountValidator : !EmailValidator.getInstance().isValid(email);
		if(emailInvalid) {
				addAlert(state, rb.getString("useact.invemail"));	
				return false;
		}
		
		// get the user
		UserEdit user = (UserEdit) state.getAttribute("user");
		//process any additional attributes
		//we continue processing these until we get an empty attribute KEY
		//counter starts at 1
		
		//data is of the form:
		//	optionalAttr_1:att1
		//	optionalAttrValue_1:value1
		//	optionalAttr_2:att2
		//	optionalAttrValue_2:value2
		
		int count = 1;
		boolean continueProcessingOptionalAttributes = true;
		
		ResourcePropertiesEdit properties;
		if(user == null) {
			properties = new BaseResourcePropertiesEdit();
		} else {
			properties = user.getPropertiesEdit();
		}
		
		//remove all properties that are in the confugred list
		//then add back in only the ones that were sent
		//this allows us to remove items via javascript and they get persisted to the db on form save
		Map<String,String> configuredProperties = getOptionalAttributes();
		for(String cp: configuredProperties.keySet()) {
			properties.removeProperty(cp);
		}
		
		
		while(continueProcessingOptionalAttributes) {
			
			//this stores the key
			String optionalAttributeKey = data.getParameters().getString("optionalAttr_"+count);
			
			if(StringUtils.isBlank(optionalAttributeKey)){
				continueProcessingOptionalAttributes = false;
				break;
			}
			
			String optionalAttributeValue = data.getParameters().getString("optionalAttrValue_"+count);
			
			//only single values properties
			//any null ones will wipe out existing ones
			//and any duplicate ones will override previous ones (currently)
			properties.addProperty(optionalAttributeKey, optionalAttributeValue);
			
			log.debug("optionalAttributeKey: {}, optionalAttributeValue: {}", optionalAttributeKey, optionalAttributeValue);
			
			count++;
		}
		

		// add if needed
		if (user == null)
		{
			// make sure we have eid
			if (isEidEditable)
			{
				if (eid == null)
				{
					addAlert(state, rb.getString("usecre.eidmis"));
					return false;
				}
			}
			
			else
			{
				// eid is not editable, so we're using the email as the eid
				if (email == null)
				{
					addAlert(state, rb.getString("useact.invemail"));
					return false;
				}
				eid = email;
			}

			// if we validate through email, passwords will be handled in AccountValidator
			TempUser tempUser = new TempUser(eid, email, null, null, eid, pw, null);
			if (!validateWithAccountValidator)
			{
				// if in create mode, make sure we have a password
				if (createUser)
				{
					if (pw == null)
					{
						addAlert(state, rb.getString("usecre.pasismis"));
						return false;
					}
				}

				// make sure we have matching password fields
				if (StringUtil.different(pw, pwConfirm))
				{
					addAlert(state, rb.getString("usecre.pass"));
					return false;
				}

				// SAK-23568 - make sure password meets policy requirements
				if (!validatePassword(pw, tempUser, state)) {
					return false;
				}
			}

			//Check if the email is duplicated
			boolean allowEmailDuplicates = ServerConfigurationService.getBoolean("user.email.allowduplicates",true);
			
			if (!allowEmailDuplicates && userDirectoryService.checkDuplicatedEmail(tempUser)){
					addAlert(state, rb.getString("useact.theuseemail1"));
					return false;
			}
			
			
			try
			{
				// add the user in one step so that all you need is add not update permission
				// (the added might be "anon", and anon has add but not update permission)
				
				//SAK-18209 only an admin user should be able to specify a ID
				if (!securityService.isSuperUser()) {
					id = null;
				}
				User newUser;
				if (validateWithAccountValidator)
				{
					// the eid is their email address. The password is random
					newUser = userDirectoryService.addUser(id, eid, firstName, lastName, email, passwordFactory.generatePassword(), type, properties);
					// Invoke AccountValidator to send an email to the user containing a link to a form on which they can set their name and password
					ValidationLogic validationLogic = (ValidationLogic) ComponentManager.get(ValidationLogic.class);
					validationLogic.createValidationAccount(newUser.getId(), ValidationAccount.ACCOUNT_STATUS_REQUEST_ACCOUNT);
				}
				else
				{
					newUser = userDirectoryService.addUser(id, eid, firstName, lastName, email, pw, type, properties);

					if (securityService.isSuperUser()) {
						if(disabled == 1){
							try {
								UserEdit editUser = userDirectoryService.editUser(newUser.getId());
								editUser.getProperties().addProperty("disabled", "true");
								userDirectoryService.commitEdit(editUser);
								newUser = editUser;
							} catch (UserAlreadyDefinedException e) {
								addAlert(state, rb.getString("useact.theuseid1"));
								return false;
							} catch (UserNotDefinedException e) {
								addAlert(state, rb.getString("usecre.disableFailed"));
								return false;
							} catch (UserLockedException e) {
								addAlert(state, rb.getString("usecre.disableFailed"));
								return false;
							}
						}
					}
				}

				// put the user in the state
				state.setAttribute("newuser", newUser);
			}
			catch (UserAlreadyDefinedException e)
			{
				addAlert(state, rb.getString("useact.theuseid1"));
				return false;
			}
			catch (UserIdInvalidException e)
			{
				addAlert(state, rb.getString("useact.theuseid2"));
				return false;
			}
			catch (UserPermissionException e)
			{
				addAlert(state, rb.getString("useact.youdonot3"));
				return false;
			}
		}

		// update
		else
		{
			if (!user.isActiveEdit())
			{
				try
				{
					// add the user in one step so that all you need is add not update permission
					// (the added might be "anon", and anon has add but not update permission)
					user = userDirectoryService.editUser(user.getId());
	
					// put the user in the state
					state.setAttribute("user", user);
				}
				catch (UserLockedException e)
				{
					addAlert(state, rb.getString("useact.somels"));
					return false;
				}
				catch (UserNotDefinedException e)
				{
					Object[] params = new Object[]{id};
					addAlert(state, rb.getFormattedMessage("useact.use_notfou", params));
					
					return false;
				}
				catch (UserPermissionException e)
				{
					addAlert(state, rb.getString("useact.youdonot3"));
					return false;
				}
			}

                  // Still needs super user to change super user password
                  // If the current user isn't a super user but is trying to change the password or email of a super user print an error
			if (!securityService.isSuperUser() && securityService.isSuperUser(user.getId())) {
			    addAlert(state, rb.getString("useact.youdonot4"));
			    return false;
			}

			
			// eid, pw, type might not be editable
			if (eid != null) user.setEid(eid);
			user.setFirstName(firstName);
			user.setLastName(lastName);
			user.setEmail(email);
			if (type != null) user.setType(type);
			
			//add in the updated props
			user.getPropertiesEdit().addAll(properties);
			
			if (securityService.isSuperUser()) {
				if(disabled == 1){
					user.getProperties().addProperty("disabled", "true");
				}else{
					user.getProperties().removeProperty("disabled");
				}
			}
			
			//validate the password only for local users
			if (!isProvidedType(user.getType())) {
			
				// make sure the old password matches, but don't check for super users
				if (!securityService.isSuperUser()) {
					if (!user.checkPassword(pwcur)) {
						addAlert(state, rb.getString("usecre.curpass"));
						return false;
					}
				}

				if (mode == null || !mode.equalsIgnoreCase("remove")) {
					// make sure we have matching password fields
					if (StringUtil.different(pw, pwConfirm))
					{
						addAlert(state, rb.getString("usecre.pass"));
						return false;
					}

					// SAK-23568 - make sure password meets policy requirements
					if (!validatePassword(pw, user, state)) {
						return false;
					}

					if (pw != null) user.setPassword(pw);
				}
			}
		}

		return true;
	}
	
	/**
	 * Get the Map of optional attributes from sakai.properties
	 * 
	 * First list defines the attribute , second the display value. If no display value the attribute name is used.
	 * 
	 * Format is:
	 * 
	 * user.additional.attribute.count=3
	 * user.additional.attribute.1=att1
	 * user.additional.attribute.2=att2
	 * user.additional.attribute.3=att3
	 *
	 * user.additional.attribute.display.att1=Attribute 1
	 * user.additional.attribute.display.att2=Attribute 2
	 * user.additional.attribute.display.att3=Attribute 3
	 * @return
	 */
	private Map<String,String> getOptionalAttributes() {
		
		Map<String,String> atts = new LinkedHashMap<String,String>();
		
		String configs[] = ServerConfigurationService.getStrings("user.additional.attribute");
		if (configs != null) {
			for (int i = 0; i < configs.length; i++) {
				String key = configs[i];
				if (!key.isEmpty()) {
					String value = ServerConfigurationService.getString("user.additional.attribute.display." + key, key);
					atts.put(key, value);
				}
			}
		}
		
		return atts;
		
	}
	
	/**
	 * Gets the current attributes (properties) for a user. Converts the ResourceProperties into a Map
	 * @param user
	 * @return
	 */
	private Map<String,String> getCurrentAttributes(UserEdit user) {
		
		Map<String,String> atts = new LinkedHashMap<String,String>();
		
		ResourceProperties rprops = user.getProperties();
		
		// no props
		if(rprops == null) {
			return atts;
		}
		
		Iterator<String> props = user.getProperties().getPropertyNames();
		
		while(props.hasNext()){
			String prop = props.next();
			atts.put(prop, rprops.getProperty(prop));
		}
		
		return atts;
	}
	
	public void doAttachments(RunData rundata, Context context) {
		
		// use special form of the helper for the admin workspace
		ToolSession session = sessionManager.getCurrentToolSession();
        session.setAttribute(FilePickerHelper.FILE_PICKER_ATTACH_LINKS, new Boolean(true).toString());
		
		// use the helper
		startHelper(rundata.getRequest(), "sakai.filepicker");
		
		// setup the parameters for the helper
		SessionState state = ((JetspeedRunData) rundata).getPortletSessionState(((JetspeedRunData) rundata).getJs_peid());
		UsersActionState sstate = (UsersActionState)getState( context, rundata, UsersActionState.class );
		
		state.setAttribute(FilePickerHelper.FILE_PICKER_ATTACHMENTS, sstate.getAttachments());
		state.setAttribute(FilePickerHelper.FILE_PICKER_MAX_ATTACHMENTS, FilePickerHelper.CARDINALITY_SINGLE);
		
		//set return status
		sstate.setStatus("processImport");
	}
	
	
	
	
	
	
	
	// ********
	// ******** functions copied from VelocityPortletStateAction ********
	// ********
	/**
	 * Get the proper state for this instance (if portlet is not known, only context).
	 * 
	 * @param context
	 *        The Template Context (it contains a reference to the portlet).
	 * @param rundata
	 *        The Jetspeed (Turbine) rundata associated with the request.
	 * @param stateClass
	 *        The Class of the ControllerState to find / create.
	 * @return The proper state object for this instance.
	 */
	protected ControllerState getState(Context context, RunData rundata, Class stateClass)
	{
		return getState(((JetspeedRunData) rundata).getJs_peid(), rundata, stateClass);

	} // getState

	/**
	 * Get the proper state for this instance (if portlet is known).
	 * 
	 * @param portlet
	 *        The portlet being rendered.
	 * @param rundata
	 *        The Jetspeed (Turbine) rundata associated with the request.
	 * @param stateClass
	 *        The Class of the ControllerState to find / create.
	 * @return The proper state object for this instance.
	 */
	protected ControllerState getState(VelocityPortlet portlet, RunData rundata, Class stateClass)
	{
		if (portlet == null)
		{
		 	log.warn("portlet null");
			return null;
		}

		return getState(portlet.getID(), rundata, stateClass);

	} // getState

	/**
	 * Get the proper state for this instance (if portlet id is known).
	 * 
	 * @param peid
	 *        The portlet id.
	 * @param rundata
	 *        The Jetspeed (Turbine) rundata associated with the request.
	 * @param stateClass
	 *        The Class of the ControllerState to find / create.
	 * @return The proper state object for this instance.
	 */
	protected ControllerState getState(String peid, RunData rundata, Class stateClass)
	{
		if (peid == null)
		{
		 	log.warn("peid null");
			return null;
		}

		try
		{
			// get the PortletSessionState
			SessionState ss = ((JetspeedRunData) rundata).getPortletSessionState(peid);

			// get the state object
			ControllerState state = (ControllerState) ss.getAttribute("state");

			if (state != null) return state;

			// if there's no "state" object in there, make one
			state = (ControllerState) stateClass.newInstance();
			state.setId(peid);

			// remember it!
			ss.setAttribute("state", state);

			return state;
		}
		catch (Exception e)
		{
		 	log.warn(e.getMessage(), e);
		}

		return null;

	} // getState

	/**
	 * Release the proper state for this instance (if portlet is not known, only context).
	 * 
	 * @param context
	 *        The Template Context (it contains a reference to the portlet).
	 * @param rundata
	 *        The Jetspeed (Turbine) rundata associated with the request.
	 */
	protected void releaseState(Context context, RunData rundata)
	{
		releaseState(((JetspeedRunData) rundata).getJs_peid(), rundata);

	} // releaseState

	/**
	 * Release the proper state for this instance (if portlet is known).
	 * 
	 * @param portlet
	 *        The portlet being rendered.
	 * @param rundata
	 *        The Jetspeed (Turbine) rundata associated with the request.
	 */
	protected void releaseState(VelocityPortlet portlet, RunData rundata)
	{
		releaseState(portlet.getID(), rundata);

	} // releaseState

	/**
	 * Release the proper state for this instance (if portlet id is known).
	 * 
	 * @param peid
	 *        The portlet id being rendered.
	 * @param rundata
	 *        The Jetspeed (Turbine) rundata associated with the request.
	 */
	protected void releaseState(String peid, RunData rundata)
	{
		try
		{
			// get the PortletSessionState
			SessionState ss = ((JetspeedRunData) rundata).getPortletSessionState(peid);

			// get the state object
			ControllerState state = (ControllerState) ss.getAttribute("state");

			// recycle the state object
			state.recycle();

			// clear out the SessionState for this Portlet
			ss.removeAttribute("state");

			ss.clear();

		}
		catch (Exception e)
		{
		 	log.warn(e.getMessage(), e);
		}

	} // releaseState

	// ******* end of copy from VelocityPortletStateAction
	
	
	private void processImportedUserFile(SessionState state, Context context, Reference file) {
		
		try{
			ContentResource resource = contentHostingService.getResource(file.getId());
			String contentType = resource.getContentType();
			
			//check mime type
			if(!StringUtils.equals(contentType, CSV_MIME_TYPE)) {
				addAlert(state, rb.getString("import.error"));
				return;
			}
			
			//SAK-21884 manual parse method so we can support arbitrary columns
			InputStream in = resource.streamContent();
			String charset = new TikaEncodingDetector().guessEncoding(resource.streamContent());
			if(StandardCharsets.UTF_8.name().equals(charset)) {
				in = new BOMInputStream(in);
			}
	
			CSVReader reader = new CSVReader(new InputStreamReader(in, charset));
		    String [] nextLine;
		    int lineCount = 0;
		    List<ImportedUser> list = new ArrayList<ImportedUser>();
		    Map<Integer,String> mapping = null;
		    
		    while ((nextLine = reader.readNext()) != null) {
		        
		    	if(lineCount == 0) {
		        	//header row, capture it
		    		mapping = mapHeaderRow(nextLine);
		        } else {
		        	//map the fields into the object
		        	list.add(mapLine(nextLine, mapping));
		        }
		    	
		        lineCount++;
		    }
			
			state.setAttribute("importedUsers", list);
			context.put("importedUsers", list);
			
		} catch (Exception e) {
		 	log.error("Error reading imported file: {}", e.getMessage(), e);
			addAlert(state, rb.getString("import.error"));
			return;
		}
		
		return;

	}
	
	/**
	 * Takes the header row from the CSV to determines the position of the columns so that we can 
	 * correctly parse any arbitrary CSV file. This is required because when we iterate over the rest of the lines, 
	 * we need to know what the column header is, so we can set the approriate ImportedUser property
	 * or add into the ResourceProperties list, which ever is required.
	 * 
	 * @param line	the already split line
	 * @return
	 */
	private Map<Integer,String> mapHeaderRow(String[] line) {
		
		Map<Integer,String> mapping = new LinkedHashMap<Integer,String>();
		
		for(int i=0;i<line.length;i++){
			mapping.put(i, line[i]);
		}
		
		return mapping;
		
	}
	
	/**
	 * Takes a row of data and maps it into the appropriate ImportedUser properties
	 * We have a fixed list of properties, anything else goes into ResourceProperties
	 * @param line
	 * @param mapping
	 * @return
	 */
	private ImportedUser mapLine(String[] line, Map<Integer,String> mapping){
		
		ImportedUser u = new ImportedUser();
		ResourceProperties p = new BaseResourcePropertiesEdit();
		
		for(Map.Entry<Integer,String> entry: mapping.entrySet()) {
			int i = entry.getKey();
			String col = entry.getValue();
			
			//now check each of the main properties in turn to determine which one to set, otherwise set into props
			if(StringUtils.equals(col, IMPORT_USER_ID)) {
				u.setEid(line[i]);
			} else if(StringUtils.equals(col, IMPORT_FIRST_NAME)) {
				u.setFirstName(line[i]);
			} else if(StringUtils.equals(col, IMPORT_LAST_NAME)) {
				u.setLastName(line[i]);
			} else if(StringUtils.equals(col, IMPORT_EMAIL)) {
				u.setEmail(line[i]);
			} else if(StringUtils.equals(col, IMPORT_PASSWORD)) {
				u.setPassword(line[i]);
			} else if(StringUtils.equals(col, IMPORT_TYPE)) {
				u.setType(line[i]);
			} else {
				//only add if not blank
				if(StringUtils.isNotBlank(line[i])) {
					p.addProperty(col, line[i]);
				}
			}
		}
		
		u.setProperties(p);
		
		return u;
	}
	
	/**
	 * Check to see if the type is in the list of known provided types
	 * @param userType User's type
	 * @return
	 */
	private boolean isProvidedType(String userType) {
		boolean provided = false;
		String[] providedTypes = ServerConfigurationService.getStrings("user.type.provided");
		if (providedTypes != null && providedTypes.length > 0) {
			List<String> typeList = Arrays.asList(providedTypes);
			if (typeList.contains(userType))
				provided = true;
		}
		return provided;
	}

	/**
	 * Determines whether Account Validator is to be used to ensure that users don't enter bogus email addresses.
	 * This is only required in the gateway's New Account tool if you're not admin.
	 * If this is true, the user account will be inactive (ie. it will be assigned a random unguessable password).
	 * Then, Account Validator will send an email to the user containing a link to a form where they can activate their account by setting their password.
	 * @return true if the state says that this is the gateway's New Account tool, and you're not a super user, and validate-through-email is set in the tool properties
	 */
	private boolean isValidatedWithAccountValidator(SessionState state)
	{
		boolean isGatewayTool = (boolean) state.getAttribute("create-user");
		if (isGatewayTool && !securityService.isSuperUser())
		{
			return (boolean) state.getAttribute(CONFIG_VALIDATE_THROUGH_EMAIL);
		}
		return false;
	}

	private boolean isEidEditable(SessionState state)
	{
		if (securityService.isSuperUser())
		{
			return true;
		}

		boolean isGatewayTool = (boolean) state.getAttribute("create-user");
		if (!isGatewayTool)
		{
			return true;
		}

		return !(Boolean)state.getAttribute(CONFIG_FORCE_EID_EQUALS_EMAIL);
	}

	/**
     * Determine user types by looking at realms that start with "!user.template."
     * Doesn't include sample type
     *
     * @return list of user types in the system
     */
    protected List<String> getUserTypes() {
        List<String> userTypes = new ArrayList<>();
        List<AuthzGroup> groups = authzGroupService.getAuthzGroups(USER_TEMPLATE_PREFIX, null);
        for (Iterator<AuthzGroup> i = groups.iterator(); i.hasNext();) {
            AuthzGroup group = (AuthzGroup) i.next();
            String type = group.getId().replaceFirst(USER_TEMPLATE_PREFIX, "");
            if (!type.equals("sample")) {
                userTypes.add(type);
            }
        }
        return userTypes;
    }
}
