Skip to content

Commit

Permalink
Merge pull request sakaiproject#870 from csev/SAK-29328
Browse files Browse the repository at this point in the history
SAK-29328 - Refactor code from LTAdmin into ContentItem and Util classes
  • Loading branch information
csev committed Jul 15, 2015
2 parents 753a3df + 5dc13c4 commit 810ac7d
Show file tree
Hide file tree
Showing 5 changed files with 277 additions and 125 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Enumeration;

import java.net.URL;
import java.net.URLEncoder;

import javax.servlet.http.HttpServletRequest;

Expand All @@ -45,12 +47,15 @@
import org.imsglobal.lti2.LTI2Util;
import org.imsglobal.lti2.LTI2Messages;
import org.imsglobal.lti2.ToolProxyBinding;
import org.imsglobal.lti2.ContentItem;
import org.imsglobal.lti2.objects.ToolConsumer;

import org.sakaiproject.lti.api.LTIService;

import org.sakaiproject.tool.api.Session;
import org.sakaiproject.tool.cover.SessionManager;
import org.sakaiproject.tool.cover.ToolManager;
import org.sakaiproject.event.cover.UsageSessionService;
import org.sakaiproject.user.api.User;
import org.sakaiproject.user.cover.UserDirectoryService;
import org.sakaiproject.site.api.ToolConfiguration;
Expand All @@ -71,6 +76,7 @@
import org.sakaiproject.util.ResourceLoader;
import org.sakaiproject.util.Web;
import org.sakaiproject.portal.util.CSSUtils;
import org.sakaiproject.portal.util.ToolUtils;
import org.sakaiproject.linktool.LinkToolUtil;
import org.sakaiproject.authz.api.SecurityAdvisor;
import org.sakaiproject.authz.cover.SecurityService;
Expand Down Expand Up @@ -987,6 +993,59 @@ public static String[] postReRegisterHTML(Long deployKey, Map<String,Object> dep
return retval;
}

/**
* Build a URL, Adding Sakai's CSRF token
*/
public static String addCSRFToken(String url)
{
Session session = SessionManager.getCurrentSession();
Object csrfToken = session.getAttribute(UsageSessionService.SAKAI_CSRF_SESSION_ATTRIBUTE);
if ( url.indexOf("?") < 0 ) {
url = url + "?";
} else {
url = url + "&";
}
url = url + "sakai_csrf_token=" + URLEncoder.encode(csrfToken.toString());
return url;
}

/**
* Create a ContentItem from the current request (may throw runtime)
*/
public static ContentItem getContentItemFromRequest(Map<String, Object> tool)
{

Placement placement = ToolManager.getCurrentPlacement();
String siteId = placement.getContext();

String toolSiteId = (String) tool.get(LTIService.LTI_SITE_ID);
if ( toolSiteId != null && ! toolSiteId.equals(siteId) ) {
throw new RuntimeException("Incorrect site id");
}

HttpServletRequest req = ToolUtils.getRequestFromThreadLocal();

String lti_log = req.getParameter("lti_log");
String lti_errorlog = req.getParameter("lti_errorlog");
if ( lti_log != null ) M_log.debug(lti_log);
if ( lti_errorlog != null ) M_log.warn(lti_errorlog);

ContentItem contentItem = new ContentItem(req);

String oauth_consumer_key = req.getParameter("oauth_consumer_key");
String oauth_secret = (String) tool.get(LTIService.LTI_SECRET);
oauth_secret = decryptSecret(oauth_secret);

String URL = getOurServletPath(req);
if ( ! contentItem.validate(oauth_consumer_key, oauth_secret, URL) ) {
M_log.warn("Provider failed to validate message: "+contentItem.getErrorMessage());
String base_string = contentItem.getBaseString();
if ( base_string != null ) M_log.warn("base_string="+base_string);
throw new RuntimeException("Failed OAuth validation");
}
return contentItem;
}

