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