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.NuxeoPrincipal;
055import org.nuxeo.ecm.core.api.repository.RepositoryManager;
056import org.nuxeo.ecm.core.io.registry.context.RenderingContext;
057import org.nuxeo.ecm.platform.rendering.api.RenderingException;
058import org.nuxeo.ecm.platform.web.common.locale.LocaleProvider;
059import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper;
060import org.nuxeo.ecm.webengine.WebEngine;
061import org.nuxeo.ecm.webengine.forms.FormData;
062import org.nuxeo.ecm.webengine.jaxrs.session.SessionFactory;
063import org.nuxeo.ecm.webengine.login.WebEngineFormAuthenticator;
064import org.nuxeo.ecm.webengine.model.AdapterResource;
065import org.nuxeo.ecm.webengine.model.Messages;
066import org.nuxeo.ecm.webengine.model.Module;
067import org.nuxeo.ecm.webengine.model.ModuleResource;
068import org.nuxeo.ecm.webengine.model.Resource;
069import org.nuxeo.ecm.webengine.model.ResourceType;
070import org.nuxeo.ecm.webengine.model.WebContext;
071import org.nuxeo.ecm.webengine.model.exceptions.WebResourceNotFoundException;
072import org.nuxeo.ecm.webengine.scripting.ScriptFile;
073import org.nuxeo.ecm.webengine.security.PermissionService;
074import org.nuxeo.ecm.webengine.session.UserSession;
075import org.nuxeo.runtime.api.Framework;
076
077/**
078 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
079 */
080public abstract class AbstractWebContext implements WebContext {
081
082    private static final Log log = LogFactory.getLog(WebContext.class);
083
084    // TODO: this should be made configurable through an extension point
085    protected static final Locale DEFAULT_LOCALE = Locale.ENGLISH;
086
087    public static final String LOCALE_SESSION_KEY = "webengine_locale";
088
089    private static boolean isRepositoryDisabled = false;
090
091    protected final WebEngine engine;
092
093    private UserSession us;
094
095    protected final LinkedList<File> scriptExecutionStack;
096
097    protected final HttpServletRequest request;
098
099    protected final HttpServletResponse response;
100
101    protected final Map<String, Object> vars;
102
103    protected Resource head;
104
105    protected Resource tail;
106
107    protected Resource root;
108
109    protected Module module;
110
111    protected FormData form;
112
113    protected String basePath;
114
115    private String repoName;
116
117    protected AbstractWebContext(HttpServletRequest request, HttpServletResponse response) {
118        engine = Framework.getService(WebEngine.class);
119        scriptExecutionStack = new LinkedList<>();
120        this.request = request;
121        this.response = response;
122        vars = new HashMap<>();
123    }
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 (NuxeoPrincipal.class == adapter) {
146            return adapter.cast(getPrincipal());
147        } else if (Resource.class == adapter) {
148            return adapter.cast(tail());
149        } else if (WebContext.class == adapter) {
150            return adapter.cast(this);
151        } else if (Module.class == adapter) {
152            return adapter.cast(module);
153        } else if (WebEngine.class == adapter) {
154            return adapter.cast(engine);
155        }
156        return null;
157    }
158
159    @Override
160    public Module getModule() {
161        return module;
162    }
163
164    @Override
165    public WebEngine getEngine() {
166        return engine;
167    }
168
169    @Override
170    public UserSession getUserSession() {
171        if (us == null) {
172            us = UserSession.getCurrentSession(request);
173        }
174        return us;
175    }
176
177    @Override
178    public CoreSession getCoreSession() {
179        if (StringUtils.isNotBlank(repoName)) {
180            return SessionFactory.getSession(request, repoName);
181        } else {
182            return SessionFactory.getSession(request);
183        }
184    }
185
186    @Override
187    public NuxeoPrincipal getPrincipal() {
188        return (NuxeoPrincipal) request.getUserPrincipal();
189    }
190
191    @Override
192    public HttpServletRequest getRequest() {
193        return request;
194    }
195
196    public HttpServletResponse getResponse() {
197        return response;
198    }
199
200    @Override
201    public String getMethod() {
202        return request.getMethod();
203    }
204
205    @Override
206    public String getModulePath() {
207        return head.getPath();
208    }
209
210    @Override
211    public String getMessage(String key) {
212        Messages messages = module.getMessages();
213        try {
214            return messages.getString(key, getLocale().getLanguage());
215        } catch (MissingResourceException e) {
216            return '!' + key + '!';
217        }
218    }
219
220    @Override
221    public String getMessage(String key, Object... args) {
222        Messages messages = module.getMessages();
223        try {
224            String msg = messages.getString(key, getLocale().getLanguage());
225            if (args != null && args.length > 0) {
226                // format the string using given args
227                msg = MessageFormat.format(msg, args);
228            }
229            return msg;
230        } catch (MissingResourceException e) {
231            return '!' + key + '!';
232        }
233    }
234
235    @Override
236    public String getMessage(String key, List<Object> args) {
237        Messages messages = module.getMessages();
238        try {
239            String msg = messages.getString(key, getLocale().getLanguage());
240            if (CollectionUtils.isNotEmpty(args)) {
241                // format the string using given args
242                msg = MessageFormat.format(msg, args.toArray());
243            }
244            return msg;
245        } catch (MissingResourceException e) {
246            return '!' + key + '!';
247        }
248    }
249
250    @Override
251    public String getMessageL(String key, String language) {
252        Messages messages = module.getMessages();
253        try {
254            return messages.getString(key, language);
255        } catch (MissingResourceException e) {
256            return '!' + key + '!';
257        }
258    }
259
260    @Override
261    public String getMessageL(String key, String locale, Object... args) {
262        Messages messages = module.getMessages();
263        try {
264            String msg = messages.getString(key, locale);
265            if (args != null && args.length > 0) {
266                // format the string using given args
267                msg = MessageFormat.format(msg, args);
268            }
269            return msg;
270        } catch (MissingResourceException e) {
271            return '!' + key + '!';
272        }
273    }
274
275    @Override
276    public String getMessageL(String key, String locale, List<Object> args) {
277        Messages messages = module.getMessages();
278        try {
279            String msg = messages.getString(key, locale);
280            if (args != null && !args.isEmpty()) {
281                // format the string using given args
282                msg = MessageFormat.format(msg, args.toArray());
283            }
284            return msg;
285        } catch (MissingResourceException e) {
286            return '!' + key + '!';
287        }
288    }
289
290    @Override
291    public Locale getLocale() {
292        LocaleProvider localeProvider = Framework.getService(LocaleProvider.class);
293        if (localeProvider != null && request.getUserPrincipal() != null) {
294            Locale userPrefLocale = localeProvider.getLocale(getCoreSession());
295            if (userPrefLocale != null) {
296                return userPrefLocale;
297            }
298        }
299
300        UserSession userSession = getUserSession();
301        if (userSession != null) {
302            Object locale = userSession.get(LOCALE_SESSION_KEY);
303            if (locale instanceof Locale) {
304                return (Locale) locale;
305            }
306        }
307
308        // take the one on request
309        Locale locale = request.getLocale();
310        return locale == null ? DEFAULT_LOCALE : locale;
311    }
312
313    @Override
314    public void setLocale(Locale locale) {
315        UserSession userSession = getUserSession();
316        if (userSession != null) {
317            userSession.put(LOCALE_SESSION_KEY, locale);
318        }
319    }
320
321    @Override
322    public Resource newObject(String typeName, Object... args) {
323        ResourceType type = module.getType(typeName);
324        if (type == null) {
325            throw new WebResourceNotFoundException("No Such Object Type: " + typeName);
326        }
327        return newObject(type, args);
328    }
329
330    @Override
331    public Resource newObject(ResourceType type, Object... args) {
332        Resource obj = type.newInstance(type.getResourceClass(), this);
333        try {
334            obj.initialize(this, type, args);
335        } finally {
336            // we must be sure the object is pushed even if an error occurred
337            // otherwise we may end up with an empty object stack and we will
338            // not be able to
339            // handle errors based on objects handleError() method
340            push(obj);
341        }
342        return obj;
343    }
344
345    @Override
346    public AdapterResource newAdapter(Resource ctx, String serviceName, Object... args) {
347        return (AdapterResource) newObject(module.getAdapter(ctx, serviceName), args);
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        rs.setPrevious(tail);
508        if (tail != null) {
509            tail.setNext(rs);
510            tail = rs;
511        } else {
512            head = tail = rs;
513        }
514        return rs;
515    }
516
517    @Override
518    public Resource pop() {
519        if (tail == null) {
520            return null;
521        }
522        Resource rs = tail;
523        if (tail == head) {
524            head = tail = null;
525        } else {
526            tail = rs.getPrevious();
527            tail.setNext(null);
528        }
529        rs.dispose();
530        return rs;
531    }
532
533    @Override
534    public Resource tail() {
535        return tail;
536    }
537
538    @Override
539    public Resource head() {
540        return head;
541    }
542
543    /** template and script resolver */
544
545    @Override
546    public ScriptFile getFile(String path) {
547        if (path == null || path.length() == 0) {
548            return null;
549        }
550        char c = path.charAt(0);
551        if (c == '.') { // local path - use the path stack to resolve it
552            File file = getCurrentScriptDirectory();
553            if (file != null) {
554                try {
555                    // get the file local path - TODO this should be done in
556                    // ScriptFile?
557                    file = new File(file, path).getCanonicalFile();
558                    if (file.isFile()) {
559                        return new ScriptFile(file);
560                    }
561                } catch (IOException e) {
562                    throw new NuxeoException(e);
563                }
564                // try using stacked roots
565                String rootPath = engine.getRootDirectory().getAbsolutePath();
566                String filePath = file.getAbsolutePath();
567                path = filePath.substring(rootPath.length());
568            } else {
569                log.warn("Relative path used but there is any running script");
570                path = new Path(path).makeAbsolute().toString();
571            }
572        }
573        return module.getFile(path);
574    }
575
576    public void pushScriptFile(File file) {
577        if (scriptExecutionStack.size() > 64) { // stack limit
578            throw new IllegalStateException("Script execution stack overflowed. More than 64 calls between scripts");
579        }
580        if (file == null) {
581            throw new IllegalArgumentException("Cannot push a null file");
582        }
583        scriptExecutionStack.add(file);
584    }
585
586    public File popScriptFile() {
587        int size = scriptExecutionStack.size();
588        if (size == 0) {
589            throw new IllegalStateException("Script execution stack underflowed. No script path to pop");
590        }
591        return scriptExecutionStack.remove(size - 1);
592    }
593
594    public File getCurrentScriptFile() {
595        int size = scriptExecutionStack.size();
596        if (size == 0) {
597            return null;
598        }
599        return scriptExecutionStack.get(size - 1);
600    }
601
602    public File getCurrentScriptDirectory() {
603        int size = scriptExecutionStack.size();
604        if (size == 0) {
605            return null;
606        }
607        return scriptExecutionStack.get(size - 1).getParentFile();
608    }
609
610    /* running scripts and rendering templates */
611
612    @Override
613    public void render(String template, Writer writer) {
614        render(template, null, writer);
615    }
616
617    @Override
618    public void render(String template, Object ctx, Writer writer) {
619        ScriptFile script = getFile(template);
620        if (script != null) {
621            render(script, ctx, writer);
622        } else {
623            throw new WebResourceNotFoundException("Template not found: " + template);
624        }
625    }
626
627    @Override
628    @SuppressWarnings({ "unchecked", "rawtypes" })
629    public void render(ScriptFile script, Object ctx, Writer writer) {
630        Map map = null;
631        if (ctx instanceof Map) {
632            map = (Map) ctx;
633        }
634        try {
635            String template = script.getURL();
636            Map<String, Object> bindings = createBindings(map);
637            if (log.isDebugEnabled()) {
638                log.debug("## Rendering: " + template);
639            }
640            pushScriptFile(script.getFile());
641            engine.getRendering().render(template, bindings, writer);
642        } catch (IOException | RenderingException e) {
643            Throwable cause = ExceptionUtils.getRootCause(e);
644            if (cause instanceof SocketException) {
645                log.debug("Output socket closed: failed to write response", e);
646                return;
647            }
648            throw new NuxeoException(
649                    "Failed to render template: " + (script == null ? script : script.getAbsolutePath()), e);
650        } finally {
651            if (!scriptExecutionStack.isEmpty()) {
652                popScriptFile();
653            }
654        }
655    }
656
657    @Override
658    public Object runScript(String script) {
659        return runScript(script, null);
660    }
661
662    @Override
663    public Object runScript(String script, Map<String, Object> args) {
664        ScriptFile sf = getFile(script);
665        if (sf != null) {
666            return runScript(sf, args);
667        } else {
668            throw new WebResourceNotFoundException("Script not found: " + script);
669        }
670    }
671
672    @Override
673    public Object runScript(ScriptFile script, Map<String, Object> args) {
674        try {
675            pushScriptFile(script.getFile());
676            return engine.getScripting().runScript(script, createBindings(args));
677        } catch (ScriptException e) {
678            throw new NuxeoException("Failed to run script " + script, e);
679        } finally {
680            if (!scriptExecutionStack.isEmpty()) {
681                popScriptFile();
682            }
683        }
684    }
685
686    @Override
687    public boolean checkGuard(String guard) throws ParseException {
688        return PermissionService.parse(guard).check(this);
689    }
690
691    public Map<String, Object> createBindings(Map<String, Object> vars) {
692        Map<String, Object> bindings = new HashMap<>();
693        if (vars != null) {
694            bindings.putAll(vars);
695        }
696        initializeBindings(bindings);
697        return bindings;
698    }
699
700    @Override
701    public Resource getTargetObject() {
702        Resource t = tail;
703        while (t != null) {
704            if (!t.isAdapter()) {
705                return t;
706            }
707            t = t.getPrevious();
708        }
709        return null;
710    }
711
712    @Override
713    public AdapterResource getTargetAdapter() {
714        Resource t = tail;
715        while (t != null) {
716            if (t.isAdapter()) {
717                return (AdapterResource) t;
718            }
719            t = t.getPrevious();
720        }
721        return null;
722    }
723
724    protected void initializeBindings(Map<String, Object> bindings) {
725        Resource obj = getTargetObject();
726        bindings.put("Context", this);
727        bindings.put("Module", module);
728        bindings.put("Engine", engine);
729        bindings.put("Runtime", Framework.getRuntime());
730        bindings.put("basePath", getBasePath());
731        bindings.put("skinPath", getSkinPathPrefix());
732        bindings.put("contextPath", VirtualHostHelper.getContextPathProperty());
733        bindings.put("Root", root);
734        if (obj != null) {
735            bindings.put("This", obj);
736            DocumentModel doc = obj.getAdapter(DocumentModel.class);
737            if (doc != null) {
738                bindings.put("Document", doc);
739            }
740            Resource adapter = getTargetAdapter();
741            if (adapter != null) {
742                bindings.put("Adapter", adapter);
743            }
744        }
745        if (!isRepositoryDisabled && getPrincipal() != null) {
746            bindings.put("Session", getCoreSession());
747        }
748    }
749
750    private String getSkinPathPrefix() {
751        if (Framework.getProperty(SKIN_PATH_PREFIX_KEY) != null) {
752            return module.getSkinPathPrefix();
753        }
754        String webenginePath = request.getHeader(NUXEO_WEBENGINE_BASE_PATH);
755        if (webenginePath == null) {
756            return module.getSkinPathPrefix();
757        } else {
758            return getBasePath() + "/" + module.getName() + "/skin";
759        }
760    }
761
762    public static boolean isRepositorySupportDisabled() {
763        return isRepositoryDisabled;
764    }
765
766    /**
767     * Can be used by the application to disable injecting repository sessions in scripting context. If the application
768     * is not deploying a repository injecting a repository session will throw exceptions each time rendering is used.
769     *
770     * @param isRepositoryDisabled true to disable repository session injection, false otherwise
771     */
772    public static void setIsRepositorySupportDisabled(boolean isRepositoryDisabled) {
773        AbstractWebContext.isRepositoryDisabled = isRepositoryDisabled;
774    }
775
776    @Override
777    public void setRepositoryName(String repoName) {
778        RepositoryManager rm = Framework.getService(RepositoryManager.class);
779        if (rm.getRepository(repoName) != null) {
780            this.repoName = repoName;
781            // set the repository name as a request attribute for later retrieval
782            request.setAttribute(RenderingContext.REPOSITORY_NAME_REQUEST_HEADER, repoName);
783        } else {
784            throw new IllegalArgumentException("Repository " + repoName + " not found");
785        }
786
787    }
788}