/**
* An LTI 2.0 ContentItemSelectionRequest launch
*
Expand Down Expand Up @@ -1020,7 +1079,7 @@ public static String[] postContentItemSelectionRequest(Long toolKey, Map<String,
setProperty(ltiProps, BasicLTIUtil.BASICLTI_SUBMIT, getRB(rb, "launch.button", "Press to Launch External Tool"));
setProperty(ltiProps, BasicLTIConstants.LTI_MESSAGE_TYPE, LTI2Messages.CONTENT_ITEM_SELECTION_REQUEST);

setProperty(ltiProps, BasicLTIConstants.ACCEPT_MEDIA_TYPES, "application/vnd.ims.lti.v1.ltilink");
setProperty(ltiProps, ContentItem.ACCEPT_MEDIA_TYPES, ContentItem.MEDIA_LTILINK);
setProperty(ltiProps, BasicLTIConstants.ACCEPT_PRESENTATION_DOCUMENT_TARGETS, "iframe,window"); // Nice to add overlay
setProperty(ltiProps, BasicLTIConstants.ACCEPT_UNSIGNED, "false");
setProperty(ltiProps, BasicLTIConstants.ACCEPT_MULTIPLE, "false");
Expand Down Expand Up @@ -1070,14 +1129,14 @@ public static String[] postContentItemSelectionRequest(Long toolKey, Map<String,
setProperty(ltiProps, BasicLTIConstants.CONTENT_ITEM_RETURN_URL, contentReturn);

// This must always be there
String context = (String) tool.get(LTIService.LTI_SITE_ID);
Site site = null;
try {
site = SiteService.getSite(context);
} catch (Exception e) {
dPrint("No site/page associated with Launch context="+context);
return postError("<p>" + getRB(rb, "error.site.missing" ,"Cannot load site.")+context+"</p>" );
}
String context = (String) tool.get(LTIService.LTI_SITE_ID);
Site site = null;
try {
site = SiteService.getSite(context);
} catch (Exception e) {
dPrint("No site/page associated with Launch context="+context);
return postError("<p>" + getRB(rb, "error.site.missing" ,"Cannot load site.")+context+"</p>" );
}

Properties lti2subst = new Properties();

Expand Down
2 changes: 1 addition & 1 deletion basiclti/basiclti-tool/src/bundle/ltitool.properties
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ contentitem.detail=This tool may be able configure itself. If you have problems
error.contentitem.missing.returnurl=Missing returnUrl on ContentItemResponse
error.contentitem.missing.data=Missing data= parameter on ContentItemResponse
error.contentitem.bad.json=Error in JSON returned in ContentItemResponse
error.contentitem.bad=Error in returned ContentItemResponse
error.contentitem.missing=Missing tool key in ContentItemResponse
error.contentitem.incorrect=Incorrect tool key in ContentItemResponse
error.contentitem.no.validate=OAuth validation error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,7 @@
import org.sakaiproject.component.api.ServerConfigurationService;
import org.sakaiproject.component.cover.ComponentManager;
import org.sakaiproject.tool.api.Session;
import org.sakaiproject.user.api.User;
import org.sakaiproject.user.cover.UserDirectoryService;
import org.sakaiproject.event.api.SessionState;
import org.sakaiproject.event.cover.UsageSessionService;
import org.sakaiproject.lti.api.LTIService;
import org.sakaiproject.lti2.SakaiLTI2Config;
import org.sakaiproject.portal.util.PortalUtils;
Expand All @@ -78,14 +75,6 @@
// import org.sakaiproject.lti.impl.DBLTIService; // HACK
import org.sakaiproject.util.foorm.SakaiFoorm;

import net.oauth.OAuthAccessor;
import net.oauth.OAuthConsumer;
import net.oauth.OAuthMessage;
import net.oauth.OAuthValidator;
import net.oauth.SimpleOAuthValidator;
import net.oauth.server.OAuthServlet;
import net.oauth.signature.OAuthSignatureMethod;

/**
* <p>
* LTIAdminTool is a Simple Velocity-based Tool
Expand Down Expand Up @@ -1456,52 +1445,28 @@ public void doContentItemPut(RunData data, Context context)
String peid = ((JetspeedRunData) data).getJs_peid();
SessionState state = ((JetspeedRunData) data).getPortletSessionState(peid);

String lti_log = data.getParameters().getString("lti_log");
String lti_errorlog = data.getParameters().getString("lti_errorlog");
if ( lti_log != null ) M_log.debug(lti_log);
if ( lti_errorlog != null ) M_log.warn(lti_errorlog);
// Check for a returned error message from LTI
String lti_errormsg = data.getParameters().getString("lti_errormsg");
if ( lti_errormsg != null ) {
addAlert(state,lti_errormsg);
switchPanel(state, "Error");
return;
}

// Check for a returned "note" from LTI
String lti_msg = data.getParameters().getString("lti_msg");
String lti_errormsg = data.getParameters().getString("lti_errormsg");
if ( lti_errormsg != null ) addAlert(state,lti_errormsg);
if ( lti_msg != null ) state.setAttribute(STATE_SUCCESS,rb.getString("success.deleted"));


// Sanity check our returnUrl
String returnUrl = data.getParameters().getString("returnUrl");
if ( returnUrl == null ) {
addAlert(state,rb.getString("error.contentitem.missing.returnurl"));
switchPanel(state, "Error");
return;
}

String returnedData = data.getParameters().getString(BasicLTIConstants.DATA);
if ( returnedData == null || returnedData.length() < 1 ) {
addAlert(state,rb.getString("error.contentitem.missing.data"));
switchPanel(state, "Error");
return;
}

String contentItems = data.getParameters().getString("content_items");
if ( returnedData == null || returnedData.length() < 1 ) {
addAlert(state,rb.getString("error.contentitem.missing.data"));
switchPanel(state, "Error");
return;
}

JSONObject returnedJSON = null;
ContentItem contentItem = null;
try {
returnedJSON = (JSONObject) JSONValue.parse(returnedData);
contentItem = new ContentItem(contentItems);
} catch(Exception e) {
addAlert(state,rb.getString("error.contentitem.bad.json")+" ("+e.getMessage()+")");
switchPanel(state, "Error");
return;
}

Placement placement = toolManager.getCurrentPlacement();
String siteId = placement.getContext();

User user = UserDirectoryService.getCurrentUser();
// Retrieve the tool associated with the content item
Long toolKey = foorm.getLongNull(data.getParameters().getString(LTIService.LTI_TOOL_ID));
if ( toolKey == 0 || toolKey < 0 ) {
addAlert(state,rb.getString("error.contentitem.missing"));
Expand All @@ -1514,70 +1479,43 @@ public void doContentItemPut(RunData data, Context context)
switchPanel(state, "Error");
return;
}
String toolSiteId = (String) tool.get(LTIService.LTI_SITE_ID);
if ( toolSiteId != null && ! toolSiteId.equals(siteId) ) {
addAlert(state,rb.getString("error.contentitem.incorrect"));
switchPanel(state, "Error");
return;
}

// Sneak across abstraction boundaries to so OAuth
HttpServletRequest req = ToolUtils.getRequestFromThreadLocal();

String oauth_consumer_key = req.getParameter("oauth_consumer_key");
String oauth_secret = (String) tool.get(LTIService.LTI_SECRET);
oauth_secret = SakaiBLTIUtil.decryptSecret(oauth_secret);

String URL = SakaiBLTIUtil.getOurServletPath(req);
OAuthMessage oam = OAuthServlet.getMessage(req, URL);
OAuthValidator oav = new SimpleOAuthValidator();
OAuthConsumer cons = new OAuthConsumer("about:blank#OAuth+CallBack+NotUsed", oauth_consumer_key,oauth_secret, null);

OAuthAccessor acc = new OAuthAccessor(cons);
String base_string = null;
try {
base_string = OAuthSignatureMethod.getBaseString(oam);
} catch (Exception e) {
M_log.error(e.getLocalizedMessage(), e);
base_string = null;
}

// Parse and validate the incoming ContentItem
ContentItem contentItem = null;
try {
oav.validateMessage(oam, acc);
} catch (Exception e) {
M_log.warn(e.getLocalizedMessage(), e);
M_log.warn("Provider failed to validate message");
if ( base_string != null ) M_log.warn("base_string="+base_string);
addAlert(state,rb.getString("error.contentitem.no.validate"));
contentItem = SakaiBLTIUtil.getContentItemFromRequest(tool);
} catch(Exception e) {
addAlert(state,rb.getString("error.contentitem.bad")+" ("+e.getMessage()+")");
switchPanel(state, "Error");
return;
}

// Parse the returned information
/* {
"@context": "http:\/\/purl.imsglobal.org\/ctx\/lti\/v1\/ContentItem",
"@graph": [ {
"@type": "LtiLink",
"@id": ":item2",
"text": "The mascot for the Sakai Project",
"title": "The fearsome mascot of the Sakai Project",
"url": "http:\/\/localhost:8888\/sakai-api-test\/tool.php?sakai=98765",
"icon": {
"@id": "fa-bullseye",
"width": 50,
"height": 50
}
} ]
} */
// Example of how to pull back the data Properties we passed in above
// Properties dataProps = contentItem.getDataProperties();
// System.out.println("dataProps="+dataProps);
// dataProps={remember=always bring a towel}

