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}