001/* 002 * (C) Copyright 2006-2008 Nuxeo SAS (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * bstefanescu 016 * 017 * $Id$ 018 */ 019 020package org.nuxeo.ecm.webengine.model.impl; 021 022import static org.nuxeo.ecm.webengine.WebEngine.SKIN_PATH_PREFIX_KEY; 023 024import java.io.File; 025import java.io.IOException; 026import java.io.Writer; 027import java.net.SocketException; 028import java.security.Principal; 029import java.text.MessageFormat; 030import java.text.ParseException; 031import java.util.HashMap; 032import java.util.LinkedList; 033import java.util.List; 034import java.util.Locale; 035import java.util.Map; 036import java.util.MissingResourceException; 037 038import javax.script.ScriptException; 039import javax.servlet.http.Cookie; 040import javax.servlet.http.HttpServletRequest; 041 042import org.apache.commons.lang.StringUtils; 043import org.apache.commons.logging.Log; 044import org.apache.commons.logging.LogFactory; 045import org.nuxeo.common.utils.ExceptionUtils; 046import org.nuxeo.common.utils.Path; 047import org.nuxeo.ecm.core.api.CoreSession; 048import org.nuxeo.ecm.core.api.DocumentModel; 049import org.nuxeo.ecm.core.api.repository.RepositoryManager; 050import org.nuxeo.ecm.platform.rendering.api.RenderingException; 051import org.nuxeo.ecm.platform.web.common.locale.LocaleProvider; 052import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper; 053import org.nuxeo.ecm.webengine.WebEngine; 054import org.nuxeo.ecm.webengine.WebException; 055import org.nuxeo.ecm.webengine.forms.FormData; 056import org.nuxeo.ecm.webengine.jaxrs.session.SessionFactory; 057import org.nuxeo.ecm.webengine.login.WebEngineFormAuthenticator; 058import org.nuxeo.ecm.webengine.model.AdapterResource; 059import org.nuxeo.ecm.webengine.model.AdapterType; 060import org.nuxeo.ecm.webengine.model.Messages; 061import org.nuxeo.ecm.webengine.model.Module; 062import org.nuxeo.ecm.webengine.model.ModuleResource; 063import org.nuxeo.ecm.webengine.model.Resource; 064import org.nuxeo.ecm.webengine.model.ResourceType; 065import org.nuxeo.ecm.webengine.model.WebContext; 066import org.nuxeo.ecm.webengine.model.exceptions.WebResourceNotFoundException; 067import org.nuxeo.ecm.webengine.scripting.ScriptFile; 068import org.nuxeo.ecm.webengine.security.PermissionService; 069import org.nuxeo.ecm.webengine.session.UserSession; 070import org.nuxeo.runtime.api.Framework; 071 072/** 073 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 074 */ 075public abstract class AbstractWebContext implements WebContext { 076 077 private static final Log log = LogFactory.getLog(WebContext.class); 078 079 // TODO: this should be made configurable through an extension point 080 public static Locale DEFAULT_LOCALE = Locale.ENGLISH; 081 082 public static final String LOCALE_SESSION_KEY = "webengine_locale"; 083 084 private static boolean isRepositoryDisabled = false; 085 086 protected final WebEngine engine; 087 088 private UserSession us; 089 090 protected final LinkedList<File> scriptExecutionStack; 091 092 protected final HttpServletRequest request; 093 094 protected final Map<String, Object> vars; 095 096 protected Resource head; 097 098 protected Resource tail; 099 100 protected Resource root; 101 102 protected Module module; 103 104 protected FormData form; 105 106 protected String basePath; 107 108 private String repoName; 109 110 protected AbstractWebContext(HttpServletRequest request) { 111 engine = Framework.getLocalService(WebEngine.class); 112 scriptExecutionStack = new LinkedList<File>(); 113 this.request = request; 114 vars = new HashMap<String, Object>(); 115 } 116 117 // public abstract HttpServletRequest getHttpServletRequest(); 118 // public abstract HttpServletResponse getHttpServletResponse(); 119 120 public void setModule(Module module) { 121 this.module = module; 122 } 123 124 @Override 125 public Resource getRoot() { 126 return root; 127 } 128 129 @Override 130 public void setRoot(Resource root) { 131 this.root = root; 132 } 133 134 @Override 135 public <T> T getAdapter(Class<T> adapter) { 136 if (CoreSession.class == adapter) { 137 return adapter.cast(getCoreSession()); 138 } else if (Principal.class == adapter) { 139 return adapter.cast(getPrincipal()); 140 } else if (Resource.class == adapter) { 141 return adapter.cast(tail()); 142 } else if (WebContext.class == adapter) { 143 return adapter.cast(this); 144 } else if (Module.class == adapter) { 145 return adapter.cast(module); 146 } else if (WebEngine.class == adapter) { 147 return adapter.cast(engine); 148 } 149 return null; 150 } 151 152 @Override 153 public Module getModule() { 154 return module; 155 } 156 157 @Override 158 public WebEngine getEngine() { 159 return engine; 160 } 161 162 @Override 163 public UserSession getUserSession() { 164 if (us == null) { 165 us = UserSession.getCurrentSession(request); 166 } 167 return us; 168 } 169 170 @Override 171 public CoreSession getCoreSession() { 172 if (StringUtils.isNotBlank(repoName)) { 173 return SessionFactory.getSession(request, repoName); 174 } else { 175 return SessionFactory.getSession(request); 176 } 177 } 178 179 @Override 180 public Principal getPrincipal() { 181 return request.getUserPrincipal(); 182 } 183 184 @Override 185 public HttpServletRequest getRequest() { 186 return request; 187 } 188 189 @Override 190 public String getMethod() { 191 return request.getMethod(); 192 } 193 194 @Override 195 public String getModulePath() { 196 return head.getPath(); 197 } 198 199 @Override 200 public String getMessage(String key) { 201 Messages messages = module.getMessages(); 202 try { 203 return messages.getString(key, getLocale().getLanguage()); 204 } catch (MissingResourceException e) { 205 return '!' + key + '!'; 206 } 207 } 208 209 @Override 210 public String getMessage(String key, Object... args) { 211 Messages messages = module.getMessages(); 212 try { 213 String msg = messages.getString(key, getLocale().getLanguage()); 214 if (args != null && args.length > 0) { 215 // format the string using given args 216 msg = MessageFormat.format(msg, args); 217 } 218 return msg; 219 } catch (MissingResourceException e) { 220 return '!' + key + '!'; 221 } 222 } 223 224 @Override 225 public String getMessage(String key, List<Object> args) { 226 Messages messages = module.getMessages(); 227 try { 228 String msg = messages.getString(key, getLocale().getLanguage()); 229 if (args != null && args.size() > 0) { 230 // format the string using given args 231 msg = MessageFormat.format(msg, args.toArray()); 232 } 233 return msg; 234 } catch (MissingResourceException e) { 235 return '!' + key + '!'; 236 } 237 } 238 239 @Override 240 public String getMessageL(String key, String language) { 241 Messages messages = module.getMessages(); 242 try { 243 return messages.getString(key, language); 244 } catch (MissingResourceException e) { 245 return '!' + key + '!'; 246 } 247 } 248 249 @Override 250 public String getMessageL(String key, String locale, Object... args) { 251 Messages messages = module.getMessages(); 252 try { 253 String msg = messages.getString(key, locale); 254 if (args != null && args.length > 0) { 255 // format the string using given args 256 msg = MessageFormat.format(msg, args); 257 } 258 return msg; 259 } catch (MissingResourceException e) { 260 return '!' + key + '!'; 261 } 262 } 263 264 @Override 265 public String getMessageL(String key, String locale, List<Object> args) { 266 Messages messages = module.getMessages(); 267 try { 268 String msg = messages.getString(key, locale); 269 if (args != null && !args.isEmpty()) { 270 // format the string using given args 271 msg = MessageFormat.format(msg, args.toArray()); 272 } 273 return msg; 274 } catch (MissingResourceException e) { 275 return '!' + key + '!'; 276 } 277 } 278 279 @Override 280 public Locale getLocale() { 281 LocaleProvider localeProvider = Framework.getLocalService(LocaleProvider.class); 282 if (localeProvider != null && request.getUserPrincipal() != null) { 283 Locale userPrefLocale = localeProvider.getLocale(getCoreSession()); 284 if (userPrefLocale != null) { 285 return userPrefLocale; 286 } 287 } 288 289 UserSession us = getUserSession(); 290 if (us != null) { 291 Object locale = us.get(LOCALE_SESSION_KEY); 292 if (locale instanceof Locale) { 293 return (Locale) locale; 294 } 295 } 296 297 // take the one on request 298 Locale locale = request.getLocale(); 299 return locale == null ? DEFAULT_LOCALE : locale; 300 } 301 302 @Override 303 public void setLocale(Locale locale) { 304 UserSession us = getUserSession(); 305 if (us != null) { 306 us.put(LOCALE_SESSION_KEY, locale); 307 } 308 } 309 310 @Override 311 public Resource newObject(String typeName, Object... args) { 312 ResourceType type = module.getType(typeName); 313 if (type == null) { 314 throw new WebResourceNotFoundException("No Such Object Type: " + typeName); 315 } 316 return newObject(type, args); 317 } 318 319 @Override 320 public Resource newObject(ResourceType type, Object... args) { 321 Resource obj = type.newInstance(); 322 try { 323 obj.initialize(this, type, args); 324 } finally { 325 // we must be sure the object is pushed even if an error occurred 326 // otherwise we may end up with an empty object stack and we will 327 // not be able to 328 // handle errors based on objects handleError() method 329 push(obj); 330 } 331 return obj; 332 } 333 334 @Override 335 public AdapterResource newAdapter(Resource ctx, String serviceName, Object... args) { 336 AdapterType st = module.getAdapter(ctx, serviceName); 337 AdapterResource service = (AdapterResource) st.newInstance(); 338 try { 339 service.initialize(this, st, args); 340 } finally { 341 // we must be sure the object is pushed even if an error occurred 342 // otherwise we may end up with an empty object stack and we will 343 // not be able to 344 // handle errors based on objects handleError() method 345 push(service); 346 } 347 return service; 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 if (tail != null) { 508 tail.setNext(rs); 509 rs.setPrevious(tail); 510 tail = rs; 511 } else { 512 rs.setPrevious(tail); 513 head = tail = rs; 514 } 515 return rs; 516 } 517 518 @Override 519 public Resource pop() { 520 if (tail == null) { 521 return null; 522 } 523 Resource rs = tail; 524 if (tail == head) { 525 head = tail = null; 526 } else { 527 tail = rs.getPrevious(); 528 tail.setNext(null); 529 } 530 rs.dispose(); 531 return rs; 532 } 533 534 @Override 535 public Resource tail() { 536 return tail; 537 } 538 539 @Override 540 public Resource head() { 541 return head; 542 } 543 544 /** template and script resolver */ 545 546 @Override 547 public ScriptFile getFile(String path) { 548 if (path == null || path.length() == 0) { 549 return null; 550 } 551 char c = path.charAt(0); 552 if (c == '.') { // local path - use the path stack to resolve it 553 File file = getCurrentScriptDirectory(); 554 if (file != null) { 555 try { 556 // get the file local path - TODO this should be done in 557 // ScriptFile? 558 file = new File(file, path).getCanonicalFile(); 559 if (file.isFile()) { 560 return new ScriptFile(file); 561 } 562 } catch (IOException e) { 563 throw WebException.wrap(e); 564 } 565 // try using stacked roots 566 String rootPath = engine.getRootDirectory().getAbsolutePath(); 567 String filePath = file.getAbsolutePath(); 568 path = filePath.substring(rootPath.length()); 569 } else { 570 log.warn("Relative path used but there is any running script"); 571 path = new Path(path).makeAbsolute().toString(); 572 } 573 } 574 return module.getFile(path); 575 } 576 577 public void pushScriptFile(File file) { 578 if (scriptExecutionStack.size() > 64) { // stack limit 579 throw new IllegalStateException("Script execution stack overflowed. More than 64 calls between scripts"); 580 } 581 if (file == null) { 582 throw new IllegalArgumentException("Cannot push a null file"); 583 } 584 scriptExecutionStack.add(file); 585 } 586 587 public File popScriptFile() { 588 int size = scriptExecutionStack.size(); 589 if (size == 0) { 590 throw new IllegalStateException("Script execution stack underflowed. No script path to pop"); 591 } 592 return scriptExecutionStack.remove(size - 1); 593 } 594 595 public File getCurrentScriptFile() { 596 int size = scriptExecutionStack.size(); 597 if (size == 0) { 598 return null; 599 } 600 return scriptExecutionStack.get(size - 1); 601 } 602 603 public File getCurrentScriptDirectory() { 604 int size = scriptExecutionStack.size(); 605 if (size == 0) { 606 return null; 607 } 608 return scriptExecutionStack.get(size - 1).getParentFile(); 609 } 610 611 /* running scripts and rendering templates */ 612 613 @Override 614 public void render(String template, Writer writer) { 615 render(template, null, writer); 616 } 617 618 @Override 619 public void render(String template, Object ctx, Writer writer) { 620 ScriptFile script = getFile(template); 621 if (script != null) { 622 render(script, ctx, writer); 623 } else { 624 throw new WebResourceNotFoundException("Template not found: " + template); 625 } 626 } 627 628 @Override 629 @SuppressWarnings({ "unchecked", "rawtypes" }) 630 public void render(ScriptFile script, Object ctx, Writer writer) { 631 Map map = null; 632 if (ctx instanceof Map) { 633 map = (Map) ctx; 634 } 635 try { 636 String template = script.getURL(); 637 Map<String, Object> bindings = createBindings(map); 638 if (log.isDebugEnabled()) { 639 log.debug("## Rendering: " + template); 640 } 641 pushScriptFile(script.getFile()); 642 engine.getRendering().render(template, bindings, writer); 643 } catch (IOException | RenderingException e) { 644 Throwable cause = ExceptionUtils.getRootCause(e); 645 if (cause instanceof SocketException) { 646 log.debug("Output socket closed: failed to write response", e); 647 return; 648 } 649 throw WebException.wrap("Failed to render template: " 650 + (script == null ? script : script.getAbsolutePath()), e); 651 } finally { 652 if (!scriptExecutionStack.isEmpty()) { 653 popScriptFile(); 654 } 655 } 656 } 657 658 @Override 659 public Object runScript(String script) { 660 return runScript(script, null); 661 } 662 663 @Override 664 public Object runScript(String script, Map<String, Object> args) { 665 ScriptFile sf = getFile(script); 666 if (sf != null) { 667 return runScript(sf, args); 668 } else { 669 throw new WebResourceNotFoundException("Script not found: " + script); 670 } 671 } 672 673 @Override 674 public Object runScript(ScriptFile script, Map<String, Object> args) { 675 try { 676 pushScriptFile(script.getFile()); 677 return engine.getScripting().runScript(script, createBindings(args)); 678 } catch (WebException e) { 679 throw e; 680 } catch (ScriptException e) { 681 throw WebException.wrap("Failed to run script " + script, e); 682 } finally { 683 if (!scriptExecutionStack.isEmpty()) { 684 popScriptFile(); 685 } 686 } 687 } 688 689 @Override 690 public boolean checkGuard(String guard) throws ParseException { 691 return PermissionService.parse(guard).check(this); 692 } 693 694 public Map<String, Object> createBindings(Map<String, Object> vars) { 695 Map<String, Object> bindings = new HashMap<String, Object>(); 696 if (vars != null) { 697 bindings.putAll(vars); 698 } 699 initializeBindings(bindings); 700 return bindings; 701 } 702 703 @Override 704 public Resource getTargetObject() { 705 Resource t = tail; 706 while (t != null) { 707 if (!t.isAdapter()) { 708 return t; 709 } 710 t = t.getPrevious(); 711 } 712 return null; 713 } 714 715 @Override 716 public AdapterResource getTargetAdapter() { 717 Resource t = tail; 718 while (t != null) { 719 if (t.isAdapter()) { 720 return (AdapterResource) t; 721 } 722 t = t.getPrevious(); 723 } 724 return null; 725 } 726 727 protected void initializeBindings(Map<String, Object> bindings) { 728 Resource obj = getTargetObject(); 729 bindings.put("Context", this); 730 bindings.put("Module", module); 731 bindings.put("Engine", engine); 732 bindings.put("Runtime", Framework.getRuntime()); 733 bindings.put("basePath", getBasePath()); 734 bindings.put("skinPath", getSkinPathPrefix()); 735 bindings.put("contextPath", VirtualHostHelper.getContextPathProperty()); 736 bindings.put("Root", root); 737 if (obj != null) { 738 bindings.put("This", obj); 739 DocumentModel doc = obj.getAdapter(DocumentModel.class); 740 if (doc != null) { 741 bindings.put("Document", doc); 742 } 743 Resource adapter = getTargetAdapter(); 744 if (adapter != null) { 745 bindings.put("Adapter", adapter); 746 } 747 } 748 if (!isRepositoryDisabled && getPrincipal() != null) { 749 bindings.put("Session", getCoreSession()); 750 } 751 } 752 753 private String getSkinPathPrefix() { 754 if (Framework.getProperty(SKIN_PATH_PREFIX_KEY) != null) { 755 return module.getSkinPathPrefix(); 756 } 757 String webenginePath = request.getHeader(NUXEO_WEBENGINE_BASE_PATH); 758 if (webenginePath == null) { 759 return module.getSkinPathPrefix(); 760 } else { 761 return getBasePath() + "/" + module.getName() + "/skin"; 762 } 763 } 764 765 public static boolean isRepositorySupportDisabled() { 766 return isRepositoryDisabled; 767 } 768 769 /** 770 * Can be used by the application to disable injecting repository sessions in scripting context. If the application 771 * is not deploying a repository injecting a repository session will throw exceptions each time rendering is used. 772 * 773 * @param isRepositoryDisabled true to disable repository session injection, false otherwise 774 */ 775 public static void setIsRepositorySupportDisabled(boolean isRepositoryDisabled) { 776 AbstractWebContext.isRepositoryDisabled = isRepositoryDisabled; 777 } 778 779 @Override 780 public void setRepositoryName(String repoName) { 781 RepositoryManager rm = Framework.getLocalService(RepositoryManager.class); 782 if (rm.getRepository(repoName) != null) { 783 this.repoName = repoName; 784 } else { 785 throw new IllegalArgumentException("Repository " + repoName + " not found"); 786 } 787 788 } 789}