diff --git a/config/alfresco/messages/webclient.properties b/config/alfresco/messages/webclient.properties
index 9429e47a81..30372cb8aa 100644
--- a/config/alfresco/messages/webclient.properties
+++ b/config/alfresco/messages/webclient.properties
@@ -996,10 +996,22 @@ reassign_select_user=Select the user to assign the task to, then press OK.
error_reassign_task=Unable to reassign the task due to system error:
part_of_workflow=Part of Workflow
initiated_by=Initiated by
-start_date=Start date
+started_on=Started on
add_resource=Add Resource
view_properties=View Content Properties
edit_properties=Edit Content Properties
+save_changes=Save Changes
+no_tasks=No tasks found.
+no_resources=No resources found.
+in_progress=In Progress
+
+# Workflow Definitions
+wf_review_due_date=Review Due Date
+wf_review_priority=Review Priority
+wf_reviewer=Reviewer
+wf_adhoc_due_date=Due Date
+wf_adhoc_priority=Priority
+wf_adhoc_assignee=Assign To
# Admin Console messages
title_admin_console=Administration Console
diff --git a/config/alfresco/web-client-config-properties.xml b/config/alfresco/web-client-config-properties.xml
index a59d27d5f7..6df11f4d08 100644
--- a/config/alfresco/web-client-config-properties.xml
+++ b/config/alfresco/web-client-config-properties.xml
@@ -230,9 +230,21 @@
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -242,19 +254,21 @@
+
+
+
-
-
-
-
+
+
+
-
+
@@ -263,12 +277,12 @@
-
-
-
+
+
+
-
-
+
+
@@ -276,10 +290,10 @@
-
+
+
+
-
-
@@ -287,9 +301,9 @@
-
-
-
+
+
+
diff --git a/config/alfresco/web-client-config-workflow-actions.xml b/config/alfresco/web-client-config-workflow-actions.xml
index 5f1fb1f61a..d8064a9fd4 100644
--- a/config/alfresco/web-client-config-workflow-actions.xml
+++ b/config/alfresco/web-client-config-workflow-actions.xml
@@ -13,19 +13,40 @@
+
+ manage_task
+ /images/icons/manage_workflow_task.gif
+ dialog:manageTask
+ #{DialogManager.setupParameters}
+
+ #{actionContext.id}
+
+
+
+
+ view_completed_task_title
+ /images/icons/view_workflow_task.gif
+ dialog:viewCompletedTask
+ #{DialogManager.setupParameters}
+
+ #{actionContext.id}
+
+
+
reassign/images/icons/reassign_task.gifdialog:reassignTask#{DialogManager.setupParameters}
- #{actionContext.id}
+ #{actionContext.id}
cancel_workflow/images/icons/cancel_workflow.gif
+ org.alfresco.web.action.evaluator.CancelWorkflowEvaluatordialog:cancelWorkflow#{DialogManager.setupParameters}
@@ -51,7 +72,7 @@
view_properties
- /images/icons/View_details.gif
+ /images/icons/view_properties.gifdialog:viewContentProperties#{BrowseBean.setupContentAction}
@@ -64,7 +85,7 @@
Writeedit_properties
- /images/icons/Change_details.gif
+ /images/icons/edit_properties.gifdialog:editContentProperties#{BrowseBean.setupContentAction}
@@ -72,6 +93,45 @@
+
+
+ org.alfresco.web.action.evaluator.CheckinDocEvaluator
+ checkin
+ /images/icons/CheckIn_icon.gif
+ #{CheckinCheckoutBean.setupWorkflowContentAction}
+ dialog:checkinFile
+
+ #{actionContext.id}
+ #{actionContext.taskId}
+
+
+
+
+
+ org.alfresco.web.action.evaluator.CheckoutDocEvaluator
+ checkout
+ /images/icons/CheckOut_icon.gif
+ #{CheckinCheckoutBean.setupWorkflowContentAction}
+ dialog:checkoutFile
+
+ #{actionContext.id}
+ #{actionContext.taskId}
+
+
+
+
+
+ org.alfresco.web.action.evaluator.CancelCheckoutDocEvaluator
+ undocheckout
+ /images/icons/undo_checkout.gif
+ #{CheckinCheckoutBean.setupWorkflowContentAction}
+ dialog:undoCheckoutFile
+
+ #{actionContext.id}
+ #{actionContext.taskId}
+
+
+
@@ -81,10 +141,12 @@
+
+
@@ -104,9 +166,9 @@
-
-
-
+
+
+
diff --git a/source/java/org/alfresco/web/action/evaluator/CancelWorkflowEvaluator.java b/source/java/org/alfresco/web/action/evaluator/CancelWorkflowEvaluator.java
new file mode 100644
index 0000000000..cdefed2495
--- /dev/null
+++ b/source/java/org/alfresco/web/action/evaluator/CancelWorkflowEvaluator.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2005 Alfresco, Inc.
+ *
+ * Licensed under the Mozilla Public License version 1.1
+ * with a permitted attribution clause. You may obtain a
+ * copy of the License at
+ *
+ * http://www.alfresco.org/legal/license.txt
+ *
+ * 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.alfresco.web.action.evaluator;
+
+import javax.faces.context.FacesContext;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.cmr.workflow.WorkflowService;
+import org.alfresco.service.cmr.workflow.WorkflowTask;
+import org.alfresco.util.ISO9075;
+import org.alfresco.web.action.ActionEvaluator;
+import org.alfresco.web.app.Application;
+import org.alfresco.web.bean.repository.Node;
+import org.alfresco.web.bean.repository.Repository;
+import org.alfresco.web.bean.repository.User;
+
+/**
+ * UI Action Evaluator for cancel workflow action. The action
+ * is only allowed if the workflow the task belongs to was
+ * started by the current user.
+ *
+ * @author gavinc
+ */
+public class CancelWorkflowEvaluator implements ActionEvaluator
+{
+ /**
+ * @see org.alfresco.web.action.ActionEvaluator#evaluate(org.alfresco.web.bean.repository.Node)
+ */
+ public boolean evaluate(Node node)
+ {
+ boolean result = false;
+
+ // get the id of the task
+ String taskId = (String)node.getProperties().get("id");
+ if (taskId != null)
+ {
+ FacesContext context = FacesContext.getCurrentInstance();
+
+ // get the initiator of the workflow the task belongs to
+ WorkflowService workflowSvc = Repository.getServiceRegistry(
+ context).getWorkflowService();
+
+ WorkflowTask task = workflowSvc.getTaskById(taskId);
+ if (task != null)
+ {
+ NodeRef initiator = task.path.instance.initiator;
+ if (initiator != null)
+ {
+ // find the current username
+ User user = Application.getCurrentUser(context);
+ String currentUserName = ISO9075.encode(user.getUserName());
+
+ // get the username of the initiator
+ NodeService nodeSvc = Repository.getServiceRegistry(
+ context).getNodeService();
+ String userName = (String)nodeSvc.getProperty(initiator, ContentModel.PROP_USERNAME);
+
+ // if the current user started the workflow allow the cancel action
+ if (currentUserName.equals(userName))
+ {
+ result = true;
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/source/java/org/alfresco/web/app/servlet/AdminAuthenticationFilter.java b/source/java/org/alfresco/web/app/servlet/AdminAuthenticationFilter.java
new file mode 100644
index 0000000000..9b9a7f69f1
--- /dev/null
+++ b/source/java/org/alfresco/web/app/servlet/AdminAuthenticationFilter.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2005 Alfresco, Inc.
+ *
+ * Licensed under the Mozilla Public License version 1.1
+ * with a permitted attribution clause. You may obtain a
+ * copy of the License at
+ *
+ * http://www.alfresco.org/legal/license.txt
+ *
+ * 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.alfresco.web.app.servlet;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.alfresco.web.bean.repository.User;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * This servlet filter is used to restrict direct URL access to administration
+ * resource in the web client, for example the admin and jBPM consoles.
+ *
+ * @author gavinc
+ */
+public class AdminAuthenticationFilter implements Filter
+{
+ private static final Log logger = LogFactory.getLog(AdminAuthenticationFilter.class);
+
+ /**
+ * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
+ */
+ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
+ throws IOException, ServletException
+ {
+ HttpServletRequest httpRequest = (HttpServletRequest)req;
+ HttpServletResponse httpResponse = (HttpServletResponse)res;
+
+ // The fact that this filter is being called means a request for a protected
+ // resource has taken place, check that the current user is in fact an
+ // administrator.
+
+ if (logger.isDebugEnabled())
+ logger.debug("Authorising request for protected resource: " + httpRequest.getRequestURI());
+
+ // there should be a user at this point so retrieve it
+ User user = AuthenticationHelper.getUser(httpRequest, httpResponse);
+
+ // if the user is present check to see whether it is an admin user
+ boolean isAdmin = (user != null && user.isAdmin());
+
+ if (isAdmin)
+ {
+ if (logger.isDebugEnabled())
+ logger.debug("Current user has admin authority, allowing access.");
+
+ // continue filter chaining if current user is admin user
+ chain.doFilter(req, res);
+ }
+ else
+ {
+ // return the 401 Forbidden error as the current user is not an administrator
+ // if the response has already been committed there's nothing we can do but
+ // print out a warning
+ if (httpResponse.isCommitted() == false)
+ {
+ if (logger.isDebugEnabled())
+ logger.debug("Current user does not have admin authority, returning 401 Forbidden error...");
+
+ httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
+ }
+ else
+ {
+ if (logger.isWarnEnabled())
+ logger.warn("Access denied to '" + httpRequest.getRequestURI() +
+ "'. The response has already been committed so a 401 Forbidden error could not be sent!");
+ }
+ }
+ }
+
+ /**
+ * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
+ */
+ public void init(FilterConfig config) throws ServletException
+ {
+ // nothing to do
+ }
+
+ /**
+ * @see javax.servlet.Filter#destroy()
+ */
+ public void destroy()
+ {
+ // nothing to do
+ }
+}
diff --git a/source/java/org/alfresco/web/app/servlet/AuthenticationHelper.java b/source/java/org/alfresco/web/app/servlet/AuthenticationHelper.java
index 7238435bf0..611e0dd28d 100644
--- a/source/java/org/alfresco/web/app/servlet/AuthenticationHelper.java
+++ b/source/java/org/alfresco/web/app/servlet/AuthenticationHelper.java
@@ -100,39 +100,15 @@ public final class AuthenticationHelper
{
HttpSession session = httpRequest.getSession();
- // examine the appropriate session for our User object
- User user = null;
+ // retrieve the User object
+ User user = getUser(httpRequest, httpResponse);
+
+ // get the login bean if we're not in the portal
LoginBean loginBean = null;
if (Application.inPortalServer() == false)
{
- user = (User)session.getAttribute(AUTHENTICATION_USER);
loginBean = (LoginBean)session.getAttribute(LOGIN_BEAN);
}
- else
- {
- // naff solution as we need to enumerate all session keys until we find the one that
- // should match our User objects - this is weak but we don't know how the underlying
- // Portal vendor has decided to encode the objects in the session
- if (portalUserKeyName.get() == null)
- {
- String userKeyPostfix = "?" + AUTHENTICATION_USER;
- Enumeration enumNames = session.getAttributeNames();
- while (enumNames.hasMoreElements())
- {
- String name = (String)enumNames.nextElement();
- if (name.endsWith(userKeyPostfix))
- {
- // cache the key value once found!
- portalUserKeyName.set(name);
- break;
- }
- }
- }
- if (portalUserKeyName.get() != null)
- {
- user = (User)session.getAttribute(portalUserKeyName.get());
- }
- }
// setup the authentication context
WebApplicationContext wc = WebApplicationContextUtils.getRequiredWebApplicationContext(context);
@@ -388,6 +364,52 @@ public final class AuthenticationHelper
return AuthenticationStatus.Failure;
}
+ /**
+ * Attempts to retrieve the User object stored in the current session.
+ *
+ * @param httpRequest The HTTP request
+ * @param httpResponse The HTTP response
+ * @return The User object representing the current user or null if it could not be found
+ */
+ public static User getUser(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
+ {
+ HttpSession session = httpRequest.getSession();
+ User user = null;
+
+ // examine the appropriate session to try and find the User object
+ if (Application.inPortalServer() == false)
+ {
+ user = (User)session.getAttribute(AUTHENTICATION_USER);
+ }
+ else
+ {
+ // naff solution as we need to enumerate all session keys until we find the one that
+ // should match our User objects - this is weak but we don't know how the underlying
+ // Portal vendor has decided to encode the objects in the session
+ if (portalUserKeyName.get() == null)
+ {
+ String userKeyPostfix = "?" + AUTHENTICATION_USER;
+ Enumeration enumNames = session.getAttributeNames();
+ while (enumNames.hasMoreElements())
+ {
+ String name = (String)enumNames.nextElement();
+ if (name.endsWith(userKeyPostfix))
+ {
+ // cache the key value once found!
+ portalUserKeyName.set(name);
+ break;
+ }
+ }
+ }
+ if (portalUserKeyName.get() != null)
+ {
+ user = (User)session.getAttribute(portalUserKeyName.get());
+ }
+ }
+
+ return user;
+ }
+
/**
* Setup the Alfresco auth cookie value.
*
diff --git a/source/java/org/alfresco/web/app/servlet/BaseDownloadContentServlet.java b/source/java/org/alfresco/web/app/servlet/BaseDownloadContentServlet.java
new file mode 100644
index 0000000000..11a965866f
--- /dev/null
+++ b/source/java/org/alfresco/web/app/servlet/BaseDownloadContentServlet.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2005 Alfresco, Inc.
+ *
+ * Licensed under the Mozilla Public License version 1.1
+ * with a permitted attribution clause. You may obtain a
+ * copy of the License at
+ *
+ * http://www.alfresco.org/legal/license.txt
+ *
+ * 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.alfresco.web.app.servlet;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.SocketException;
+import java.net.URLEncoder;
+import java.text.MessageFormat;
+import java.util.Date;
+import java.util.StringTokenizer;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.model.ContentModel;
+import org.alfresco.repo.content.filestore.FileContentReader;
+import org.alfresco.service.ServiceRegistry;
+import org.alfresco.service.cmr.repository.ContentReader;
+import org.alfresco.service.cmr.repository.ContentService;
+import org.alfresco.service.cmr.repository.MimetypeService;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.cmr.repository.StoreRef;
+import org.alfresco.service.cmr.security.AccessStatus;
+import org.alfresco.service.cmr.security.PermissionService;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.web.app.Application;
+import org.alfresco.web.ui.common.Utils;
+import org.apache.commons.logging.Log;
+
+/**
+ * Base class for the download content servlets. Provides common
+ * processing for the request.
+ *
+ * @see org.alfresco.web.app.servlet.DownloadContentServlet
+ * @see org.alfresco.web.app.servlet.GuestDownloadContentServlet
+ *
+ * @author Kevin Roast
+ * @author gavinc
+ */
+public abstract class BaseDownloadContentServlet extends BaseServlet
+{
+ private static final long serialVersionUID = -4558907921887235966L;
+
+ protected static final String MIMETYPE_OCTET_STREAM = "application/octet-stream";
+
+ protected static final String MSG_ERROR_CONTENT_MISSING = "error_content_missing";
+
+ protected static final String ARG_PROPERTY = "property";
+ protected static final String ARG_ATTACH = "attach";
+ protected static final String ARG_PATH = "path";
+
+ /**
+ * Gets the logger to use for this request.
+ *
+ * This will show all debug entries from this class as though they
+ * came from the subclass.
+ *
+ * @return The logger
+ */
+ protected abstract Log getLogger();
+
+ /**
+ * Processes the download request using the current context i.e. no
+ * authentication checks are made, it is presumed they have already
+ * been done.
+ *
+ * @param req The HTTP request
+ * @param res The HTTP response
+ * @param redirectToLogin Flag to determine whether to redirect to the login
+ * page if the user does not have the correct permissions
+ */
+ protected void processDownloadRequest(HttpServletRequest req, HttpServletResponse res,
+ boolean redirectToLogin)
+ throws ServletException, IOException
+ {
+ Log logger = getLogger();
+ String uri = req.getRequestURI();
+
+ if (logger.isDebugEnabled())
+ {
+ String queryString = req.getQueryString();
+ logger.debug("Processing URL: " + uri +
+ ((queryString != null && queryString.length() > 0) ? ("?" + queryString) : ""));
+ }
+
+ // TODO: add compression here?
+ // see http://servlets.com/jservlet2/examples/ch06/ViewResourceCompress.java for example
+ // only really needed if we don't use the built in compression of the servlet container
+ uri = uri.substring(req.getContextPath().length());
+ StringTokenizer t = new StringTokenizer(uri, "/");
+ int tokenCount = t.countTokens();
+
+ t.nextToken(); // skip servlet name
+
+ // attachment mode (either 'attach' or 'direct')
+ String attachToken = t.nextToken();
+ boolean attachment = attachToken.equals(ARG_ATTACH);
+
+ // get or calculate the noderef and filename to download as
+ NodeRef nodeRef;
+ String filename;
+
+ // do we have a path parameter instead of a NodeRef?
+ String path = req.getParameter(ARG_PATH);
+ if (path != null && path.length() != 0)
+ {
+ // process the name based path to resolve the NodeRef and the Filename element
+ PathRefInfo pathInfo = resolveNamePath(getServletContext(), path);
+
+ nodeRef = pathInfo.NodeRef;
+ filename = pathInfo.Filename;
+ }
+ else
+ {
+ // a NodeRef must have been specified if no path has been found
+ if (tokenCount < 6)
+ {
+ throw new IllegalArgumentException("Download URL did not contain all required args: " + uri);
+ }
+
+ // assume 'workspace' or other NodeRef based protocol for remaining URL elements
+ StoreRef storeRef = new StoreRef(t.nextToken(), t.nextToken());
+ String id = t.nextToken();
+ // build noderef from the appropriate URL elements
+ nodeRef = new NodeRef(storeRef, id);
+
+ // filename is last remaining token
+ filename = t.nextToken();
+ }
+
+ // get qualified of the property to get content from - default to ContentModel.PROP_CONTENT
+ QName propertyQName = ContentModel.PROP_CONTENT;
+ String property = req.getParameter(ARG_PROPERTY);
+ if (property != null && property.length() != 0)
+ {
+ propertyQName = QName.createQName(property);
+ }
+
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Found NodeRef: " + nodeRef.toString());
+ logger.debug("Will use filename: " + filename);
+ logger.debug("For property: " + propertyQName);
+ logger.debug("With attachment mode: " + attachment);
+ }
+
+ // get the services we need to retrieve the content
+ ServiceRegistry serviceRegistry = getServiceRegistry(getServletContext());
+ NodeService nodeService = serviceRegistry.getNodeService();
+ ContentService contentService = serviceRegistry.getContentService();
+ PermissionService permissionService = serviceRegistry.getPermissionService();
+
+ try
+ {
+ // check that the user has at least READ_CONTENT access - else redirect to the login page
+ if (permissionService.hasPermission(nodeRef, PermissionService.READ_CONTENT) == AccessStatus.DENIED)
+ {
+ if (logger.isDebugEnabled())
+ logger.debug("User does not have permissions to read content for NodeRef: " + nodeRef.toString());
+
+ if (redirectToLogin)
+ {
+ if (logger.isDebugEnabled())
+ logger.debug("Redirecting to login page...");
+
+ redirectToLoginPage(req, res, getServletContext());
+ }
+ else
+ {
+ if (logger.isDebugEnabled())
+ logger.debug("Returning 403 Forbidden error...");
+
+ res.sendError(HttpServletResponse.SC_FORBIDDEN);
+ }
+ return;
+ }
+
+ // check If-Modified-Since header and set Last-Modified header as appropriate
+ Date modified = (Date)nodeService.getProperty(nodeRef, ContentModel.PROP_MODIFIED);
+ long modifiedSince = req.getDateHeader("If-Modified-Since");
+ if (modifiedSince > 0L)
+ {
+ // round the date to the ignore millisecond value which is not supplied by header
+ long modDate = (modified.getTime() / 1000L) * 1000L;
+ if (modDate <= modifiedSince)
+ {
+ res.setStatus(304);
+ return;
+ }
+ }
+ res.setDateHeader("Last-Modified", modified.getTime());
+
+ if (attachment == true)
+ {
+ // set header based on filename - will force a Save As from the browse if it doesn't recognise it
+ // this is better than the default response of the browser trying to display the contents
+ res.setHeader("Content-Disposition", "attachment");
+ }
+
+ // get the content reader
+ ContentReader reader = contentService.getReader(nodeRef, propertyQName);
+ // ensure that it is safe to use
+ reader = FileContentReader.getSafeContentReader(
+ reader,
+ Application.getMessage(req.getSession(), MSG_ERROR_CONTENT_MISSING),
+ nodeRef, reader);
+
+ String mimetype = reader.getMimetype();
+ // fall back if unable to resolve mimetype property
+ if (mimetype == null || mimetype.length() == 0)
+ {
+ MimetypeService mimetypeMap = serviceRegistry.getMimetypeService();
+ mimetype = MIMETYPE_OCTET_STREAM;
+ int extIndex = filename.lastIndexOf('.');
+ if (extIndex != -1)
+ {
+ String ext = filename.substring(extIndex + 1);
+ String mt = mimetypeMap.getMimetypesByExtension().get(ext);
+ if (mt != null)
+ {
+ mimetype = mt;
+ }
+ }
+ }
+ // set mimetype for the content and the character encoding for the stream
+ res.setContentType(mimetype);
+ res.setCharacterEncoding(reader.getEncoding());
+
+ // get the content and stream directly to the response output stream
+ // assuming the repo is capable of streaming in chunks, this should allow large files
+ // to be streamed directly to the browser response stream.
+ try
+ {
+ reader.getContent( res.getOutputStream() );
+ }
+ catch (SocketException e)
+ {
+ if (e.getMessage().contains("ClientAbortException"))
+ {
+ // the client cut the connection - our mission was accomplished apart from a little error message
+ logger.error("Client aborted stream read:\n node: " + nodeRef + "\n content: " + reader);
+ }
+ else
+ {
+ throw e;
+ }
+ }
+ }
+ catch (Throwable err)
+ {
+ throw new AlfrescoRuntimeException("Error during download content servlet processing: " + err.getMessage(), err);
+ }
+ }
+
+ /**
+ * Helper to generate a URL to a content node for downloading content from the server.
+ *
+ * @param pattern The pattern to use for the URL
+ * @param ref NodeRef of the content node to generate URL for (cannot be null)
+ * @param name File name to return in the URL (cannot be null)
+ *
+ * @return URL to download the content from the specified node
+ */
+ protected final static String generateUrl(String pattern, NodeRef ref, String name)
+ {
+ String url = null;
+
+ try
+ {
+ url = MessageFormat.format(pattern, new Object[] {
+ ref.getStoreRef().getProtocol(),
+ ref.getStoreRef().getIdentifier(),
+ ref.getId(),
+ Utils.replace(URLEncoder.encode(name, "UTF-8"), "+", "%20") } );
+ }
+ catch (UnsupportedEncodingException uee)
+ {
+ throw new AlfrescoRuntimeException("Failed to encode content URL for node: " + ref, uee);
+ }
+
+ return url;
+ }
+}
diff --git a/source/java/org/alfresco/web/app/servlet/CommandServlet.java b/source/java/org/alfresco/web/app/servlet/CommandServlet.java
index 0beb509ddc..69923e6026 100644
--- a/source/java/org/alfresco/web/app/servlet/CommandServlet.java
+++ b/source/java/org/alfresco/web/app/servlet/CommandServlet.java
@@ -73,7 +73,7 @@ public class CommandServlet extends BaseServlet
/**
* @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
- protected void doGet(HttpServletRequest req, HttpServletResponse res)
+ protected void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException
{
String uri = req.getRequestURI();
diff --git a/source/java/org/alfresco/web/app/servlet/DownloadContentServlet.java b/source/java/org/alfresco/web/app/servlet/DownloadContentServlet.java
index 951f1c2b3f..afcd7b9bf4 100644
--- a/source/java/org/alfresco/web/app/servlet/DownloadContentServlet.java
+++ b/source/java/org/alfresco/web/app/servlet/DownloadContentServlet.java
@@ -17,32 +17,12 @@
package org.alfresco.web.app.servlet;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.net.SocketException;
-import java.net.URLEncoder;
-import java.text.MessageFormat;
-import java.util.Date;
-import java.util.StringTokenizer;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
-import org.alfresco.error.AlfrescoRuntimeException;
-import org.alfresco.model.ContentModel;
-import org.alfresco.repo.content.filestore.FileContentReader;
-import org.alfresco.service.ServiceRegistry;
-import org.alfresco.service.cmr.repository.ContentReader;
-import org.alfresco.service.cmr.repository.ContentService;
-import org.alfresco.service.cmr.repository.MimetypeService;
import org.alfresco.service.cmr.repository.NodeRef;
-import org.alfresco.service.cmr.repository.NodeService;
-import org.alfresco.service.cmr.repository.StoreRef;
-import org.alfresco.service.cmr.security.AccessStatus;
-import org.alfresco.service.cmr.security.PermissionService;
-import org.alfresco.service.namespace.QName;
-import org.alfresco.web.app.Application;
-import org.alfresco.web.ui.common.Utils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -71,26 +51,30 @@ import org.apache.commons.logging.LogFactory;
* Like most Alfresco servlets, the URL may be followed by a valid 'ticket' argument for authentication:
* ?ticket=1234567890
*
- * And/or also followed by the "?guest=true" argument to force guest access login for the URL.
+ * And/or also followed by the "?guest=true" argument to force guest access login for the URL. If the
+ * guest=true parameter is used the current session will be logged out and the guest user logged in.
+ * Therefore upon completion of this request the current user will be "guest".
+ *
+ * If the user attempting the request is not authorised to access the requested node the login page
+ * will be redirected to.
*
* @author Kevin Roast
+ * @author gavinc
*/
-public class DownloadContentServlet extends BaseServlet
+public class DownloadContentServlet extends BaseDownloadContentServlet
{
- private static final long serialVersionUID = -4558907921887235966L;
-
+ private static final long serialVersionUID = -576405943603122206L;
+
private static Log logger = LogFactory.getLog(DownloadContentServlet.class);
private static final String DOWNLOAD_URL = "/download/attach/{0}/{1}/{2}/{3}";
private static final String BROWSER_URL = "/download/direct/{0}/{1}/{2}/{3}";
- private static final String MIMETYPE_OCTET_STREAM = "application/octet-stream";
-
- private static final String MSG_ERROR_CONTENT_MISSING = "error_content_missing";
-
- private static final String ARG_PROPERTY = "property";
- private static final String ARG_ATTACH = "attach";
- private static final String ARG_PATH = "path";
+ @Override
+ protected Log getLogger()
+ {
+ return logger;
+ }
/**
* @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
@@ -98,10 +82,12 @@ public class DownloadContentServlet extends BaseServlet
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException
{
- String uri = req.getRequestURI();
-
if (logger.isDebugEnabled())
- logger.debug("Processing URL: " + uri + (req.getQueryString() != null ? ("?" + req.getQueryString()) : ""));
+ {
+ String queryString = req.getQueryString();
+ logger.debug("Authenticating request to URL: " + req.getRequestURI() +
+ ((queryString != null && queryString.length() > 0) ? ("?" + queryString) : ""));
+ }
AuthenticationStatus status = servletAuthenticate(req, res);
if (status == AuthenticationStatus.Failure)
@@ -109,159 +95,7 @@ public class DownloadContentServlet extends BaseServlet
return;
}
- // TODO: add compression here?
- // see http://servlets.com/jservlet2/examples/ch06/ViewResourceCompress.java for example
- // only really needed if we don't use the built in compression of the servlet container
- uri = uri.substring(req.getContextPath().length());
- StringTokenizer t = new StringTokenizer(uri, "/");
- int tokenCount = t.countTokens();
-
- t.nextToken(); // skip servlet name
-
- // attachment mode (either 'attach' or 'direct')
- String attachToken = t.nextToken();
- boolean attachment = attachToken.equals(ARG_ATTACH);
-
- // get or calculate the noderef and filename to download as
- NodeRef nodeRef;
- String filename;
-
- // do we have a path parameter instead of a NodeRef?
- String path = req.getParameter(ARG_PATH);
- if (path != null && path.length() != 0)
- {
- // process the name based path to resolve the NodeRef and the Filename element
- PathRefInfo pathInfo = resolveNamePath(getServletContext(), path);
-
- nodeRef = pathInfo.NodeRef;
- filename = pathInfo.Filename;
- }
- else
- {
- // a NodeRef must have been specified if no path has been found
- if (tokenCount < 6)
- {
- throw new IllegalArgumentException("Download URL did not contain all required args: " + uri);
- }
-
- // assume 'workspace' or other NodeRef based protocol for remaining URL elements
- StoreRef storeRef = new StoreRef(t.nextToken(), t.nextToken());
- String id = t.nextToken();
- // build noderef from the appropriate URL elements
- nodeRef = new NodeRef(storeRef, id);
-
- // filename is last remaining token
- filename = t.nextToken();
- }
-
- // get qualified of the property to get content from - default to ContentModel.PROP_CONTENT
- QName propertyQName = ContentModel.PROP_CONTENT;
- String property = req.getParameter(ARG_PROPERTY);
- if (property != null && property.length() != 0)
- {
- propertyQName = QName.createQName(property);
- }
-
- if (logger.isDebugEnabled())
- {
- logger.debug("Found NodeRef: " + nodeRef.toString());
- logger.debug("Will use filename: " + filename);
- logger.debug("For property: " + propertyQName);
- logger.debug("With attachment mode: " + attachment);
- }
-
- // get the services we need to retrieve the content
- ServiceRegistry serviceRegistry = getServiceRegistry(getServletContext());
- NodeService nodeService = serviceRegistry.getNodeService();
- ContentService contentService = serviceRegistry.getContentService();
- PermissionService permissionService = serviceRegistry.getPermissionService();
-
- try
- {
- // check that the user has at least READ_CONTENT access - else redirect to the login page
- if (permissionService.hasPermission(nodeRef, PermissionService.READ_CONTENT) == AccessStatus.DENIED)
- {
- if (logger.isDebugEnabled())
- logger.debug("User does not have permissions to read content for NodeRef: " + nodeRef.toString());
- redirectToLoginPage(req, res, getServletContext());
- return;
- }
-
- // check If-Modified-Since header and set Last-Modified header as appropriate
- Date modified = (Date)nodeService.getProperty(nodeRef, ContentModel.PROP_MODIFIED);
- long modifiedSince = req.getDateHeader("If-Modified-Since");
- if (modifiedSince > 0L)
- {
- // round the date to the ignore millisecond value which is not supplied by header
- long modDate = (modified.getTime() / 1000L) * 1000L;
- if (modDate <= modifiedSince)
- {
- res.setStatus(304);
- return;
- }
- }
- res.setDateHeader("Last-Modified", modified.getTime());
-
- if (attachment == true)
- {
- // set header based on filename - will force a Save As from the browse if it doesn't recognise it
- // this is better than the default response of the browser trying to display the contents
- res.setHeader("Content-Disposition", "attachment");
- }
-
- // get the content reader
- ContentReader reader = contentService.getReader(nodeRef, propertyQName);
- // ensure that it is safe to use
- reader = FileContentReader.getSafeContentReader(
- reader,
- Application.getMessage(req.getSession(), MSG_ERROR_CONTENT_MISSING),
- nodeRef, reader);
-
- String mimetype = reader.getMimetype();
- // fall back if unable to resolve mimetype property
- if (mimetype == null || mimetype.length() == 0)
- {
- MimetypeService mimetypeMap = serviceRegistry.getMimetypeService();
- mimetype = MIMETYPE_OCTET_STREAM;
- int extIndex = filename.lastIndexOf('.');
- if (extIndex != -1)
- {
- String ext = filename.substring(extIndex + 1);
- String mt = mimetypeMap.getMimetypesByExtension().get(ext);
- if (mt != null)
- {
- mimetype = mt;
- }
- }
- }
- // set mimetype for the content and the character encoding for the stream
- res.setContentType(mimetype);
- res.setCharacterEncoding(reader.getEncoding());
-
- // get the content and stream directly to the response output stream
- // assuming the repo is capable of streaming in chunks, this should allow large files
- // to be streamed directly to the browser response stream.
- try
- {
- reader.getContent( res.getOutputStream() );
- }
- catch (SocketException e)
- {
- if (e.getMessage().contains("ClientAbortException"))
- {
- // the client cut the connection - our mission was accomplished apart from a little error message
- logger.error("Client aborted stream read:\n node: " + nodeRef + "\n content: " + reader);
- }
- else
- {
- throw e;
- }
- }
- }
- catch (Throwable err)
- {
- throw new AlfrescoRuntimeException("Error during download content servlet processing: " + err.getMessage(), err);
- }
+ processDownloadRequest(req, res, true);
}
/**
@@ -276,22 +110,7 @@ public class DownloadContentServlet extends BaseServlet
*/
public final static String generateDownloadURL(NodeRef ref, String name)
{
- String url = null;
-
- try
- {
- url = MessageFormat.format(DOWNLOAD_URL, new Object[] {
- ref.getStoreRef().getProtocol(),
- ref.getStoreRef().getIdentifier(),
- ref.getId(),
- Utils.replace(URLEncoder.encode(name, "UTF-8"), "+", "%20") } );
- }
- catch (UnsupportedEncodingException uee)
- {
- throw new AlfrescoRuntimeException("Failed to encode content URL for node: " + ref, uee);
- }
-
- return url;
+ return generateUrl(DOWNLOAD_URL, ref, name);
}
/**
@@ -306,21 +125,6 @@ public class DownloadContentServlet extends BaseServlet
*/
public final static String generateBrowserURL(NodeRef ref, String name)
{
- String url = null;
-
- try
- {
- url = MessageFormat.format(BROWSER_URL, new Object[] {
- ref.getStoreRef().getProtocol(),
- ref.getStoreRef().getIdentifier(),
- ref.getId(),
- Utils.replace(URLEncoder.encode(name, "UTF-8"), "+", "%20") } );
- }
- catch (UnsupportedEncodingException uee)
- {
- throw new AlfrescoRuntimeException("Failed to encode content URL for node: " + ref, uee);
- }
-
- return url;
+ return generateUrl(BROWSER_URL, ref, name);
}
}
diff --git a/source/java/org/alfresco/web/app/servlet/FacesHelper.java b/source/java/org/alfresco/web/app/servlet/FacesHelper.java
index 7b8a008a0b..701ac2b0f2 100644
--- a/source/java/org/alfresco/web/app/servlet/FacesHelper.java
+++ b/source/java/org/alfresco/web/app/servlet/FacesHelper.java
@@ -145,12 +145,31 @@ public final class FacesHelper
else
{
// make sure we do not have illegal characters in the id
+ id = makeLegalId(id);
+ }
+
+ component.setId(id);
+ }
+
+ /**
+ * Makes the given id a legal JSF component id by replacing illegal
+ * characters with underscores.
+ *
+ * @param id The id to make legal
+ * @return The legalised id
+ */
+ public static String makeLegalId(String id)
+ {
+ if (id != null)
+ {
+ // replace illegal ID characters with an underscore
id = id.replace(':', '_');
+ id = id.replace(' ', '_');
// TODO: check all other illegal characters - only allowed dash and underscore
}
- component.setId(id);
+ return id;
}
/**
diff --git a/source/java/org/alfresco/web/app/servlet/GuestDownloadContentServlet.java b/source/java/org/alfresco/web/app/servlet/GuestDownloadContentServlet.java
new file mode 100644
index 0000000000..5a61b3a868
--- /dev/null
+++ b/source/java/org/alfresco/web/app/servlet/GuestDownloadContentServlet.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2005 Alfresco, Inc.
+ *
+ * Licensed under the Mozilla Public License version 1.1
+ * with a permitted attribution clause. You may obtain a
+ * copy of the License at
+ *
+ * http://www.alfresco.org/legal/license.txt
+ *
+ * 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.alfresco.web.app.servlet;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
+import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.security.PermissionService;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Servlet responsible for streaming node content from the repo directly to the response stream.
+ * The appropriate mimetype is calculated based on filename extension.
+ *
+ * The URL to the servlet should be generated thus:
+ *
+ * The protocol, followed by either the store and Id (NodeRef) or instead specify a name based
+ * encoded Path to the content, note that the filename element is used for mimetype lookup and
+ * as the returning filename for the response stream.
+ *
+ * The 'attach' or 'direct' element is used to indicate whether to display the stream directly
+ * in the browser or download it as a file attachment.
+ *
+ * By default, the download assumes that the content is on the
+ * {@link org.alfresco.model.ContentModel#PROP_CONTENT content property}.
+ * To retrieve the content of a specific model property, use a 'property' arg, providing the workspace,
+ * node ID AND the qualified name of the property.
+ *
+ * This servlet only accesses content available to the guest user. If the guest user does not
+ * have access to the requested a 401 Forbidden response is returned to the caller.
+ *
+ * This servlet does not effect the current session, therefore if guest access is required to a
+ * resource this servlet can be used without logging out the current user.
+ *
+ * @author gavinc
+ */
+public class GuestDownloadContentServlet extends BaseDownloadContentServlet
+{
+ private static final long serialVersionUID = -5258137503339817457L;
+
+ private static Log logger = LogFactory.getLog(GuestDownloadContentServlet.class);
+
+ private static final String DOWNLOAD_URL = "/guestDownload/attach/{0}/{1}/{2}/{3}";
+ private static final String BROWSER_URL = "/guestDownload/direct/{0}/{1}/{2}/{3}";
+
+ @Override
+ protected Log getLogger()
+ {
+ return logger;
+ }
+
+ /**
+ * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+ */
+ protected void doGet(HttpServletRequest req, HttpServletResponse res)
+ throws ServletException, IOException
+ {
+ if (logger.isDebugEnabled())
+ {
+ String queryString = req.getQueryString();
+ logger.debug("Setting up guest access to URL: " + req.getRequestURI() +
+ ((queryString != null && queryString.length() > 0) ? ("?" + queryString) : ""));
+ }
+
+ DownloadContentWork dcw = new DownloadContentWork(req, res);
+ AuthenticationUtil.runAs(dcw, PermissionService.GUEST_AUTHORITY);
+ }
+
+ /**
+ * Helper to generate a URL to a content node for downloading content from the server.
+ * The content is supplied as an HTTP1.1 attachment to the response. This generally means
+ * a browser should prompt the user to save the content to specified location.
+ *
+ * @param ref NodeRef of the content node to generate URL for (cannot be null)
+ * @param name File name to return in the URL (cannot be null)
+ *
+ * @return URL to download the content from the specified node
+ */
+ public final static String generateDownloadURL(NodeRef ref, String name)
+ {
+ return generateUrl(DOWNLOAD_URL, ref, name);
+ }
+
+ /**
+ * Helper to generate a URL to a content node for downloading content from the server.
+ * The content is supplied directly in the reponse. This generally means a browser will
+ * attempt to open the content directly if possible, else it will prompt to save the file.
+ *
+ * @param ref NodeRef of the content node to generate URL for (cannot be null)
+ * @param name File name to return in the URL (cannot be null)
+ *
+ * @return URL to download the content from the specified node
+ */
+ public final static String generateBrowserURL(NodeRef ref, String name)
+ {
+ return generateUrl(BROWSER_URL, ref, name);
+ }
+
+ /**
+ * Class to wrap the call to processDownloadRequest.
+ *
+ * @author gavinc
+ */
+ public class DownloadContentWork implements RunAsWork