001/* 002 * (C) Copyright 2013-2017 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Damien Metzler 018 * Florent Guillaume 019 */ 020package org.nuxeo.ecm.restapi.server.jaxrs.blob; 021 022import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; 023import static org.nuxeo.ecm.core.io.download.DownloadService.BLOBHOLDER_PREFIX; 024 025import java.io.Serializable; 026 027import javax.servlet.http.HttpServletRequest; 028import javax.ws.rs.DELETE; 029import javax.ws.rs.GET; 030import javax.ws.rs.PUT; 031import javax.ws.rs.core.Context; 032import javax.ws.rs.core.EntityTag; 033import javax.ws.rs.core.Request; 034import javax.ws.rs.core.Response; 035 036import org.nuxeo.ecm.core.api.Blob; 037import org.nuxeo.ecm.core.api.CoreSession; 038import org.nuxeo.ecm.core.api.DocumentModel; 039import org.nuxeo.ecm.core.api.NuxeoException; 040import org.nuxeo.ecm.core.api.blobholder.BlobHolder; 041import org.nuxeo.ecm.core.api.blobholder.DocumentBlobHolder; 042import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; 043import org.nuxeo.ecm.core.api.versioning.VersioningService; 044import org.nuxeo.ecm.core.schema.SchemaManager; 045import org.nuxeo.ecm.core.schema.types.Schema; 046import org.nuxeo.ecm.platform.web.common.ServletHelper; 047import org.nuxeo.ecm.webengine.forms.FormData; 048import org.nuxeo.ecm.webengine.model.WebObject; 049import org.nuxeo.ecm.webengine.model.exceptions.WebResourceNotFoundException; 050import org.nuxeo.ecm.webengine.model.impl.DefaultObject; 051import org.nuxeo.runtime.api.Framework; 052 053/** 054 * @since 5.8 055 */ 056@WebObject(type = "blob") 057public class BlobObject extends DefaultObject { 058 059 protected DocumentModel doc; 060 061 protected DocumentBlobHolder blobHolder; 062 063 // null if blob is not directly from a property 064 protected String xpath; 065 066 @Override 067 protected void initialize(Object... args) { 068 super.initialize(args); 069 if (args.length != 2) { 070 throw new IllegalArgumentException("BlobObject takes 2 parameters"); 071 } 072 String path = (String) args[0]; 073 doc = (DocumentModel) args[1]; 074 BlobHolder bh = doc.getAdapter(BlobHolder.class); 075 if (path == null) { 076 // use default blob holder 077 if (bh == null) { 078 throw new WebResourceNotFoundException("No BlobHolder found"); 079 } 080 if (!(bh instanceof DocumentBlobHolder)) { 081 throw new WebResourceNotFoundException("Unknown BlobHolder class: " + bh.getClass().getName()); 082 } 083 blobHolder = (DocumentBlobHolder) bh; 084 } else if (path.startsWith(BLOBHOLDER_PREFIX)) { 085 // use index in default blob holder 086 // decoding logic from DownloadServiceImpl 087 if (bh == null) { 088 throw new WebResourceNotFoundException("No BlobHolder found"); 089 } 090 if (!(bh instanceof DocumentBlobHolder)) { 091 throw new WebResourceNotFoundException("Unknown BlobHolder class: " + bh.getClass().getName()); 092 } 093 // suffix check 094 String suffix = path.substring(BLOBHOLDER_PREFIX.length()); 095 int index; 096 try { 097 index = Integer.parseInt(suffix); 098 } catch (NumberFormatException e) { 099 throw new WebResourceNotFoundException("Invalid xpath: " + path); 100 } 101 if (!suffix.equals(Integer.toString(index))) { 102 // attempt to use a non-canonical integer, could be used to bypass 103 // a permission function checking just "blobholder:1" and receiving "blobholder:01" 104 throw new WebResourceNotFoundException("Invalid xpath: " + path); 105 } 106 // find best BlobHolder to use 107 if (index == 0) { 108 blobHolder = (DocumentBlobHolder) bh; 109 } else { 110 blobHolder = ((DocumentBlobHolder) bh).asDirectBlobHolder(index); 111 } 112 } else { 113 // use xpath 114 // if the default adapted blob holder is the one with the same xpath, use it 115 if (bh instanceof DocumentBlobHolder && ((DocumentBlobHolder) bh).getXpath().equals(path)) { 116 blobHolder = (DocumentBlobHolder) bh; 117 } else { 118 // checking logic from DownloadServiceImpl 119 if (!path.contains(":")) { 120 // attempt to use a xpath not prefix-qualified, could be used to bypass 121 // a permission function checking just "file:content" and receiving "content" 122 // try to add prefix 123 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 124 // TODO precompute this in SchemaManagerImpl 125 int slash = path.indexOf('/'); 126 String first = slash == -1 ? path : path.substring(0, slash); 127 for (Schema schema : schemaManager.getSchemas()) { 128 if (!schema.getNamespace().hasPrefix()) { 129 // schema without prefix, try it 130 if (schema.getField(first) != null) { 131 path = schema.getName() + ":" + path; 132 break; 133 } 134 } 135 } 136 } 137 if (!path.contains(":")) { 138 throw new WebResourceNotFoundException("Invalid xpath: " + path); 139 } 140 blobHolder = new DocumentBlobHolder(doc, path); 141 } 142 } 143 xpath = blobHolder.getXpath(); 144 } 145 146 @Override 147 public <A> A getAdapter(Class<A> adapter) { 148 if (Blob.class.isAssignableFrom(adapter)) { 149 return adapter.cast(blobHolder.getBlob()); 150 } 151 if (BlobHolder.class.isAssignableFrom(adapter)) { 152 return adapter.cast(blobHolder); 153 } 154 return super.getAdapter(adapter); 155 } 156 157 public DocumentBlobHolder getBlobHolder() { 158 return blobHolder; 159 } 160 161 @GET 162 public Object doGet(@Context Request request) { 163 if (blobHolder instanceof DocumentBlobHolder) { 164 // managed by DocumentBlobHolderWriter 165 return blobHolder; 166 } else { 167 // managed by BlobWriter 168 Blob blob; 169 try { 170 blob = blobHolder.getBlob(); 171 } catch (PropertyNotFoundException e) { 172 throw new WebResourceNotFoundException("Invalid xpath"); 173 } 174 return blob; 175 } 176 } 177 178 /** 179 * @deprecated since 7.3. Now returns directly the Blob and use default {@code BlobWriter}. 180 */ 181 @Deprecated 182 public static Response buildResponseFromBlob(Request request, HttpServletRequest httpServletRequest, Blob blob, 183 String filename) { 184 if (filename == null) { 185 filename = blob.getFilename(); 186 } 187 188 String digest = blob.getDigest(); 189 EntityTag etag = digest == null ? null : new EntityTag(digest); 190 if (etag != null) { 191 Response.ResponseBuilder builder = request.evaluatePreconditions(etag); 192 if (builder != null) { 193 return builder.build(); 194 } 195 } 196 String contentDisposition = ServletHelper.getRFC2231ContentDisposition(httpServletRequest, filename); 197 // cached resource did change or no ETag -> serve updated content 198 Response.ResponseBuilder builder = Response.ok(blob).header("Content-Disposition", contentDisposition).type( 199 blob.getMimeType()); 200 if (etag != null) { 201 builder.tag(etag); 202 } 203 return builder.build(); 204 } 205 206 @DELETE 207 public Response doDelete() { 208 try { 209 doc.getProperty(xpath).remove(); 210 } catch (PropertyNotFoundException e) { 211 throw new NuxeoException("Failed to delete attached file into property: " + xpath, e, SC_BAD_REQUEST); 212 } 213 CoreSession session = ctx.getCoreSession(); 214 session.saveDocument(doc); 215 session.save(); 216 return Response.noContent().build(); 217 } 218 219 @PUT 220 public Response doPut() { 221 FormData form = ctx.getForm(); 222 Blob blob = form.getFirstBlob(); 223 if (blob == null) { 224 throw new IllegalArgumentException("Could not find any uploaded file"); 225 } 226 try { 227 doc.setPropertyValue(xpath, (Serializable) blob); 228 } catch (PropertyNotFoundException e) { 229 throw new NuxeoException("Failed to attach file into property: " + xpath, e, SC_BAD_REQUEST); 230 } 231 // make snapshot 232 doc.putContextData(VersioningService.VERSIONING_OPTION, form.getVersioningOption()); 233 CoreSession session = ctx.getCoreSession(); 234 session.saveDocument(doc); 235 session.save(); 236 return Response.ok("blob updated").build(); 237 } 238 239}