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    if (today.after(docShare.getProperty("dc:expired").getValue(Date.class))) {
168
169      //Email notification
170      Map<String, Object> mail = new HashMap<>();
171      sendNotification("easyShareExpired", docShare, mail);
172
173      return false;
174    }
175    return true;
176  }
177
178
179  private static AutomationService getAutomationService() {
180    if (automationService == null) {
181      automationService = Framework.getService(AutomationService.class);
182    }
183    return automationService;
184  }
185
186
187  @Path("{shareId}/{folderId}")
188  @GET
189  public Object getFolderListing(@PathParam("shareId") String shareId, @PathParam("folderId") final String folderId,
190                                 @DefaultValue(DEFAULT_PAGE_INDEX) @QueryParam("p") final Long pageIndex) {
191    return buildUnrestrictedRunner(folderId, pageIndex).runUnrestricted(shareId);
192  }
193
194  @Path("{shareId}")
195  @GET
196  public Object getShareListing(@PathParam("shareId") String shareId,
197                                @DefaultValue(DEFAULT_PAGE_INDEX) @QueryParam("p") Long pageIndex) {
198    return buildUnrestrictedRunner(shareId, pageIndex).runUnrestricted(shareId);
199  }
200
201  public String getFileName(DocumentModel doc) throws NuxeoException {
202    BlobHolder blobHolder = doc.getAdapter(BlobHolder.class);
203    if (blobHolder != null && blobHolder.getBlob() != null) {
204      return blobHolder.getBlob().getFilename();
205    }
206    return doc.getName();
207  }
208
209  @GET
210  @Path("{shareId}/{fileId}/{fileName}")
211  public Response getFileStream(@PathParam("shareId") final String shareId, @PathParam("fileId") String fileId) throws NuxeoException {
212
213    return (Response) new EasyShareUnrestrictedRunner() {
214      @Override
215      public Object run(CoreSession session, IdRef docRef) throws NuxeoException {
216                if (session.exists(docRef)) {
217                    DocumentModel doc = session.getDocument(docRef);
218                    try (OperationContext ctx = new OperationContext(session)) {
219                        DocumentModel docShare = session.getDocument(new IdRef(shareId));
220
221                        if (!checkIfShareIsValid(docShare)) {
222                            return Response.serverError().status(Response.Status.NOT_FOUND).build();
223                        }
224
225                        Blob blob = doc.getAdapter(BlobHolder.class).getBlob();
226
227                        // Audit Log
228                        ctx.setInput(doc);
229
230                        // Audit.Log automation parameter setting
231                        Map<String, Object> params = new HashMap<>();
232                        params.put("event", "Download");
233                        params.put("category", "Document");
234                        params.put("comment", "IP: " + getIpAddr());
235                        AutomationService service = Framework.getService(AutomationService.class);
236                        service.run(ctx, "Audit.Log", params);
237
238                        if (doc.isProxy()) {
239                            DocumentModel liveDoc = session.getSourceDocument(docRef);
240                            ctx.setInput(liveDoc);
241                            service.run(ctx, "Audit.Log", params);
242
243                        }
244
245                        // Email notification
246                        Map<String, Object> mail = new HashMap<>();
247                        mail.put("filename", blob.getFilename());
248                        sendNotification("easyShareDownload", docShare, mail);
249
250                        return Response.ok(blob.getStream(), blob.getMimeType()).build();
251
252                    } catch (Exception ex) {
253                        log.error("error ", ex);
254                        return Response.serverError().status(Response.Status.NOT_FOUND).build();
255                    }
256
257                } else {
258                    return Response.serverError().status(Response.Status.NOT_FOUND).build();
259                }
260            }
261        }.runUnrestricted(fileId);
262
263  }
264
265  public void sendNotification(String notification, DocumentModel docShare, Map<String, Object> mail) {
266
267    Boolean hasNotification = docShare.getProperty("eshare:hasNotification").getValue(Boolean.class);
268
269    if (hasNotification) {
270      //Email notification
271      String email = docShare.getProperty("eshare:contactEmail").getValue(String.class);
272      if (StringUtils.isEmpty(email)) {
273          return;
274      }
275      try {
276        log.debug("Easyshare: starting email");
277        EmailHelper emailHelper = new EmailHelper();
278        Map<String, Object> mailProps = new HashMap<>();
279        mailProps.put("mail.from", Framework.getProperty("mail.from", "system@nuxeo.com"));
280        mailProps.put("mail.to", email);
281        mailProps.put("ip", getIpAddr());
282        mailProps.put("docShare", docShare);
283
284        try {
285          Notification notif = NotificationServiceHelper.getNotificationService().getNotificationByName(notification);
286
287          if (notif.getSubjectTemplate() != null) {
288            mailProps.put(NotificationConstants.SUBJECT_TEMPLATE_KEY, notif.getSubjectTemplate());
289          }
290
291          mailProps.put(NotificationConstants.SUBJECT_KEY, NotificationServiceHelper.getNotificationService().getEMailSubjectPrefix() + " " + notif.getSubject());
292          mailProps.put(NotificationConstants.TEMPLATE_KEY, notif.getTemplate());
293
294          mailProps.putAll(mail);
295
296          emailHelper.sendmail(mailProps);
297
298        } catch (NuxeoException e) {
299          log.warn(e.getMessage());
300        }
301
302        log.debug("Easyshare: completed email");
303      } catch (Exception ex) {
304        log.error("Cannot send easyShare notification email", ex);
305      }
306    }
307  }
308
309
310  protected String getIpAddr() {
311      String ip = request.getHeader("X-FORWARDED-FOR");
312      if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
313          ip = request.getHeader("Proxy-Client-IP");
314      }
315      if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
316          ip = request.getRemoteAddr();
317      }
318      return ip;
319   }
320}