/*******************************************************************************
* Copyright (c) 2010, 2014 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.orion.internal.server.servlets.workspace;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.StringTokenizer;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.URIUtil;
import org.eclipse.orion.internal.server.servlets.Activator;
import org.eclipse.orion.internal.server.servlets.ServletResourceHandler;
import org.eclipse.orion.internal.server.servlets.file.NewFileServlet;
import org.eclipse.orion.server.core.LogHelper;
import org.eclipse.orion.server.core.OrionConfiguration;
import org.eclipse.orion.server.core.PreferenceHelper;
import org.eclipse.orion.server.core.ProtocolConstants;
import org.eclipse.orion.server.core.ServerConstants;
import org.eclipse.orion.server.core.ServerStatus;
import org.eclipse.orion.server.core.metastore.IMetaStore;
import org.eclipse.orion.server.core.metastore.ProjectInfo;
import org.eclipse.orion.server.core.metastore.WorkspaceInfo;
import org.eclipse.orion.server.servlets.OrionServlet;
import org.eclipse.osgi.util.NLS;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Handles requests against a single workspace.
*/
public class WorkspaceResourceHandler extends MetadataInfoResourceHandler<WorkspaceInfo> {
static final int CREATE_COPY = 0x1;
static final int CREATE_MOVE = 0x2;
static final int CREATE_NO_OVERWRITE = 0x4;
private final ServletResourceHandler<IStatus> statusHandler;
public static void computeProjectLocation(HttpServletRequest request, ProjectInfo project, String location, boolean init) throws CoreException {
String user = request.getRemoteUser();
URI contentURI;
if (location == null) {
contentURI = generateProjectLocation(project, user);
} else {
//use the content location specified by the user
try {
contentURI = new URI(location);
EFS.getFileSystem(contentURI.getScheme());//check if we support this scheme
} catch (Exception e) {
//if this is not a valid URI or scheme try to parse it as file path
contentURI = new File(location).toURI();
}
if (init) {
project.setContentLocation(contentURI);
IFileStore child = NewFileServlet.getFileStore(request, project);
child.mkdir(EFS.NONE, null);
}
}
project.setContentLocation(contentURI);
}
/**
* Returns the location of the project's content (conforming to File REST API).
*/
static URI computeProjectURI(URI parentLocation, WorkspaceInfo workspace, ProjectInfo project) {
return URIUtil.append(parentLocation, ".." + Activator.LOCATION_FILE_SERVLET + '/' + workspace.getUniqueId() + '/' + project.getFullName() + '/'); //$NON-NLS-1$
}
/**
* Generates a file system location for newly created project. Creates a new
* folder in the file system and ensures it is empty.
*/
private static URI generateProjectLocation(ProjectInfo project, String user) throws CoreException {
IFileStore projectStore = OrionConfiguration.getMetaStore().getDefaultContentLocation(project);
if (projectStore.fetchInfo().exists()) {
//This folder must be empty initially or we risk showing another user's old private data
projectStore.delete(EFS.NONE, null);
}
projectStore.mkdir(EFS.NONE, null);
return projectStore.toURI();
}
/**
* Returns the project for the given project metadata location. The expected format of the
* location is a URI whose path is of the form /workspace/workspaceId/project/projectName.
* Returns <code>null</code> if there was no such project corresponding to the given location.
* @throws CoreException If there was an error reading the project metadata
*/
public static ProjectInfo projectForMetadataLocation(IMetaStore store, String sourceLocation) throws CoreException {
if (sourceLocation == null)
return null;
URI sourceURI;
try {
sourceURI = new URI(sourceLocation);
} catch (URISyntaxException e) {
//bad location
return null;
}
String pathString = sourceURI.getPath();
if (pathString == null)
return null;
IPath path = new Path(pathString);
//path format is /workspace/<workspaceId>/project/<projectName>
if (path.segmentCount() < 4)
return null;
String workspaceId = path.segment(1);
String projectName = path.segment(3);
return store.readProject(workspaceId, projectName);
}
public static void removeProject(String user, WorkspaceInfo workspace, ProjectInfo project) throws CoreException {
// remove the project folder
URI contentURI = project.getContentLocation();
// only delete project contents if they are in default location
IFileStore projectStore = OrionConfiguration.getMetaStore().getDefaultContentLocation(project);
URI defaultLocation = projectStore.toURI();
if (URIUtil.sameURI(defaultLocation, contentURI)) {
projectStore.delete(EFS.NONE, null);
}
OrionConfiguration.getMetaStore().deleteProject(workspace.getUniqueId(), project.getFullName());
}
/**
* Returns a JSON representation of the workspace, conforming to Orion
* workspace API protocol.
* @param workspace The workspace to store
* @param requestLocation The location of the current request
* @param baseLocation The base location for the workspace servlet
*/
public static JSONObject toJSON(WorkspaceInfo workspace, URI requestLocation, URI baseLocation) {
JSONObject result = MetadataInfoResourceHandler.toJSON(workspace);
JSONArray projects = new JSONArray();
URI workspaceLocation = URIUtil.append(baseLocation, workspace.getUniqueId());
URI projectBaseLocation = URIUtil.append(workspaceLocation, "project"); //$NON-NLS-1$
//add children element to conform to file API structure
JSONArray children = new JSONArray();
IMetaStore metaStore = OrionConfiguration.getMetaStore();
for (String projectName : workspace.getProjectNames()) {
try {
ProjectInfo project = metaStore.readProject(workspace.getUniqueId(), projectName);
//augment project objects with their location
JSONObject projectObject = new JSONObject();
projectObject.put(ProtocolConstants.KEY_ID, project.getUniqueId());
//this is the location of the project metadata
projectObject.put(ProtocolConstants.KEY_LOCATION, URIUtil.append(projectBaseLocation, projectName));
projects.put(projectObject);
//remote folders are listed separately
IFileStore projectStore = null;
try {
projectStore = project.getProjectStore();
} catch (CoreException e) {
//ignore and treat as local
}
JSONObject child = new JSONObject();
child.put(ProtocolConstants.KEY_NAME, project.getFullName());
child.put(ProtocolConstants.KEY_DIRECTORY, true);
//this is the location of the project file contents
URI contentLocation = computeProjectURI(baseLocation, workspace, project);
child.put(ProtocolConstants.KEY_LOCATION, contentLocation);
try {
if (projectStore != null)
child.put(ProtocolConstants.KEY_LOCAL_TIMESTAMP, projectStore.fetchInfo(EFS.NONE, null).getLastModified());
} catch (CoreException coreException) {
//just omit the timestamp in this case because the project location is unreachable
}
try {
child.put(ProtocolConstants.KEY_CHILDREN_LOCATION, new URI(contentLocation.getScheme(), contentLocation.getUserInfo(), contentLocation.getHost(), contentLocation.getPort(), contentLocation.getPath(), ProtocolConstants.PARM_DEPTH + "=1", contentLocation.getFragment())); //$NON-NLS-1$
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
child.put(ProtocolConstants.KEY_ID, project.getUniqueId());
children.put(child);
} catch (Exception e) {
//ignore malformed children
}
}
try {
//add basic fields to workspace result
result.put(ProtocolConstants.KEY_LOCATION, workspaceLocation);
result.put(ProtocolConstants.KEY_CHILDREN_LOCATION, workspaceLocation);
result.put(ProtocolConstants.KEY_PROJECTS, projects);
result.put(ProtocolConstants.KEY_DIRECTORY, "true"); //$NON-NLS-1$
//add children to match file API
result.put(ProtocolConstants.KEY_CHILDREN, children);
} catch (JSONException e) {
//cannot happen
}
return result;
}
public WorkspaceResourceHandler(ServletResourceHandler<IStatus> statusHandler) {
this.statusHandler = statusHandler;
}
/**
* Returns a bit-mask of create options as specified by the request.
*/
private int getCreateOptions(HttpServletRequest request) {
int result = 0;
String optionString = request.getHeader(ProtocolConstants.HEADER_CREATE_OPTIONS);
if (optionString != null) {
for (String option : optionString.split(",")) { //$NON-NLS-1$
if (ProtocolConstants.OPTION_COPY.equalsIgnoreCase(option))
result |= CREATE_COPY;
else if (ProtocolConstants.OPTION_MOVE.equalsIgnoreCase(option))
result |= CREATE_MOVE;
else if (ProtocolConstants.OPTION_NO_OVERWRITE.equalsIgnoreCase(option))
result |= CREATE_NO_OVERWRITE;
}
}
return result;
}
private boolean getInit(JSONObject toAdd) {
return Boolean.valueOf(toAdd.optBoolean(ProtocolConstants.KEY_CREATE_IF_DOESNT_EXIST));
}
private boolean handleAddOrRemoveProject(HttpServletRequest request, HttpServletResponse response, WorkspaceInfo workspace) throws IOException, JSONException, ServletException {
//make sure required fields are set
JSONObject data = OrionServlet.readJSONRequest(request);
if (!data.isNull("Remove")) //$NON-NLS-1$
return handleRemoveProject(request, response, workspace);
int options = getCreateOptions(request);
if ((options & (CREATE_COPY | CREATE_MOVE)) != 0) {
return handleCopyMoveProject(request, response, workspace, data);
}
handleAddProject(request, response, workspace, data);
return true;
}
private boolean handleCopyMoveProject(HttpServletRequest request, HttpServletResponse response, WorkspaceInfo workspace, JSONObject data) throws ServletException, IOException {
//resolve the source location to a file system location
String sourceLocation = data.optString(ProtocolConstants.HEADER_LOCATION);
IFileStore source = null;
ProjectInfo sourceProject = null;
try {
if (sourceLocation != null) {
//could be either a workspace or file location
if (sourceLocation.startsWith(Activator.LOCATION_WORKSPACE_SERVLET)) {
sourceProject = projectForMetadataLocation(getMetaStore(), toOrionLocation(request, sourceLocation));
if (sourceProject != null)
source = sourceProject.getProjectStore();
} else {
//file location - remove servlet name prefix
source = resolveSourceLocation(request, sourceLocation);
}
}
} catch (Exception e) {
handleError(request, response, HttpServletResponse.SC_BAD_REQUEST, NLS.bind("Invalid source location: {0}", sourceLocation), e);
return true;
}
//null result means we didn't find a matching project
if (source == null) {
handleError(request, response, HttpServletResponse.SC_BAD_REQUEST, NLS.bind("Source does not exist: {0}", sourceLocation));
return true;
}
int options1 = getCreateOptions(request);
if (!validateOptions(request, response, options1))
return true;
//get the slug first
String destinationName = request.getHeader(ProtocolConstants.HEADER_SLUG);
//If the data has a name then it must be used due to UTF-8 issues with names Bug 376671
try {
if (data.has(ProtocolConstants.KEY_NAME)) {
destinationName = data.getString(ProtocolConstants.KEY_NAME);
}
} catch (JSONException e) {
//key is valid so cannot happen
}
if (!validateProjectName(workspace, destinationName, request, response))
return true;
if ((options1 & CREATE_MOVE) != 0) {
return handleMoveProject(request, response, workspace, source, sourceProject, sourceLocation, destinationName);
} else if ((options1 & CREATE_COPY) != 0) {
//first create the destination project
JSONObject projectObject = new JSONObject();
try {
projectObject.put(ProtocolConstants.KEY_NAME, destinationName);
} catch (JSONException e) {
//should never happen
throw new RuntimeException(e);
}
//copy the project data from source
ProjectInfo destinationProject = createProject(request, response, workspace, projectObject);
String sourceName = source.getName();
try {
source.copy(destinationProject.getProjectStore(), EFS.OVERWRITE, null);
} catch (CoreException e) {
handleError(request, response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, NLS.bind("Error copying project {0} to {1}", sourceName, destinationName));
return true;
}
URI baseLocation = getURI(request);
JSONObject result = ProjectInfoResourceHandler.toJSON(workspace, destinationProject, baseLocation);
OrionServlet.writeJSONResponse(request, response, result);
response.setHeader(ProtocolConstants.HEADER_LOCATION, result.optString(ProtocolConstants.KEY_LOCATION, "")); //$NON-NLS-1$
response.setStatus(HttpServletResponse.SC_CREATED);
return true;
}
//if we got here, it isn't a copy or a move, so we don't know how to handle the request
return false;
}
private ProjectInfo handleAddProject(HttpServletRequest request, HttpServletResponse response, WorkspaceInfo workspace, JSONObject data) throws IOException, ServletException {
ProjectInfo project = createProject(request, response, workspace, data);
if (project == null)
return null;
//serialize the new project in the response
//the baseLocation should be the workspace location
URI baseLocation = getURI(request);
JSONObject result = ProjectInfoResourceHandler.toJSON(workspace, project, baseLocation);
OrionServlet.writeJSONResponse(request, response, result);
//add project location to response header
response.setHeader(ProtocolConstants.HEADER_LOCATION, result.optString(ProtocolConstants.KEY_LOCATION));
response.setStatus(HttpServletResponse.SC_CREATED);
return project;
}
/**
* Creates a new project and returns the metadata of the created project. Returns <code>null</code>
* if there was an error creating the project. In the case of an error this method will handle setting an appropriate response
* to the servlet.
*/
private ProjectInfo createProject(HttpServletRequest request, HttpServletResponse response, WorkspaceInfo workspace, JSONObject data) throws ServletException {
JSONObject toAdd = data;
String id = toAdd.optString(ProtocolConstants.KEY_ID, null);
String name = toAdd.optString(ProtocolConstants.KEY_NAME, null);
//make sure required fields are set
if (name == null)
name = request.getHeader(ProtocolConstants.HEADER_SLUG);
if (!validateProjectName(workspace, name, request, response))
return null;
ProjectInfo project = new ProjectInfo();
if (id != null)
project.setUniqueId(id);
project.setFullName(name);
project.setWorkspaceId(workspace.getUniqueId());
String content = toAdd.optString(ProtocolConstants.KEY_CONTENT_LOCATION, null);
if (!isAllowedLinkDestination(content, request.getRemoteUser())) {
String msg = NLS.bind("Cannot link to server path {0}. Use the orion.file.allowedPaths property to specify server locations where content can be linked.", content);
statusHandler.handleRequest(request, response, new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_FORBIDDEN, msg, null));
return null;
}
try {
computeProjectLocation(request, project, content, getInit(toAdd));
//project creation will assign unique project id
getMetaStore().createProject(project);
} catch (CoreException e) {
String msg = "Error persisting project state";
statusHandler.handleRequest(request, response, new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg, e));
return null;
}
try {
getMetaStore().updateProject(project);
} catch (CoreException e) {
boolean authFail = handleAuthFailure(request, response, e);
//delete the project so we don't end up with a project in a bad location
try {
getMetaStore().deleteProject(workspace.getUniqueId(), project.getFullName());
} catch (CoreException e1) {
//swallow secondary error
LogHelper.log(e1);
}
if (authFail) {
return null;
}
//we are unable to write in the platform location!
String msg = NLS.bind("Cannot create project: {0}", project.getFullName());
statusHandler.handleRequest(request, response, new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg, e));
return null;
}
return project;
}
private boolean handleError(HttpServletRequest request, HttpServletResponse response, int httpCode, String message) throws ServletException {
return handleError(request, response, httpCode, message, null);
}
private boolean handleError(HttpServletRequest request, HttpServletResponse response, int httpCode, String message, Throwable cause) throws ServletException {
return statusHandler.handleRequest(request, response, new ServerStatus(IStatus.ERROR, httpCode, message, cause));
}
private boolean handleGetWorkspaceMetadata(HttpServletRequest request, HttpServletResponse response, WorkspaceInfo workspace) throws IOException {
//we need the base location for the workspace servlet. Since this is a GET
//on workspace servlet we need to strip off all but the first segment of the request path
URI requestLocation = getURI(request);
URI baseLocation;
try {
baseLocation = new URI(requestLocation.getScheme(), null, request.getServletPath(), null, null);
} catch (URISyntaxException e) {
//should never happen
throw new RuntimeException(e);
}
OrionServlet.writeJSONResponse(request, response, toJSON(workspace, requestLocation, baseLocation));
return true;
}
/**
* Implementation of project move. Returns whether the move requested was handled.
* Returns <code>false</code> if this method doesn't know how to interpret the request.
*/
private boolean handleMoveProject(HttpServletRequest request, HttpServletResponse response, WorkspaceInfo workspace, IFileStore source, ProjectInfo projectInfo, String sourceLocation, String destinationName) throws ServletException, IOException {
try {
final IMetaStore metaStore = getMetaStore();
boolean created = false;
if (projectInfo == null) {
//moving a folder to become a project
JSONObject data = new JSONObject();
try {
data.put(ProtocolConstants.KEY_NAME, destinationName);
} catch (JSONException e) {
//cannot happen
}
projectInfo = createProject(request, response, workspace, data);
if (projectInfo == null)
return true;
//move the contents
source.move(projectInfo.getProjectStore(), EFS.OVERWRITE, null);
created = true;
} else {
//a project move is simply a rename
projectInfo.setFullName(destinationName);
metaStore.updateProject(projectInfo);
}
//location doesn't change on move project
URI baseLocation = getURI(request);
JSONObject result = ProjectInfoResourceHandler.toJSON(workspace, projectInfo, baseLocation);
OrionServlet.writeJSONResponse(request, response, result);
response.setHeader(ProtocolConstants.HEADER_LOCATION, sourceLocation);
response.setStatus(created ? HttpServletResponse.SC_CREATED : HttpServletResponse.SC_OK);
} catch (CoreException e) {
String msg = NLS.bind("Error persisting project state: {0}", source.getName());
return handleError(request, response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg, e);
}
return true;
}
private boolean handlePutWorkspaceMetadata(HttpServletRequest request, HttpServletResponse response, WorkspaceInfo workspace) {
return false;
}
private boolean handleRemoveProject(HttpServletRequest request, HttpServletResponse response, WorkspaceInfo workspace) throws IOException, JSONException, ServletException {
IPath path = new Path(request.getPathInfo());
//format is /workspaceId/project/<projectId>
if (path.segmentCount() != 3)
return false;
String workspaceId = path.segment(0);
String projectName = path.segment(2);
try {
ProjectInfo project = getMetaStore().readProject(workspaceId, projectName);
if (project == null) {
//nothing to do if project does not exist
return true;
}
removeProject(request.getRemoteUser(), workspace, project);
} catch (CoreException e) {
ServerStatus error = new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Error removing project", e);
LogHelper.log(error);
return statusHandler.handleRequest(request, response, error);
}
return true;
}
@Override
public boolean handleRequest(HttpServletRequest request, HttpServletResponse response, WorkspaceInfo workspace) throws ServletException {
if (workspace == null)
return statusHandler.handleRequest(request, response, new Status(IStatus.ERROR, Activator.PI_SERVER_SERVLETS, "Workspace not specified"));
//we could split and handle different API versions here if needed
try {
switch (getMethod(request)) {
case GET :
return handleGetWorkspaceMetadata(request, response, workspace);
case PUT :
return handlePutWorkspaceMetadata(request, response, workspace);
case POST :
return handleAddOrRemoveProject(request, response, workspace);
case DELETE :
//TBD could also handle deleting the workspace itself
return handleRemoveProject(request, response, workspace);
default :
//fall through
}
} catch (IOException e) {
String msg = NLS.bind("Error handling request against workspace {0}", workspace.getUniqueId());
statusHandler.handleRequest(request, response, new Status(IStatus.ERROR, Activator.PI_SERVER_SERVLETS, msg, e));
} catch (JSONException e) {
return statusHandler.handleRequest(request, response, new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_BAD_REQUEST, "Syntax error in request", e));
}
return false;
}
/**
* Returns <code>true</code> if the provided content location URI is a valid
* place to store project data for the given user. Returns <code>false</code>
* otherwise.
*/
private boolean isAllowedLinkDestination(String content, String user) {
if (content == null)
return true;
String prefixes = PreferenceHelper.getString(ServerConstants.CONFIG_FILE_ALLOWED_PATHS);
if (prefixes == null)
prefixes = ServletTestingSupport.allowedPrefixes;
if (prefixes != null) {
StringTokenizer t = new StringTokenizer(prefixes, ","); //$NON-NLS-1$
while (t.hasMoreTokens()) {
String prefix = t.nextToken();
if (content.startsWith(prefix)) {
if (content.contains("..")) {
// Bugzilla 420795 do not allow parent in paths
return false;
}
return true;
}
}
}
URI contentURI = null;
//use the content location specified by the user
try {
URI candidate = new URI(content);
//check if we support this scheme
String scheme = candidate.getScheme();
if (scheme != null) {
if (EFS.getFileSystem(scheme) != null)
contentURI = candidate;
//we only restrict local file system access
if (!EFS.SCHEME_FILE.equals(scheme))
return true;
}
} catch (URISyntaxException e) {
//if this is not a valid URI try to parse it as file path below
} catch (CoreException e) {
//if we don't support given scheme try to parse as location as a file path below
}
String userArea = System.getProperty(Activator.PROP_USER_AREA);
if (userArea != null) {
IPath path = new Path(userArea).append(user);
if (contentURI == null)
contentURI = new File(content).toURI();
if (contentURI.toString().startsWith(path.toFile().toURI().toString()))
return true;
}
return false;
}
/**
* Asserts that request options are valid. If options are not valid then this method handles the request response and return false. If the options
* are valid this method return true.
*/
private boolean validateOptions(HttpServletRequest request, HttpServletResponse response, int options) throws ServletException {
//operation cannot be both copy and move
int copyMove = CREATE_COPY | CREATE_MOVE;
if ((options & copyMove) == copyMove) {
handleError(request, response, HttpServletResponse.SC_BAD_REQUEST, "Syntax error in request");
return false;
}
return true;
}
/**
* Validates that the provided project name is valid. Returns <code>true</code> if the
* project name is valid, and <code>false</code> otherwise. This method takes care of
* setting the error response when the project name is not valid.
*/
private boolean validateProjectName(WorkspaceInfo workspace, String name, HttpServletRequest request, HttpServletResponse response) throws ServletException {
if (name == null || name.trim().length() == 0) {
statusHandler.handleRequest(request, response, new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_BAD_REQUEST, "Project name cannot be empty", null));
return false;
}
if (name.contains("/") || name.equals("workspace") || name.endsWith(".json") || name.equals("user") || name.contains("OrionContent")) { //$NON-NLS-1$
statusHandler.handleRequest(request, response, new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_BAD_REQUEST, NLS.bind("Invalid project name: {0}", name), null));
return false;
}
try {
if (getMetaStore().readProject(workspace.getUniqueId(), name) != null) {
statusHandler.handleRequest(request, response, new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_BAD_REQUEST, NLS.bind("Duplicate project name: {0}", name), null));
return false;
}
} catch (CoreException e) {
LogHelper.log(e);
//this is just pre-validation so let it continue and fail in the actual creation
}
return true;
}
}