/**********************************************************************************
* $URL: https://source.sakaiproject.org/svn/web/trunk/web-portlet/src/java/org/sakaiproject/portlets/PortletIFrame.java $
* $Id: PortletIFrame.java 132825 2013-12-19 21:22:20Z csev@umich.edu $
***********************************************************************************
*
* Copyright (c) 2005-2013 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.io.File;
import java.net.URL;
import java.net.URI;
import java.net.URLEncoder;
import java.net.HttpURLConnection;
import java.util.Map;
import java.util.Set;
import java.util.Properties;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import java.util.ResourceBundle;
import java.util.Locale;
import java.util.Date;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.httpclient.util.URIUtil;
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.event.api.UsageSession;
import org.sakaiproject.event.cover.UsageSessionService;
import org.sakaiproject.component.cover.ServerConfigurationService;
import org.sakaiproject.component.cover.ComponentManager;
import org.sakaiproject.event.api.EventTrackingService;
import javax.servlet.ServletRequest;
import org.sakaiproject.thread_local.cover.ThreadLocalManager;
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.tool.api.Placement;
import org.sakaiproject.tool.cover.ToolManager;
import org.sakaiproject.tool.api.ToolSession;
import org.sakaiproject.user.cover.UserDirectoryService;
import org.sakaiproject.user.api.User;
import org.sakaiproject.user.api.UserNotDefinedException;
import org.sakaiproject.exception.IdUnusedException;
import org.sakaiproject.authz.api.AuthzGroup;
import org.sakaiproject.authz.api.GroupNotDefinedException;
import org.sakaiproject.entity.api.Reference;
import org.sakaiproject.entity.cover.EntityManager;
import org.sakaiproject.authz.api.AuthzGroup;
import org.sakaiproject.authz.api.GroupNotDefinedException;
import org.sakaiproject.authz.api.Role;
import org.sakaiproject.authz.cover.AuthzGroupService;
import org.apache.commons.validator.routines.UrlValidator;
// Velocity
import org.apache.velocity.VelocityContext;
import org.apache.velocity.context.Context;
import org.apache.velocity.app.VelocityEngine;
/**
* a simple PortletIFrame Portlet
*/
public class PortletIFrame extends GenericPortlet {
private static final Log M_log = LogFactory.getLog(PortletIFrame.class);
// 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 config and context. */
protected final static String SOURCE = "source";
/** The value in context for the source URL to actually used, as computed from special and URL. */
protected final static String URL = "url";
/** The height, in 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 = "maximize";
protected final static String TITLE = "title";
private static final String FORM_PAGE_TITLE = "title-of-page";
private static final int MAX_TITLE_LENGTH = 99;
private static final int MAX_SITE_INFO_URL_LENGTH = 255;
private static String ALERT_MESSAGE = "sakai:alert-message";
/** The Annotated URL Tool's url attribute, in config and context. */
protected final static String TARGETPAGE_URL = "TargetPageUrl";
/** The Annotated URL Tool's name attribute, in config and context. */
protected final static String TARGETPAGE_NAME = "TargetPageName";
/** The Annotated URL Tool's text attribute, in config and context. */
protected final static String ANNOTATED_TEXT = "desp";
/** The special attribute in config and context. */
protected final static String SPECIAL = "special";
/** Special value for site. */
protected final static String SPECIAL_SITE = "site";
/** Special value for Annotated URL Tool. */
protected final static String SPECIAL_ANNOTATEDURL = "annotatedurl";
/** Special value for myworkspace. */
protected final static String SPECIAL_WORKSPACE = "workspace";
/** Special value for worksite. */
protected final static String SPECIAL_WORKSITE = "worksite";
/** Support an external url defined in sakai.properties, in config and context. */
protected final static String SAKAI_PROPERTIES_URL_KEY = "sakai.properties.url.key";
/** If set, always hide the OPTIONS button */
protected final static String HIDE_OPTIONS = "hide.options";
private final static String PASS_PID = "passthroughPID";
/**
* Expand macros to insert session information into the URL?
*/
private final static String MACRO_EXPANSION = "expandMacros";
/** Macro name: Site id (GUID) */
protected static final String MACRO_SITE_ID = "${SITE_ID}";
/** Macro name: User id */
protected static final String MACRO_USER_ID = "${USER_ID}";
/** Macro name: User enterprise id */
protected static final String MACRO_USER_EID = "${USER_EID}";
/** Macro name: First name */
protected static final String MACRO_USER_FIRST_NAME = "${USER_FIRST_NAME}";
/** Macro name: Last name */
protected static final String MACRO_USER_LAST_NAME = "${USER_LAST_NAME}";
/** Macro name: Role */
protected static final String MACRO_USER_ROLE = "${USER_ROLE}";
/** Macro name: Session */
protected static final String MACRO_SESSION_ID = "${SESSION_ID}";
private static final String MACRO_CLASS_SITE_PROP = "SITE_PROP:";
private static final String IFRAME_ALLOWED_MACROS_PROPERTY = "iframe.allowed.macros";
private static final String MACRO_DEFAULT_ALLOWED = "${USER_ID},${USER_EID},${USER_FIRST_NAME},${USER_LAST_NAME},${SITE_ID},${USER_ROLE}";
// Default is six hours
private static final String IFRAME_XFRAME_CACHETIME = "iframe.xframe.cachetime";
private static final int IFRAME_XFRAME_CACHETIME_DEFAULT = 3600*1000*6;
private static final String XFRAME_LAST_TIME = "xframe-last-time";
private static final String XFRAME_LAST_STATUS = "xframe-last-status";
private static final String IFRAME_XFRAME_LOADTIME = "iframe.xframe.loadtime";
private static final int IFRAME_XFRAME_LOADTIME_DEFAULT = 8000;
private static long xframeCache = IFRAME_XFRAME_CACHETIME_DEFAULT;
private static long xframeLoad = IFRAME_XFRAME_LOADTIME_DEFAULT;
// Regular expressions
private static final String IFRAME_XFRAME_POPUP = "iframe.xframe.popup";
private static final String IFRAME_XFRAME_INLINE = "iframe.xframe.inline";
public final static String CURRENT_HTTP_REQUEST = "org.sakaiproject.util.RequestFilter.http_request";
private static ArrayList allowedMacrosList;
static
{
xframeCache = IFRAME_XFRAME_CACHETIME_DEFAULT;
String xframeCacheS =
ServerConfigurationService.getString(IFRAME_XFRAME_CACHETIME, null);
try {
if ( xframeCacheS != null ) xframeCache = Long.parseLong(xframeCacheS);
} catch (NumberFormatException nfe) {
xframeCache = IFRAME_XFRAME_CACHETIME_DEFAULT;
}
xframeLoad = IFRAME_XFRAME_LOADTIME_DEFAULT;
String xframeLoadS =
ServerConfigurationService.getString(IFRAME_XFRAME_LOADTIME, null);
try {
if ( xframeLoadS != null ) xframeLoad = Long.parseLong(xframeLoadS);
} catch (NumberFormatException nfe) {
xframeLoad = IFRAME_XFRAME_LOADTIME_DEFAULT;
}
allowedMacrosList = new ArrayList();
final String allowedMacros =
ServerConfigurationService.getString(IFRAME_ALLOWED_MACROS_PROPERTY, MACRO_DEFAULT_ALLOWED);
String parts[] = allowedMacros.split(",");
if(parts != null) {
for(int i = 0; i < parts.length; i++) {
allowedMacrosList.add(parts[i]);
}
}
}
/** Choices of pixels displayed in the customization page */
public String[] ourPixels = { "300px", "450px", "600px", "750px", "900px", "1200px", "1800px", "2400px" };
// 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();
Context context = new VelocityContext();
Placement placement = ToolManager.getCurrentPlacement();
Properties config = getAllProperties(placement);
response.setTitle(placement.getTitle());
String source = config.getProperty(SOURCE);
if ( source == null ) source = "";
String height = config.getProperty(HEIGHT);
if ( height == null ) height = "1200px";
String sakaiPropertiesUrlKey = config.getProperty(SAKAI_PROPERTIES_URL_KEY);
String hideOptions = config.getProperty(HIDE_OPTIONS);
String special = getSpecial(config);
// Handle the situation where we are displaying the worksite information
if ( SPECIAL_WORKSITE.equals(special) ) {
try
{
// If the site does not have an info url, we show description or title
Site s = SiteService.getSite(placement.getContext());
String rv = StringUtils.trimToNull(s.getInfoUrlFull());
if (rv == null)
{
String siteInfo = StringUtils.trimToNull(s.getDescription());
if ( siteInfo == null ) {
siteInfo = StringUtils.trimToNull(s.getTitle());
}
StringBuilder alertMsg = new StringBuilder();
if ( siteInfo != null ) siteInfo = validator.processFormattedText(siteInfo, alertMsg);
context.put("siteInfo", siteInfo);
vHelper.doTemplate(vengine, "/vm/info.vm", context, out);
return;
}
}
catch (Exception any)
{
any.printStackTrace();
}
}
boolean popup = "true".equals(placement.getPlacementConfig().getProperty(POPUP));
boolean maximize = "true".equals(placement.getPlacementConfig().getProperty(MAXIMIZE));
// set the pass_pid parameter
String passPidStr = config.getProperty(PASS_PID, "false");
boolean passPid = "true".equalsIgnoreCase(passPidStr);
// Set the macro expansion
String macroExpansionStr = config.getProperty(MACRO_EXPANSION, "true");
boolean macroExpansion = ! ( "false".equalsIgnoreCase(macroExpansionStr));
// Compute the URL
String url = sourceUrl(special, source, placement.getContext(), macroExpansion, passPid, placement.getId(), sakaiPropertiesUrlKey);
//System.out.println("special="+special+" source="+source+" pgc="+placement.getContext()+" macroExpansion="+macroExpansion+" passPid="+passPid+" PGID="+placement.getId()+" sakaiPropertiesUrlKey="+sakaiPropertiesUrlKey+" url="+url);
if ( url != null && url.trim().length() > 0 ) {
url = sanitizeHrefURL(url);
if ( url == null || ! validateURL(url) ) {
M_log.warn("invalid URL suppressed placement="+placement.getId()+" site="+placement.getContext()+" url="+url);
url = "about:blank";
}
// Check if the site sets X-Frame options
popup = popup || popupXFrame(request, placement, url);
Session session = SessionManager.getCurrentSession();
String csrfToken = (String) session.getAttribute(UsageSessionService.SAKAI_CSRF_SESSION_ATTRIBUTE);
if ( csrfToken != null ) context.put("sakai_csrf_token", csrfToken);
context.put("tlang", rb);
context.put("validator", validator);
// http://stackoverflow.com/questions/573184/java-convert-string-to-valid-uri-object
context.put("source",URIUtil.encodeQuery(url));
context.put("height",height);
sendAlert(request,context);
context.put("popup", Boolean.valueOf(popup));
context.put("popupdone", Boolean.valueOf(popupDone != null));
context.put("maximize", Boolean.valueOf(maximize));
context.put("placement", placement.getId().replaceAll("[^a-zA-Z0-9]","_"));
context.put("loadTime", new Long(xframeLoad));
// SAK-23566 capture the view calendar events
if (placement != null && placement.getContext() != null && placement.getId() != null) {
EventTrackingService ets = (EventTrackingService) ComponentManager.get(EventTrackingService.class);
if (ets != null) {
String eventRef = "/web/"+placement.getContext()+"/id/"+placement.getId()+"/url/"+URLEncoder.encode(url, "UTF-8");
eventRef = StringUtils.abbreviate(eventRef, 240); // ensure the ref won't pass 255 chars
ets.post(ets.newEvent("webcontent.read", eventRef, false));
}
}
// TODO: state.setAttribute(TARGETPAGE_URL,config.getProperty(TARGETPAGE_URL));
// TODO: state.setAttribute(TARGETPAGE_NAME,config.getProperty(TARGETPAGE_NAME));
vHelper.doTemplate(vengine, "/vm/main.vm", context, out);
} else {
out.println("Not yet configured");
}
// TODO: state.setAttribute(EVENT_ACCESS_WEB_CONTENT, config.getProperty(EVENT_ACCESS_WEB_CONTENT));
// TODO: state.setAttribute(EVENT_REVISE_WEB_CONTENT, config.getProperty(EVENT_REVISE_WEB_CONTENT));
// System.out.println("==== doView complete ====");
}
// Determine if we should pop up due to an X-Frame-Options : [SAMEORIGIN]
public boolean popupXFrame(RenderRequest request, Placement placement, String url)
{
if ( xframeCache < 1 ) return false;
// Only check http:// and https:// urls
if ( ! (url.startsWith("http://") || url.startsWith("https://")) ) return false;
// Check the "Always POPUP" and "Always INLINE" regular expressions
String pattern = null;
Pattern p = null;
Matcher m = null;
pattern = ServerConfigurationService.getString(IFRAME_XFRAME_POPUP, null);
if ( pattern != null && pattern.length() > 1 ) {
p = Pattern.compile(pattern);
m = p.matcher(url.toLowerCase());
if ( m.find() ) {
return true;
}
}
pattern = ServerConfigurationService.getString(IFRAME_XFRAME_INLINE, null);
if ( pattern != null && pattern.length() > 1 ) {
p = Pattern.compile(pattern);
m = p.matcher(url.toLowerCase());
if ( m.find() ) {
return false;
}
}
// Don't check Local URLs
String serverUrl = ServerConfigurationService.getServerUrl();
if ( url.startsWith(serverUrl) ) return false;
if ( url.startsWith(ServerConfigurationService.getAccessUrl()) ) return false;
// Force http:// to pop-up if we are https://
if ( request.isSecure() || ( serverUrl != null && serverUrl.startsWith("https://") ) ) {
if ( url.startsWith("http://") ) return true;
}
// Check to see if time has expired...
Date date = new Date();
long nowTime = date.getTime();
String lastTimeS = placement.getPlacementConfig().getProperty(XFRAME_LAST_TIME);
long lastTime = -1;
try {
lastTime = Long.parseLong(lastTimeS);
} catch (NumberFormatException nfe) {
lastTime = -1;
}
M_log.debug("lastTime="+lastTime+" nowTime="+nowTime);
if ( lastTime > 0 && nowTime < lastTime + xframeCache ) {
String lastXF = placement.getPlacementConfig().getProperty(XFRAME_LAST_STATUS);
M_log.debug("Status from placement="+lastXF);
return "true".equals(lastXF);
}
placement.getPlacementConfig().setProperty(XFRAME_LAST_TIME, String.valueOf(nowTime));
boolean retval = false;
try {
// note : you may also need
// HttpURLConnection.setInstanceFollowRedirects(false)
HttpURLConnection.setFollowRedirects(true);
HttpURLConnection con =
(HttpURLConnection) new URL(url).openConnection();
con.setRequestMethod("HEAD");
Map headerfields = con.getHeaderFields();
Set headers = headerfields.entrySet();
for(Iterator i = headers.iterator(); i.hasNext();) {
Map.Entry map = (Map.Entry)i.next();
String key = (String) map.getKey();
if ( key == null ) continue;
key = key.toLowerCase();
if ( ! "x-frame-options".equals(key) ) continue;
// Since the valid entries are SAMEORIGIN, DENY, or ALLOW-URI
// we can pretty much assume the answer is "not us" if the header
// is present
retval = true;
break;
}
}
catch (Exception e) {
// Fail pretty silently because this could be pretty chatty with bad urls and all
M_log.debug(e.getMessage());
retval = false;
}
placement.getPlacementConfig().setProperty(XFRAME_LAST_STATUS, String.valueOf(retval));
// Permanently set popup to true as we don't expect that a site will go back
if ( retval == true ) placement.getPlacementConfig().setProperty(POPUP, "true");
placement.save();
M_log.debug("Retrieved="+url+" XFrame="+retval);
return retval;
}
public void doEdit(RenderRequest request, RenderResponse response)
throws PortletException, IOException
{
// System.out.println("==== doEdit called ====");
response.setContentType("text/html");
PrintWriter out = response.getWriter();
String title = getTitleString(request);
if ( title != null ) response.setTitle(title);
Context context = new VelocityContext();
Session session = SessionManager.getCurrentSession();
String csrfToken = (String) session.getAttribute(UsageSessionService.SAKAI_CSRF_SESSION_ATTRIBUTE);
if ( csrfToken != null ) context.put("sakai_csrf_token", csrfToken);
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");
Placement placement = ToolManager.getCurrentPlacement();
Properties config = getAllProperties(placement);
String special = getSpecial(config);
context.put("title", validator.escapeHtml(placement.getTitle(), false));
String source = placement.getPlacementConfig().getProperty(SOURCE);
if ( source == null ) source = "";
if ( special == null ) context.put("source",source);
String height = placement.getPlacementConfig().getProperty(HEIGHT);
if ( height == null ) height = "1200px";
context.put("height",height);
ToolConfiguration toolConfig = SiteService.findTool(placement.getId());
if ( toolConfig != null )
{
try
{
Site site = SiteService.getSite(toolConfig.getSiteId());
String siteId = site.getId();
SitePage page = site.getPage(toolConfig.getPageId());
// if this is the only tool on that page, update the page's title also
if ((page.getTools() != null) && (page.getTools().size() == 1))
{
context.put("showPopup", Boolean.TRUE);
boolean popup = "true".equals(placement.getPlacementConfig().getProperty(POPUP));
context.put("popup", Boolean.valueOf(popup));
boolean maximize = "true".equals(placement.getPlacementConfig().getProperty(MAXIMIZE));
context.put("maximize", Boolean.valueOf(maximize));
context.put("pageTitleEditable", Boolean.TRUE);
context.put("page_title", validator.escapeHtml(page.getTitle(), false));
}
}
catch (Throwable e)
{
}
}
if (special == null)
{
context.put("heading", rb.getString("gen.custom"));
}
// set the heading based on special
else
{
if (SPECIAL_SITE.equals(special))
{
context.put("heading", rb.getString("gen.custom.site"));
}
else if (SPECIAL_WORKSPACE.equals(special))
{
context.put("heading", rb.getString("gen.custom.workspace"));
}
else if (SPECIAL_WORKSITE.equals(special))
{
context.put("heading", rb.getString("gen.custom.worksite"));
// for worksite, also include the Site's infourl and description
try
{
Site s = SiteService.getSite(ToolManager.getCurrentPlacement().getContext());
String siteId = s.getId();
String infoUrl = StringUtils.trimToNull(s.getInfoUrl());
if (infoUrl != null)
{
context.put("info_url", FormattedText.escapeHtmlFormattedTextarea(infoUrl));
}
String description = StringUtils.trimToNull(s.getDescription());
if (description != null)
{
description = FormattedText.escapeHtmlFormattedTextarea(description);
context.put("description", description);
}
}
catch (Throwable e)
{
}
}
else if (SPECIAL_ANNOTATEDURL.equals(special))
{
context.put("heading", rb.getString("gen.custom.annotatedurl"));
// for Annotated URL Tool page, also include the description
try
{
String desp = config.getProperty(ANNOTATED_TEXT);
context.put("description", desp);
}
catch (Throwable e)
{
}
}
else
{
context.put("heading", rb.getString("gen.custom"));
}
}
boolean selected = false;
for (int i = 0; i < ourPixels.length; i++)
{
if (height.equals(ourPixels[i]))
{
selected = true;
continue;
}
}
if (!selected)
{
String[] strings = height.trim().split("px");
context.put("custom_height", strings[0]);
height = rb.getString("gen.heisomelse");
}
context.put("height", height);
// TODO: tracking event
// output the max limit
context.put("max_length_title", MAX_TITLE_LENGTH);
context.put("max_length_info_url", MAX_SITE_INFO_URL_LENGTH);
String template = "/vm/edit.vm";
if (SPECIAL_SITE.equals(special)) template = "/vm/edit-site.vm";
if (SPECIAL_WORKSITE.equals(special)) template = "/vm/edit-site.vm";
if (SPECIAL_ANNOTATEDURL.equals(special)) template = "/vm/edit-annotatedurl.vm";
// System.out.println("EDIT TEMP="+template+" special="+special);
vHelper.doTemplate(vengine, template, context, out);
// System.out.println("==== doEdit done ====");
}
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);
// get the site toolConfiguration, if this is part of a site.
Placement placement = ToolManager.getCurrentPlacement();
ToolConfiguration toolConfig = SiteService.findTool(placement.getId());
Properties config = getAllProperties(placement);
String special = getSpecial(config);
// Get and verify the source
String source = StringUtils.trimToEmpty(request.getParameter("source"));
// If this is a normal placement we do not allow blank (i.e. not special)
if ( special == null ) {
if (StringUtils.isBlank(source))
{
addAlert(request, rb.getString("gen.url.empty"));
return;
}
}
// If we have a URL from the user, lets validate it
if ((!StringUtils.isBlank(source)) && (!validateURL(source)) ) {
addAlert(request, rb.getString("gen.url.invalid"));
return;
}
// update state
if ( source == null ) source = "";
placement.getPlacementConfig().setProperty(SOURCE, source);
// site info url
String infoUrl = StringUtils.trimToNull(request.getParameter("infourl"));
if (infoUrl != null && infoUrl.length() > MAX_SITE_INFO_URL_LENGTH)
{
addAlert(request, rb.getString("gen.info.url.toolong"));
return;
}
// If we have an infourl from the user, lets validate it
if ((!StringUtils.isBlank(infoUrl)) && (!validateURL(infoUrl)) ) {
addAlert(request, rb.getString("gen.url.invalid"));
return;
}
String height = request.getParameter(HEIGHT);
if (height.equals(rb.getString("gen.heisomelse")))
{
String customHeight = request.getParameter(CUSTOM_HEIGHT);
if ((customHeight != null) && (!customHeight.equals("")))
{
if (!checkDigits(customHeight))
{
addAlert(request,rb.getString("java.alert.pleentval"));
return;
}
height = customHeight + "px";
placement.getPlacementConfig().setProperty(HEIGHT, height);
}
else
{
addAlert(request,rb.getString("java.alert.pleentval"));
return;
}
}
else
{
placement.getPlacementConfig().setProperty(HEIGHT, height);
}
// title
String title = request.getParameter(TITLE);
if (StringUtils.isBlank(title))
{
addAlert(request,rb.getString("gen.tootit.empty"));
return;
// SAK-19515 check for LENGTH of tool title
}
else if (title.length() > MAX_TITLE_LENGTH)
{
addAlert(request,rb.getString("gen.tootit.toolong"));
return;
}
placement.setTitle(title);
try
{
Site site = SiteService.getSite(toolConfig.getSiteId());
SitePage page = site.getPage(toolConfig.getPageId());
page.setTitleCustom(true);
// for web content tool, if it is a site page tool, and the only tool on the page, update the page title / popup.
if (toolConfig != null && ! SPECIAL_WORKSITE.equals(special) && ! SPECIAL_WORKSPACE.equals(special) )
{
// if this is the only tool on that page, update the page's title also
if ((page.getTools() != null) && (page.getTools().size() == 1))
{
String newPageTitle = request.getParameter(FORM_PAGE_TITLE);
if (StringUtils.isBlank(newPageTitle))
{
addAlert(request,rb.getString("gen.pagtit.empty"));
return;
}
else if (newPageTitle.length() > MAX_TITLE_LENGTH)
{
addAlert(request,rb.getString("gen.pagtit.toolong"));
return;
}
page.setTitle(newPageTitle);
}
}
SiteService.save(site);
}
catch (Exception ignore)
{
M_log.warn("doConfigure_update: " + ignore);
}
// popup and maximize
String spop = request.getParameter("popup");
if ( ! "true".equals(spop) ) spop = "false";
placement.getPlacementConfig().setProperty(POPUP, spop);
String smax = request.getParameter("maximize");
if ( ! "true".equals(smax) ) smax = "false";
placement.getPlacementConfig().setProperty(MAXIMIZE, smax);
// Make sure we re-check X-Frame-Options
placement.getPlacementConfig().setProperty(XFRAME_LAST_STATUS, "");
placement.getPlacementConfig().setProperty(XFRAME_LAST_TIME, "-1");
placement.save();
// Handle the infoUrl
if (SPECIAL_WORKSITE.equals(special))
{
if ((infoUrl != null) && (infoUrl.length() > 0) && (!infoUrl.startsWith("/")) && (infoUrl.indexOf("://") == -1))
{
infoUrl = "http://" + infoUrl;
}
String description = StringUtils.trimToNull(request.getParameter("description"));
// update the site info
try
{
SiteService.saveSiteInfo(ToolManager.getCurrentPlacement().getContext(), description, infoUrl);
}
catch (Throwable e)
{
M_log.warn("doConfigure_update: " + e);
}
}
response.setPortletMode(PortletMode.VIEW);
}
/** Valid digits for custom height from user input **/
protected static final String VALID_DIGITS = "0123456789";
/**
* 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;
}
/**
* Get the special type of this placement, compensating for legacy patterns
*/
protected String getSpecial(Properties config)
{
String special = config.getProperty(SPECIAL);
// check for an older way the ChefWebPagePortlet took parameters, converting to our "special" values
if (special == null)
{
if ("true".equals(config.getProperty("site")))
{
special = SPECIAL_SITE;
}
else if ("true".equals(config.getProperty("workspace")))
{
special = SPECIAL_WORKSPACE;
}
else if ("true".equals(config.getProperty("worksite")))
{
special = SPECIAL_WORKSITE;
}
else if ("true".equals(config.getProperty("annotatedurl")))
{
special = SPECIAL_ANNOTATEDURL;
}
}
return special;
}
/**
* Compute the actual URL we will used, based on the configuration special and source URLs
*/
protected String sourceUrl(String special, String source, String context, boolean macroExpansion, boolean passPid, String pid, String sakaiPropertiesUrlKey)
{
String rv = StringUtils.trimToNull(source);
// if marked for "site", use the site intro from the properties
if (SPECIAL_SITE.equals(special))
{
rv = StringUtils.trimToNull(getLocalizedURL("server.info.url"));
}
// if marked for "workspace", use the "user" site info from the properties
else if (SPECIAL_WORKSPACE.equals(special))
{
rv = StringUtils.trimToNull(getLocalizedURL("myworkspace.info.url"));
}
// if marked for "worksite", use the setting from the site's definition
else if (SPECIAL_WORKSITE.equals(special))
{
// set the url to the site of this request's config'ed url
try
{
// get the site's info URL, if defined
Site s = SiteService.getSite(context);
rv = StringUtils.trimToNull(s.getInfoUrlFull());
// compute the info url for the site if it has no specific InfoUrl
if (rv == null)
{
// access will show the site description or title...
rv = ServerConfigurationService.getAccessUrl() + s.getReference();
}
}
catch (Exception any)
{
}
}
else if (sakaiPropertiesUrlKey != null && sakaiPropertiesUrlKey.length() > 1)
{
// set the url to a string defined in sakai.properties
rv = StringUtils.trimToNull(ServerConfigurationService.getString(sakaiPropertiesUrlKey));
}
// if it's not special, and we have no value yet, set it to the webcontent instruction page, as configured
if (rv == null || rv.equals("http://") || rv.equals("https://"))
{
rv = StringUtils.trimToNull(getLocalizedURL("webcontent.instructions.url"));
}
if (rv != null)
{
// accept a partial reference url (i.e. "/content/group/sakai/test.gif"), convert to full url
rv = convertReferenceUrl(rv);
// pass the PID through on the URL, IF configured to do so
if (passPid)
{
if (rv.indexOf("?") < 0)
{
rv = rv + "?";
}
else
{
rv = rv + "&";
}
rv = rv + "pid=" + pid;
}
if (macroExpansion)
{
rv = doMacroExpansion(rv);
}
}
return rv;
}
/** Construct and return localized filepath, if it exists
**/
private String getLocalizedURL(String property) {
String filename = ServerConfigurationService.getString(property);
if ( filename == null || filename.trim().length()==0 )
return filename;
else
filename = filename.trim();
int extIndex = filename.lastIndexOf(".") >= 0 ? filename.lastIndexOf(".") : filename.length()-1;
String ext = filename.substring(extIndex);
String doc = filename.substring(0,extIndex);
Locale locale = new ResourceLoader().getLocale();
if (locale != null){
// check if localized file exists for current language/locale/variant
String localizedFile = doc + "_" + locale.toString() + ext;
String filePath = getPortletConfig().getPortletContext().getRealPath(".."+localizedFile);
if ( (new File(filePath)).exists() )
return localizedFile;
// otherwise, check if localized file exists for current language
localizedFile = doc + "_" + locale.getLanguage() + ext;
filePath = getPortletConfig().getPortletContext().getRealPath(".."+localizedFile);
if ( (new File(filePath)).exists() )
return localizedFile;
}
return filename;
}
/**
* If the url is a valid reference, convert it to a URL, else return it unchanged.
*/
protected String convertReferenceUrl(String url)
{
// make a reference
Reference ref = EntityManager.newReference(url);
// if it didn't recognize this, return it unchanged
if (ref.isKnownType())
{
// return the reference's url
String refUrl = ref.getUrl();
if (refUrl != null)
{
return refUrl;
}
}
return url;
}
/**
* Get the current user id
* @throws SessionDataException
* @return User id
*/
private String getUserId() throws SessionDataException
{
Session session = SessionManager.getCurrentSession();
if (session == null)
{
throw new SessionDataException("No current user session");
}
return session.getUserId();
}
/**
* Get the current session id
* @throws SessionDataException
* @return Session id
*/
private String getSessionId() throws SessionDataException
{
Session session = SessionManager.getCurrentSession();
if (session == null)
{
throw new SessionDataException("No current user session");
}
return session.getId();
}
/**
* Get the current user eid
* @throws SessionDataException
* @return User eid
*/
private String getUserEid() throws SessionDataException
{
Session session = SessionManager.getCurrentSession();
if (session == null)
{
throw new SessionDataException("No current user session");
}
return session.getUserEid();
}
/**
* Get current User information
* @throws IdUnusedException, SessionDataException
* @return {@link User} data
* @throws UserNotDefinedException
*/
private User getUser() throws IdUnusedException, SessionDataException, UserNotDefinedException
{
return UserDirectoryService.getUser(this.getUserId());
}
/**
* Get the current site id
* @throws SessionDataException
* @return Site id (GUID)
*/
private String getSiteId() throws SessionDataException
{
Placement placement = ToolManager.getCurrentPlacement();
if (placement == null)
{
throw new SessionDataException("No current tool placement");
}
return placement.getContext();
}
/**
* Fetch the user role in the current site
* @throws IdUnusedException, SessionDataException
* @return Role
* @throws GroupNotDefinedException
*/
private String getUserRole() throws IdUnusedException, SessionDataException, GroupNotDefinedException
{
AuthzGroup group;
Role role;
group = AuthzGroupService.getAuthzGroup("/site/" + getSiteId());
if (group == null)
{
throw new SessionDataException("No current group");
}
role = group.getUserRole(this.getUserId());
if (role == null)
{
throw new SessionDataException("No current role");
}
return role.getId();
}
/**
* Get a site property by name
*
* @param name Property name
* @throws IdUnusedException, SessionDataException
* @return The property value (null if none)
*/
private String getSiteProperty(String name) throws IdUnusedException, SessionDataException
{
Site site;
site = SiteService.getSite(getSiteId());
return site.getProperties().getProperty(name);
}
/**
* Lookup value for requested macro name
*/
private String getMacroValue(String macroName)
{
try
{
if (macroName.equals(MACRO_USER_ID))
{
return this.getUserId();
}
if (macroName.equals(MACRO_USER_EID))
{
return this.getUserEid();
}
if (macroName.equals(MACRO_USER_FIRST_NAME))
{
return this.getUser().getFirstName();
}
if (macroName.equals(MACRO_USER_LAST_NAME))
{
return this.getUser().getLastName();
}
if (macroName.equals(MACRO_SITE_ID))
{
return getSiteId();
}
if (macroName.equals(MACRO_USER_ROLE))
{
return this.getUserRole();
}
if (macroName.equals(MACRO_SESSION_ID))
{
return this.getSessionId();
}
if (macroName.startsWith("${"+MACRO_CLASS_SITE_PROP))
{
macroName = macroName.substring(2); // Remove leading "${"
macroName = macroName.substring(0, macroName.length()-1); // Remove trailing "}"
// at this point we have "SITE_PROP:some-property-name"
// separate the property name from the prefix then return the property value
String[] sitePropertyKey = macroName.split(":");
if (sitePropertyKey != null && sitePropertyKey.length > 1) {
String sitePropertyValue = getSiteProperty(sitePropertyKey[1]);
return (sitePropertyValue == null) ? "" : sitePropertyValue;
}
}
}
catch (Throwable throwable)
{
return "";
}
/*
* An unsupported macro: use the original text "as is"
*/
return macroName;
}
/**
* Expand one macro reference
* @param text Expand macros found in this text
* @param macroName Macro name
*/
private void expand(StringBuilder sb, String macroName)
{
int index;
/*
* Replace every occurance of the macro in the parameter list
*/
index = sb.indexOf(macroName);
while (index != -1)
{
String macroValue = URLEncoder.encode(getMacroValue(macroName));
sb.replace(index, (index + macroName.length()), macroValue);
index = sb.indexOf(macroName, (index + macroValue.length()));
}
}
/**
* Expand macros, inserting session and site information
* @param originalText Expand macros found in this text
* @return [possibly] Updated text
*/
private String doMacroExpansion(String originalText)
{
StringBuilder sb;
/*
* Quit now if no macros are embedded in the text
*/
if (originalText.indexOf("${") == -1)
{
return originalText;
}
/*
* Expand each macro
*/
sb = new StringBuilder(originalText);
Iterator i = allowedMacrosList.iterator();
while(i.hasNext()) {
String macro = (String) i.next();
expand(sb, macro);
}
return sb.toString();
}
// Work around lack of final config values in placementConfig();
private Properties getAllProperties(Placement placement)
{
Properties config = placement.getTool().getRegisteredConfig();
Properties mconfig = placement.getPlacementConfig();
for ( Object okey : mconfig.keySet() ) {
String key = (String) okey;
config.setProperty(key,mconfig.getProperty(key));
}
return config;
}
/**
* Note a "local" problem (we failed to get session or site data)
*/
private static class SessionDataException extends Exception
{
public SessionDataException(String text)
{
super(text);
}
}
// TODO: When FormattedText KNL-1105 is updated take those methods
/* (non-Javadoc)
* @see org.sakaiproject.util.api.FormattedText#validateURL(java.lang.String)
*/
private static final String PROTOCOL_PREFIX = "http:";
private static final String HOST_PREFIX = "http://127.0.0.1";
private static final String ABOUT_BLANK = "about:blank";
public boolean validateURL(String urlToValidate) {
// return FormattedText.validateURL(urlToValidate); // KNL-1105
if (StringUtils.isBlank(urlToValidate)) return false;
if ( ABOUT_BLANK.equals(urlToValidate) ) return true;
// Check if the url is "Escapable" - run through the URL-URI-URL gauntlet
String escapedURL = sanitizeHrefURL(urlToValidate);
if ( escapedURL == null ) return false;
// For a protocol-relative URL, we validate with protocol attached
// RFC 1808 Section 4
if ((urlToValidate.startsWith("//")) && (urlToValidate.indexOf("://") == -1))
{
urlToValidate = PROTOCOL_PREFIX + urlToValidate;
}
// For a site-relative URL, we validate with host name and protocol attached
// SAK-13787 SAK-23752
if ((urlToValidate.startsWith("/")) && (urlToValidate.indexOf("://") == -1))
{
urlToValidate = HOST_PREFIX + urlToValidate;
}
// Validate the url
UrlValidator urlValidator = new UrlValidator(UrlValidator.ALLOW_LOCAL_URLS);
return urlValidator.isValid(urlToValidate);
}
/* (non-Javadoc)
* @see org.sakaiproject.util.api.FormattedText#sanitizeHrefURL(java.lang.String)
*/
public String sanitizeHrefURL(String urlToEscape) {
// return FormattedText.sanitizeHrefURL(urlToEscape); // KNL-1105
if ( urlToEscape == null ) return null;
if (StringUtils.isBlank(urlToEscape)) return null;
if ( ABOUT_BLANK.equals(urlToEscape) ) return ABOUT_BLANK;
boolean trimProtocol = false;
boolean trimHost = false;
// For a protocol-relative URL, we validate with protocol attached
// RFC 1808 Section 4
if ((urlToEscape.startsWith("//")) && (urlToEscape.indexOf("://") == -1))
{
urlToEscape = PROTOCOL_PREFIX + urlToEscape;
trimProtocol = true;
}
// For a site-relative URL, we validate with host name and protocol attached
// SAK-13787 SAK-23752
if ((urlToEscape.startsWith("/")) && (urlToEscape.indexOf("://") == -1))
{
urlToEscape = HOST_PREFIX + urlToEscape;
trimHost = true;
}
// KNL-1105
try {
URL rawUrl = new URL(urlToEscape);
URI uri = new URI(rawUrl.getProtocol(), rawUrl.getUserInfo(), rawUrl.getHost(),
rawUrl.getPort(), rawUrl.getPath(), rawUrl.getQuery(), rawUrl.getRef());
URL encoded = uri.toURL();
String retval = encoded.toString();
// Un-trim the added bits
if ( trimHost && retval.startsWith(HOST_PREFIX) )
{
retval = retval.substring(HOST_PREFIX.length());
}
if ( trimProtocol && retval.startsWith(PROTOCOL_PREFIX) )
{
retval = retval.substring(PROTOCOL_PREFIX.length());
}
// http://stackoverflow.com/questions/7731919/why-doesnt-uri-escape-escape-single-quotes
// We want these to be usable in JavaScript string values so we map single quotes
retval = retval.replace("'", "%27");
// We want anchors to work
retval = retval.replace("%23", "#");
// Sorry - these just need to come out - they cause to much trouble
// Note that ampersand is not encoded as it is used for parameters.
retval = retval.replace("", "");
return retval;
} catch ( java.net.URISyntaxException e ) {
M_log.info("Failure during encode of href url: " + e);
return null;
} catch ( java.net.MalformedURLException e ) {
M_log.info("Failure during encode of href url: " + e);
return null;
}
}
}