/**
* $URL: https://source.sakaiproject.org/svn/basiclti/trunk/basiclti-impl/src/java/org/sakaiproject/basiclti/impl/BasicLTISecurityServiceImpl.java $
* $Id: BasicLTISecurityServiceImpl.java 131688 2013-11-17 02:14:16Z csev@umich.edu $
*
* Copyright (c) 2009 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.basiclti.impl;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletOutputStream;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.imsglobal.basiclti.BasicLTIUtil;
import org.sakaiproject.authz.cover.SecurityService;
import org.sakaiproject.entity.api.Entity;
import org.sakaiproject.entity.api.EntityAccessOverloadException;
import org.sakaiproject.entity.api.EntityCopyrightException;
import org.sakaiproject.entity.cover.EntityManager;
import org.sakaiproject.entity.api.EntityNotDefinedException;
import org.sakaiproject.entity.api.EntityPermissionException;
import org.sakaiproject.entity.api.EntityProducer;
import org.sakaiproject.entity.api.HttpAccess;
import org.sakaiproject.entity.api.Reference;
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.tool.cover.SessionManager;
import org.sakaiproject.tool.cover.ToolManager;
import org.sakaiproject.site.api.Site;
import org.sakaiproject.site.cover.SiteService;
import org.sakaiproject.component.cover.ComponentManager;
import org.sakaiproject.component.cover.ServerConfigurationService;
import org.sakaiproject.util.StringUtil;
import org.sakaiproject.util.FormattedText;
import org.sakaiproject.exception.IdUnusedException;
import org.sakaiproject.exception.PermissionException;
import org.sakaiproject.util.ResourceLoader;
import org.sakaiproject.event.api.Event;
import org.sakaiproject.event.api.NotificationService;
import org.sakaiproject.lti.api.LTIService;
//import org.sakaiproject.event.cover.EventTrackingService;
import org.sakaiproject.component.cover.ComponentManager;
import org.sakaiproject.util.Validator;
import org.sakaiproject.util.Web;
import org.sakaiproject.site.api.SitePage;
import org.sakaiproject.site.api.ToolConfiguration;
import org.sakaiproject.util.foorm.SakaiFoorm;
import org.sakaiproject.basiclti.LocalEventTrackingService;
import org.sakaiproject.basiclti.util.SakaiBLTIUtil;
import org.sakaiproject.basiclti.impl.BasicLTIArchiveBean;
@SuppressWarnings("deprecation")
public class BasicLTISecurityServiceImpl implements EntityProducer {
public static final String SERVICE_NAME = BasicLTISecurityServiceImpl.class.getName();
private static ResourceLoader rb = new ResourceLoader("basicltisvc");
public static final String MIME_TYPE_BLTI="ims/basiclti";
public static final String REFERENCE_ROOT="/basiclti";
public static final String APPLICATION_ID = "sakai:basiclti";
public static final String TOOL_REGISTRATION = "sakai.basiclti";
public static final String EVENT_BASICLTI_LAUNCH = "basiclti.launch";
protected static SakaiFoorm foorm = new SakaiFoorm();
// Note: security needs a proper Resource reference
/*******************************************************************************
* Dependencies and their setter methods
*******************************************************************************/
/** Dependency: a logger component. */
private Log logger = LogFactory.getLog(BasicLTISecurityServiceImpl.class);
/**
* Check security for this entity.
*
* @param ref
* The Reference to the entity.
* @return true if allowed, false if not.
*/
protected boolean checkSecurity(Reference ref)
{
String contextId = ref.getContext();
try
{
Site site = SiteService.getSiteVisit(contextId);
if ( site != null ) return true;
}
catch(IdUnusedException ex)
{
return false;
}
catch(PermissionException ex)
{
return false;
}
// System.out.println("ID="+ref.getId());
// System.out.println("Type="+ref.getType());
// System.out.println("SubType="+ref.getSubType());
return false;
}
/*******************************************************************************
* Init and Destroy
*******************************************************************************/
/** A service */
protected static LTIService ltiService = null;
/**
* Final initialization, once all dependencies are set.
*/
public void init()
{
logger.info(this +".init()");
if (ServerConfigurationService.getString(SakaiBLTIUtil.BASICLTI_ENCRYPTION_KEY, null) == null) {
logger.warn("BasicLTI secrets in database unencrypted, please set "+ SakaiBLTIUtil.BASICLTI_ENCRYPTION_KEY);
}
try
{
// register as an entity producer
EntityManager.registerEntityProducer(this,REFERENCE_ROOT);
}
catch (Throwable t)
{
logger.warn("init(): ", t);
}
if ( ltiService == null ) ltiService = (LTIService) ComponentManager.get("org.sakaiproject.lti.api.LTIService");
}
/**
* Final cleanup.
*/
public void destroy()
{
logger.info(this +".destroy()");
}
/**
*
*/
public BasicLTISecurityServiceImpl() {
super();
}
public boolean isSuperUser(String userId)
{
return SecurityService.isSuperUser(userId);
}
/*******************************************************************************************************************************
* EntityProducer
******************************************************************************************************************************/
/**
* {@inheritDoc}
/access/basiclti/site/12-siteid-456/98-placement-id
/access/basiclti/content/ --- content path ---- (Future)
*/
public boolean parseEntityReference(String reference, Reference ref)
{
if (reference.startsWith(REFERENCE_ROOT))
{
// we will get null, simplelti, site, <context>, <placement>
// we will store the context, and the ContentHosting reference in our id field.
String id = null;
String context = null;
String[] parts = StringUtil.split(reference, Entity.SEPARATOR);
if ( parts.length == 5 && parts[2].equals("site") )
{
context = parts[3];
id = parts[4];
//Should the slashes below be entityseparator
// id = "/" + StringUtil.unsplit(parts, 2, parts.length - 2, "/");
}
ref.set(APPLICATION_ID, "site", id, null, context);
return true;
}
return false;
}
private void sendHTMLPage(HttpServletResponse res, String body)
{
try
{
res.setContentType("text/html; charset=UTF-8");
res.setCharacterEncoding("utf-8");
res.addDateHeader("Expires", System.currentTimeMillis() - (1000L * 60L * 60L * 24L * 365L));
res.addDateHeader("Last-Modified", System.currentTimeMillis());
res.addHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0, post-check=0, pre-check=0");
res.addHeader("Pragma", "no-cache");
java.io.PrintWriter out = res.getWriter();
out.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");
out.println("<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\" xml:lang=\"en\">");
out.println("<html>\n<head>");
out.println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />");
out.println("</head>\n<body>\n");
out.println(body);
out.println("\n</body>\n</html>");
}
catch (Exception e)
{
e.printStackTrace();
}
}
private void doSplash(HttpServletRequest req, HttpServletResponse res, String splash, ResourceLoader rb)
{
// req.getRequestURL()=http://localhost:8080/access/basiclti/site/85fd092b-1755-4aa9-8abc-e6549527dce0/content:0
// req.getRequestURI()=/access/basiclti/site/85fd092b-1755-4aa9-8abc-e6549527dce0/content:0
String acceptPath = req.getRequestURI().toString() + "?splash=bypass";
String body = "<div align=\"center\" style=\"text-align:left;width:80%;margin-top:5px;margin-left:auto;margin-right:auto;border-width:1px 1px 1px 1px;border-style:solid;border-color: gray;padding:.5em;font-family:Verdana,Arial,Helvetica,sans-serif;font-size:.8em\">";
body += splash+"</div><p>";
String txt = rb.getString("launch.button", "Press to continue to external tool.");
body += "<form><input type=\"submit\" onclick=\"window.location='"+acceptPath+"';return false;\" value=\"";
body += rb.getString("launch.button", "Press to continue to proceed to external tool.");
body += "\"></form></p>\n";
sendHTMLPage(res, body);
}
/**
* {@inheritDoc}
*/
public HttpAccess getHttpAccess()
{
return new HttpAccess()
{
@SuppressWarnings("unchecked")
public void handleAccess(HttpServletRequest req, HttpServletResponse res, Reference ref,
Collection copyrightAcceptedRefs) throws EntityPermissionException, EntityNotDefinedException,
EntityAccessOverloadException, EntityCopyrightException
{
// decide on security
if (!checkSecurity(ref))
{
throw new EntityPermissionException(SessionManager.getCurrentSessionUserId(), "basiclti", ref.getReference());
}
String refId = ref.getId();
String [] retval = null;
if ( refId.startsWith("deploy:") && refId.length() > 7 )
{
if ("!admin".equals(ref.getContext()) )
{
throw new EntityPermissionException(SessionManager.getCurrentSessionUserId(), "basiclti", ref.getReference());
}
Map<String,Object> deploy = null;
String deployStr = refId.substring(7);
Long deployKey = foorm.getLongKey(deployStr);
if ( deployKey >= 0 ) deploy = ltiService.getDeployDao(deployKey);
String placementId = req.getParameter("placement");
// System.out.println("deployStr="+deployStr+" deployKey="+deployKey+" placementId="+placementId);
// System.out.println(deploy);
Long reg_state = foorm.getLongKey(deploy.get(LTIService.LTI_REG_STATE));
if ( reg_state == 0 )
{
retval = SakaiBLTIUtil.postRegisterHTML(deployKey, deploy, rb, placementId);
}
else
{
retval = SakaiBLTIUtil.postReRegisterHTML(deployKey, deploy, rb, placementId);
}
}
else if ( refId.startsWith("content:") && refId.length() > 8 )
{
Map<String,Object> content = null;
Map<String,Object> tool = null;
String contentStr = refId.substring(8);
Long contentKey = foorm.getLongKey(contentStr);
if ( contentKey >= 0 )
{
content = ltiService.getContentDao(contentKey,ref.getContext());
if ( content != null )
{
String siteId = (String) content.get(LTIService.LTI_SITE_ID);
if ( siteId == null || ! siteId.equals(ref.getContext()) )
{
content = null;
}
}
if ( content != null )
{
Long toolKey = foorm.getLongKey(content.get(LTIService.LTI_TOOL_ID));
if ( toolKey >= 0 ) tool = ltiService.getToolDao(toolKey, ref.getContext());
if ( tool != null )
{
// SITE_ID can be null for the tool
String siteId = (String) tool.get(LTIService.LTI_SITE_ID);
if ( siteId != null && ! siteId.equals(ref.getContext()) )
{
tool = null;
}
}
}
// Adjust the content items based on the tool items
if ( tool != null || content != null )
{
ltiService.filterContent(content, tool);
}
}
String splash = null;
if ( tool != null ) splash = (String) tool.get("splash");
String splashParm = req.getParameter("splash");
String siteId = null;
if ( tool != null ) siteId = (String) tool.get(LTIService.LTI_SITE_ID);
if ( splashParm == null && splash != null && splash.trim().length() > 1 )
{
// XSS Note: Administrator-created tools can put HTML in the splash.
if ( siteId != null ) splash = FormattedText.escapeHtml(splash,false);
doSplash(req, res, splash, rb);
return;
}
retval = SakaiBLTIUtil.postLaunchHTML(content, tool, ltiService, rb);
}
else
{
String splashParm = req.getParameter("splash");
if ( splashParm == null )
{
ToolConfiguration placement = SiteService.findTool(refId);
Properties config = placement == null ? null : placement.getConfig();
if ( placement != null )
{
// XSS Note: Only the Administrator can set overridesplash - so we allow HTML
String splash = SakaiBLTIUtil.toNull(SakaiBLTIUtil.getCorrectProperty(config,"overridesplash", placement));
if ( splash == null )
{
// This may be user-set so no HTML
splash = SakaiBLTIUtil.toNull(SakaiBLTIUtil.getCorrectProperty(config,"splash", placement));
if ( splash != null ) splash = FormattedText.escapeHtml(splash,false);
}
// XSS Note: Only the Administrator can set defaultsplash - so we allow HTML
if ( splash == null )
{
splash = SakaiBLTIUtil.toNull(SakaiBLTIUtil.getCorrectProperty(config,"defaultsplash", placement));
}
if ( splash != null && splash.trim().length() > 1 )
{
doSplash(req, res, splash, rb);
return;
}
}
}
// Get the post data for the placement
retval = SakaiBLTIUtil.postLaunchHTML(refId, rb);
}
try
{
sendHTMLPage(res, retval[0]);
String refstring = ref.getReference();
if ( retval.length > 1 ) refstring = retval[1];
// Cool 2.6 Event call
Event event = LocalEventTrackingService.newEvent(EVENT_BASICLTI_LAUNCH, refstring, ref.getContext(), false, NotificationService.NOTI_OPTIONAL);
// 2.5 Event call
// Event event = EventTrackingService.newEvent(EVENT_BASICLTI_LAUNCH, refstring, false);
LocalEventTrackingService.post(event);
}
catch (Exception e)
{
e.printStackTrace();
}
}
};
}
/**
* {@inheritDoc}
*/
public Entity getEntity(Reference ref)
{
return null;
}
/**
* {@inheritDoc}
*/
public Collection<String> getEntityAuthzGroups(Reference ref, String userId)
{
// Since we handle security ourself, we won't support anyone else asking
return null;
}
/**
* {@inheritDoc}
*/
public String getEntityDescription(Reference ref)
{
return null;
}
/**
* {@inheritDoc}
*/
public ResourceProperties getEntityResourceProperties(Reference ref)
{
return null;
}
/**
* {@inheritDoc}
*/
public String getEntityUrl(Reference ref)
{
return ServerConfigurationService.getAccessUrl() + ref.getReference();
}
/**
* {@inheritDoc}
*/
public String getLabel()
{
return "basiclti";
}
public boolean willArchiveMerge()
{
return true;
}
@SuppressWarnings("unchecked")
public String merge(String siteId, Element root, String archivePath, String fromSiteId, Map attachmentNames, Map userIdTrans,
Set userListAllowImport)
{
StringBuilder results = new StringBuilder("Merging BasicLTI ");
org.w3c.dom.NodeList nodeList = root.getElementsByTagName("basicLTI");
try {
Site site = SiteService.getSite(siteId);
for(int i=0; i < nodeList.getLength(); i++)
{
BasicLTIArchiveBean basicLTI = new BasicLTIArchiveBean(nodeList.item(i));
logger.info("BASIC LTI: " + basicLTI);
results.append(", merging basicLTI tool " + basicLTI.getPageTitle());
SitePage sitePage = site.addPage();
sitePage.setTitle(basicLTI.getPageTitle());
// This property affects both the Tool and SitePage.
sitePage.setTitleCustom(true);
ToolConfiguration toolConfiguration = sitePage.addTool();
toolConfiguration.setTool(TOOL_REGISTRATION, ToolManager.getTool(TOOL_REGISTRATION));
toolConfiguration.setTitle(basicLTI.getToolTitle());
for(Object key: basicLTI.getSiteToolProperties().keySet())
{
toolConfiguration.getPlacementConfig().setProperty((String)key, (String)basicLTI.getSiteToolProperties().get(key));
}
SiteService.save(site);
}
} catch (IdUnusedException ie) {
// This would be thrown by SiteService.getSite(siteId)
ie.printStackTrace();
} catch (PermissionException pe) {
// This would be thrown by SiteService.save(site)
pe.printStackTrace();
} catch (Exception e) {
// This is a generic exception that would be thrown by the BasicLTIArchiveBean constructor.
e.printStackTrace();
}
results.append(".");
return results.toString();
}
@SuppressWarnings("unchecked")
public String archive(String siteId, Document doc, Stack stack, String archivePath, List attachments)
{
logger.info("-------basic-lti-------- archive('"
+ StringUtils.join(new Object[] { siteId, doc, stack,
archivePath, attachments }, "','") + "')");
StringBuilder results = new StringBuilder("archiving basiclti "+siteId+"\n");
int count = 0;
try {
Site site = SiteService.getSite(siteId);
logger.info("SITE: " + site.getId() + " : " + site.getTitle());
Element basicLtiList = doc.createElement("org.sakaiproject.basiclti.service.BasicLTISecurityService");
for (SitePage sitePage : site.getPages()) {
for (ToolConfiguration toolConfiguration : sitePage.getTools()) {
if ( toolConfiguration.getTool() == null ) continue;
if (toolConfiguration.getTool().getId().equals(
TOOL_REGISTRATION)) {
// results.append(" tool=" + toolConfiguration.getId() + "\n");
count++;
BasicLTIArchiveBean basicLTIArchiveBean = new BasicLTIArchiveBean();
basicLTIArchiveBean.setPageTitle(sitePage.getTitle());
basicLTIArchiveBean.setToolTitle(toolConfiguration.getTitle());
basicLTIArchiveBean.setSiteToolProperties(toolConfiguration.getConfig());
Node newNode = basicLTIArchiveBean.toNode(doc);
basicLtiList.appendChild(newNode);
}
}
}
((Element) stack.peek()).appendChild(basicLtiList);
stack.push(basicLtiList);
stack.pop();
}
catch (IdUnusedException iue) {
logger.info("SITE ID " + siteId + " DOES NOT EXIST.");
results.append("Basic LTI Site does not exist\n");
}
// Something we did not expect
catch (Exception e) {
e.printStackTrace();
results.append("basiclti exception:"+e.getClass().getName()+"\n");
}
results.append("archiving basiclti ("+count+") tools archived\n");
return results.toString();
}
}