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