001/*
002 * (C) Copyright 2014-2018 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 *     Michal Obrebski - Nuxeo
018 */
019
020package org.nuxeo.easyshare;
021
022import java.util.Date;
023import java.util.HashMap;
024import java.util.Map;
025
026import javax.ws.rs.DefaultValue;
027import javax.ws.rs.GET;
028import javax.ws.rs.Path;
029import javax.ws.rs.PathParam;
030import javax.ws.rs.Produces;
031import javax.ws.rs.QueryParam;
032import javax.ws.rs.core.Response;
033
034import org.apache.commons.lang3.StringUtils;
035import org.apache.commons.logging.Log;
036import org.apache.commons.logging.LogFactory;
037import org.nuxeo.ecm.automation.AutomationService;
038import org.nuxeo.ecm.automation.OperationChain;
039import org.nuxeo.ecm.automation.OperationContext;
040import org.nuxeo.ecm.automation.jaxrs.io.documents.PaginableDocumentModelListImpl;
041import org.nuxeo.ecm.core.api.Blob;
042import org.nuxeo.ecm.core.api.CoreSession;
043import org.nuxeo.ecm.core.api.DocumentModel;
044import org.nuxeo.ecm.core.api.IdRef;
045import org.nuxeo.ecm.core.api.NuxeoException;
046import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
047import org.nuxeo.ecm.platform.ec.notification.NotificationConstants;
048import org.nuxeo.ecm.platform.ec.notification.email.EmailHelper;
049import org.nuxeo.ecm.platform.ec.notification.service.NotificationServiceHelper;
050import org.nuxeo.ecm.platform.notification.api.Notification;
051import org.nuxeo.ecm.webengine.model.WebObject;
052import org.nuxeo.ecm.webengine.model.impl.ModuleRoot;
053import org.nuxeo.runtime.api.Framework;
054
055/**
056 * The root entry for the WebEngine module.
057 *
058 * @author mikeobrebski
059 */
060@Path("/easyshare")
061@Produces("text/html;charset=UTF-8")
062@WebObject(type = "EasyShare")
063public class EasyShare extends ModuleRoot {
064
065    private static final String DEFAULT_PAGE_INDEX = "0";
066
067    private static final Long PAGE_SIZE = 20L;
068
069    private static final String SHARE_DOC_TYPE = "EasyShareFolder";
070
071    protected final Log log = LogFactory.getLog(EasyShare.class);
072
073    @GET
074    public Object doGet() {
075        return getView("index");
076    }
077
078    public EasyShareUnrestrictedRunner buildUnrestrictedRunner(final String docId, final Long pageIndex) {
079
080        return new EasyShareUnrestrictedRunner() {
081            @Override
082            public Object run(CoreSession session, IdRef docRef) throws NuxeoException {
083                if (session.exists(docRef)) {
084                    DocumentModel docShare = session.getDocument(docRef);
085
086                    if (!SHARE_DOC_TYPE.equals(docShare.getType())) {
087                        return Response.serverError().status(Response.Status.NOT_FOUND).build();
088                    }
089
090                    if (!checkIfShareIsValid(docShare)) {
091                        return getView("expired").arg("docShare", docShare);
092                    }
093
094                    DocumentModel document = session.getDocument(new IdRef(docId));
095
096                    String query = buildQuery(document);
097
098                    if (query == null) {
099                        return getView("denied");
100                    }
101
102                    try (OperationContext opCtx = new OperationContext(session)) {
103                        OperationChain chain = new OperationChain("getEasyShareContent");
104                        chain.add("Document.Query")
105                             .set("query", query)
106                             .set("currentPageIndex", pageIndex)
107                             .set("pageSize", PAGE_SIZE);
108
109                        AutomationService automationService = Framework.getService(AutomationService.class);
110                        PaginableDocumentModelListImpl paginable = (PaginableDocumentModelListImpl) automationService.run(
111                                opCtx, chain);
112
113                        try (OperationContext ctx = new OperationContext(session)) {
114                            ctx.setInput(docShare);
115
116                            // Audit Log
117                            Map<String, Object> params = new HashMap<>();
118                            params.put("event", "Access");
119                            params.put("category", "Document");
120                            params.put("comment", "IP: " + getIpAddr());
121                            automationService.run(ctx, "Audit.Log", params);
122                        }
123
124                        return getView("folderList")
125                                                    .arg("isFolder",
126                                                            document.isFolder()
127                                                                    && !SHARE_DOC_TYPE.equals(document.getType())) // Backward
128                                                                                                                   // compatibility
129                                                                                                                   // to
130                                                                                                                   // non-collection
131                                                    .arg("currentPageIndex", paginable.getCurrentPageIndex())
132                                                    .arg("numberOfPages", paginable.getNumberOfPages())
133                                                    .arg("docShare", docShare)
134                                                    .arg("docList", paginable)
135                                                    .arg("previousPageAvailable", paginable.isPreviousPageAvailable())
136                                                    .arg("nextPageAvailable", paginable.isNextPageAvailable())
137                                                    .arg("currentPageStatus",
138                                                            paginable.getProvider().getCurrentPageStatus());
139
140                    } catch (Exception ex) {
141                        log.error(ex.getMessage());
142                        return getView("denied");
143                    }
144
145                } else {
146                    return getView("denied");
147                }
148            }
149        };
150    }
151
152    protected static String buildQuery(DocumentModel documentModel) {
153
154        // Backward compatibility to non-collection
155        if (documentModel.isFolder() && !SHARE_DOC_TYPE.equals(documentModel.getType())) {
156            return " SELECT * FROM Document WHERE ecm:parentId = '" + documentModel.getId() + "' AND "
157                    + "ecm:mixinType != 'HiddenInNavigation' AND " + "ecm:mixinType != 'NotCollectionMember' AND "
158                    + "ecm:isVersion = 0 AND " + "ecm:isTrashed = 0" + "ORDER BY dc:title";
159
160        } else if (SHARE_DOC_TYPE.equals(documentModel.getType())) {
161            return "SELECT * FROM Document where ecm:mixinType != 'HiddenInNavigation' AND "
162                    + "ecm:isVersion = 0 AND ecm:isTrashed = 0 " + "AND collectionMember:collectionIds/* = '"
163                    + documentModel.getId() + "'" + "OR ecm:parentId = '" + documentModel.getId() + "'"
164                    + "ORDER BY dc:title";
165        }
166        return null;
167    }
168
169    private boolean checkIfShareIsValid(DocumentModel docShare) {
170        Date today = new Date();
171        Date expired = docShare.getProperty("dc:expired").getValue(Date.class);
172        if (expired == null) {
173            log.error("Invalid null dc:expired for share: " + docShare.getTitle() + " (" + docShare.getId() + ")");
174            // consider the share as expired
175            return false;
176        }
177        if (today.after(expired)) {
178
179            // Email notification
180            Map<String, Object> mail = new HashMap<>();
181            sendNotification("easyShareExpired", docShare, mail);
182
183            return false;
184        }
185        return true;
186    }
187
188    @Path("{shareId}/{folderId}")
189    @GET
190    public Object getFolderListing(@PathParam("shareId") String shareId, @PathParam("folderId") final String folderId,
191            @DefaultValue(DEFAULT_PAGE_INDEX) @QueryParam("p") final Long pageIndex) {
192        return buildUnrestrictedRunner(folderId, pageIndex).runUnrestricted(shareId);
193    }
194
195    @Path("{shareId}")
196    @GET
197    public Object getShareListing(@PathParam("shareId") String shareId,
198            @DefaultValue(DEFAULT_PAGE_INDEX) @QueryParam("p") Long pageIndex) {
199        return buildUnrestrictedRunner(shareId, pageIndex).runUnrestricted(shareId);
200    }
201
202    public String getFileName(DocumentModel doc) throws NuxeoException {
203        BlobHolder blobHolder = doc.getAdapter(BlobHolder.class);
204        if (blobHolder != null && blobHolder.getBlob() != null) {
205            return blobHolder.getBlob().getFilename();
206        }
207        return doc.getName();
208    }
209
210    @GET
211    @Path("{shareId}/{fileId}/{fileName}")
212    public Response getFileStream(@PathParam("shareId") final String shareId, @PathParam("fileId") String fileId)
213            throws NuxeoException {
214
215        return (Response) new EasyShareUnrestrictedRunner() {
216            @Override
217            public Object run(CoreSession session, IdRef docRef) throws NuxeoException {
218                if (session.exists(docRef)) {
219                    DocumentModel doc = session.getDocument(docRef);
220                    try (OperationContext ctx = new OperationContext(session)) {
221                        DocumentModel docShare = session.getDocument(new IdRef(shareId));
222
223                        if (!checkIfShareIsValid(docShare)) {
224                            return Response.serverError().status(Response.Status.NOT_FOUND).build();
225                        }
226
227                        Blob blob = doc.getAdapter(BlobHolder.class).getBlob();
228
229                        // Audit Log
230                        ctx.setInput(doc);
231
232                        // Audit.Log automation parameter setting
233                        Map<String, Object> params = new HashMap<>();
234                        params.put("event", "Download");
235                        params.put("category", "Document");
236                        params.put("comment", "IP: " + getIpAddr());
237                        AutomationService service = Framework.getService(AutomationService.class);
238                        service.run(ctx, "Audit.Log", params);
239
240                        if (doc.isProxy()) {
241                            DocumentModel liveDoc = session.getSourceDocument(docRef);
242                            ctx.setInput(liveDoc);
243                            service.run(ctx, "Audit.Log", params);
244
245                        }
246
247                        // Email notification
248                        Map<String, Object> mail = new HashMap<>();
249                        mail.put("filename", blob.getFilename());
250                        sendNotification("easyShareDownload", docShare, mail);
251
252                        return Response.ok(blob.getStream(), blob.getMimeType()).build();
253
254                    } catch (Exception ex) {
255                        log.error("error ", ex);
256                        return Response.serverError().status(Response.Status.NOT_FOUND).build();
257                    }
258
259                } else {
260                    return Response.serverError().status(Response.Status.NOT_FOUND).build();
261                }
262            }
263        }.runUnrestricted(fileId);
264
265    }
266
267    public void sendNotification(String notification, DocumentModel docShare, Map<String, Object> mail) {
268
269        Boolean hasNotification = docShare.getProperty("eshare:hasNotification").getValue(Boolean.class);
270
271        if (hasNotification) {
272            // Email notification
273            String email = docShare.getProperty("eshare:contactEmail").getValue(String.class);
274            if (StringUtils.isEmpty(email)) {
275                return;
276            }
277            try {
278                log.debug("Easyshare: starting email");
279                EmailHelper emailHelper = new EmailHelper();
280                Map<String, Object> mailProps = new HashMap<>();
281                mailProps.put("mail.from", Framework.getProperty("mail.from", "system@nuxeo.com"));
282                mailProps.put("mail.to", email);
283                mailProps.put("ip", getIpAddr());
284                mailProps.put("docShare", docShare);
285
286                try {
287                    Notification notif = NotificationServiceHelper.getNotificationService()
288                                                                  .getNotificationByName(notification);
289
290                    if (notif.getSubjectTemplate() != null) {
291                        mailProps.put(NotificationConstants.SUBJECT_TEMPLATE_KEY, notif.getSubjectTemplate());
292                    }
293
294                    mailProps.put(NotificationConstants.SUBJECT_KEY,
295                            NotificationServiceHelper.getNotificationService().getEMailSubjectPrefix() + " "
296                                    + notif.getSubject());
297                    mailProps.put(NotificationConstants.TEMPLATE_KEY, notif.getTemplate());
298
299                    mailProps.putAll(mail);
300
301                    emailHelper.sendmail(mailProps);
302
303                } catch (NuxeoException e) {
304                    log.warn(e.getMessage());
305                }
306
307                log.debug("Easyshare: completed email");
308            } catch (Exception ex) {
309                log.error("Cannot send easyShare notification email", ex);
310            }
311        }
312    }
313
314    protected String getIpAddr() {
315        String ip = request.getHeader("X-FORWARDED-FOR");
316        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
317            ip = request.getHeader("Proxy-Client-IP");
318        }
319        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
320            ip = request.getRemoteAddr();
321        }
322        return ip;
323    }
324}