/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/basiclti/trunk/web-ifp/src/java/org/sakaiproject/portlets/SakaiIFrame.java $ * $Id: SakaiIFrame.java 132825 2013-12-19 21:22:20Z csev@umich.edu $ *********************************************************************************** * * Copyright (c) 2005, 2006 The Sakai Foundation. * * Licensed under the Educational Community License, Version 1.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/ecl1.php * * 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.portlets; import java.lang.Integer; import java.io.PrintWriter; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.ResourceBundle; import java.util.List; import java.util.Enumeration; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import javax.portlet.GenericPortlet; import javax.portlet.RenderRequest; import javax.portlet.ActionRequest; import javax.portlet.ActionResponse; import javax.portlet.RenderResponse; import javax.portlet.PortletRequest; import javax.portlet.PortletException; import javax.portlet.PortletURL; import javax.portlet.PortletPreferences; import javax.portlet.PortletContext; import javax.portlet.PortletConfig; import javax.portlet.WindowState; import javax.portlet.PortletMode; import javax.portlet.PortletSession; import javax.portlet.ReadOnlyException; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.portlet.util.VelocityHelper; import org.sakaiproject.portlet.util.JSPHelper; import org.sakaiproject.util.FormattedText; import org.sakaiproject.util.ResourceLoader; import org.sakaiproject.tool.api.Session; import org.sakaiproject.tool.cover.SessionManager; import org.sakaiproject.site.api.Site; import org.sakaiproject.site.api.SitePage; import org.sakaiproject.site.api.ToolConfiguration; import org.sakaiproject.site.cover.SiteService; import org.sakaiproject.component.cover.ServerConfigurationService; import org.sakaiproject.tool.api.Placement; import org.sakaiproject.tool.cover.ToolManager; import org.sakaiproject.tool.api.ToolSession; import javax.servlet.ServletRequest; import org.sakaiproject.thread_local.cover.ThreadLocalManager; // Velocity import org.apache.velocity.VelocityContext; import org.apache.velocity.context.Context; import org.apache.velocity.app.VelocityEngine; import org.sakaiproject.component.cover.ComponentManager; // lti service import org.sakaiproject.lti.api.LTIService; /** * a simple SakaiIFrame Portlet */ public class SakaiIFrame extends GenericPortlet { private static final Log M_log = LogFactory.getLog(SakaiIFrame.class); private LTIService m_ltiService = (LTIService) ComponentManager.get("org.sakaiproject.lti.api.LTIService"); // This is old-style internationalization (i.e. not dynamic based // on user preference) to do that would make this depend on // Sakai Unique APIs. :( // private static ResourceBundle rb = ResourceBundle.getBundle("iframe"); protected static ResourceLoader rb = new ResourceLoader("iframe"); protected final FormattedText validator = new FormattedText(); private final VelocityHelper vHelper = new VelocityHelper(); VelocityEngine vengine = null; private PortletContext pContext; // TODO: Perhaps these constancts should come from portlet.xml /** The source URL, in state, config and context. */ protected final static String SOURCE = "source"; /** The value in state and context for the source URL to actually used, as computed from special and URL. */ protected final static String URL = "url"; /** The height, in state, config and context. */ protected final static String HEIGHT = "height"; /** The custom height from user input * */ protected final static String CUSTOM_HEIGHT = "customNumberField"; protected final String POPUP = "popup"; protected final String MAXIMIZE = "sakai:maximize"; protected final static String TITLE = "title"; private static final String FORM_PAGE_TITLE = "title-of-page"; private static final String FORM_TOOL_TITLE = "title-of-tool"; private static final int MAX_TITLE_LENGTH = 99; private static String ALERT_MESSAGE = "sakai:alert-message"; public final static String CURRENT_HTTP_REQUEST = "org.sakaiproject.util.RequestFilter.http_request"; // If the property is final, the property wins. If it is not final, // the portlet preferences take precedence. public String getTitleString(RenderRequest request) { Placement placement = ToolManager.getCurrentPlacement(); return placement.getTitle(); } public void init(PortletConfig config) throws PortletException { super.init(config); pContext = config.getPortletContext(); try { vengine = vHelper.makeEngine(pContext); } catch(Exception e) { throw new PortletException("Cannot initialize Velocity ", e); } M_log.info("iFrame Portlet vengine="+vengine+" rb="+rb); } private void addAlert(ActionRequest request,String message) { PortletSession pSession = request.getPortletSession(true); pSession.setAttribute(ALERT_MESSAGE, message); } private void sendAlert(RenderRequest request, Context context) { PortletSession pSession = request.getPortletSession(true); String str = (String) pSession.getAttribute(ALERT_MESSAGE); pSession.removeAttribute(ALERT_MESSAGE); if ( str != null && str.length() > 0 ) context.put("alertMessage", validator.escapeHtml(str, false)); } // Render the portlet - this is not supposed to change the state of the portlet // Render may be called many times so if it changes the state - that is tacky // Render will be called when someone presses "refresh" or when another portlet // onthe same page is handed an Action. public void doView(RenderRequest request, RenderResponse response) throws PortletException, IOException { response.setContentType("text/html"); // System.out.println("==== doView called ===="); // Grab that underlying request to get a GET parameter ServletRequest req = (ServletRequest) ThreadLocalManager.get(CURRENT_HTTP_REQUEST); String popupDone = req.getParameter("sakai.popup"); PrintWriter out = response.getWriter(); Placement placement = ToolManager.getCurrentPlacement(); response.setTitle(placement.getTitle()); String source = placement.getPlacementConfig().getProperty(SOURCE); if ( source == null ) source = ""; String height = placement.getPlacementConfig().getProperty(HEIGHT); if ( height == null ) height = "1200px"; boolean maximize = "true".equals(placement.getPlacementConfig().getProperty(MAXIMIZE)); boolean popup = false; // Comes from content item boolean oldPopup = "true".equals(placement.getPlacementConfig().getProperty(POPUP)); // Retrieve the corresponding content item and tool to check the launch Map<String, Object> content = null; Map<String, Object> tool = null; Long key = getContentIdFromSource(source); if ( key < 1 ) { out.println(rb.getString("get.info.notconfig")); M_log.warn("Cannot find content id placement="+placement.getId()+" source="+source); return; } try { content = m_ltiService.getContent(key); // If we are supposed ot popup (per the content), do so and optionally // copy the calue into the placement to communicate with the portal popup = getLongNull(content.get("newpage")) == 1; if ( oldPopup != popup ) { placement.getPlacementConfig().setProperty(POPUP, popup ? "true" : "false"); placement.save(); } String launch = (String) content.get("launch"); Long tool_id = getLongNull(content.get("tool_id")); if ( launch == null && tool_id != null ) { tool = m_ltiService.getTool(tool_id); launch = (String) tool.get("launch"); } // Force http:// to pop-up if we are https:// String serverUrl = ServerConfigurationService.getServerUrl(); if ( request.isSecure() || ( serverUrl != null && serverUrl.startsWith("https://") ) ) { if ( launch != null && launch.startsWith("http://") ) popup = true; } } catch (Exception e) { out.println(rb.getString("get.info.notconfig")); e.printStackTrace(); return; } if ( source != null && source.trim().length() > 0 ) { Context context = new VelocityContext(); context.put("tlang", rb); context.put("validator", validator); context.put("source",source); context.put("height",height); sendAlert(request,context); context.put("popupdone", Boolean.valueOf(popupDone != null)); context.put("popup", Boolean.valueOf(popup)); context.put("maximize", Boolean.valueOf(maximize)); vHelper.doTemplate(vengine, "/vm/main.vm", context, out); } else { out.println(rb.getString("get.info.notconfig")); } // System.out.println("==== doView complete ===="); } public void doEdit(RenderRequest request, RenderResponse response) throws PortletException, IOException { response.setContentType("text/html"); PrintWriter out = response.getWriter(); String title = getTitleString(request); if ( title != null ) response.setTitle(title); Context context = new VelocityContext(); context.put("tlang", rb); context.put("validator", validator); sendAlert(request,context); PortletURL url = response.createActionURL(); context.put("actionUrl", url.toString()); context.put("doCancel", "sakai.cancel"); context.put("doUpdate", "sakai.update"); // get current site Placement placement = ToolManager.getCurrentPlacement(); String siteId = ""; // find the right LTIContent object for this page String source = placement.getPlacementConfig().getProperty(SOURCE); Long key = getContentIdFromSource(source); if ( key < 1 ) { out.println(rb.getString("get.info.notconfig")); M_log.warn("Cannot find content id placement="+placement.getId()+" source="+source); return; } Map<String, Object> content = m_ltiService.getContent(key); if ( content == null ) { out.println(rb.getString("get.info.notconfig")); M_log.warn("Cannot find content item placement="+placement.getId()+" key="+key); return; } // attach the ltiToolId to each model attribute, so that we could have the tool configuration page for multiple tools String foundLtiToolId = content.get(m_ltiService.LTI_TOOL_ID).toString(); Map<String, Object> tool = m_ltiService.getTool(Long.valueOf(foundLtiToolId)); if ( tool == null ) { out.println(rb.getString("get.info.notconfig")); M_log.warn("Cannot find tool placement="+placement.getId()+" key="+foundLtiToolId); return; } String[] contentToolModel=m_ltiService.getContentModel(Long.valueOf(foundLtiToolId)); String formInput=m_ltiService.formInput(content, contentToolModel); context.put("formInput", formInput); vHelper.doTemplate(vengine, "/vm/edit.vm", context, out); } public void doHelp(RenderRequest request, RenderResponse response) throws PortletException, IOException { // System.out.println("==== doHelp called ===="); // sendToJSP(request, response, "/help.jsp"); JSPHelper.sendToJSP(pContext, request, response, "/help.jsp"); // System.out.println("==== doHelp done ===="); } // Process action is called for action URLs / form posts, etc // Process action is called once for each click - doView may be called many times // Hence an obsession in process action with putting things in session to // Send to the render process. public void processAction(ActionRequest request, ActionResponse response) throws PortletException, IOException { // System.out.println("==== processAction called ===="); PortletSession pSession = request.getPortletSession(true); // Our first challenge is to figure out which action we want to take // The view selects the "next action" either as a URL parameter // or as a hidden field in the POST data - we check both String doCancel = request.getParameter("sakai.cancel"); String doUpdate = request.getParameter("sakai.update"); // Our next challenge is to pick which action the previous view // has told us to do. Note that the view may place several actions // on the screen and the user may have an option to pick between // them. Make sure we handle the "no action" fall-through. pSession.removeAttribute("error.message"); if ( doCancel != null ) { response.setPortletMode(PortletMode.VIEW); } else if ( doUpdate != null ) { processActionEdit(request, response); } else { // System.out.println("Unknown action"); response.setPortletMode(PortletMode.VIEW); } // System.out.println("==== End of ProcessAction ===="); } public void processActionEdit(ActionRequest request, ActionResponse response) throws PortletException, IOException { // TODO: Check Role // Stay in EDIT mode unless we are successful response.setPortletMode(PortletMode.EDIT); Placement placement = ToolManager.getCurrentPlacement(); // get the site toolConfiguration, if this is part of a site. ToolConfiguration toolConfig = SiteService.findTool(placement.getId()); String id = request.getParameter(LTIService.LTI_ID); String toolId = request.getParameter(LTIService.LTI_TOOL_ID); Properties reqProps = new Properties(); Enumeration names = request.getParameterNames(); while (names.hasMoreElements()) { String name = (String) names.nextElement(); reqProps.setProperty(name, request.getParameter(name)); } Object retval = m_ltiService.updateContent(Long.parseLong(id), reqProps); placement.save(); response.setPortletMode(PortletMode.VIEW); } /** Valid digits for custom height from user input **/ protected static final String VALID_DIGITS = "0123456789"; /* Parse the source URL to extract the content identifier */ private Long getContentIdFromSource(String source) { int pos = source.indexOf("/content:"); if ( pos < 1 ) return null; pos = pos + "/content:".length(); if ( pos < source.length()-1 ) { String sContentId = source.substring(pos); try { Long key = new Long(sContentId); return key; } catch (Exception e) { return null; } } return null; } /** * Check if the string from user input contains any characters other than digits * * @param height * String from user input * @return True if all are digits. Or False if any is not digit. */ private boolean checkDigits(String height) { for (int i = 0; i < height.length(); i++) { if (VALID_DIGITS.indexOf(height.charAt(i)) == -1) return false; } return true; } private Long getLongNull(Object key) { if (key == null) return null; if (key instanceof Number) return new Long(((Number) key).longValue()); if (key instanceof String) { try { return new Long((String) key); } catch (Exception e) { return null; } } return null; } }