/*
* #%L
* Course Signup Implementation
* %%
* Copyright (C) 2010 - 2013 University of Oxford
* %%
* 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://opensource.org/licenses/ecl2
*
* 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.
* #L%
*/
package uk.ac.ox.oucs.vle.proxy;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.*;
import java.util.Map.Entry;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.antivirus.api.VirusFoundException;
import org.sakaiproject.authz.api.SecurityAdvisor;
import org.sakaiproject.authz.api.SecurityService;
import org.sakaiproject.component.api.ServerConfigurationService;
import org.sakaiproject.content.api.ContentHostingService;
import org.sakaiproject.content.api.ContentResource;
import org.sakaiproject.content.api.ContentResourceEdit;
import org.sakaiproject.email.api.EmailService;
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.event.api.Event;
import org.sakaiproject.event.api.EventTrackingService;
import org.sakaiproject.event.api.NotificationService;
import org.sakaiproject.exception.IdUnusedException;
import org.sakaiproject.exception.InUseException;
import org.sakaiproject.exception.OverQuotaException;
import org.sakaiproject.exception.PermissionException;
import org.sakaiproject.exception.ServerOverloadException;
import org.sakaiproject.exception.TypeException;
import org.sakaiproject.portal.api.PortalService;
import org.sakaiproject.site.api.Site;
import org.sakaiproject.site.api.SiteService;
import org.sakaiproject.site.api.ToolConfiguration;
import org.sakaiproject.tool.api.Placement;
import org.sakaiproject.tool.api.SessionManager;
import org.sakaiproject.tool.api.ToolManager;
import org.sakaiproject.user.api.User;
import org.sakaiproject.user.api.UserAlreadyDefinedException;
import org.sakaiproject.user.api.UserDirectoryService;
import org.sakaiproject.user.api.UserIdInvalidException;
import org.sakaiproject.user.api.UserNotDefinedException;
import org.sakaiproject.user.api.UserPermissionException;
import org.sakaiproject.util.ResourceLoader;
import uk.ac.ox.oucs.vle.*;
/**
* This is the actual Sakai proxy which talks to the Sakai services.
* @author buckett
*
*/
public class SakaiProxyImpl implements SakaiProxy {
private final static Log log = LogFactory.getLog(SakaiProxyImpl.class);
private final static ResourceLoader rb = new ResourceLoader("messages");
private String fromAddress;
/**
* Allow all access to content.
*/
private SecurityAdvisor allowContentRead = new SecurityAdvisor() {
@Override
public SecurityAdvice isAllowed(String userId, String function, String reference) {
if (ContentHostingService.AUTH_RESOURCE_READ.equals(function)) {
return SecurityAdvice.ALLOWED;
}
return SecurityAdvice.PASS;
}
};
/**
* The type to create new users as. Defaults to "guest" which is the same as Site Info tool.
*/
private String userType = "guest";
public void setUserType(String userType) {
this.userType = userType;
}
/**
*
*/
private UserDirectoryService userService;
public void setUserService(UserDirectoryService userService) {
this.userService = userService;
}
/**
*
*/
private EmailService emailService;
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
}
/**
*
*/
private EventTrackingService eventService;
public void setEventService(EventTrackingService eventService) {
this.eventService = eventService;
}
/**
*
*/
private ToolManager toolManager;
public void setToolManager(ToolManager toolManager) {
this.toolManager = toolManager;
}
/**
*
*/
private SiteService siteService;
public void setSiteService(SiteService siteService) {
this.siteService = siteService;
}
/**
*
*/
private PortalService portalService;
public void setPortalService(PortalService portalService) {
this.portalService = portalService;
}
/**
*
*/
private AdditionalUserDetails additionalUserDetails;
public void setAdditionalUserDetails(AdditionalUserDetails additionalUserDetails) {
this.additionalUserDetails = additionalUserDetails;
}
/**
*
*/
private ServerConfigurationService serverConfigurationService;
public void setServerConfigurationService(ServerConfigurationService serverConfigurationService) {
this.serverConfigurationService = serverConfigurationService;
}
/**
*
*/
private ContentHostingService contentHostingService;
public void setContentHostingService(ContentHostingService contentHostingService) {
this.contentHostingService = contentHostingService;
}
/**
*
*/
private SessionManager sessionManager;
public void setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
private SecurityService securityService;
public void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}
public void init() {
if (fromAddress == null) {
fromAddress = serverConfigurationService.getString("course-signup.from", null);
}
}
public UserProxy getCurrentUser() {
User sakaiUser = userService.getCurrentUser();
UserProxy user = wrapUserProxy(sakaiUser);
return user;
}
@Override
public boolean isAnonymousUser() {
return userService.getAnonymousUser().equals(userService.getCurrentUser());
}
@Override
public boolean isAdministrator() {
return securityService.isSuperUser();
}
public UserProxy findUserById(String id) {
try {
return wrapUserProxy(userService.getUser(id));
} catch (UserNotDefinedException unde) {
return null;
}
}
public UserProxy findUserByEmail(String email) {
Collection<User> users = userService.findUsersByEmail(email);
if (users.size() == 0) {
return null;
} else {
if (users.size() > 1) {
log.warn("More than one user found with email: "+ email);
}
return wrapUserProxy(users.iterator().next());
}
}
public UserProxy findUserByEid(String eid) {
try {
return wrapUserProxy(userService.getUserByAid(eid));
} catch (UserNotDefinedException unde) {
return null;
}
}
public UserProxy newUser(String name, String email) {
User sakaiUser;
String id = null;
String eid = email;
String firstName = null;
String lastName = null;
// Authentication never works in Sakai if the password is null.
String pw = null;
String type = userType;
String trimmedName = name.trim();
int i = trimmedName.lastIndexOf(" ");
if (i > 0) {
firstName = trimmedName.substring(0, i);
lastName = trimmedName.substring(i+1);
} else {
firstName = trimmedName;
}
try {
sakaiUser = userService.addUser(id, eid, firstName, lastName, email, pw, type, null);
} catch (UserIdInvalidException e) {
throw new IllegalArgumentException("Failed to add user because of bad ID", e);
} catch (UserPermissionException e) {
throw new PermissionDeniedException(e.getUser(), "Current user doesn't have permission to add users.", e);
} catch (UserAlreadyDefinedException e) {
return findUserByEmail(email);
}
UserProxy user = wrapUserProxy(sakaiUser);
return user;
}
public void sendEmail(String to, String subject, String body) {
String from = fromAddress;
if (from == null) {
from = getCurrentUser().getEmail();
}
emailService.send(
from, // from address
to, // to address
subject, // subject
body, // message body
null, // header to string
null, // Reply to string
null // Additional headers
);
}
public void logEvent(String resourceId, String eventType, String placementId) {
Placement placement = getPlacement(placementId);
String context = placement.getContext();
String resource = "/coursesignup/group/"+ resourceId;
Event event = eventService.newEvent(eventType, resource, context, false, NotificationService.NOTI_OPTIONAL);
eventService.post(event);
}
public String getCurrentPlacementId() {
return getPlacement(null).getId();
}
/**
* Just get the current placement.
* @return The current placement.
* @throws RuntimeException If there isn't a current placement, this happens
* when a request comes through that isn't processed by the portal.
*/
private Placement getPlacement(String placementId) {
Placement placement = null;
if (null == placementId) {
placement = toolManager.getCurrentPlacement();
} else {
placement = siteService.findTool(placementId);
}
if (placement == null) {
try {
String defaultSiteId = getSiteId();
if (null == defaultSiteId) {
throw new RuntimeException("No default tool placement set.");
}
Site site = siteService.getSite(defaultSiteId);
placement = site.getToolForCommonId("course.signup");
} catch(Exception e) {
throw new RuntimeException("No current tool placement set.");
}
}
if (placement == null) {
throw new RuntimeException("No current tool placement set.");
}
return placement;
}
protected String getSiteId() {
if (null != serverConfigurationService) {
return serverConfigurationService.getString("ses.default.siteId", "d0c31496-d5b9-41fd-9ea9-349a7ac3a01a");
}
return null;
}
@SuppressWarnings("unchecked")
private UserProxy wrapUserProxy(User sakaiUser) {
if(sakaiUser == null) {
return null;
}
List<String> units = sakaiUser.getProperties().getPropertyList("units");
return new UserProxy(sakaiUser.getId(), sakaiUser.getEid(),
sakaiUser.getFirstName(), sakaiUser.getLastName(), sakaiUser.getDisplayName(),
sakaiUser.getEmail(),
sakaiUser.getDisplayId(),
sakaiUser.getProperties().getProperty("oakOSSID"),
sakaiUser.getProperties().getProperty("yearOfStudy"),
sakaiUser.getProperties().getProperty("oakStatus"),
sakaiUser.getProperties().getProperty("primaryOrgUnit"),
(units == null)?Collections.EMPTY_LIST:units,
additionalUserDetails);
}
public String getAdminUrl() {
return getUrl("/static/admin.jsp");
}
public String getConfirmUrl(String signupId) {
return getConfirmUrl(signupId, null);
}
public String getConfirmUrl(String signupId, String placementId) {
if (null == signupId) {
return getUrl("/static/pending.jsp", placementId);
}
return getUrl("/static/pending.jsp#"+ signupId, placementId);
}
public String getDirectUrl(String courseId) {
return getUrl("/static/index.jsp?openCourse="+ courseId);
}
public String getApproveUrl(String signupId) {
return getApproveUrl(signupId, null);
}
public String getApproveUrl(String signupId, String placementId) {
if (null == signupId) {
return getUrl("/static/approve.jsp", placementId);
}
return getUrl("/static/approve.jsp#"+ signupId, placementId);
}
public String getAdvanceUrl(String signupId, String status, String placementId) {
String urlSafe = encode(signupId+"$"+status+"$"+getPlacement(placementId).getId());
return serverConfigurationService.getServerUrl() +
"/course-signup/rest/signup/advance/"+urlSafe;
}
public String encode(String uncoded) {
byte[] encrypted = aes(uncoded.getBytes(), Cipher.ENCRYPT_MODE);
if (encrypted != null) {
String base64String = new String(Base64.encodeBase64(encrypted));
return base64String.replace('+','-').replace('/','_');
} else {
// Return obvious note that encryption failed.
return "encryption.failed";
}
}
public String uncode(String encoded) {
String base64String = encoded.replace('-','+').replace('_','/');
byte[] encrypted = Base64.decodeBase64(base64String.getBytes());
String decrypted = new String(aes(encrypted, Cipher.DECRYPT_MODE));
// On failed decryption we have to return null.
return decrypted;
}
public String getMyUrl() {
return getMyUrl(null);
}
public String getMyUrl(String placementId) {
return getUrl("/static/my.jsp", placementId);
}
public String getAdminUrl(String placementId) {
return getUrl("/static/admin.jsp", placementId);
}
@Override
public String getMessage(String key) {
return rb.getString(key);
}
private String getUrl(String toolState) {
return getUrl(toolState, null);
}
private String getUrl(String toolState, String placementId) {
Placement currentPlacement = getPlacement(placementId);
//String siteId = currentPlacement.getContext();
ToolConfiguration toolConfiguration = siteService.findTool(currentPlacement.getId());
String pageUrl = toolConfiguration.getContainingPage().getUrl();
Map<String, String[]> encodedToolState = portalService.encodeToolState(currentPlacement.getId(), toolState);
StringBuilder params = new StringBuilder();
for (Entry<String, String[]> entry : encodedToolState.entrySet()) {
for(String value: entry.getValue()) {
params.append("&");
params.append(entry.getKey());
params.append("=");
params.append(URLEncoder.encode(value));
}
}
if (params.length() > 0) {
pageUrl += "?"+ params.substring(1); // Trim the leading &
}
return pageUrl;
}
protected String getSecretKey() {
// Return null if not set.
String key = serverConfigurationService.getString("aes.secret.key", null);
if (key == null) {
log.error("No secret key specified. Please set 'aes.secret.key' in configuration");
}
int length = key.getBytes().length;
if ((length % 8) != 0) {
log.error("aes.secret.key must be a multiple of 8 bytes long, not "+ length);
return null;
}
return key;
}
protected byte[] aes(byte[] source, int mode) {
String secretKey = getSecretKey();
if (secretKey == null) {
return null;
}
SecretKeySpec skeySpec = new SecretKeySpec(secretKey.getBytes(), "AES");
try {
// Instantiate the cipher
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(mode, skeySpec);
byte[] encrypted = cipher.doFinal(source);
return encrypted;
} catch (Exception e) {
String type = (mode == Cipher.DECRYPT_MODE)? "decryption" :
(mode == Cipher.ENCRYPT_MODE)? "encryption" :
"unknown";
log.warn("AES "+ type+ " failed.", e);
}
return null;
}
public Integer getConfigParam(String param, int dflt) {
return Integer.parseInt(serverConfigurationService.getString(param, new Integer(dflt).toString()));
}
public String getConfigParam(String param, String dflt) {
return serverConfigurationService.getString(param, dflt);
}
/**
*
* @param contentId
* @param contentDisplayName
* @param bytes
* @throws VirusFoundException
* @throws OverQuotaException
* @throws ServerOverloadException
* @throws PermissionException
* @throws TypeException
* @throws InUseException
*/
public void writeLog(String contentId, String contentDisplayName, byte[] bytes) {
switchUser();
ContentResourceEdit cre = null;
try {
cre = getContentResourceEdit(contentId, contentDisplayName);
cre.setContent(bytes);
// Don't notify anyone about this resource.
contentHostingService.commitResource(cre, NotificationService.NOTI_NONE);
} catch (PermissionException e) {
log.error("PermissionException ["+e.getMessage()+"]", e);
} catch (TypeException e) {
log.error("TypeException ["+e.getMessage()+"]", e);
} catch (InUseException e) {
log.error("InUseException ["+e.getMessage()+"]", e);
} catch (VirusFoundException e) {
log.error("VirusFoundException ["+e.getMessage()+"]", e);
} catch (OverQuotaException e) {
log.error("OverQuotaException ["+e.getMessage()+"]", e);
} catch (ServerOverloadException e) {
log.error("ServerOverloadException ["+e.getMessage()+"]", e);
} finally {
if (cre != null && cre.isActiveEdit()) {
contentHostingService.cancelResource(cre);
}
}
}
public void prependLog(String contentId, String contentDisplayName, byte[] logBytes) {
switchUser();
File tempFile = null;
OutputStream out = null;
ContentResourceEdit cre = null;
try {
cre = getContentResourceEdit(contentId, contentDisplayName);
tempFile = File.createTempFile("ses", ".tmp");
tempFile.deleteOnExit();
out = new FileOutputStream(tempFile);
out.write(logBytes);
IOUtils.copy(cre.streamContent(), out);
out.flush();
cre.setContent(new FileInputStream(tempFile));
// Don't notify anyone about this resource.
contentHostingService.commitResource(cre, NotificationService.NOTI_NONE);
} catch (IOException e) {
log.error("IOException ["+e.getMessage()+"]", e);
} catch (ServerOverloadException e) {
log.error("ServerOverloadException ["+e.getMessage()+"]", e);
} catch (PermissionException e) {
log.error("PermissionException ["+e.getMessage()+"]", e);
} catch (TypeException e) {
log.error("TypeException ["+e.getMessage()+"]", e);
} catch (InUseException e) {
log.error("InUseException ["+e.getMessage()+"]", e);
} catch (VirusFoundException e) {
log.error("VirusFoundException ["+e.getMessage()+"]", e);
} catch (OverQuotaException e) {
log.error("OverQuotaException ["+e.getMessage()+"]", e);
} finally {
// This is to make sure that we don't leave it locked.
if (cre != null && cre.isActiveEdit()) {
contentHostingService.cancelResource(cre);
}
try {
if (null != out) {
out.close();
}
if (null != tempFile) {
tempFile.delete();
}
} catch (IOException e) {
log.error("IOException ["+e.getMessage()+"]", e);
}
}
}
@Override
public Properties getCategoryMapping() {
String siteId = getConfigParam("course-signup.site-id", "course-signup");
String filename = getConfigParam("course-signup.category-mapping", "category-mapping.properties");
String filePath = contentHostingService.getSiteCollection(siteId)+filename;
InputStream input = null;
try {
securityService.pushAdvisor(allowContentRead);
ContentResource resource = contentHostingService.getResource(filePath);
input = resource.streamContent();
Properties props = new Properties();
props.load(input);
return props;
} catch (IdUnusedException iue) {
// This may well be missing.
log.debug("Couldn't find category mapping file: "+ filePath);
} catch (Exception e) {
log.warn("Failed to load properties from: "+ filePath, e);
} finally {
securityService.popAdvisor(allowContentRead);
if (input != null) {
try {
input.close();
} catch (IOException ioe) {
// Ignore.
}
}
}
return null;
}
/**
*
* @param contentId
* @param contentDisplayName
* @return
* @throws PermissionException
* @throws TypeException
* @throws InUseException
*/
private ContentResourceEdit getContentResourceEdit(String contentId, String contentDisplayName)
throws PermissionException, TypeException, InUseException {
ContentResourceEdit cre = null;
String siteId = getConfigParam("course-signup.site-id", "course-signup");
String jsonResourceEId = contentHostingService.getSiteCollection(siteId)+"logs/"+ contentId;
try {
// editResource() doesn't throw IdUnusedExcpetion but PermissionException
// when the resource is missing so we first just tco to find it.
contentHostingService.getResource(jsonResourceEId);
cre = contentHostingService.editResource(jsonResourceEId);
} catch (IdUnusedException e) {
try {
cre = contentHostingService.addResource(jsonResourceEId);
ResourceProperties props = cre.getPropertiesEdit();
props.addProperty(ResourceProperties.PROP_DISPLAY_NAME, contentDisplayName);
cre.setContentType("text/html");
} catch (Exception e1) {
log.warn("Failed to create the import log file.", e1);
}
}
return cre;
}
/**
* This sets up the user for the current request.
*/
private void switchUser() {
if (null != sessionManager) {
org.sakaiproject.tool.api.Session session = sessionManager.getCurrentSession();
session.setUserEid("admin");
session.setUserId("admin");
}
}
}