package com.atlassian.labs.speakeasy.rest; import com.atlassian.labs.speakeasy.external.PluginType; import com.atlassian.labs.speakeasy.external.SpeakeasyService; import com.atlassian.labs.speakeasy.external.UnauthorizedAccessException; import com.atlassian.labs.speakeasy.manager.PluginOperationFailedException; import com.atlassian.labs.speakeasy.model.*; import com.atlassian.plugins.rest.common.json.JaxbJsonMarshaller; import com.atlassian.plugins.rest.common.security.RequiresXsrfCheck; import com.atlassian.sal.api.user.UserManager; import com.atlassian.sal.api.xsrf.XsrfTokenValidator; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.FileUploadException; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.json.JSONException; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import java.io.File; import java.net.URI; import java.net.URISyntaxException; import java.util.List; import static com.atlassian.labs.speakeasy.util.KeyExtractor.createExtractableTempFile; import static com.atlassian.labs.speakeasy.util.KeyExtractor.extractFromFilename; /** * */ @Path("/plugins") public class PluginsResource { private final SpeakeasyService speakeasyService; private final UserManager userManager; private final JaxbJsonMarshaller jaxbJsonMarshaller; private final XsrfTokenValidator xsrfTokenValidator; private static final Logger log = LoggerFactory.getLogger(PluginsResource.class); public PluginsResource(UserManager userManager, JaxbJsonMarshaller jaxbJsonMarshaller, SpeakeasyService speakeasyService, XsrfTokenValidator xsrfTokenValidator) { this.userManager = userManager; this.jaxbJsonMarshaller = jaxbJsonMarshaller; this.speakeasyService = speakeasyService; this.xsrfTokenValidator = xsrfTokenValidator; } @GET @Path("atom") @Produces("application/atom+xml") public Response atom() throws UnauthorizedAccessException { String user = userManager.getRemoteUsername(); return Response.ok().entity(speakeasyService.getPluginFeed(user)).build(); } @DELETE @Path("plugin/{pluginKey}") @Produces("application/json") public Response uninstallPlugin(@PathParam("pluginKey") String pluginKey) throws UnauthorizedAccessException { String user = userManager.getRemoteUsername(); UserPlugins entity = speakeasyService.uninstallPlugin(pluginKey, user); return Response.ok().entity(entity).build(); } @GET @Path("download/project/{pluginKey}-project.zip") @Produces("application/octet-stream") public Response getAsAmpsProject(@PathParam("pluginKey") String pluginKey) throws UnauthorizedAccessException { String user = userManager.getRemoteUsername(); File file = speakeasyService.getPluginAsProject(pluginKey, user); return Response.ok().entity(file).build(); } @GET @Path("screenshot/{pluginKey}.png") @Produces("image/png") public Response getScreenshot(@PathParam("pluginKey") String pluginKey) throws UnauthorizedAccessException, URISyntaxException { String user = userManager.getRemoteUsername(); String url = speakeasyService.getScreenshotUrl(pluginKey, user); return Response.status(301).location(new URI(url)).build(); } @GET @Path("download/extension/{pluginKeyAndExtension}") @Produces("application/octet-stream") public Response getAsExtension(@PathParam("pluginKeyAndExtension") String pluginKeyAndExtension) throws UnauthorizedAccessException { String user = userManager.getRemoteUsername(); int pos = pluginKeyAndExtension.lastIndexOf('.'); if (pos > 0) { File file = speakeasyService.getPluginArtifact(pluginKeyAndExtension.substring(0, pos), user); return Response.ok().entity(file).build(); } else { throw new PluginOperationFailedException("Missing extension on '" + pluginKeyAndExtension, null); } } @POST @Path("search") @Produces("application/json") @RequiresXsrfCheck public Response search(@FormParam("q") String searchQuery) throws UnauthorizedAccessException { SearchResults entity = speakeasyService.search(searchQuery, userManager.getRemoteUsername()); return Response.ok().entity(entity).build(); } @POST @Path("fork/{pluginKey}") @Produces("application/json") @RequiresXsrfCheck public Response fork(@PathParam("pluginKey") String pluginKey, @FormParam("description") String description) throws UnauthorizedAccessException { UserPlugins entity = speakeasyService.fork(pluginKey, userManager.getRemoteUsername(), description); return Response.ok().entity(entity).build(); } @POST @Path("feedback/{pluginKey}") @Produces("application/json") @RequiresXsrfCheck public Response feedback(@PathParam("pluginKey") String pluginKey, Feedback feedback) throws UnauthorizedAccessException { speakeasyService.sendFeedback(pluginKey, feedback, userManager.getRemoteUsername()); return Response.ok().build(); } @POST @Path("broken/{pluginKey}") @Produces("application/json") @RequiresXsrfCheck public Response broken(@PathParam("pluginKey") String pluginKey, Feedback feedback) throws UnauthorizedAccessException { speakeasyService.reportBroken(pluginKey, feedback, userManager.getRemoteUsername()); return Response.ok().build(); } @POST @Path("favorite/{pluginKey}") @Produces("application/json") @RequiresXsrfCheck public Response favorite(@PathParam("pluginKey") String pluginKey) throws UnauthorizedAccessException { UserPlugins entity = speakeasyService.favorite(pluginKey, userManager.getRemoteUsername()); return Response.ok().entity(entity).build(); } @DELETE @Path("favorite/{pluginKey}") @Produces("application/json") public Response unfavorite(@PathParam("pluginKey") String pluginKey) throws UnauthorizedAccessException { UserPlugins entity = speakeasyService.unfavorite(pluginKey, userManager.getRemoteUsername()); return Response.ok().entity(entity).build(); } @POST @Path("create/{pluginKey}") @Produces("application/json") @RequiresXsrfCheck public Response create(@PathParam("pluginKey") String pluginKey, @FormParam("description") String description, @FormParam("name") String name) throws UnauthorizedAccessException { UserPlugins entity = speakeasyService.createExtension(pluginKey, PluginType.ZIP, userManager.getRemoteUsername(), description, name); return Response.ok().entity(entity).build(); } @DELETE @Path("global/{pluginKey}") @Produces("application/json") public Response disableGlobally(@PathParam("pluginKey") String pluginKey) throws UnauthorizedAccessException { UserPlugins entity = speakeasyService.disableGlobally(pluginKey, userManager.getRemoteUsername()); return Response.ok().entity(entity).build(); } @PUT @Path("global/{pluginKey}") @Produces("application/json") @RequiresXsrfCheck public Response enableGlobally(@PathParam("pluginKey") String pluginKey) throws UnauthorizedAccessException { UserPlugins entity = speakeasyService.enableGlobally(pluginKey, userManager.getRemoteUsername()); return Response.ok().entity(entity).build(); } @GET @Path("plugin/{pluginKey}/index") @Produces("application/json") public Response getIndex(@PathParam("pluginKey") String pluginKey) throws UnauthorizedAccessException { String user = userManager.getRemoteUsername(); PluginIndex index = new PluginIndex(); index.setFiles(speakeasyService.getPluginFileNames(pluginKey, user)); return Response.ok().entity(index).build(); } @GET @Path("plugin/{pluginKey}/file") @Produces("text/plain") public Response getFileText(@PathParam("pluginKey") String pluginKey, @QueryParam("path") String fileName) throws UnauthorizedAccessException { String user = userManager.getRemoteUsername(); Object pluginFile = speakeasyService.getPluginFile(pluginKey, fileName, user); return Response.ok().entity(pluginFile).build(); } @GET @Path("plugin/{pluginKey}/binary") @Produces("application/octet-stream") public Response getFileBinary(@PathParam("pluginKey") String pluginKey, @QueryParam("path") String fileName) throws UnauthorizedAccessException { String user = userManager.getRemoteUsername(); Object pluginFile = speakeasyService.getPluginFile(pluginKey, fileName, user); return Response.ok().entity(pluginFile).build(); } @PUT @Path("plugin/{pluginKey}/file") @Consumes("text/plain") @Produces("application/json") public Response saveAndRebuild(@PathParam("pluginKey") String pluginKey, @QueryParam("path") String fileName, String contents) throws UnauthorizedAccessException { String user = userManager.getRemoteUsername(); final UserExtension extension = speakeasyService.saveAndRebuild(pluginKey, fileName, contents, user); return Response.ok().entity(extension).build(); } @POST @Produces("text/html") public Response uploadPlugin(@Context HttpServletRequest request) { String user = userManager.getRemoteUsername(request); try { xsrfTokenValidator.validateFormEncodedToken(request); File uploadedFile = extractPluginFile(request); UserPlugins plugins = speakeasyService.installPlugin(uploadedFile, user); return Response.ok().entity(wrapBodyInTextArea(jaxbJsonMarshaller.marshal(plugins))).build(); } catch (PluginOperationFailedException e) { log.error(e.getError(), e.getCause()); return Response.ok().entity(wrapBodyInTextArea(createErrorJson(user, e))).build(); } catch (UnauthorizedAccessException e) { return Response.ok().entity(wrapBodyInTextArea(createErrorJson(user, e))).build(); } catch (RuntimeException e) { log.error(e.getMessage(), e); return Response.ok().entity(wrapBodyInTextArea(createErrorJson(user, e))).build(); } } private String createErrorJson(String user, Exception e) { JSONObject obj = new JSONObject(); try { obj.put("error", e.toString()); obj.put("plugins", new JSONObject(jaxbJsonMarshaller.marshal(speakeasyService.getRemotePluginList(user)))); } catch (JSONException e1) { throw new PluginOperationFailedException("Unable to serialize error", e1, null); } catch (UnauthorizedAccessException e1) { throw new PluginOperationFailedException("Unauthorized access", e1, null); } return obj.toString(); } private File extractPluginFile(HttpServletRequest request) { if (!ServletFileUpload.isMultipartContent(request)) { return null; } DiskFileItemFactory factory = new DiskFileItemFactory(); factory.setSizeThreshold(1024 * 1024); ServletFileUpload upload = new ServletFileUpload(factory); upload.setFileSizeMax(1024 * 1024 * 10); List<FileItem> items = null; try { items = upload.parseRequest(request); } catch (FileUploadException e) { throw new RuntimeException(e); } File pluginFile = null; if (items != null) { for (FileItem item : items) { if (!item.isFormField() && item.getSize() > 0 && "plugin-file".equals(item.getFieldName())) { try { String fileName = processFileName(item.getName()); String key = extractFromFilename(fileName); pluginFile = createExtractableTempFile(key, fileName); item.write(pluginFile); } catch (Exception e) { throw new RuntimeException(e); } } } } return pluginFile; } private String wrapBodyInTextArea(String body) { return "JSON_MARKER||" + body + "||"; } private String processFileName(String fileNameInput) { return fileNameInput.substring(fileNameInput.lastIndexOf("\\") + 1, fileNameInput.length()); } }