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