/**********************************************************************************
* $URL:
* $Id:
***********************************************************************************
*
* Author: Charles Hedrick, hedrick@rutgers.edu
*
* Copyright (c) 2011 Rutgers, the State University of New Jersey
*
* 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.lessonbuildertool.service;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedInputStream;
import java.net.URI;
import java.util.Collection;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.StringTokenizer;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.net.SocketException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import org.sakaiproject.time.api.Time;
import org.sakaiproject.time.cover.TimeService;
import org.sakaiproject.authz.api.SecurityAdvisor;
import org.sakaiproject.authz.api.SecurityService;
import org.sakaiproject.authz.api.SecurityAdvisor.SecurityAdvice;
import org.sakaiproject.authz.cover.AuthzGroupService;
import org.sakaiproject.component.cover.ServerConfigurationService;
import org.sakaiproject.content.api.ContentHostingService;
import org.sakaiproject.content.api.ContentResource;
import org.sakaiproject.content.api.ContentEntity;
import org.sakaiproject.user.cover.UserDirectoryService;
import org.sakaiproject.entity.api.EntityAccessOverloadException;
import org.sakaiproject.entity.api.EntityCopyrightException;
import org.sakaiproject.entity.api.EntityNotDefinedException;
import org.sakaiproject.entity.api.EntityPermissionException;
import org.sakaiproject.entity.api.EntityPropertyNotDefinedException;
import org.sakaiproject.entity.api.HttpAccess;
import org.sakaiproject.entity.api.Reference;
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.event.api.EventTrackingService;
import org.sakaiproject.exception.IdUnusedException;
import org.sakaiproject.exception.PermissionException;
import org.sakaiproject.exception.ServerOverloadException;
import org.sakaiproject.exception.TypeException;
import org.sakaiproject.lessonbuildertool.LessonBuilderAccessAPI;
import org.sakaiproject.lessonbuildertool.SimplePageItem;
import org.sakaiproject.lessonbuildertool.SimplePageProperty;
import org.sakaiproject.lessonbuildertool.SimplePage;
import org.sakaiproject.lessonbuildertool.model.SimplePageToolDao;
import org.sakaiproject.lessonbuildertool.tool.beans.SimplePageBean;
import org.sakaiproject.memory.api.Cache;
import org.sakaiproject.memory.api.MemoryService;
import org.sakaiproject.site.api.SiteService;
import org.sakaiproject.tool.api.SessionManager;
import org.sakaiproject.event.api.UsageSession;
import org.sakaiproject.event.cover.UsageSessionService;
import org.sakaiproject.tool.api.ToolManager;
import org.sakaiproject.tool.api.Placement;
import org.sakaiproject.util.Validator;
import org.sakaiproject.util.Web;
import org.sakaiproject.id.cover.IdManager;
import org.sakaiproject.tool.api.Session;
import uk.org.ponder.messageutil.MessageLocator;
/**
* <p>
* LessonBuilderAccessService implements /access/lessonbuilder
* </p>
*/
public class LessonBuilderAccessService {
private static Log M_log = LogFactory.getLog(LessonBuilderAccessService.class);
public static final String ATTR_SESSION = "sakai.session";
LessonBuilderAccessAPI lessonBuilderAccessAPI = null;
public void setLessonBuilderAccessAPI(LessonBuilderAccessAPI s) {
lessonBuilderAccessAPI = s;
}
SimplePageToolDao simplePageToolDao = null;
public void setSimplePageToolDao(SimplePageToolDao d) {
simplePageToolDao = d;
}
SecurityService securityService = null;
public void setSecurityService(SecurityService s) {
securityService = s;
}
ContentHostingService contentHostingService = null;
public void setContentHostingService(ContentHostingService s) {
contentHostingService = s;
}
EventTrackingService eventTrackingService = null;
public void setEventTrackingService(EventTrackingService s) {
eventTrackingService = s;
}
SessionManager sessionManager = null;
public void setSessionManager(SessionManager s) {
sessionManager = s;
}
public MessageLocator messageLocator;
public void setMessageLocator(MessageLocator s) {
messageLocator = s;
}
private ToolManager toolManager;
public void setToolManager(ToolManager s) {
toolManager = s;
}
private SiteService siteService;
public void setSiteService(SiteService s) {
siteService = s;
}
LessonEntity forumEntity = null;
public void setForumEntity(Object e) {
forumEntity = (LessonEntity) e;
}
LessonEntity quizEntity = null;
public void setQuizEntity(Object e) {
quizEntity = (LessonEntity) e;
}
LessonEntity assignmentEntity = null;
public void setAssignmentEntity(Object e) {
assignmentEntity = (LessonEntity) e;
}
LessonEntity bltiEntity = null;
public void setBltiEntity(Object e) {
bltiEntity = (LessonEntity)e;
}
static MemoryService memoryService = null;
public void setMemoryService(MemoryService m) {
memoryService = m;
}
private GradebookIfc gradebookIfc = null;
public void setGradebookIfc(GradebookIfc g) {
gradebookIfc = g;
}
protected static final long MAX_URL_LENGTH = 8192;
protected static final int STREAM_BUFFER_SIZE = 102400;
public static final String INLINEHTML = "lessonbuilder.inlinehtml";
private boolean inlineHtml = ServerConfigurationService.getBoolean(INLINEHTML, true);
protected static final String MIME_SEPARATOR = "SAKAI_MIME_BOUNDARY";
// uuid is the private key for sharing the session id
private SecretKey sessionKey = null;
public SecretKey getSessionKey() {
return sessionKey;
}
// cache for availability check. Is the item available?
// we cache only positive answers, because they can easily change
// from no to yes in less than 10 min. going back is very unusual
// item : userid => string true
private static Cache accessCache = null;
protected static final int DEFAULT_EXPIRATION = 10 * 60;
SecurityAdvisor allowReadAdvisor = new SecurityAdvisor() {
public SecurityAdvice isAllowed(String userId, String function, String reference) {
if("content.read".equals(function) || "content.hidden".equals(function)) {
return SecurityAdvice.ALLOWED;
}else {
return SecurityAdvice.PASS;
}
}
};
public void init() {
lessonBuilderAccessAPI.setHttpAccess(getHttpAccess());
accessCache = memoryService.newCache("org.sakaiproject.lessonbuildertool.service.LessonBuilderAccessService.cache");
SimplePageItem metaItem = null;
// Get crypto session key from metadata item
// we have to keep it in the database so it's the same on all servers
// There's no entirely sound way to create the item if it doesn't exist
// other than by getting a table lock. Fortunately this will only be
// needed when the entry is created.
SimplePageProperty prop = simplePageToolDao.findProperty("accessCryptoKey");
if (prop == null) {
try {
sessionKey = KeyGenerator.getInstance("Blowfish").generateKey();
// need string version to save in item
byte[] keyBytes = ((SecretKeySpec)sessionKey).getEncoded();
// set attribute to hex version of key
prop = simplePageToolDao.makeProperty("accessCryptoKey", DatatypeConverter.printHexBinary(keyBytes));
simplePageToolDao.quickSaveItem(prop);
} catch (Exception e) {
System.out.println("unable to init cipher for session " + e);
// in case of race condition, our save will fail, but we'll be able to get a value
// saved by someone else
simplePageToolDao.flush();
prop = simplePageToolDao.findProperty("accessCryptoKey");
}
}
if (prop != null) {
String keyString = prop.getValue();
byte[] keyBytes = DatatypeConverter.parseHexBinary(keyString);
sessionKey = new SecretKeySpec(keyBytes, "Blowfish");
}
}
public void destroy() {
accessCache.destroy();
accessCache = null;
}
// references are currently of the form /access/lessonbuilder/item/NNN/group/MMM/...
// the purpose of using /access/lessonbuilder rather than /access/content is
// that we can do access control by Lesson Builder's rules. However users can
// still go to the item directly unless you hide the content in resources.
// we can't obscure the URLs, or references in HTML won't work.
// access checks:
// target URL must be in the same site as the item. /access/content will
// be used to access material in other sites. That's to prevent people
// from using an item in a site they control to access other user's data
// if the target URL is the actual item referred to in /item/NNN, just do
// normal Lesson Builder checks
// otherwise it should be a file referred to, e.g. images referred to in
// an HTML file. We have no good way to check whether that's OK. So we
// just check the item in /item/NNN. But in addition, we see whether there's
// a Lesson Builder item in the same site that has prerequisites. If so, we
// refuse the access. That lets you control something like an answer sheet and
// make sure a user can't get to it by crafting a URL based on a item that
// they have acccess to. But there's no obvious way to know whether an
// arbitrary file in resources is OK to show or not.
public HttpAccess getHttpAccess() {
return new HttpAccess() {
public void handleAccess(HttpServletRequest req, HttpServletResponse res, Reference ref,
Collection copyrightAcceptedRefs) throws EntityPermissionException, EntityNotDefinedException,
EntityAccessOverloadException, EntityCopyrightException {
// preauthorized by encrypted key
boolean isAuth = false;
// if the id is null, the request was for just ".../content"
String refId = ref.getId();
if (refId == null) {
refId = "";
}
if (!refId.startsWith("/item")) {
throw new EntityNotDefinedException(ref.getReference());
}
String itemString = refId.substring("/item/".length());
// string is of form /item/NNN/url. get the number
int i = itemString.indexOf("/");
if (i < 0) {
throw new EntityNotDefinedException(ref.getReference());
}
// get session. The problem here is that some multimedia tools don't reliably
// pass JSESSIONID
String sessionParam = req.getParameter("lb.session");
if (sessionParam != null) {
try {
Cipher sessionCipher = Cipher.getInstance("Blowfish");
sessionCipher.init(Cipher.DECRYPT_MODE, sessionKey);
byte[] sessionBytes = DatatypeConverter.parseHexBinary(sessionParam);
sessionBytes = sessionCipher.doFinal(sessionBytes);
String sessionString = new String(sessionBytes);
int j = sessionString.indexOf(":");
String sessionId = sessionString.substring(0, j);
String url = sessionString.substring(j+1);
UsageSession s = UsageSessionService.getSession(sessionId);
if (s == null || s.isClosed() || url == null || ! url.equals(refId)) {
throw new EntityPermissionException(sessionManager.getCurrentSessionUserId(),
ContentHostingService.AUTH_RESOURCE_READ, ref.getReference());
} else {
isAuth = true;
}
} catch (Exception e) {
System.out.println("unable to decode lb.session " + e);
}
}
// basically there are two checks to be done: is the item accessible in Lessons,
// and is the underlying resource accessible in Sakai.
// This code really does check both. Sort of.
// 1) it checks accessibility to the containing page by seeing if it has been visited.
// This is stricter than necessary, but there's no obvious reason to let people use this
// who aren't following an actual URL we gave them.
// 2) it checks group access as part of the normal resource permission check. Sakai
// should sync the two. We actually don't check it for items in student home directories,
// as far as I can tell
// 3) it checks availability (prerequisites) by calling the code from SimplePageBean
// We could rewrite this with the new LessonsAccess methods, but since we have to do
// resource permission checking also, and there's some duplication, it doesn't seem worth
// rewriting this code. What I've done is review it to make sure it does the same thing.
String id = itemString.substring(i);
itemString = itemString.substring(0, i);
boolean pushedAdvisor = false;
try {
securityService.pushAdvisor(allowReadAdvisor);
pushedAdvisor = true;
Long itemId = 0L;
try {
itemId = (Long) Long.parseLong(itemString);
} catch (Exception e) {
throw new EntityNotDefinedException(ref.getReference());
}
// code here is also in simplePageBean.isItemVisible. change it there
// too if you change this logic
SimplePageItem item = simplePageToolDao.findItem(itemId.longValue());
SimplePage currentPage = simplePageToolDao.getPage(item.getPageId());
String owner = currentPage.getOwner(); // if student content
String group = currentPage.getGroup(); // if student content
if (group != null)
group = "/site/" + currentPage.getSiteId() + "/group/" + group;
String currentSiteId = currentPage.getSiteId();
// first let's make sure the user is allowed to access
// the containing page
if (!isAuth && !canReadPage(currentSiteId)) {
throw new EntityPermissionException(sessionManager.getCurrentSessionUserId(),
ContentHostingService.AUTH_RESOURCE_READ, ref.getReference());
}
// If the resource is the actual one in the item, or
// it is in the containing folder, then do lesson builder checking.
// otherwise do normal resource checking
if (!isAuth) {
boolean useLb = false;
// I've seen sakai id's with //, not sure why. they work but will mess up the comparison
String itemResource = item.getSakaiId().replace("//","/");
// only use lb security if the user has visited the page
// this handles the various page release issues, although
// it's not quite as flexible as the real code. But I don't
// see any reason to help the user follow URLs that they can't
// legitimately have seen.
if (simplePageToolDao.isPageVisited(item.getPageId(), sessionManager.getCurrentSessionUserId(), owner)) {
if (id.equals(itemResource))
useLb = true;
else {
// not exact, but see if it's in the containing folder
int endFolder = itemResource.lastIndexOf("/");
if (endFolder > 0) {
String folder = itemResource.substring(0, endFolder+1);
if (id.startsWith(folder))
useLb = true;
}
}
}
if (useLb) {
// key into access cache
String accessKey = itemString + ":" + sessionManager.getCurrentSessionUserId();
// special access if we have a student site and item is in worksite of one of the students
// Normally we require that the person doing the access be able to see the file, but in
// that specific case we allow the access. Note that in order to get a sakaiid pointing
// into the user's space, the person editing the page must have been able to read the file.
// this allows a user in your group to share any of your resources that he can see.
String usersite = null;
if (owner != null && group != null && id.startsWith("/user/")) {
String username = id.substring(6);
int slash = username.indexOf("/");
if (slash > 0)
usersite = username.substring(0,slash);
// normally it is /user/EID, so convert to userid
try {
usersite = UserDirectoryService.getUserId(usersite);
} catch (Exception e) {};
String itemcreator = item.getAttribute("addedby");
// suppose a member of the group adds a resource from another member of
// the group. (This will only work if they have read access to it.)
// We don't want to gimick access in that case. I think if you
// add your own item, you've given consent. But not if someone else does.
// itemcreator == null is for items added before this patch. I'm going to
// continue to allow access for them, to avoid breaking existing content.
if (usersite != null && itemcreator != null && !usersite.equals(itemcreator))
usersite = null;
}
// code here is also in simplePageBean.isItemVisible. change it there
// too if you change this logic
// for a student page, if it's in one of the groups' worksites, allow it
// The assumption is that only one of those people can put content in the
// page, and then only if the can see it.
if (owner != null && usersite != null && AuthzGroupService.getUserRole(usersite, group) != null) {
// OK
} else if (owner != null && group == null && id.startsWith("/user/" + owner)) {
// OK
} else {
// do normal checking for other content
if (pushedAdvisor) {
securityService.popAdvisor();
pushedAdvisor = false;
}
// our version of allowget does not check hidden but does everything else
// if it's a student page, however use the normal check so students can't
// use this to bypass release control
if (owner == null && !allowGetResource(id, currentSiteId) ||
owner != null && !contentHostingService.allowGetResource(id)) {
throw new EntityPermissionException(sessionManager.getCurrentSessionUserId(),
ContentHostingService.AUTH_RESOURCE_READ, ref.getReference());
}
securityService.pushAdvisor(allowReadAdvisor);
pushedAdvisor = true;
}
// now enforce LB access restrictions if any
if (item != null && item.isPrerequisite() && !"true".equals((String) accessCache.get(accessKey))) {
// computing requirements is so messy that it's worth
// instantiating
// a SimplePageBean to do it. Otherwise we have to duplicate
// lots of
// code that changes. And we want it to be a transient bean
// because there are
// caches that we aren't trying to manage in the long term
// but don't do this unless the item needs checking
if (!canSeeAll(currentPage.getSiteId())) {
SimplePageBean simplePageBean = new SimplePageBean();
simplePageBean.setMessageLocator(messageLocator);
simplePageBean.setToolManager(toolManager);
simplePageBean.setSecurityService(securityService);
simplePageBean.setSessionManager(sessionManager);
simplePageBean.setSiteService(siteService);
simplePageBean.setContentHostingService(contentHostingService);
simplePageBean.setSimplePageToolDao(simplePageToolDao);
simplePageBean.setForumEntity(forumEntity);
simplePageBean.setQuizEntity(quizEntity);
simplePageBean.setAssignmentEntity(assignmentEntity);
simplePageBean.setBltiEntity(bltiEntity);
simplePageBean.setGradebookIfc(gradebookIfc);
simplePageBean.setMemoryService(memoryService);
simplePageBean.setCurrentSiteId(currentPage.getSiteId());
simplePageBean.setCurrentPage(currentPage);
simplePageBean.setCurrentPageId(currentPage.getPageId());
simplePageBean.init();
if (!simplePageBean.isItemAvailable(item, item.getPageId())) {
throw new EntityPermissionException(null, null, null);
}
}
accessCache.put(accessKey, "true", DEFAULT_EXPIRATION);
}
} else {
// normal security. no reason to use advisor
if(pushedAdvisor) securityService.popAdvisor();
pushedAdvisor = false;
// not uselb -- their allowget, not ours. theirs checks hidden
if (!contentHostingService.allowGetResource(id)) {
throw new EntityPermissionException(sessionManager.getCurrentSessionUserId(),
ContentHostingService.AUTH_RESOURCE_READ, ref.getReference());
}
}
}
// access checks are OK, get the thing
// first see if it's not in resources, i.e.
// if it doesn't start with /access/content it's something odd. redirect to it.
// probably resources access control won't apply to it
String url = contentHostingService.getUrl(id);
// https://heidelberg.rutgers.edu/access/citation/content/group/24da8519-08c2-4c8c-baeb-8abdfd6c69d7/New%20Citation%20List
int n = url.indexOf("//");
if (n > 0) {
n = url.indexOf("/", n+2);
if (n > 0) {
String path = url.substring(n);
if (!path.startsWith("/access/content")) {
res.sendRedirect(url);
return;
}
}
}
ContentResource resource = null;
try {
resource = contentHostingService.getResource(id);
} catch (IdUnusedException e) {
throw new EntityNotDefinedException(e.getId());
} catch (PermissionException e) {
throw new EntityPermissionException(e.getUser(), e.getLock(), e.getResource());
} catch (TypeException e) {
throw new EntityNotDefinedException(id);
}
// no copyright enforcement, I don't think
try {
// following cast is redundant is current kernels, but is needed for Sakai 2.6.1
long len = (long)resource.getContentLength();
String contentType = resource.getContentType();
// for url resource type, encode a redirect to the body URL
if (contentType.equalsIgnoreCase(ResourceProperties.TYPE_URL)) {
if (len < MAX_URL_LENGTH) {
byte[] content = resource.getContent();
if ((content == null) || (content.length == 0)) {
throw new IdUnusedException(ref.getReference());
}
// An invalid URI format will get caught by the
// outermost catch block
URI uri = new URI(new String(content, "UTF-8"));
eventTrackingService.post(eventTrackingService.newEvent(ContentHostingService.EVENT_RESOURCE_READ,
resource.getReference(null), false));
res.sendRedirect(uri.toASCIIString());
} else {
// we have a text/url mime type, but the body is too
// long to issue as a redirect
throw new EntityNotDefinedException(ref.getReference());
}
} else {
// use the last part, the file name part of the id, for
// the download file name
String fileName = Web.encodeFileName(req, Validator.getFileName(ref.getId()));
String disposition = null;
boolean inline = false;
if (Validator.letBrowserInline(contentType)) {
// type can be inline, but if HTML we have more checks to do
if (inlineHtml ||
(! "text/html".equalsIgnoreCase(contentType) && ! "application/xhtml+xml".equals(contentType)))
// easy cases: not HTML or HTML always OK
inline = true;
else {
// HTML and html is not allowed globally. code copied from BaseContentServices
ResourceProperties rp = resource.getProperties();
boolean fileInline = false;
boolean folderInline = false;
try {
fileInline = rp.getBooleanProperty(ResourceProperties.PROP_ALLOW_INLINE);
}
catch (EntityPropertyNotDefinedException e) {
// we expect this so nothing to do!
}
if (!fileInline)
try
{
folderInline = resource.getContainingCollection().getProperties().getBooleanProperty(ResourceProperties.PROP_ALLOW_INLINE);
}
catch (EntityPropertyNotDefinedException e) {
// we expect this so nothing to do!
}
if (fileInline || folderInline) {
inline = true;
}
}
}
if (inline) {
disposition = "inline; filename=\"" + fileName + "\"";
} else {
disposition = "attachment; filename=\"" + fileName + "\"";
}
// NOTE: Only set the encoding on the content we have
// to.
// Files uploaded by the user may have been created with
// different encodings, such as ISO-8859-1;
// rather than (sometimes wrongly) saying its UTF-8, let
// the browser auto-detect the encoding.
// If the content was created through the WYSIWYG
// editor, the encoding does need to be set (UTF-8).
String encoding = resource.getProperties().getProperty(ResourceProperties.PROP_CONTENT_ENCODING);
if (encoding != null && encoding.length() > 0) {
contentType = contentType + "; charset=" + encoding;
}
// from contenthosting
ArrayList<Range> ranges = parseRange(req, res, len);
if (req.getHeader("Range") == null || (ranges == null) || (ranges.isEmpty())) {
// stream the content using a small buffer to keep memory managed
InputStream content = null;
OutputStream out = null;
try
{
content = resource.streamContent();
if (content == null)
{
throw new IdUnusedException(ref.getReference());
}
res.setContentType(contentType);
res.addHeader("Content-Disposition", disposition);
res.addHeader("Accept-Ranges", "bytes");
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4187336
if (len <= Integer.MAX_VALUE){
res.setContentLength((int)len);
} else {
res.addHeader("Content-Length", Long.toString(len));
}
// set the buffer of the response to match what we are reading from the request
if (len < STREAM_BUFFER_SIZE)
{
res.setBufferSize((int)len);
}
else
{
res.setBufferSize(STREAM_BUFFER_SIZE);
}
out = res.getOutputStream();
copyRange(content, out, 0, len-1);
}
catch (ServerOverloadException e)
{
throw e;
}
catch (Exception ignore)
{
}
finally
{
// be a good little program and close the stream - freeing up valuable system resources
if (content != null)
{
content.close();
}
if (out != null)
{
try
{
out.close();
}
catch (Exception ignore)
{
}
}
}
}
else
{
// Output partial content. Adapted from Apache Tomcat 5.5.27 DefaultServlet.java
res.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
if (ranges.size() == 1) {
// Single response
Range range = (Range) ranges.get(0);
res.addHeader("Content-Range", "bytes "
+ range.start
+ "-" + range.end + "/"
+ range.length);
long length = range.end - range.start + 1;
if (length < Integer.MAX_VALUE) {
res.setContentLength((int) length);
} else {
// Set the content-length as String to be able to use a long
res.setHeader("content-length", "" + length);
}
res.addHeader("Content-Disposition", disposition);
if (contentType != null) {
res.setContentType(contentType);
}
// stream the content using a small buffer to keep memory managed
InputStream content = null;
OutputStream out = null;
try
{
content = resource.streamContent();
if (content == null)
{
throw new IdUnusedException(ref.getReference());
}
// set the buffer of the response to match what we are reading from the request
if (len < STREAM_BUFFER_SIZE)
{
res.setBufferSize((int)len);
}
else
{
res.setBufferSize(STREAM_BUFFER_SIZE);
}
out = res.getOutputStream();
copyRange(content, out, range.start, range.end);
}
catch (ServerOverloadException e)
{
throw e;
}
catch (SocketException e)
{
//a socket exception usualy means the client aborted the connection or similar
if (M_log.isDebugEnabled())
{
M_log.debug("SocketExcetion", e);
}
}
catch (Exception ignore)
{
}
finally
{
// be a good little program and close the stream - freeing up valuable system resources
if (content != null)
{
content.close();
}
if (out != null)
{
try
{
out.close();
}
catch (IOException ignore)
{
// ignore
}
}
}
} else {
// Multipart response
res.setContentType("multipart/byteranges; boundary=" + MIME_SEPARATOR);
// stream the content using a small buffer to keep memory managed
OutputStream out = null;
try
{
// set the buffer of the response to match what we are reading from the request
if (len < STREAM_BUFFER_SIZE)
{
res.setBufferSize((int)len);
}
else
{
res.setBufferSize(STREAM_BUFFER_SIZE);
}
out = res.getOutputStream();
copyRanges(resource, out, ranges.iterator(), contentType);
}
catch (SocketException e)
{
//a socket exception usualy means the client aborted the connection or similar
if (M_log.isDebugEnabled())
{
M_log.debug("SocketExcetion", e);
}
}
catch (Exception ignore)
{
M_log.error("Swallowing exception", ignore);
}
finally
{
// be a good little program and close the stream - freeing up valuable system resources
if (out != null)
{
try
{
out.close();
}
catch (IOException ignore)
{
// ignore
}
}
}
} // output multiple ranges
} // output partial content
}
} catch (Exception t) {
throw new EntityNotDefinedException(ref.getReference());
// following won't work in 2.7.1
// throw new EntityNotDefinedException(ref.getReference(), t);
}
} catch(Exception ex) {
throw new EntityNotDefinedException(ref.getReference());
}finally {
if(pushedAdvisor) securityService.popAdvisor();
}
}
};
}
/**
* Range inner class. From Apache Tomcat DefaultServlet.java
*
*/
protected class Range {
public long start;
public long end;
public long length;
/**
* Validate range.
*/
public boolean validate() {
if (end >= length)
end = length - 1;
return ( (start >= 0) && (end >= 0) && (start <= end)
&& (length > 0) );
}
public void recycle() {
start = 0;
end = 0;
length = 0;
}
}
/**
* Parse the range header.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @return Vector of ranges
*/
protected ArrayList<Range> parseRange(HttpServletRequest request,
HttpServletResponse response,
long fileLength)
throws IOException {
/* Commented out pending implementation of last-modified / if-modified.
* See http://jira.sakaiproject.org/jira/browse/SAK-3916
// Checking If-Range
String headerValue = request.getHeader("If-Range");
if (headerValue != null) {
long headerValueTime = (-1L);
try {
headerValueTime = request.getDateHeader("If-Range");
} catch (Exception e) {
;
}
String eTag = getETag(resourceAttributes);
long lastModified = resourceAttributes.getLastModified();
if (headerValueTime == (-1L)) {
// If the ETag the client gave does not match the entity
// etag, then the entire entity is returned.
if (!eTag.equals(headerValue.trim()))
return FULL;
} else {
// If the timestamp of the entity the client got is older than
// the last modification date of the entity, the entire entity
// is returned.
if (lastModified > (headerValueTime + 1000))
return FULL;
}
}
*/
if (fileLength == 0)
return null;
// Retrieving the range header (if any is specified
String rangeHeader = request.getHeader("Range");
if (rangeHeader == null)
return null;
// bytes is the only range unit supported (and I don't see the point
// of adding new ones).
if (!rangeHeader.startsWith("bytes")) {
response.addHeader("Content-Range", "bytes */" + fileLength);
response.sendError
(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
rangeHeader = rangeHeader.substring(6);
// Vector which will contain all the ranges which are successfully
// parsed.
ArrayList result = new ArrayList();
StringTokenizer commaTokenizer = new StringTokenizer(rangeHeader, ",");
// Parsing the range list
while (commaTokenizer.hasMoreTokens()) {
String rangeDefinition = commaTokenizer.nextToken().trim();
Range currentRange = new Range();
currentRange.length = fileLength;
int dashPos = rangeDefinition.indexOf('-');
if (dashPos == -1) {
response.addHeader("Content-Range", "bytes */" + fileLength);
response.sendError
(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
if (dashPos == 0) {
try {
long offset = Long.parseLong(rangeDefinition);
currentRange.start = fileLength + offset;
currentRange.end = fileLength - 1;
} catch (NumberFormatException e) {
response.addHeader("Content-Range",
"bytes */" + fileLength);
response.sendError
(HttpServletResponse
.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
} else {
try {
currentRange.start = Long.parseLong
(rangeDefinition.substring(0, dashPos));
if (dashPos < rangeDefinition.length() - 1)
currentRange.end = Long.parseLong
(rangeDefinition.substring
(dashPos + 1, rangeDefinition.length()));
else
currentRange.end = fileLength - 1;
} catch (NumberFormatException e) {
response.addHeader("Content-Range",
"bytes */" + fileLength);
response.sendError
(HttpServletResponse
.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
}
if (!currentRange.validate()) {
response.addHeader("Content-Range", "bytes */" + fileLength);
response.sendError
(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
result.add(currentRange);
}
return result;
}
/**
* Copy the partial contents of the specified input stream to the specified
* output stream.
*
* @param istream The input stream to read from
* @param ostream The output stream to write to
* @param start Start of the range which will be copied
* @param end End of the range which will be copied
* @return Exception which occurred during processing
*/
protected IOException copyRange(InputStream istream,
OutputStream ostream,
long start, long end) {
try {
istream.skip(start);
} catch (IOException e) {
return e;
}
IOException exception = null;
long bytesToRead = end - start + 1;
byte buffer[] = new byte[STREAM_BUFFER_SIZE];
int len = buffer.length;
while ( (bytesToRead > 0) && (len >= buffer.length)) {
try {
len = istream.read(buffer);
if (bytesToRead >= len) {
ostream.write(buffer, 0, len);
bytesToRead -= len;
} else {
ostream.write(buffer, 0, (int) bytesToRead);
bytesToRead = 0;
}
} catch (IOException e) {
exception = e;
len = -1;
}
if (len < buffer.length)
break;
}
return exception;
}
/**
* Copy the contents of the specified input stream to the specified
* output stream in a set of chunks as per the specified ranges.
*
* @param InputStream The input stream to read from
* @param out The output stream to write to
* @param ranges Enumeration of the ranges the client wanted to retrieve
* @param contentType Content type of the resource
* @exception IOException if an input/output error occurs
*/
protected void copyRanges(ContentResource content, OutputStream out,
Iterator ranges, String contentType)
throws IOException {
IOException exception = null;
while ( (exception == null) && (ranges.hasNext()) ) {
Range currentRange = (Range) ranges.next();
// Writing MIME header.
IOUtils.write("\r\n--" + MIME_SEPARATOR + "\r\n", out);
if (contentType != null)
IOUtils.write("Content-Type: " + contentType + "\r\n", out);
IOUtils.write("Content-Range: bytes " + currentRange.start
+ "-" + currentRange.end + "/"
+ currentRange.length + "\r\n", out);
IOUtils.write("\r\n", out);
// Printing content
InputStream in = null;
try {
in = content.streamContent();
} catch (ServerOverloadException se) {
exception = new IOException("ServerOverloadException reported getting inputstream");
}
InputStream istream =
new BufferedInputStream(in, STREAM_BUFFER_SIZE);
exception = copyRange(istream, out, currentRange.start, currentRange.end);
try {
istream.close();
} catch (IOException e) {
// ignore
}
}
IOUtils.write("\r\n--" + MIME_SEPARATOR + "--\r\n", out);
// Rethrow any exception that has occurred
if (exception != null) {
throw exception;
}
}
// simplified versions of stuff from BaseContentService
public boolean allowGetResource(String id, String siteId) {
return unlockCheck(ContentHostingService.AUTH_RESOURCE_READ, id, siteId);
}
public String getReference(String id) {
return "/content" + id; // apparently
}
// specialized version for resources only. assumes it is called with advisor in place
protected boolean unlockCheck(String lock, String id, String siteId) {
boolean isAllowed = securityService.isSuperUser();
if (!isAllowed) {
// make a reference from the resource id, if specified
String ref = null;
if (id != null) {
ref = getReference(id);
}
// if site maintainer or see all, allow any access.
// used to check this after the unlock below, but a user with see all
// may be prevented by group access from seeing the resource, but
// we still want them to see it.
if (canSeeAll(siteId) && id.startsWith("/group/" + siteId))
return true;
// this will check basic access and group access. FOr that normal Sakai
// checking is fine.
isAllowed = ref != null && securityService.unlock(lock, ref);
// availability is for hidden and release date. Do our own, because
// we implement release date but ignore hidden. That lets faculty hide
// resources from normal view but still see them through Lessons
if (isAllowed) {
boolean pushedAdvisor = false;
ContentResource resource = null;
// we're used with allow all advisor in effect, so this is OK
try {
// need advisor so we can look at resource to get its properties
securityService.pushAdvisor(allowReadAdvisor);
pushedAdvisor = true;
resource = contentHostingService.getResource(id);
isAllowed = isAvailable(resource);
securityService.popAdvisor();
pushedAdvisor = false;
} catch (Exception e) {
isAllowed = false;
} finally {
if (pushedAdvisor)
securityService.popAdvisor();
}
}
}
return isAllowed;
}
// check release dates, both resource and collection. assumes it is called with advisor in place
// NOTE: does not enforce hidden
protected boolean isAvailable(ContentEntity entity) {
Time now = TimeService.newTime();
Time releaseDate = entity.getReleaseDate();
if (releaseDate != null && ! releaseDate.before(now))
return false;
Time retractDate = entity.getRetractDate();
if (retractDate != null && ! retractDate.after(now))
return false;
ContentEntity parent = (ContentEntity)entity.getContainingCollection();
if (parent != null)
return isAvailable(parent);
else
return true;
}
public boolean canReadPage(String siteId) {
String ref = "/site/" + siteId;
return securityService.unlock(SimplePage.PERMISSION_LESSONBUILDER_READ, ref);
}
public boolean canSeeAll(String siteId) {
String ref = "/site/" + siteId;
if (securityService.unlock(SimplePage.PERMISSION_LESSONBUILDER_UPDATE, ref))
return true;
if (securityService.unlock(SimplePage.PERMISSION_LESSONBUILDER_SEE_ALL, ref))
return true;
return false;
}
}