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}