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 022import java.util.Date; 023import java.util.HashMap; 024import java.util.Map; 025 026import javax.ws.rs.DefaultValue; 027import javax.ws.rs.GET; 028import javax.ws.rs.Path; 029import javax.ws.rs.PathParam; 030import javax.ws.rs.Produces; 031import javax.ws.rs.QueryParam; 032import javax.ws.rs.core.Response; 033 034import org.apache.commons.lang3.StringUtils; 035import org.apache.commons.logging.Log; 036import org.apache.commons.logging.LogFactory; 037import org.nuxeo.ecm.automation.AutomationService; 038import org.nuxeo.ecm.automation.OperationChain; 039import org.nuxeo.ecm.automation.OperationContext; 040import org.nuxeo.ecm.automation.jaxrs.io.documents.PaginableDocumentModelListImpl; 041import org.nuxeo.ecm.core.api.Blob; 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.NuxeoException; 046import org.nuxeo.ecm.core.api.blobholder.BlobHolder; 047import org.nuxeo.ecm.platform.ec.notification.NotificationConstants; 048import org.nuxeo.ecm.platform.ec.notification.email.EmailHelper; 049import org.nuxeo.ecm.platform.ec.notification.service.NotificationServiceHelper; 050import org.nuxeo.ecm.platform.notification.api.Notification; 051import org.nuxeo.ecm.webengine.model.WebObject; 052import org.nuxeo.ecm.webengine.model.impl.ModuleRoot; 053import org.nuxeo.runtime.api.Framework; 054 055/** 056 * The root entry for the WebEngine module. 057 * 058 * @author mikeobrebski 059 */ 060@Path("/easyshare") 061@Produces("text/html;charset=UTF-8") 062@WebObject(type = "EasyShare") 063public class EasyShare extends ModuleRoot { 064 065 private static final String DEFAULT_PAGE_INDEX = "0"; 066 067 private static final Long PAGE_SIZE = 20L; 068 069 private static final String SHARE_DOC_TYPE = "EasyShareFolder"; 070 071 protected final Log log = LogFactory.getLog(EasyShare.class); 072 073 @GET 074 public Object doGet() { 075 return getView("index"); 076 } 077 078 public EasyShareUnrestrictedRunner buildUnrestrictedRunner(final String docId, final Long pageIndex) { 079 080 return new EasyShareUnrestrictedRunner() { 081 @Override 082 public Object run(CoreSession session, IdRef docRef) throws NuxeoException { 083 if (session.exists(docRef)) { 084 DocumentModel docShare = session.getDocument(docRef); 085 086 if (!SHARE_DOC_TYPE.equals(docShare.getType())) { 087 return Response.serverError().status(Response.Status.NOT_FOUND).build(); 088 } 089 090 if (!checkIfShareIsValid(docShare)) { 091 return getView("expired").arg("docShare", docShare); 092 } 093 094 DocumentModel document = session.getDocument(new IdRef(docId)); 095 096 String query = buildQuery(document); 097 098 if (query == null) { 099 return getView("denied"); 100 } 101 102 try (OperationContext opCtx = new OperationContext(session)) { 103 OperationChain chain = new OperationChain("getEasyShareContent"); 104 chain.add("Document.Query") 105 .set("query", query) 106 .set("currentPageIndex", pageIndex) 107 .set("pageSize", PAGE_SIZE); 108 109 AutomationService automationService = Framework.getService(AutomationService.class); 110 PaginableDocumentModelListImpl paginable = (PaginableDocumentModelListImpl) automationService.run( 111 opCtx, chain); 112 113 try (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 automationService.run(ctx, "Audit.Log", params); 122 } 123 124 return getView("folderList") 125 .arg("isFolder", 126 document.isFolder() 127 && !SHARE_DOC_TYPE.equals(document.getType())) // Backward 128 // compatibility 129 // to 130 // non-collection 131 .arg("currentPageIndex", paginable.getCurrentPageIndex()) 132 .arg("numberOfPages", paginable.getNumberOfPages()) 133 .arg("docShare", docShare) 134 .arg("docList", paginable) 135 .arg("previousPageAvailable", paginable.isPreviousPageAvailable()) 136 .arg("nextPageAvailable", paginable.isNextPageAvailable()) 137 .arg("currentPageStatus", 138 paginable.getProvider().getCurrentPageStatus()); 139 140 } catch (Exception ex) { 141 log.error(ex.getMessage()); 142 return getView("denied"); 143 } 144 145 } else { 146 return getView("denied"); 147 } 148 } 149 }; 150 } 151 152 protected static String buildQuery(DocumentModel documentModel) { 153 154 // Backward compatibility to non-collection 155 if (documentModel.isFolder() && !SHARE_DOC_TYPE.equals(documentModel.getType())) { 156 return " SELECT * FROM Document WHERE ecm:parentId = '" + documentModel.getId() + "' AND " 157 + "ecm:mixinType != 'HiddenInNavigation' AND " + "ecm:mixinType != 'NotCollectionMember' AND " 158 + "ecm:isVersion = 0 AND " + "ecm:isTrashed = 0" + "ORDER BY dc:title"; 159 160 } else if (SHARE_DOC_TYPE.equals(documentModel.getType())) { 161 return "SELECT * FROM Document where ecm:mixinType != 'HiddenInNavigation' AND " 162 + "ecm:isVersion = 0 AND ecm:isTrashed = 0 " + "AND collectionMember:collectionIds/* = '" 163 + documentModel.getId() + "'" + "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 Date expired = docShare.getProperty("dc:expired").getValue(Date.class); 172 if (expired == null) { 173 log.error("Invalid null dc:expired for share: " + docShare.getTitle() + " (" + docShare.getId() + ")"); 174 // consider the share as expired 175 return false; 176 } 177 if (today.after(expired)) { 178 179 // Email notification 180 Map<String, Object> mail = new HashMap<>(); 181 sendNotification("easyShareExpired", docShare, mail); 182 183 return false; 184 } 185 return true; 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) 213 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 HashMap<>(); 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() 288 .getNotificationByName(notification); 289 290 if (notif.getSubjectTemplate() != null) { 291 mailProps.put(NotificationConstants.SUBJECT_TEMPLATE_KEY, notif.getSubjectTemplate()); 292 } 293 294 mailProps.put(NotificationConstants.SUBJECT_KEY, 295 NotificationServiceHelper.getNotificationService().getEMailSubjectPrefix() + " " 296 + 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 protected String getIpAddr() { 315 String ip = request.getHeader("X-FORWARDED-FOR"); 316 if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 317 ip = request.getHeader("Proxy-Client-IP"); 318 } 319 if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 320 ip = request.getRemoteAddr(); 321 } 322 return ip; 323 } 324}