001/* 002 * (C) Copyright 2006-2008 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 * bstefanescu 018 * 019 * $Id$ 020 */ 021 022package org.nuxeo.ecm.webengine.model.impl; 023 024import static org.nuxeo.ecm.webengine.WebEngine.SKIN_PATH_PREFIX_KEY; 025 026import java.io.File; 027import java.io.IOException; 028import java.io.Writer; 029import java.net.SocketException; 030import java.security.Principal; 031import java.text.MessageFormat; 032import java.text.ParseException; 033import java.util.HashMap; 034import java.util.LinkedList; 035import java.util.List; 036import java.util.Locale; 037import java.util.Map; 038import java.util.MissingResourceException; 039 040import javax.script.ScriptException; 041import javax.servlet.http.Cookie; 042import javax.servlet.http.HttpServletRequest; 043import javax.servlet.http.HttpServletResponse; 044 045import org.apache.commons.collections.CollectionUtils; 046import org.apache.commons.lang3.StringUtils; 047import org.apache.commons.logging.Log; 048import org.apache.commons.logging.LogFactory; 049import org.nuxeo.common.utils.ExceptionUtils; 050import org.nuxeo.common.utils.Path; 051import org.nuxeo.ecm.core.api.CoreSession; 052import org.nuxeo.ecm.core.api.DocumentModel; 053import org.nuxeo.ecm.core.api.NuxeoException; 054import org.nuxeo.ecm.core.api.NuxeoPrincipal; 055import org.nuxeo.ecm.core.api.repository.RepositoryManager; 056import org.nuxeo.ecm.core.io.registry.context.RenderingContext; 057import org.nuxeo.ecm.platform.rendering.api.RenderingException; 058import org.nuxeo.ecm.platform.web.common.locale.LocaleProvider; 059import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper; 060import org.nuxeo.ecm.webengine.WebEngine; 061import org.nuxeo.ecm.webengine.forms.FormData; 062import org.nuxeo.ecm.webengine.jaxrs.session.SessionFactory; 063import org.nuxeo.ecm.webengine.login.WebEngineFormAuthenticator; 064import org.nuxeo.ecm.webengine.model.AdapterResource; 065import org.nuxeo.ecm.webengine.model.Messages; 066import org.nuxeo.ecm.webengine.model.Module; 067import org.nuxeo.ecm.webengine.model.ModuleResource; 068import org.nuxeo.ecm.webengine.model.Resource; 069import org.nuxeo.ecm.webengine.model.ResourceType; 070import org.nuxeo.ecm.webengine.model.WebContext; 071import org.nuxeo.ecm.webengine.model.exceptions.WebResourceNotFoundException; 072import org.nuxeo.ecm.webengine.scripting.ScriptFile; 073import org.nuxeo.ecm.webengine.security.PermissionService; 074import org.nuxeo.ecm.webengine.session.UserSession; 075import org.nuxeo.runtime.api.Framework; 076 077/** 078 * @author <a href="mailto:[email protected]">Bogdan Stefanescu</a> 079 */ 080public abstract class AbstractWebContext implements WebContext { 081 082 private static final Log log = LogFactory.getLog(WebContext.class); 083 084 // TODO: this should be made configurable through an extension point 085 protected static final Locale DEFAULT_LOCALE = Locale.ENGLISH; 086 087 public static final String LOCALE_SESSION_KEY = "webengine_locale"; 088 089 private static boolean isRepositoryDisabled = false; 090 091 protected final WebEngine engine; 092 093 private UserSession us; 094 095 protected final LinkedList<File> scriptExecutionStack; 096 097 protected final HttpServletRequest request; 098 099 protected final HttpServletResponse response; 100 101 protected final Map<String, Object> vars; 102 103 protected Resource head; 104 105 protected Resource tail; 106 107 protected Resource root; 108 109 protected Module module; 110 111 protected FormData form; 112 113 protected String basePath; 114 115 private String repoName; 116 117 protected AbstractWebContext(HttpServletRequest request, HttpServletResponse response) { 118 engine = Framework.getService(WebEngine.class); 119 scriptExecutionStack = new LinkedList<>(); 120 this.request = request; 121 this.response = response; 122 vars = new HashMap<>(); 123 } 124 125 public void setModule(Module module) { 126 this.module = module; 127 } 128 129 @Override 130 public Resource getRoot() { 131 return root; 132 } 133 134 @Override 135 public void setRoot(Resource root) { 136 this.root = root; 137 } 138 139 @Override 140 public <T> T getAdapter(Class<T> adapter) { 141 if (CoreSession.class == adapter) { 142 return adapter.cast(getCoreSession()); 143 } else if (Principal.class == adapter) { 144 return adapter.cast(getPrincipal()); 145 } else if (NuxeoPrincipal.class == adapter) { 146 return adapter.cast(getPrincipal()); 147 } else if (Resource.class == adapter) { 148 return adapter.cast(tail()); 149 } else if (WebContext.class == adapter) { 150 return adapter.cast(this); 151 } else if (Module.class == adapter) { 152 return adapter.cast(module); 153 } else if (WebEngine.class == adapter) { 154 return adapter.cast(engine); 155 } 156 return null; 157 } 158 159 @Override 160 public Module getModule() { 161 return module; 162 } 163 164 @Override 165 public WebEngine getEngine() { 166 return engine; 167 } 168 169 @Override 170 public UserSession getUserSession() { 171 if (us == null) { 172 us = UserSession.getCurrentSession(request); 173 } 174 return us; 175 } 176 177 @Override 178 public CoreSession getCoreSession() { 179 if (StringUtils.isNotBlank(repoName)) { 180 return SessionFactory.getSession(request, repoName); 181 } else { 182 return SessionFactory.getSession(request); 183 } 184 } 185 186 @Override 187 public NuxeoPrincipal getPrincipal() { 188 return (NuxeoPrincipal) request.getUserPrincipal(); 189 } 190 191 @Override 192 public HttpServletRequest getRequest() { 193 return request; 194 } 195 196 public HttpServletResponse getResponse() { 197 return response; 198 } 199 200 @Override 201 public String getMethod() { 202 return request.getMethod(); 203 } 204 205 @Override 206 public String getModulePath() { 207 return head.getPath(); 208 } 209 210 @Override 211 public String getMessage(String key) { 212 Messages messages = module.getMessages(); 213 try { 214 return messages.getString(key, getLocale().getLanguage()); 215 } catch (MissingResourceException e) { 216 return '!' + key + '!'; 217 } 218 } 219 220 @Override 221 public String getMessage(String key, Object... args) { 222 Messages messages = module.getMessages(); 223 try { 224 String msg = messages.getString(key, getLocale().getLanguage()); 225 if (args != null && args.length > 0) { 226 // format the string using given args 227 msg = MessageFormat.format(msg, args); 228 } 229 return msg; 230 } catch (MissingResourceException e) { 231 return '!' + key + '!'; 232 } 233 } 234 235 @Override 236 public String getMessage(String key, List<Object> args) { 237 Messages messages = module.getMessages(); 238 try { 239 String msg = messages.getString(key, getLocale().getLanguage()); 240 if (CollectionUtils.isNotEmpty(args)) { 241 // format the string using given args 242 msg = MessageFormat.format(msg, args.toArray()); 243 } 244 return msg; 245 } catch (MissingResourceException e) { 246 return '!' + key + '!'; 247 } 248 } 249 250 @Override 251 public String getMessageL(String key, String language) { 252 Messages messages = module.getMessages(); 253 try { 254 return messages.getString(key, language); 255 } catch (MissingResourceException e) { 256 return '!' + key + '!'; 257 } 258 } 259 260 @Override 261 public String getMessageL(String key, String locale, Object... args) { 262 Messages messages = module.getMessages(); 263 try { 264 String msg = messages.getString(key, locale); 265 if (args != null && args.length > 0) { 266 // format the string using given args 267 msg = MessageFormat.format(msg, args); 268 } 269 return msg; 270 } catch (MissingResourceException e) { 271 return '!' + key + '!'; 272 } 273 } 274 275 @Override 276 public String getMessageL(String key, String locale, List<Object> args) { 277 Messages messages = module.getMessages(); 278 try { 279 String msg = messages.getString(key, locale); 280 if (args != null && !args.isEmpty()) { 281 // format the string using given args 282 msg = MessageFormat.format(msg, args.toArray()); 283 } 284 return msg; 285 } catch (MissingResourceException e) { 286 return '!' + key + '!'; 287 } 288 } 289 290 @Override 291 public Locale getLocale() { 292 LocaleProvider localeProvider = Framework.getService(LocaleProvider.class); 293 if (localeProvider != null && request.getUserPrincipal() != null) { 294 Locale userPrefLocale = localeProvider.getLocale(getCoreSession()); 295 if (userPrefLocale != null) { 296 return userPrefLocale; 297 } 298 } 299 300 UserSession userSession = getUserSession(); 301 if (userSession != null) { 302 Object locale = userSession.get(LOCALE_SESSION_KEY); 303 if (locale instanceof Locale) { 304 return (Locale) locale; 305 } 306 } 307 308 // take the one on request 309 Locale locale = request.getLocale(); 310 return locale == null ? DEFAULT_LOCALE : locale; 311 } 312 313 @Override 314 public void setLocale(Locale locale) { 315 UserSession userSession = getUserSession(); 316 if (userSession != null) { 317 userSession.put(LOCALE_SESSION_KEY, locale); 318 } 319 } 320 321 @Override 322 public Resource newObject(String typeName, Object... args) { 323 ResourceType type = module.getType(typeName); 324 if (type == null) { 325 throw new WebResourceNotFoundException("No Such Object Type: " + typeName); 326 } 327 return newObject(type, args); 328 } 329 330 @Override 331 public Resource newObject(ResourceType type, Object... args) { 332 Resource obj = type.newInstance(type.getResourceClass(), this); 333 try { 334 obj.initialize(this, type, args); 335 } finally { 336 // we must be sure the object is pushed even if an error occurred 337 // otherwise we may end up with an empty object stack and we will 338 // not be able to 339 // handle errors based on objects handleError() method 340 push(obj); 341 } 342 return obj; 343 } 344 345 @Override 346 public AdapterResource newAdapter(Resource ctx, String serviceName, Object... args) { 347 return (AdapterResource) newObject(module.getAdapter(ctx, serviceName), args); 348 } 349 350 @Override 351 public void setProperty(String key, Object value) { 352 vars.put(key, value); 353 } 354 355 // TODO: use FormData to get query params? 356 @Override 357 public Object getProperty(String key) { 358 Object value = getUriInfo().getPathParameters().getFirst(key); 359 if (value == null) { 360 value = request.getParameter(key); 361 if (value == null) { 362 value = vars.get(key); 363 } 364 } 365 return value; 366 } 367 368 @Override 369 public Object getProperty(String key, Object defaultValue) { 370 Object value = getProperty(key); 371 return value == null ? defaultValue : value; 372 } 373 374 @Override 375 public String getCookie(String name) { 376 Cookie[] cookies = request.getCookies(); 377 if (cookies != null) { 378 for (Cookie cookie : cookies) { 379 if (name.equals(cookie.getName())) { 380 return cookie.getValue(); 381 } 382 } 383 } 384 return null; 385 } 386 387 @Override 388 public String getCookie(String name, String defaultValue) { 389 String value = getCookie(name); 390 return value == null ? defaultValue : value; 391 } 392 393 @Override 394 public FormData getForm() { 395 if (form == null) { 396 form = new FormData(request); 397 } 398 return form; 399 } 400 401 @Override 402 public String getBasePath() { 403 if (basePath == null) { 404 String webenginePath = request.getHeader(NUXEO_WEBENGINE_BASE_PATH); 405 if (",".equals(webenginePath)) { 406 // when the parameter is empty, request.getHeader return ',' on 407 // apache server. 408 webenginePath = ""; 409 } 410 basePath = webenginePath != null ? webenginePath : getDefaultBasePath(); 411 } 412 return basePath; 413 } 414 415 private String getDefaultBasePath() { 416 StringBuilder buf = new StringBuilder(request.getRequestURI().length()); 417 String path = request.getContextPath(); 418 if (path == null) { 419 path = "/nuxeo/site"; // for testing 420 } 421 buf.append(path).append(request.getServletPath()); 422 if ("/".equals(path)) { 423 return ""; 424 } 425 int len = buf.length(); 426 if (len > 0 && buf.charAt(len - 1) == '/') { 427 buf.setLength(len - 1); 428 } 429 return buf.toString(); 430 } 431 432 @Override 433 public String getBaseURL() { 434 StringBuffer sb = request.getRequestURL(); 435 int p = sb.indexOf(getBasePath()); 436 if (p > -1) { 437 return sb.substring(0, p); 438 } 439 return sb.toString(); 440 } 441 442 @Override 443 public StringBuilder getServerURL() { 444 StringBuilder url = new StringBuilder(VirtualHostHelper.getServerURL(request)); 445 if (url.toString().endsWith("/")) { 446 url.deleteCharAt(url.length() - 1); 447 } 448 return url; 449 } 450 451 @Override 452 public String getURI() { 453 return request.getRequestURI(); 454 } 455 456 @Override 457 public String getURL() { 458 StringBuffer sb = request.getRequestURL(); 459 if (sb.charAt(sb.length() - 1) == '/') { 460 sb.setLength(sb.length() - 1); 461 } 462 return sb.toString(); 463 } 464 465 public StringBuilder getUrlPathBuffer() { 466 StringBuilder buf = new StringBuilder(getBasePath()); 467 String pathInfo = request.getPathInfo(); 468 if (pathInfo != null) { 469 buf.append(pathInfo); 470 } 471 return buf; 472 } 473 474 @Override 475 public String getUrlPath() { 476 return getUrlPathBuffer().toString(); 477 } 478 479 @Override 480 public String getLoginPath() { 481 StringBuilder buf = getUrlPathBuffer(); 482 int len = buf.length(); 483 if (len > 0 && buf.charAt(len - 1) == '/') { // remove trailing / 484 buf.setLength(len - 1); 485 } 486 buf.append(WebEngineFormAuthenticator.LOGIN_KEY); 487 return buf.toString(); 488 } 489 490 /** 491 * This method is working only for root objects that implement {@link ModuleResource} 492 */ 493 @Override 494 public String getUrlPath(DocumentModel document) { 495 return ((ModuleResource) head).getLink(document); 496 } 497 498 @Override 499 public Log getLog() { 500 return log; 501 } 502 503 /* object stack API */ 504 505 @Override 506 public Resource push(Resource rs) { 507 rs.setPrevious(tail); 508 if (tail != null) { 509 tail.setNext(rs); 510 tail = rs; 511 } else { 512 head = tail = rs; 513 } 514 return rs; 515 } 516 517 @Override 518 public Resource pop() { 519 if (tail == null) { 520 return null; 521 } 522 Resource rs = tail; 523 if (tail == head) { 524 head = tail = null; 525 } else { 526 tail = rs.getPrevious(); 527 tail.setNext(null); 528 } 529 rs.dispose(); 530 return rs; 531 } 532 533 @Override 534 public Resource tail() { 535 return tail; 536 } 537 538 @Override 539 public Resource head() { 540 return head; 541 } 542 543 /** template and script resolver */ 544 545 @Override 546 public ScriptFile getFile(String path) { 547 if (path == null || path.length() == 0) { 548 return null; 549 } 550 char c = path.charAt(0); 551 if (c == '.') { // local path - use the path stack to resolve it 552 File file = getCurrentScriptDirectory(); 553 if (file != null) { 554 try { 555 // get the file local path - TODO this should be done in 556 // ScriptFile? 557 file = new File(file, path).getCanonicalFile(); 558 if (file.isFile()) { 559 return new ScriptFile(file); 560 } 561 } catch (IOException e) { 562 throw new NuxeoException(e); 563 } 564 // try using stacked roots 565 String rootPath = engine.getRootDirectory().getAbsolutePath(); 566 String filePath = file.getAbsolutePath(); 567 path = filePath.substring(rootPath.length()); 568 } else { 569 log.warn("Relative path used but there is any running script"); 570 path = new Path(path).makeAbsolute().toString(); 571 } 572 } 573 return module.getFile(path); 574 } 575 576 public void pushScriptFile(File file) { 577 if (scriptExecutionStack.size() > 64) { // stack limit 578 throw new IllegalStateException("Script execution stack overflowed. More than 64 calls between scripts"); 579 } 580 if (file == null) { 581 throw new IllegalArgumentException("Cannot push a null file"); 582 } 583 scriptExecutionStack.add(file); 584 } 585 586 public File popScriptFile() { 587 int size = scriptExecutionStack.size(); 588 if (size == 0) { 589 throw new IllegalStateException("Script execution stack underflowed. No script path to pop"); 590 } 591 return scriptExecutionStack.remove(size - 1); 592 } 593 594 public File getCurrentScriptFile() { 595 int size = scriptExecutionStack.size(); 596 if (size == 0) { 597 return null; 598 } 599 return scriptExecutionStack.get(size - 1); 600 } 601 602 public File getCurrentScriptDirectory() { 603 int size = scriptExecutionStack.size(); 604 if (size == 0) { 605 return null; 606 } 607 return scriptExecutionStack.get(size - 1).getParentFile(); 608 } 609 610 /* running scripts and rendering templates */ 611 612 @Override 613 public void render(String template, Writer writer) { 614 render(template, null, writer); 615 } 616 617 @Override 618 public void render(String template, Object ctx, Writer writer) { 619 ScriptFile script = getFile(template); 620 if (script != null) { 621 render(script, ctx, writer); 622 } else { 623 throw new WebResourceNotFoundException("Template not found: " + template); 624 } 625 } 626 627 @Override 628 @SuppressWarnings({ "unchecked", "rawtypes" }) 629 public void render(ScriptFile script, Object ctx, Writer writer) { 630 Map map = null; 631 if (ctx instanceof Map) { 632 map = (Map) ctx; 633 } 634 try { 635 String template = script.getURL(); 636 Map<String, Object> bindings = createBindings(map); 637 if (log.isDebugEnabled()) { 638 log.debug("## Rendering: " + template); 639 } 640 pushScriptFile(script.getFile()); 641 engine.getRendering().render(template, bindings, writer); 642 } catch (IOException | RenderingException e) { 643 Throwable cause = ExceptionUtils.getRootCause(e); 644 if (cause instanceof SocketException) { 645 log.debug("Output socket closed: failed to write response", e); 646 return; 647 } 648 throw new NuxeoException( 649 "Failed to render template: " + (script == null ? script : script.getAbsolutePath()), e); 650 } finally { 651 if (!scriptExecutionStack.isEmpty()) { 652 popScriptFile(); 653 } 654 } 655 } 656 657 @Override 658 public Object runScript(String script) { 659 return runScript(script, null); 660 } 661 662 @Override 663 public Object runScript(String script, Map<String, Object> args) { 664 ScriptFile sf = getFile(script); 665 if (sf != null) { 666 return runScript(sf, args); 667 } else { 668 throw new WebResourceNotFoundException("Script not found: " + script); 669 } 670 } 671 672 @Override 673 public Object runScript(ScriptFile script, Map<String, Object> args) { 674 try { 675 pushScriptFile(script.getFile()); 676 return engine.getScripting().runScript(script, createBindings(args)); 677 } catch (ScriptException e) { 678 throw new NuxeoException("Failed to run script " + script, e); 679 } finally { 680 if (!scriptExecutionStack.isEmpty()) { 681 popScriptFile(); 682 } 683 } 684 } 685 686 @Override 687 public boolean checkGuard(String guard) throws ParseException { 688 return PermissionService.parse(guard).check(this); 689 } 690 691 public Map<String, Object> createBindings(Map<String, Object> vars) { 692 Map<String, Object> bindings = new HashMap<>(); 693 if (vars != null) { 694 bindings.putAll(vars); 695 } 696 initializeBindings(bindings); 697 return bindings; 698 } 699 700 @Override 701 public Resource getTargetObject() { 702 Resource t = tail; 703 while (t != null) { 704 if (!t.isAdapter()) { 705 return t; 706 } 707 t = t.getPrevious(); 708 } 709 return null; 710 } 711 712 @Override 713 public AdapterResource getTargetAdapter() { 714 Resource t = tail; 715 while (t != null) { 716 if (t.isAdapter()) { 717 return (AdapterResource) t; 718 } 719 t = t.getPrevious(); 720 } 721 return null; 722 } 723 724 protected void initializeBindings(Map<String, Object> bindings) { 725 Resource obj = getTargetObject(); 726 bindings.put("Context", this); 727 bindings.put("Module", module); 728 bindings.put("Engine", engine); 729 bindings.put("Runtime", Framework.getRuntime()); 730 bindings.put("basePath", getBasePath()); 731 bindings.put("skinPath", getSkinPathPrefix()); 732 bindings.put("contextPath", VirtualHostHelper.getContextPathProperty()); 733 bindings.put("Root", root); 734 if (obj != null) { 735 bindings.put("This", obj); 736 DocumentModel doc = obj.getAdapter(DocumentModel.class); 737 if (doc != null) { 738 bindings.put("Document", doc); 739 } 740 Resource adapter = getTargetAdapter(); 741 if (adapter != null) { 742 bindings.put("Adapter", adapter); 743 } 744 } 745 if (!isRepositoryDisabled && getPrincipal() != null) { 746 bindings.put("Session", getCoreSession()); 747 } 748 } 749 750 private String getSkinPathPrefix() { 751 if (Framework.getProperty(SKIN_PATH_PREFIX_KEY) != null) { 752 return module.getSkinPathPrefix(); 753 } 754 String webenginePath = request.getHeader(NUXEO_WEBENGINE_BASE_PATH); 755 if (webenginePath == null) { 756 return module.getSkinPathPrefix(); 757 } else { 758 return getBasePath() + "/" + module.getName() + "/skin"; 759 } 760 } 761 762 public static boolean isRepositorySupportDisabled() { 763 return isRepositoryDisabled; 764 } 765 766 /** 767 * Can be used by the application to disable injecting repository sessions in scripting context. If the application 768 * is not deploying a repository injecting a repository session will throw exceptions each time rendering is used. 769 * 770 * @param isRepositoryDisabled true to disable repository session injection, false otherwise 771 */ 772 public static void setIsRepositorySupportDisabled(boolean isRepositoryDisabled) { 773 AbstractWebContext.isRepositoryDisabled = isRepositoryDisabled; 774 } 775 776 @Override 777 public void setRepositoryName(String repoName) { 778 RepositoryManager rm = Framework.getService(RepositoryManager.class); 779 if (rm.getRepository(repoName) != null) { 780 this.repoName = repoName; 781 // set the repository name as a request attribute for later retrieval 782 request.setAttribute(RenderingContext.REPOSITORY_NAME_REQUEST_HEADER, repoName); 783 } else { 784 throw new IllegalArgumentException("Repository " + repoName + " not found"); 785 } 786 787 } 788}