001/*
002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Michal Obrebski - Nuxeo
016 */
017
018package org.nuxeo.easyshare;
019
020
021import java.util.HashMap;
022import java.util.Hashtable;
023import java.util.Map;
024
025import javax.ws.rs.GET;
026import javax.ws.rs.Path;
027import javax.ws.rs.PathParam;
028import javax.ws.rs.Produces;
029import javax.ws.rs.core.Response;
030
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033import org.nuxeo.ecm.automation.AutomationService;
034import org.nuxeo.ecm.automation.OperationChain;
035import org.nuxeo.ecm.automation.OperationContext;
036import org.nuxeo.ecm.automation.jaxrs.io.documents.PaginableDocumentModelListImpl;
037import org.nuxeo.ecm.core.api.Blob;
038import org.nuxeo.ecm.core.api.NuxeoException;
039import org.nuxeo.ecm.core.api.CoreSession;
040import org.nuxeo.ecm.core.api.DocumentModel;
041import org.nuxeo.ecm.core.api.IdRef;
042import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
043import org.nuxeo.ecm.platform.ec.notification.NotificationConstants;
044import org.nuxeo.ecm.platform.ec.notification.email.EmailHelper;
045import org.nuxeo.ecm.platform.ec.notification.service.NotificationServiceHelper;
046import org.nuxeo.ecm.platform.notification.api.Notification;
047import org.nuxeo.ecm.webengine.model.WebObject;
048import org.nuxeo.ecm.webengine.model.impl.ModuleRoot;
049import org.nuxeo.runtime.api.Framework;
050
051import javax.ws.rs.DefaultValue;
052import javax.ws.rs.QueryParam;
053
054import java.util.Date;
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 {
102
103            OperationContext opCtx = new OperationContext(session);
104            OperationChain chain = new OperationChain("getEasyShareContent");
105            chain.add("Document.Query")
106                .set("query", query)
107                .set("currentPageIndex", pageIndex)
108                .set("pageSize", PAGE_SIZE);
109
110            PaginableDocumentModelListImpl paginable = (PaginableDocumentModelListImpl) getAutomationService().run(opCtx, chain);
111
112            OperationContext ctx = new OperationContext(session);
113            ctx.setInput(docShare);
114
115            // Audit Log
116            Map<String, Object> params = new HashMap<>();
117            params.put("event", "Access");
118            params.put("category", "Document");
119            params.put("comment", "IP: " + getIpAddr());
120            getAutomationService().run(ctx, "Audit.Log", params);
121
122            return getView("folderList")
123                .arg("isFolder", document.isFolder() && !SHARE_DOC_TYPE.equals(document.getType()))  //Backward compatibility to non-collection
124                .arg("currentPageIndex", paginable.getCurrentPageIndex())
125                .arg("numberOfPages", paginable.getNumberOfPages())
126                .arg("docShare", docShare)
127                .arg("docList", paginable)
128                .arg("previousPageAvailable", paginable.isPreviousPageAvailable())
129                .arg("nextPageAvailable", paginable.isNextPageAvailable())
130                .arg("currentPageStatus", paginable.getProvider().getCurrentPageStatus());
131
132          } catch (Exception ex) {
133            log.error(ex.getMessage());
134            return getView("denied");
135          }
136
137        } else {
138          return getView("denied");
139        }
140      }
141    };
142  }
143
144
145  protected static String buildQuery(DocumentModel documentModel) {
146
147          //Backward compatibility to non-collection
148    if (documentModel.isFolder() && !SHARE_DOC_TYPE.equals(documentModel.getType())) {
149      return " SELECT * FROM Document WHERE ecm:parentId = '" + documentModel.getId() + "' AND " +
150          "ecm:mixinType != 'HiddenInNavigation' AND " +
151          "ecm:mixinType != 'NotCollectionMember' AND " +
152          "ecm:isCheckedInVersion = 0 AND " +
153          "ecm:currentLifeCycleState != 'deleted'"
154          + "ORDER BY dc:title";
155
156    } else if (SHARE_DOC_TYPE.equals(documentModel.getType())) {
157      return "SELECT * FROM Document where ecm:mixinType != 'HiddenInNavigation' AND " +
158          "ecm:isCheckedInVersion = 0 AND ecm:currentLifeCycleState != 'deleted' " +
159          "AND collectionMember:collectionIds/* = '" + documentModel.getId() + "'" +
160          "OR ecm:parentId = '" + documentModel.getId() + "'"
161                  + "ORDER BY dc:title";
162    }
163    return null;
164  }
165
166  private boolean checkIfShareIsValid(DocumentModel docShare) {
167    Date today = new Date();
168    if (today.after(docShare.getProperty("dc:expired").getValue(Date.class))) {
169
170      //Email notification
171      Map<String, Object> mail = new HashMap<>();
172      sendNotification("easyShareExpired", docShare, mail);
173
174      return false;
175    }
176    return true;
177  }
178
179
180  private static AutomationService getAutomationService() {
181    if (automationService == null) {
182      automationService = Framework.getService(AutomationService.class);
183    }
184    return automationService;
185  }
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) throws NuxeoException {
213
214    return (Response) new EasyShareUnrestrictedRunner() {
215      @Override
216      public Object run(CoreSession session, IdRef docRef) throws NuxeoException {
217        if (session.exists(docRef)) {
218          try {
219            DocumentModel doc = session.getDocument(docRef);
220            DocumentModel docShare = session.getDocument(new IdRef(shareId));
221
222            if (!checkIfShareIsValid(docShare)) {
223              return Response.serverError().status(Response.Status.NOT_FOUND).build();
224            }
225
226            Blob blob = doc.getAdapter(BlobHolder.class).getBlob();
227
228            // Audit Log
229            OperationContext ctx = new OperationContext(session);
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.getLocalService(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      try {
275        log.debug("Easyshare: starting email");
276        EmailHelper emailHelper = new EmailHelper();
277        Map<String, Object> mailProps = new Hashtable<>();
278        mailProps.put("mail.from", Framework.getProperty("mail.from", "system@nuxeo.com"));
279        mailProps.put("mail.to", email);
280        mailProps.put("ip", getIpAddr());
281        mailProps.put("docShare", docShare);
282
283        try {
284          Notification notif = NotificationServiceHelper.getNotificationService().getNotificationByName(notification);
285
286          if (notif.getSubjectTemplate() != null) {
287            mailProps.put(NotificationConstants.SUBJECT_TEMPLATE_KEY, notif.getSubjectTemplate());
288          }
289
290          mailProps.put(NotificationConstants.SUBJECT_KEY, NotificationServiceHelper.getNotificationService().getEMailSubjectPrefix() + " " + notif.getSubject());
291          mailProps.put(NotificationConstants.TEMPLATE_KEY, notif.getTemplate());
292
293          mailProps.putAll(mail);
294
295          emailHelper.sendmail(mailProps);
296
297        } catch (NuxeoException e) {
298          log.warn(e.getMessage());
299        }
300
301        log.debug("Easyshare: completed email");
302      } catch (Exception ex) {
303        log.error("Cannot send easyShare notification email", ex);
304      }
305    }
306  }
307
308
309  protected String getIpAddr() {
310      String ip = request.getHeader("X-FORWARDED-FOR");
311      if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
312          ip = request.getHeader("Proxy-Client-IP");
313      }
314      if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
315          ip = request.getRemoteAddr();
316      }
317      return ip;
318   }
319}