/********************************************************************************** * $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; } }