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.schema.SchemaManager;
044import org.nuxeo.ecm.core.schema.types.Schema;
045import org.nuxeo.ecm.core.versioning.VersioningService;
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}