// Extract the content item data
JSONObject item = contentItem.getItemOfType("LtiLink");
JSONObject item = contentItem.getItemOfType(ContentItem.TYPE_LTILINK);
if ( item == null ) {
addAlert(state,rb.getString("error.contentitem.no.ltilink"));
switchPanel(state, "Error");
return;
}

// Parse the returned information to insert a Content Item
/* {
"@type": "LtiLink",
"@id": ":item2",
"text": "The mascot for the Sakai Project",
"title": "The fearsome mascot of the Sakai Project",
"url": "http:\/\/localhost:8888\/sakai-api-test\/tool.php?sakai=98765",
"icon": {
"@id": "fa-bullseye",
"width": 50,
"height": 50
}
} */
String title = getString(item,"title");
String text = getString(item,"text");
String url = getString(item,"url");
Expand All @@ -1587,10 +1525,10 @@ public void doContentItemPut(RunData data, Context context)
String icon = getString(iconObject, LTI2Constants.JSONLD_ID);
if ( ! icon.startsWith("fa-") ) icon = null;

// Pass off the data to the next phase
// Prepare data for the next phase
state.removeAttribute(STATE_POST);
Properties reqProps = new Properties();
reqProps.setProperty(LTIService.LTI_CONTENTITEM, contentItems);
reqProps.setProperty(LTIService.LTI_CONTENTITEM, contentItem.toString());
reqProps.setProperty("returnUrl", returnUrl);
reqProps.setProperty("tool_id", toolKey+"");
if ( url != null ) reqProps.setProperty("launch", url);
Expand All @@ -1609,10 +1547,9 @@ public void doContentItemPut(RunData data, Context context)
return;
}

