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