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