001/* 002 * (C) Copyright 2015-2016 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 * Florent Guillaume 018 * Estelle Giuly <egiuly@nuxeo.com> 019 */ 020package org.nuxeo.ecm.core.io.download; 021 022import java.io.IOException; 023import java.io.InputStream; 024import java.io.OutputStream; 025import java.io.Serializable; 026import java.io.UncheckedIOException; 027import java.net.URI; 028import java.net.URLEncoder; 029import java.security.Principal; 030import java.util.ArrayList; 031import java.util.Collections; 032import java.util.HashMap; 033import java.util.List; 034import java.util.Map; 035import java.util.Objects; 036import java.util.UUID; 037import java.util.function.Consumer; 038import java.util.function.Supplier; 039 040import javax.script.Invocable; 041import javax.script.ScriptContext; 042import javax.script.ScriptEngine; 043import javax.script.ScriptEngineManager; 044import javax.script.ScriptException; 045import javax.servlet.http.HttpServletRequest; 046import javax.servlet.http.HttpServletResponse; 047 048import org.apache.commons.codec.digest.DigestUtils; 049import org.apache.commons.io.IOUtils; 050import org.apache.commons.lang.StringUtils; 051import org.apache.commons.lang3.tuple.Pair; 052import org.apache.commons.logging.Log; 053import org.apache.commons.logging.LogFactory; 054import org.nuxeo.common.utils.URIUtils; 055import org.nuxeo.ecm.core.api.Blob; 056import org.nuxeo.ecm.core.api.CoreInstance; 057import org.nuxeo.ecm.core.api.CoreSession; 058import org.nuxeo.ecm.core.api.DocumentModel; 059import org.nuxeo.ecm.core.api.DocumentRef; 060import org.nuxeo.ecm.core.api.DocumentSecurityException; 061import org.nuxeo.ecm.core.api.IdRef; 062import org.nuxeo.ecm.core.api.NuxeoException; 063import org.nuxeo.ecm.core.api.NuxeoPrincipal; 064import org.nuxeo.ecm.core.api.SystemPrincipal; 065import org.nuxeo.ecm.core.api.blobholder.BlobHolder; 066import org.nuxeo.ecm.core.api.event.CoreEventConstants; 067import org.nuxeo.ecm.core.api.local.ClientLoginModule; 068import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; 069import org.nuxeo.ecm.core.blob.BlobManager.UsageHint; 070import org.nuxeo.ecm.core.event.Event; 071import org.nuxeo.ecm.core.event.EventContext; 072import org.nuxeo.ecm.core.event.EventService; 073import org.nuxeo.ecm.core.event.impl.DocumentEventContext; 074import org.nuxeo.ecm.core.event.impl.EventContextImpl; 075import org.nuxeo.ecm.core.transientstore.api.TransientStore; 076import org.nuxeo.ecm.core.transientstore.api.TransientStoreService; 077import org.nuxeo.runtime.api.Framework; 078import org.nuxeo.runtime.model.ComponentInstance; 079import org.nuxeo.runtime.model.DefaultComponent; 080import org.nuxeo.runtime.model.SimpleContributionRegistry; 081import org.nuxeo.runtime.transaction.TransactionHelper; 082 083/** 084 * This service allows the download of blobs to a HTTP response. 085 * 086 * @since 7.3 087 */ 088public class DownloadServiceImpl extends DefaultComponent implements DownloadService { 089 090 private static final Log log = LogFactory.getLog(DownloadServiceImpl.class); 091 092 protected static final int DOWNLOAD_BUFFER_SIZE = 1024 * 512; 093 094 private static final String NUXEO_VIRTUAL_HOST = "nuxeo-virtual-host"; 095 096 private static final String VH_PARAM = "nuxeo.virtual.host"; 097 098 private static final String FORCE_NO_CACHE_ON_MSIE = "org.nuxeo.download.force.nocache.msie"; 099 100 private static final String XP = "permissions"; 101 102 private static final String REDIRECT_RESOLVER = "redirectResolver"; 103 104 private static final String RUN_FUNCTION = "run"; 105 106 protected static enum Action {DOWNLOAD, DOWNLOAD_FROM_DOC, INFO}; 107 108 private DownloadPermissionRegistry registry = new DownloadPermissionRegistry(); 109 110 private ScriptEngineManager scriptEngineManager; 111 112 protected RedirectResolver redirectResolver = new DefaultRedirectResolver(); 113 114 protected List<RedirectResolverDescriptor> redirectResolverContributions = new ArrayList<>(); 115 116 public static class DownloadPermissionRegistry extends SimpleContributionRegistry<DownloadPermissionDescriptor> { 117 118 @Override 119 public String getContributionId(DownloadPermissionDescriptor contrib) { 120 return contrib.getName(); 121 } 122 123 @Override 124 public boolean isSupportingMerge() { 125 return true; 126 } 127 128 @Override 129 public DownloadPermissionDescriptor clone(DownloadPermissionDescriptor orig) { 130 return new DownloadPermissionDescriptor(orig); 131 } 132 133 @Override 134 public void merge(DownloadPermissionDescriptor src, DownloadPermissionDescriptor dst) { 135 dst.merge(src); 136 } 137 138 public DownloadPermissionDescriptor getDownloadPermissionDescriptor(String id) { 139 return getCurrentContribution(id); 140 } 141 142 /** Returns descriptors sorted by name. */ 143 public List<DownloadPermissionDescriptor> getDownloadPermissionDescriptors() { 144 List<DownloadPermissionDescriptor> descriptors = new ArrayList<>(currentContribs.values()); 145 Collections.sort(descriptors); 146 return descriptors; 147 } 148 } 149 150 public DownloadServiceImpl() { 151 scriptEngineManager = new ScriptEngineManager(); 152 } 153 154 @Override 155 public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 156 if (XP.equals(extensionPoint)) { 157 DownloadPermissionDescriptor descriptor = (DownloadPermissionDescriptor) contribution; 158 registry.addContribution(descriptor); 159 } else if (REDIRECT_RESOLVER.equals(extensionPoint)) { 160 redirectResolver = ((RedirectResolverDescriptor) contribution).getObject(); 161 // Save contribution 162 redirectResolverContributions.add((RedirectResolverDescriptor) contribution); 163 } else { 164 throw new UnsupportedOperationException(extensionPoint); 165 } 166 } 167 168 @Override 169 public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 170 if (XP.equals(extensionPoint)) { 171 DownloadPermissionDescriptor descriptor = (DownloadPermissionDescriptor) contribution; 172 registry.removeContribution(descriptor); 173 } else if (REDIRECT_RESOLVER.equals(extensionPoint)) { 174 redirectResolverContributions.remove(contribution); 175 if (redirectResolverContributions.size() == 0) { 176 // If no more custom contribution go back to the default one 177 redirectResolver = new DefaultRedirectResolver(); 178 } else { 179 // Go back to the last contribution added 180 redirectResolver = redirectResolverContributions.get(redirectResolverContributions.size() - 1) 181 .getObject(); 182 } 183 } else { 184 throw new UnsupportedOperationException(extensionPoint); 185 } 186 } 187 188 /** 189 * {@inheritDoc} 190 * 191 * Multipart download are not yet supported. You can only provide 192 * a blob singleton at this time. 193 */ 194 @Override 195 public String storeBlobs(List<Blob> blobs) { 196 if (blobs.size() > 1) { 197 throw new IllegalArgumentException("multipart download not yet implemented"); 198 } 199 TransientStore ts = Framework.getService(TransientStoreService.class).getStore("download"); 200 String storeKey = UUID.randomUUID().toString(); 201 ts.putBlobs(storeKey, blobs); 202 return storeKey; 203 } 204 205 206 207 @Override 208 public String getDownloadUrl(DocumentModel doc, String xpath, String filename) { 209 return getDownloadUrl(doc.getRepositoryName(), doc.getId(), xpath, filename); 210 } 211 212 @Override 213 public String getDownloadUrl(String repositoryName, String docId, String xpath, String filename) { 214 StringBuilder sb = new StringBuilder(); 215 sb.append(NXFILE); 216 sb.append("/").append(repositoryName); 217 sb.append("/").append(docId); 218 if (xpath != null) { 219 sb.append("/").append(xpath); 220 if (filename != null) { 221 sb.append("/").append(URIUtils.quoteURIPathComponent(filename, true)); 222 } 223 } 224 return sb.toString(); 225 } 226 227 @Override 228 public String getDownloadUrl(String storeKey) { 229 return NXBIGBLOB + "/" + storeKey; 230 } 231 232 /** 233 * Gets the download path and action of the URL to use to download blobs. For instance, from the path 234 * "nxfile/default/3727ef6b-cf8c-4f27-ab2c-79de0171a2c8/files:files/0/file/image.png", the pair 235 * ("default/3727ef6b-cf8c-4f27-ab2c-79de0171a2c8/files:files/0/file/image.png", Action.DOWNLOAD_FROM_DOC) is returned. 236 * 237 * @param path the path of the URL to use to download blobs 238 * @return the pair download path and action 239 * @since 9.1 240 */ 241 protected Pair<String, Action> getDownloadPathAndAction(String path) { 242 if (path.startsWith("/")) { 243 path = path.substring(1); 244 } 245 int slash = path.indexOf('/'); 246 if (slash < 0) { 247 return null; 248 } 249 String type = path.substring(0, slash); 250 String downloadPath = path.substring(slash + 1); 251 switch (type) { 252 case NXDOWNLOADINFO: 253 // used by nxdropout.js 254 return Pair.of(downloadPath, Action.INFO); 255 case NXFILE: 256 case NXBIGFILE: 257 return Pair.of(downloadPath, Action.DOWNLOAD_FROM_DOC); 258 case NXBIGZIPFILE: 259 case NXBIGBLOB: 260 return Pair.of(downloadPath, Action.DOWNLOAD); 261 default: 262 return null; 263 } 264 } 265 266 @Override 267 public Blob resolveBlobFromDownloadUrl(String url) { 268 String nuxeoUrl = Framework.getProperty("nuxeo.url"); 269 if (!url.startsWith(nuxeoUrl)) { 270 return null; 271 } 272 String path = url.substring(nuxeoUrl.length() + 1); 273 Pair<String, Action> pair = getDownloadPathAndAction(path); 274 if (pair == null) { 275 return null; 276 } 277 String downloadPath = pair.getLeft(); 278 try { 279 DownloadBlobInfo downloadBlobInfo = new DownloadBlobInfo(downloadPath); 280 try (CoreSession session = CoreInstance.openCoreSession(downloadBlobInfo.repository)) { 281 DocumentRef docRef = new IdRef(downloadBlobInfo.docId); 282 if (!session.exists(docRef)) { 283 return null; 284 } 285 DocumentModel doc = session.getDocument(docRef); 286 return resolveBlob(doc, downloadBlobInfo.xpath); 287 } 288 } catch (IllegalArgumentException e) { 289 return null; 290 } 291 } 292 293 @Override 294 public void handleDownload(HttpServletRequest req, HttpServletResponse resp, String baseUrl, String path) 295 throws IOException { 296 Pair<String, Action> pair = getDownloadPathAndAction(path); 297 if (pair == null) { 298 resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Invalid URL syntax"); 299 return; 300 } 301 String downloadPath = pair.getLeft(); 302 Action action = pair.getRight(); 303 switch (action) { 304 case INFO: 305 handleDownload(req, resp, downloadPath, baseUrl, true); 306 break; 307 case DOWNLOAD_FROM_DOC: 308 handleDownload(req, resp, downloadPath, baseUrl, false); 309 break; 310 case DOWNLOAD: 311 downloadBlob(req, resp, downloadPath, "download"); 312 break; 313 default: 314 resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Invalid URL syntax"); 315 } 316 } 317 318 protected void handleDownload(HttpServletRequest req, HttpServletResponse resp, String downloadPath, String baseUrl, 319 boolean info) throws IOException { 320 boolean tx = false; 321 DownloadBlobInfo downloadBlobInfo; 322 try { 323 downloadBlobInfo = new DownloadBlobInfo(downloadPath); 324 } catch (IllegalArgumentException e) { 325 resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Invalid URL syntax"); 326 return; 327 } 328 329 try { 330 if (!TransactionHelper.isTransactionActive()) { 331 // Manually start and stop a transaction around repository access to be able to release transactional 332 // resources without waiting for the download that can take a long time (longer than the transaction 333 // timeout) especially if the client or the connection is slow. 334 tx = TransactionHelper.startTransaction(); 335 } 336 String xpath = downloadBlobInfo.xpath; 337 String filename = downloadBlobInfo.filename; 338 try (CoreSession session = CoreInstance.openCoreSession(downloadBlobInfo.repository)) { 339 DocumentRef docRef = new IdRef(downloadBlobInfo.docId); 340 if (!session.exists(docRef)) { 341 // Send a security exception to force authentication, if the current user is anonymous 342 Principal principal = req.getUserPrincipal(); 343 if (principal instanceof NuxeoPrincipal) { 344 NuxeoPrincipal nuxeoPrincipal = (NuxeoPrincipal) principal; 345 if (nuxeoPrincipal.isAnonymous()) { 346 throw new DocumentSecurityException( 347 "Authentication is needed for downloading the blob"); 348 } 349 } 350 resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No document found"); 351 return; 352 } 353 DocumentModel doc = session.getDocument(docRef); 354 if (info) { 355 Blob blob = resolveBlob(doc, xpath); 356 if (blob == null) { 357 resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No blob found"); 358 return; 359 } 360 String downloadUrl = baseUrl + getDownloadUrl(doc, xpath, filename); 361 String result = blob.getMimeType() + ':' + URLEncoder.encode(blob.getFilename(), "UTF-8") + ':' 362 + downloadUrl; 363 resp.setContentType("text/plain"); 364 resp.getWriter().write(result); 365 resp.getWriter().flush(); 366 } else { 367 downloadBlob(req, resp, doc, xpath, null, filename, "download"); 368 } 369 } 370 } catch (NuxeoException e) { 371 if (tx) { 372 TransactionHelper.setTransactionRollbackOnly(); 373 } 374 throw new IOException(e); 375 } finally { 376 if (tx) { 377 TransactionHelper.commitOrRollbackTransaction(); 378 } 379 } 380 } 381 382 @Override 383 public void downloadBlob(HttpServletRequest request, HttpServletResponse response, String key, String reason) throws IOException { 384 TransientStore ts = Framework.getService(TransientStoreService.class).getStore("download"); 385 try { 386 List<Blob> blobs = ts.getBlobs(key); 387 if (blobs == null) { 388 throw new IllegalArgumentException("no such blobs referenced with " + key); 389 } 390 if (blobs.size() > 1) { 391 throw new IllegalArgumentException("multipart download not yet implemented"); 392 } 393 Blob blob = blobs.get(0); 394 downloadBlob(request, response, null, null, blob, blob.getFilename(), reason); 395 } finally { 396 ts.remove(key); 397 } 398 } 399 400 @Override 401 public void downloadBlob(HttpServletRequest request, HttpServletResponse response, DocumentModel doc, String xpath, 402 Blob blob, String filename, String reason) throws IOException { 403 downloadBlob(request, response, doc, xpath, blob, filename, reason, Collections.emptyMap()); 404 } 405 406 @Override 407 public void downloadBlob(HttpServletRequest request, HttpServletResponse response, DocumentModel doc, String xpath, 408 Blob blob, String filename, String reason, Map<String, Serializable> extendedInfos) throws IOException { 409 downloadBlob(request, response, doc, xpath, blob, filename, reason, extendedInfos, null); 410 } 411 412 @Override 413 public void downloadBlob(HttpServletRequest request, HttpServletResponse response, DocumentModel doc, String xpath, 414 Blob blob, String filename, String reason, Map<String, Serializable> extendedInfos, Boolean inline) 415 throws IOException { 416 if (blob == null) { 417 if (doc == null || xpath == null) { 418 throw new NuxeoException("No blob or doc xpath"); 419 } 420 blob = resolveBlob(doc, xpath); 421 if (blob == null) { 422 response.sendError(HttpServletResponse.SC_NOT_FOUND, "No blob found"); 423 return; 424 } 425 } 426 final Blob fblob = blob; 427 downloadBlob(request, response, doc, xpath, blob, filename, reason, extendedInfos, inline, 428 byteRange -> transferBlobWithByteRange(fblob, byteRange, response)); 429 } 430 431 @Override 432 public void downloadBlob(HttpServletRequest request, HttpServletResponse response, DocumentModel doc, String xpath, 433 Blob blob, String filename, String reason, Map<String, Serializable> extendedInfos, Boolean inline, 434 Consumer<ByteRange> blobTransferer) throws IOException { 435 Objects.requireNonNull(blob); 436 // check blob permissions 437 if (!checkPermission(doc, xpath, blob, reason, extendedInfos)) { 438 response.sendError(HttpServletResponse.SC_FORBIDDEN, "Permission denied"); 439 return; 440 } 441 442 // check Blob Manager external download link 443 URI uri = redirectResolver.getURI(blob, UsageHint.DOWNLOAD, request); 444 if (uri != null) { 445 try { 446 Map<String, Serializable> ei = new HashMap<>(); 447 if (extendedInfos != null) { 448 ei.putAll(extendedInfos); 449 } 450 ei.put("redirect", uri.toString()); 451 logDownload(doc, xpath, filename, reason, ei); 452 response.sendRedirect(uri.toString()); 453 } catch (IOException ioe) { 454 DownloadHelper.handleClientDisconnect(ioe); 455 } 456 return; 457 } 458 459 try { 460 String digest = blob.getDigest(); 461 if (digest == null) { 462 digest = DigestUtils.md5Hex(blob.getStream()); 463 } 464 String etag = '"' + digest + '"'; // with quotes per RFC7232 2.3 465 response.setHeader("ETag", etag); // re-send even on SC_NOT_MODIFIED 466 addCacheControlHeaders(request, response); 467 468 String ifNoneMatch = request.getHeader("If-None-Match"); 469 if (ifNoneMatch != null) { 470 boolean match = false; 471 if (ifNoneMatch.equals("*")) { 472 match = true; 473 } else { 474 for (String previousEtag : StringUtils.split(ifNoneMatch, ", ")) { 475 if (previousEtag.equals(etag)) { 476 match = true; 477 break; 478 } 479 } 480 } 481 if (match) { 482 String method = request.getMethod(); 483 if (method.equals("GET") || method.equals("HEAD")) { 484 response.sendError(HttpServletResponse.SC_NOT_MODIFIED); 485 } else { 486 // per RFC7232 3.2 487 response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); 488 } 489 return; 490 } 491 } 492 493 // regular processing 494 495 if (StringUtils.isBlank(filename)) { 496 filename = StringUtils.defaultIfBlank(blob.getFilename(), "file"); 497 } 498 String contentDisposition = DownloadHelper.getRFC2231ContentDisposition(request, filename, inline); 499 response.setHeader("Content-Disposition", contentDisposition); 500 response.setContentType(blob.getMimeType()); 501 if (blob.getEncoding() != null) { 502 response.setCharacterEncoding(blob.getEncoding()); 503 } 504 505 long length = blob.getLength(); 506 response.setHeader("Accept-Ranges", "bytes"); 507 String range = request.getHeader("Range"); 508 ByteRange byteRange; 509 if (StringUtils.isBlank(range)) { 510 byteRange = null; 511 } else { 512 byteRange = DownloadHelper.parseRange(range, length); 513 if (byteRange == null) { 514 log.error("Invalid byte range received: " + range); 515 } else { 516 response.setHeader("Content-Range", "bytes " + byteRange.getStart() + "-" + byteRange.getEnd() 517 + "/" + length); 518 response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); 519 } 520 } 521 long contentLength = byteRange == null ? length : byteRange.getLength(); 522 if (contentLength < Integer.MAX_VALUE) { 523 response.setContentLength((int) contentLength); 524 } 525 526 logDownload(doc, xpath, filename, reason, extendedInfos); 527 528 // execute the final download 529 blobTransferer.accept(byteRange); 530 } catch (UncheckedIOException e) { 531 DownloadHelper.handleClientDisconnect(e.getCause()); 532 } catch (IOException ioe) { 533 DownloadHelper.handleClientDisconnect(ioe); 534 } 535 } 536 537 protected void transferBlobWithByteRange(Blob blob, ByteRange byteRange, HttpServletResponse response) 538 throws UncheckedIOException { 539 transferBlobWithByteRange(blob, byteRange, () -> { 540 try { 541 return response.getOutputStream(); 542 } catch (IOException e) { 543 throw new UncheckedIOException(e); 544 } 545 }); 546 try { 547 response.flushBuffer(); 548 } catch (IOException e) { 549 throw new UncheckedIOException(e); 550 } 551 } 552 553 @Override 554 public void transferBlobWithByteRange(Blob blob, ByteRange byteRange, Supplier<OutputStream> outputStreamSupplier) 555 throws UncheckedIOException { 556 try (InputStream in = blob.getStream()) { 557 @SuppressWarnings("resource") 558 OutputStream out = outputStreamSupplier.get(); // not ours to close 559 BufferingServletOutputStream.stopBuffering(out); 560 if (byteRange == null) { 561 IOUtils.copy(in, out); 562 } else { 563 IOUtils.copyLarge(in, out, byteRange.getStart(), byteRange.getLength()); 564 } 565 out.flush(); 566 } catch (IOException e) { 567 throw new UncheckedIOException(e); 568 } 569 } 570 571 protected String fixXPath(String xpath) { 572 // Hack for Flash Url wich doesn't support ':' char 573 return xpath == null ? null : xpath.replace(';', ':'); 574 } 575 576 @Override 577 public Blob resolveBlob(DocumentModel doc, String xpath) { 578 xpath = fixXPath(xpath); 579 Blob blob; 580 if (xpath.startsWith(BLOBHOLDER_PREFIX)) { 581 BlobHolder bh = doc.getAdapter(BlobHolder.class); 582 if (bh == null) { 583 log.debug("Not a BlobHolder"); 584 return null; 585 } 586 String suffix = xpath.substring(BLOBHOLDER_PREFIX.length()); 587 int index; 588 try { 589 index = Integer.parseInt(suffix); 590 } catch (NumberFormatException e) { 591 log.debug(e.getMessage()); 592 return null; 593 } 594 if (!suffix.equals(Integer.toString(index))) { 595 // attempt to use a non-canonical integer, could be used to bypass 596 // a permission function checking just "blobholder:1" and receiving "blobholder:01" 597 log.debug("Non-canonical index: " + suffix); 598 return null; 599 } 600 if (index == 0) { 601 blob = bh.getBlob(); 602 } else { 603 blob = bh.getBlobs().get(index); 604 } 605 } else { 606 if (!xpath.contains(":")) { 607 // attempt to use a xpath not prefix-qualified, could be used to bypass 608 // a permission function checking just "file:content" and receiving "content" 609 log.debug("Non-canonical xpath: " + xpath); 610 return null; 611 } 612 try { 613 blob = (Blob) doc.getPropertyValue(xpath); 614 } catch (PropertyNotFoundException e) { 615 log.debug(e.getMessage()); 616 return null; 617 } 618 } 619 return blob; 620 } 621 622 @Override 623 public boolean checkPermission(DocumentModel doc, String xpath, Blob blob, String reason, 624 Map<String, Serializable> extendedInfos) { 625 List<DownloadPermissionDescriptor> descriptors = registry.getDownloadPermissionDescriptors(); 626 if (descriptors.isEmpty()) { 627 return true; 628 } 629 xpath = fixXPath(xpath); 630 Map<String, Object> context = new HashMap<>(); 631 Map<String, Serializable> ei = extendedInfos == null ? Collections.emptyMap() : extendedInfos; 632 NuxeoPrincipal currentUser = ClientLoginModule.getCurrentPrincipal(); 633 context.put("Document", doc); 634 context.put("XPath", xpath); 635 context.put("Blob", blob); 636 context.put("Reason", reason); 637 context.put("Infos", ei); 638 context.put("Rendition", ei.get("rendition")); 639 context.put("CurrentUser", currentUser); 640 for (DownloadPermissionDescriptor descriptor : descriptors) { 641 ScriptEngine engine = scriptEngineManager.getEngineByName(descriptor.getScriptLanguage()); 642 if (engine == null) { 643 throw new NuxeoException("Engine not found for language: " + descriptor.getScriptLanguage() 644 + " in permission: " + descriptor.getName()); 645 } 646 if (!(engine instanceof Invocable)) { 647 throw new NuxeoException("Engine " + engine.getClass().getName() + " not Invocable for language: " 648 + descriptor.getScriptLanguage() + " in permission: " + descriptor.getName()); 649 } 650 Object result; 651 try { 652 engine.eval(descriptor.getScript()); 653 engine.getBindings(ScriptContext.ENGINE_SCOPE).putAll(context); 654 result = ((Invocable) engine).invokeFunction(RUN_FUNCTION); 655 } catch (NoSuchMethodException e) { 656 throw new NuxeoException("Script does not contain function: " + RUN_FUNCTION + "() in permission: " 657 + descriptor.getName(), e); 658 } catch (ScriptException e) { 659 log.error("Failed to evaluate script: " + descriptor.getName(), e); 660 continue; 661 } 662 if (!(result instanceof Boolean)) { 663 log.error("Failed to get boolean result from permission: " + descriptor.getName() + " (" + result + ")"); 664 continue; 665 } 666 boolean allow = ((Boolean) result).booleanValue(); 667 if (!allow) { 668 return false; 669 } 670 } 671 return true; 672 } 673 674 /** 675 * Internet Explorer file downloads over SSL do not work with certain HTTP cache control headers 676 * <p> 677 * See http://support.microsoft.com/kb/323308/ 678 * <p> 679 * What is not mentioned in the above Knowledge Base is that "Pragma: no-cache" also breaks download in MSIE over 680 * SSL 681 */ 682 protected void addCacheControlHeaders(HttpServletRequest request, HttpServletResponse response) { 683 String userAgent = request.getHeader("User-Agent"); 684 boolean secure = request.isSecure(); 685 if (!secure) { 686 String nvh = request.getHeader(NUXEO_VIRTUAL_HOST); 687 if (nvh == null) { 688 nvh = Framework.getProperty(VH_PARAM); 689 } 690 if (nvh != null) { 691 secure = nvh.startsWith("https"); 692 } 693 } 694 if (userAgent != null && userAgent.contains("MSIE") && (secure || forceNoCacheOnMSIE())) { 695 String cacheControl = "max-age=15, must-revalidate"; 696 log.debug("Setting Cache-Control: " + cacheControl); 697 response.setHeader("Cache-Control", cacheControl); 698 } 699 } 700 701 protected static boolean forceNoCacheOnMSIE() { 702 // see NXP-7759 703 return Framework.isBooleanPropertyTrue(FORCE_NO_CACHE_ON_MSIE); 704 } 705 706 @Override 707 public void logDownload(DocumentModel doc, String xpath, String filename, String reason, 708 Map<String, Serializable> extendedInfos) { 709 if ("webengine".equals(reason)) { 710 // don't log JSON operation results as downloads 711 return; 712 } 713 EventService eventService = Framework.getService(EventService.class); 714 if (eventService == null) { 715 return; 716 } 717 EventContext ctx; 718 if (doc != null) { 719 @SuppressWarnings("resource") 720 CoreSession session = doc.getCoreSession(); 721 Principal principal = session == null ? getPrincipal() : session.getPrincipal(); 722 ctx = new DocumentEventContext(session, principal, doc); 723 ctx.setProperty(CoreEventConstants.REPOSITORY_NAME, doc.getRepositoryName()); 724 ctx.setProperty(CoreEventConstants.SESSION_ID, doc.getSessionId()); 725 } else { 726 ctx = new EventContextImpl(null, getPrincipal()); 727 } 728 Map<String, Serializable> map = new HashMap<>(); 729 map.put("blobXPath", xpath); 730 map.put("blobFilename", filename); 731 map.put("downloadReason", reason); 732 if (extendedInfos != null) { 733 map.putAll(extendedInfos); 734 } 735 ctx.setProperty("extendedInfos", (Serializable) map); 736 ctx.setProperty("comment", filename); 737 Event event = ctx.newEvent(EVENT_NAME); 738 eventService.fireEvent(event); 739 } 740 741 protected static NuxeoPrincipal getPrincipal() { 742 NuxeoPrincipal principal = ClientLoginModule.getCurrentPrincipal(); 743 if (principal == null) { 744 if (!Framework.isTestModeSet()) { 745 throw new NuxeoException("Missing security context, login() not done"); 746 } 747 principal = new SystemPrincipal(null); 748 } 749 return principal; 750 } 751 752}