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 (OperationContext opCtx = new OperationContext(session)) {
105            OperationChain chain = new OperationChain("getEasyShareContent");
106            chain.add("Document.Query")
107                .set("query", query)
108                .set("currentPageIndex", pageIndex)
109                .set("pageSize", PAGE_SIZE);
110
111            PaginableDocumentModelListImpl paginable = (PaginableDocumentModelListImpl) getAutomationService().run(opCtx, chain);
112
113            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            getAutomationService().run(ctx, "Audit.Log", params);
122
123            return getView("folderList")
124                .arg("isFolder", document.isFolder() && !SHARE_DOC_TYPE.equals(document.getType()))  //Backward compatibility to non-collection
125                .arg("currentPageIndex", paginable.getCurrentPageIndex())
126                .arg("numberOfPages", paginable.getNumberOfPages())
127                .arg("docShare", docShare)
128                .arg("docList", paginable)
129                .arg("previousPageAvailable", paginable.isPreviousPageAvailable())
130                .arg("nextPageAvailable", paginable.isNextPageAvailable())
131                .arg("currentPageStatus", paginable.getProvider().getCurrentPageStatus());
132
133          } catch (Exception ex) {
134            log.error(ex.getMessage());
135            return getView("denied");
136          }
137
138        } else {
139          return getView("denied");
140        }
141      }
142    };
143  }
144
145
146  protected static String buildQuery(DocumentModel documentModel) {
147
148          //Backward compatibility to non-collection
149    if (documentModel.isFolder() && !SHARE_DOC_TYPE.equals(documentModel.getType())) {
150      return " SELECT * FROM Document WHERE ecm:parentId = '" + documentModel.getId() + "' AND " +
151          "ecm:mixinType != 'HiddenInNavigation' AND " +
152          "ecm:mixinType != 'NotCollectionMember' AND " +
153          "ecm:isCheckedInVersion = 0 AND " +
154          "ecm:currentLifeCycleState != 'deleted'"
155          + "ORDER BY dc:title";
156
157    } else if (SHARE_DOC_TYPE.equals(documentModel.getType())) {
158      return "SELECT * FROM Document where ecm:mixinType != 'HiddenInNavigation' AND " +
159          "ecm:isCheckedInVersion = 0 AND ecm:currentLifeCycleState != 'deleted' " +
160          "AND collectionMember:collectionIds/* = '" + documentModel.getId() + "'" +
161          "OR ecm:parentId = '" + documentModel.getId() + "'"
162                  + "ORDER BY dc:title";
163    }
164    return null;
165  }
166
167  private boolean checkIfShareIsValid(DocumentModel docShare) {
168    Date today = new Date();
169    if (today.after(docShare.getProperty("dc:expired").getValue(Date.class))) {
170
171      //Email notification
172      Map<String, Object> mail = new HashMap<>();
173      sendNotification("easyShareExpired", docShare, mail);
174
175      return false;
176    }
177    return true;
178  }
179
180
181  private static AutomationService getAutomationService() {
182    if (automationService == null) {
183      automationService = Framework.getService(AutomationService.class);
184    }
185    return automationService;
186  }
187
188
189  @Path("{shareId}/{folderId}")
190  @GET
191  public Object getFolderListing(@PathParam("shareId") String shareId, @PathParam("folderId") final String folderId,
192                                 @DefaultValue(DEFAULT_PAGE_INDEX) @QueryParam("p") final Long pageIndex) {
193    return buildUnrestrictedRunner(folderId, pageIndex).runUnrestricted(shareId);
194  }
195
196  @Path("{shareId}")
197  @GET
198  public Object getShareListing(@PathParam("shareId") String shareId,
199                                @DefaultValue(DEFAULT_PAGE_INDEX) @QueryParam("p") Long pageIndex) {
200    return buildUnrestrictedRunner(shareId, pageIndex).runUnrestricted(shareId);
201  }
202
203  public String getFileName(DocumentModel doc) throws NuxeoException {
204    BlobHolder blobHolder = doc.getAdapter(BlobHolder.class);
205    if (blobHolder != null && blobHolder.getBlob() != null) {
206      return blobHolder.getBlob().getFilename();
207    }
208    return doc.getName();
209  }
210
211  @GET
212  @Path("{shareId}/{fileId}/{fileName}")
213  public Response getFileStream(@PathParam("shareId") final String shareId, @PathParam("fileId") String fileId) 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 Hashtable<>();
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().getNotificationByName(notification);
288
289          if (notif.getSubjectTemplate() != null) {
290            mailProps.put(NotificationConstants.SUBJECT_TEMPLATE_KEY, notif.getSubjectTemplate());
291          }
292
293          mailProps.put(NotificationConstants.SUBJECT_KEY, NotificationServiceHelper.getNotificationService().getEMailSubjectPrefix() + " " + notif.getSubject());
294          mailProps.put(NotificationConstants.TEMPLATE_KEY, notif.getTemplate());
295
296          mailProps.putAll(mail);
297
298          emailHelper.sendmail(mailProps);
299
300        } catch (NuxeoException e) {
301          log.warn(e.getMessage());
302        }
303
304        log.debug("Easyshare: completed email");
305      } catch (Exception ex) {
306        log.error("Cannot send easyShare notification email", ex);
307      }
308    }
309  }
310
311
312  protected String getIpAddr() {
313      String ip = request.getHeader("X-FORWARDED-FOR");
314      if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
315          ip = request.getHeader("Proxy-Client-IP");
316      }
317      if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
318          ip = request.getRemoteAddr();
319      }
320      return ip;
321   }
322}