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