/**********************************************************************************
* $URL: https://source.sakaiproject.org/svn/user/trunk/user-tool/tool/src/java/org/sakaiproject/user/tool/UsersAction.java $
* $Id: UsersAction.java 126569 2013-07-01 20:22:06Z gjthomas@iupui.edu $
***********************************************************************************
*
* 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.InputStreamReader;
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 net.tanesha.recaptcha.ReCaptcha;
import net.tanesha.recaptcha.ReCaptchaFactory;
import net.tanesha.recaptcha.ReCaptchaResponse;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.validator.routines.EmailValidator;
import org.sakaiproject.authz.cover.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.ServerConfigurationService;
import org.sakaiproject.content.api.ContentResource;
import org.sakaiproject.content.api.FilePickerHelper;
import org.sakaiproject.content.cover.ContentHostingService;
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.cover.UsageSessionService;
import org.sakaiproject.thread_local.cover.ThreadLocalManager;
import org.sakaiproject.tool.api.Session;
import org.sakaiproject.tool.api.ToolSession;
import org.sakaiproject.tool.cover.SessionManager;
import org.sakaiproject.user.api.Authentication;
import org.sakaiproject.user.api.AuthenticationException;
import org.sakaiproject.user.api.Evidence;
import org.sakaiproject.user.api.User;
import org.sakaiproject.user.api.UserAlreadyDefinedException;
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.cover.AuthenticationManager;
import org.sakaiproject.user.cover.UserDirectoryService;
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 au.com.bytecode.opencsv.CSVReader;
/**
* <p>
* UsersAction is the Sakai users editor.
* </p>
*/
public class UsersAction extends PagedResourceActionII
{
private static ResourceLoader rb = new ResourceLoader("admin");
//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";
/**
* {@inheritDoc}
*/
protected List 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("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("chef", "recaptcha is enabled but no public key is found.");
enabled = Boolean.FALSE;
}
if (privateKey == null || privateKey.length() == 0)
{
Log.warn("chef", "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);
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);
}
// 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("successMessage"));
state.removeAttribute("successMessage");
// 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("chef", "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.getInstance());
// 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
//addListPagingMenus(bar, state);
int pageSize = Integer.valueOf(state.getAttribute(STATE_PAGESIZE).toString()).intValue();
int currentPageNubmer = Integer.valueOf(state.getAttribute(STATE_CURRENT_PAGE).toString()).intValue();
int startNumber = pageSize * (currentPageNubmer - 1) + 1;
int endNumber = pageSize * currentPageNubmer;
int totalNumber = 0;
Object[] params;
ArrayList 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"));
// add the refresh commands
addRefreshMenus(bar, state);
if (bar.size() > 0)
{
context.put(Menu.CONTEXT_MENU, bar);
}
return "_list";
} // buildListContext
/**
* 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.getInstance());
// 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.getInstance());
// is the type to be pre-set
context.put("type", state.getAttribute("create-type"));
// 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"));
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.getInstance());
// 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"));
// 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);
//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)
{
if (Log.getLogger("chef").isDebugEnabled())
{
Log.debug("chef", this + ".buildViewContext");
}
// 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)
{
}
// disable auto-updates while not in list mode
disableObservers(state);
}
catch (UserNotDefinedException e)
{
Log.warn("chef", "UsersAction.doEdit: user not found: " + id);
Object[] params = new Object[]{id};
addAlert(state, rb.getFormattedMessage("useact.use_notfou", params));
state.removeAttribute("mode");
// make sure auto-updates are enabled
enableObserver(state);
}
return "_view";
} // buildViewContext
/**
* 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);
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");
// disable auto-updates while not in list mode
disableObservers(state);
} // 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("chef", "doImport");
List<ImportedUser> users = (List<ImportedUser>)state.getAttribute("importedUsers");
if(users !=null && users.size() > 0) {
for(ImportedUser user: users) {
try {
User newUser = UserDirectoryService.addUser(null, user.getEid(), user.getFirstName(), user.getLastName(), user.getEmail(), user.getPassword(), user.getType(), user.getProperties());
}
catch (UserAlreadyDefinedException e){
//ok, just skip
continue;
}
catch (UserIdInvalidException e) {
addAlert(state, rb.getString("useact.theuseid2") + ": " + user.getEid());
Log.error("chef", "Import user error: " + e.getClass() + ":" + e.getMessage());
//try to import the rest
continue;
}
catch (UserPermissionException e){
addAlert(state, rb.getString("useact.youdonot3"));
Log.error("chef", "Import user error: " + e.getClass() + ":" + e.getMessage());
//this is bad so return
return;
}
}
//set a message to show it was successful
state.setAttribute("successMessage", rb.getString("import.success"));
//cleanup
state.removeAttribute("importedUsers");
state.removeAttribute("mode");
// make sure auto-updates are enabled
enableObserver(state);
}
} // 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");
// disable auto-updates while not in list mode
disableObservers(state);
}
catch (UserNotDefinedException e)
{
Log.warn("chef", "UsersAction.doEdit: user not found: " + id);
Object[] params = new Object[]{id};
addAlert(state, rb.getFormattedMessage("useact.use_notfou", params));
state.removeAttribute("mode");
// make sure auto-updates are enabled
enableObserver(state);
}
catch (UserPermissionException e)
{
addAlert(state, rb.getFormattedMessage("useact.youdonot1", new Object[]{id}));
state.removeAttribute("mode");
// make sure auto-updates are enabled
enableObserver(state);
}
catch (UserLockedException e)
{
addAlert(state, rb.getFormattedMessage("useact.somels", new Object[]{id}));
state.removeAttribute("mode");
// make sure auto-updates are enabled
enableObserver(state);
}
} // doEdit
/**
* doModify called when "eventSubmit_doModify" is in the request parameters to edit a user
*/
public void doModify(RunData data, Context context)
{
if (Log.getLogger("chef").isDebugEnabled())
{
Log.debug("chef", this + ".doModify");
}
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");
// disable auto-updates while not in list mode
disableObservers(state);
}
catch (UserNotDefinedException e)
{
Log.warn("chef", "UsersAction.doEdit: user not found: " + id);
Object[] params = new Object[]{id};
addAlert(state, rb.getFormattedMessage("useact.use_notfou", params));
state.removeAttribute("mode");
// make sure auto-updates are enabled
enableObserver(state);
}
catch (UserPermissionException e)
{
addAlert(state, rb.getFormattedMessage("useact.youdonot1", new Object[]{id}));
state.removeAttribute("mode");
// make sure auto-updates are enabled
enableObserver(state);
}
catch (UserLockedException e)
{
addAlert(state, rb.getFormattedMessage("useact.somels", new Object[]{id}));
state.removeAttribute("mode");
// make sure auto-updates are enabled
enableObserver(state);
}
} // 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)
{
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("chef", "UsersAction.doSave()" + e);
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");
// make sure auto-updates are enabled
enableObserver(state);
if ((user != null) && ((Boolean) state.getAttribute("create-login")).booleanValue())
{
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("chef", "UsersAction.doSave: authentication failure: " + ex);
}
// 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");
// make sure auto-updates are enabled
enableObserver(state);
} // 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");
// make sure auto-updates are enabled
enableObserver(state);
} // 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");
// remove
try
{
UserDirectoryService.removeUser(user);
}
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");
// make sure auto-updates are enabled
enableObserver(state);
} // 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
/**
* 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
// 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();
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;
}
}
//insure valid email address
//email.matches(".+@.+\\..+")
if(email != null && !EmailValidator.getInstance().isValid(email)) {
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);
//System.out.println("optionalAttributeKey: " + optionalAttributeKey + ", optionalAttributeValue: " + optionalAttributeValue);
count++;
}
// add if needed
if (user == null)
{
// make sure we have eid
if (eid == null)
{
addAlert(state, rb.getString("usecre.eidmis"));
return false;
}
// 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;
}
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 = 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");
newUser = editUser;
} 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;
}
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("chef", ".getState(): 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("chef", ".getState(): 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("chef", "getState: " + e.getClass() + ":" + e.getMessage());
}
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("chef", "releaseState: " + e.getClass() + ":" + e.getMessage());
}
} // 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-21405 SAK-21884 original parse method, auto maps column headers to bean properties
/*
HeaderColumnNameTranslateMappingStrategy<ImportedUser> strat = new HeaderColumnNameTranslateMappingStrategy<ImportedUser>();
strat.setType(ImportedUser.class);
//map the column headers to the field names in the ImportedUser class
Map<String, String> map = new HashMap<String, String>();
map.put("user id", "eid");
map.put("first name", "firstName");
map.put("last name", "lastName");
map.put("email", "email");
map.put("password", "password");
map.put("type", "type");
map.put("properties", "rawProps"); //specially formatted string, see ImportedUser class.
strat.setColumnMapping(map);
CsvToBean<ImportedUser> csv = new CsvToBean<ImportedUser>();
List<ImportedUser> list = new ArrayList<ImportedUser>();
list = csv.parse(strat, new CSVReader(new InputStreamReader(resource.streamContent())));
*/
//SAK-21884 manual parse method so we can support arbitrary columns
CSVReader reader = new CSVReader(new InputStreamReader(resource.streamContent()));
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("chef", "Error reading imported file: " + e.getClass() + " : " + e.getMessage());
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;
}
}