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", "[email protected]")); 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}