// Time to store our content item
// Time to store our content item and redirect back to our helpee
M_log.debug("Content Item complete toolKey="+toolKey);
doContentPutInternal(data, context, reqProps);

}

public String buildRedirectPanelContext(VelocityPortlet portlet, Context context,
Expand Down Expand Up @@ -1691,22 +1628,31 @@ public String buildContentConfigPanelContext(VelocityPortlet portlet, Context co
return "lti_error";
}

// Reach across abstraction boundaries to grab the CSRF for later
Session session = SessionManager.getCurrentSession();
Object csrfToken = session.getAttribute(UsageSessionService.SAKAI_CSRF_SESSION_ATTRIBUTE);

// Create a POSTable URL back to this application with the right parameters
// Since the external tool will be setting all the POST data we need to
// include GET data for things that we might normally have sent as "hidden" data
Placement placement = toolManager.getCurrentPlacement();
String contentReturn = serverConfigurationService.getToolUrl() + "/" + placement.getId() +
"/sakai.basiclti.admin.helper.helper" +
"?eventSubmit_doContentItemPut=Save" +
"&returnUrl=" + URLEncoder.encode(returnUrl) +
"&panel=PostContentItem" +
"&sakai_csrf_token=" +URLEncoder.encode(csrfToken.toString()) +
"&tool_id=" + tool.get(LTIService.LTI_ID);

String contentConfig = ltiService.getToolLaunch(tool, placement.getContext());
contentConfig = contentConfig + "?contentReturn=" + URLEncoder.encode(contentReturn);
contentConfig = contentConfig + "&tool_id=" + tool.get(LTIService.LTI_ID);
// Add CSRF protection so it actually makes it into the "do" code
contentReturn = SakaiBLTIUtil.addCSRFToken(contentReturn);

// /acccess/blti/context/tool:12 (does not have a querystring)
String contentLaunch = ltiService.getToolLaunch(tool, placement.getContext());

// Can set ContentItemSelection launch values or put in our own data items
// which will come back later. Be mindful of GET length limitations enroute
// to the access servlet.
Properties contentData = new Properties();
contentData.setProperty(ContentItem.ACCEPT_MEDIA_TYPES, ContentItem.MEDIA_LTILINK);
contentData.setProperty("remember", "always bring a towel"); // An example

contentLaunch = ContentItem.buildLaunch(contentLaunch , contentReturn, contentData);

Object previousData = null;
if ( content != null ) {
Expand Down Expand Up @@ -1736,7 +1682,7 @@ public String buildContentConfigPanelContext(VelocityPortlet portlet, Context co
context.put("doAction", BUTTON + "doContentPut");
if ( ! returnUrl.startsWith("about:blank") ) context.put("cancelUrl", returnUrl);
context.put("returnUrl", returnUrl);
if ( allowContentItem > 0 ) context.put("contentConfig", contentConfig);
if ( allowContentItem > 0 ) context.put("contentLaunch", contentLaunch);
context.put(LTIService.LTI_TOOL_ID,toolKey);
context.put("tool_description", tool.get(LTIService.LTI_DESCRIPTION));
context.put("tool_title", tool.get(LTIService.LTI_TITLE));
Expand Down
Loading

0 comments on commit 810ac7d

Please sign in to comment.