001/*
002 * (C) Copyright 2006-2013 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 *     Nuxeo - initial API and implementation
018 *
019 * $Id: URLPolicyServiceImpl.java 29556 2008-01-23 00:59:39Z jcarsique $
020 */
021
022package org.nuxeo.ecm.platform.ui.web.rest.services;
023
024import java.util.ArrayList;
025import java.util.LinkedHashMap;
026import java.util.List;
027import java.util.Map;
028
029import javax.el.ELContext;
030import javax.el.ELException;
031import javax.el.ExpressionFactory;
032import javax.el.MethodExpression;
033import javax.el.ValueExpression;
034import javax.faces.context.FacesContext;
035import javax.servlet.ServletContext;
036import javax.servlet.http.HttpServletRequest;
037import javax.servlet.http.HttpServletResponse;
038
039import org.apache.commons.lang.StringUtils;
040import org.apache.commons.logging.Log;
041import org.apache.commons.logging.LogFactory;
042import org.nuxeo.common.utils.URIUtils;
043import org.nuxeo.ecm.core.api.DocumentLocation;
044import org.nuxeo.ecm.core.io.download.DownloadService;
045import org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants;
046import org.nuxeo.ecm.platform.ui.web.auth.NuxeoAuthenticationFilter;
047import org.nuxeo.ecm.platform.ui.web.rest.StaticNavigationHandler;
048import org.nuxeo.ecm.platform.ui.web.rest.api.URLPolicyService;
049import org.nuxeo.ecm.platform.ui.web.rest.descriptors.URLPatternDescriptor;
050import org.nuxeo.ecm.platform.ui.web.rest.descriptors.ValueBindingDescriptor;
051import org.nuxeo.ecm.platform.ui.web.util.BaseURL;
052import org.nuxeo.ecm.platform.ui.web.util.ComponentTagUtils;
053import org.nuxeo.ecm.platform.url.api.DocumentView;
054import org.nuxeo.ecm.platform.url.api.DocumentViewCodecManager;
055import org.nuxeo.ecm.platform.url.codec.DocumentFileCodec;
056import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper;
057import org.nuxeo.runtime.api.Framework;
058
059public class URLPolicyServiceImpl implements URLPolicyService {
060
061    public static final String NAME = URLPolicyServiceImpl.class.getName();
062
063    private static final Log log = LogFactory.getLog(URLPolicyServiceImpl.class);
064
065    // this used to be a codec but now delegates to DownloadService
066    public static final String DOWNLOADFILE_PATTERN = "downloadFile";
067
068    protected final Map<String, URLPatternDescriptor> descriptors;
069
070    protected StaticNavigationHandler viewIdManager;
071
072    public URLPolicyServiceImpl() {
073        // make sure the descriptors list order follows registration order, as
074        // order may have an impact on resolved pattern
075        descriptors = new LinkedHashMap<String, URLPatternDescriptor>();
076    }
077
078    protected List<URLPatternDescriptor> getURLPatternDescriptors() {
079        // TODO: add cache
080        List<URLPatternDescriptor> lst = new ArrayList<URLPatternDescriptor>();
081        for (URLPatternDescriptor desc : descriptors.values()) {
082            if (desc.getEnabled()) {
083                // add default at first
084                if (desc.getDefaultURLPolicy()) {
085                    lst.add(0, desc);
086                } else {
087                    lst.add(desc);
088                }
089            }
090        }
091        return lst;
092    }
093
094    protected URLPatternDescriptor getDefaultPatternDescriptor() {
095        for (URLPatternDescriptor desc : descriptors.values()) {
096            if (desc.getEnabled()) {
097                if (desc.getDefaultURLPolicy()) {
098                    return desc;
099                }
100            }
101        }
102        return null;
103    }
104
105    @Override
106    public String getDefaultPatternName() {
107        URLPatternDescriptor desc = getDefaultPatternDescriptor();
108        if (desc != null) {
109            return desc.getName();
110        }
111        return null;
112    }
113
114    @Override
115    public boolean hasPattern(String name) {
116        URLPatternDescriptor desc = descriptors.get(name);
117        return desc != null;
118    }
119
120    protected static DocumentViewCodecManager getDocumentViewCodecService() {
121        return Framework.getService(DocumentViewCodecManager.class);
122    }
123
124    protected URLPatternDescriptor getURLPatternDescriptor(String patternName) {
125        URLPatternDescriptor desc = descriptors.get(patternName);
126        if (desc == null) {
127            throw new IllegalArgumentException("Unknown pattern " + patternName);
128        }
129        return desc;
130    }
131
132    @Override
133    public boolean isCandidateForDecoding(HttpServletRequest httpRequest) {
134        // only rewrite GET/HEAD URLs
135        String method = httpRequest.getMethod();
136        if (!method.equals("GET") && !method.equals("HEAD")) {
137            return false;
138        }
139
140        // look for appropriate pattern and see if it needs filter
141        // preprocessing
142        URLPatternDescriptor desc = getURLPatternDescriptor(httpRequest);
143        if (desc != null) {
144            return desc.getNeedFilterPreprocessing();
145        }
146        // return default pattern descriptor behaviour
147        URLPatternDescriptor defaultPattern = getDefaultPatternDescriptor();
148        if (defaultPattern != null) {
149            return defaultPattern.getNeedFilterPreprocessing();
150        }
151        return false;
152    }
153
154    @Override
155    public boolean isCandidateForEncoding(HttpServletRequest httpRequest) {
156        Boolean forceEncoding = Boolean.FALSE;
157        Object forceEncodingValue = httpRequest.getAttribute(FORCE_URL_ENCODING_REQUEST_KEY);
158        if (forceEncodingValue instanceof Boolean) {
159            forceEncoding = (Boolean) forceEncodingValue;
160        }
161
162        // only POST access need a redirect,unless with force encoding (this
163        // happens when redirect is triggered after a seam page has been
164        // processed)
165        if (!forceEncoding.booleanValue() && !httpRequest.getMethod().equals("POST")) {
166            return false;
167        }
168
169        Object skipRedirect = httpRequest.getAttribute(NXAuthConstants.DISABLE_REDIRECT_REQUEST_KEY);
170        if (skipRedirect instanceof Boolean && ((Boolean) skipRedirect).booleanValue()) {
171            return false;
172        }
173
174        // look for appropriate pattern and see if it needs redirect
175        URLPatternDescriptor desc = getURLPatternDescriptor(httpRequest);
176        if (desc != null) {
177            return desc.getNeedRedirectFilter();
178        }
179        // return default pattern descriptor behaviour
180        URLPatternDescriptor defaultPattern = getDefaultPatternDescriptor();
181        if (defaultPattern != null) {
182            return defaultPattern.getNeedRedirectFilter();
183        }
184        return false;
185    }
186
187    @Override
188    public void setDocumentViewInRequest(HttpServletRequest request, DocumentView docView) {
189        request.setAttribute(NXAuthConstants.REQUESTED_URL, NuxeoAuthenticationFilter.getRequestedUrl(request));
190        request.setAttribute(DOCUMENT_VIEW_REQUEST_KEY, docView);
191    }
192
193    protected URLPatternDescriptor getURLPatternDescriptor(HttpServletRequest request) {
194        URLPatternDescriptor res = null;
195        for (URLPatternDescriptor desc : getURLPatternDescriptors()) {
196            DocumentView docView = getDocumentViewFromRequest(desc.getName(), request);
197            if (docView != null) {
198                res = desc;
199                break;
200            }
201        }
202        // if (res == null && log.isDebugEnabled()) {
203        // log.debug("Could not get url pattern for request "
204        // + request.getRequestURL());
205        // }
206        return res;
207    }
208
209    @Override
210    public DocumentView getDocumentViewFromRequest(HttpServletRequest request) {
211        DocumentView docView = null;
212        for (URLPatternDescriptor desc : getURLPatternDescriptors()) {
213            docView = getDocumentViewFromRequest(desc.getName(), request);
214            if (docView != null) {
215                break;
216            }
217        }
218
219        // if (docView == null && log.isDebugEnabled()) {
220        // log.debug("Could not get document view from request "
221        // + request.getRequestURL());
222        // }
223        return docView;
224    }
225
226    @Override
227    public DocumentView getDocumentViewFromRequest(String patternName, HttpServletRequest request) {
228        Object value = request.getAttribute(DOCUMENT_VIEW_REQUEST_KEY);
229        if (value instanceof DocumentView) {
230            DocumentView requestDocView = (DocumentView) value;
231            // check if document view in request was set thanks to this pattern
232            if (patternName.equals(requestDocView.getPatternName())) {
233                return requestDocView;
234            }
235        }
236
237        // try to build it from the request
238        String url;
239        String queryString = request.getQueryString();
240        if (queryString != null) {
241            url = new String(request.getRequestURL() + "?" + queryString);
242        } else {
243            url = new String(request.getRequestURL());
244        }
245        URLPatternDescriptor desc = getURLPatternDescriptor(patternName);
246        String codecName = desc.getDocumentViewCodecName();
247        DocumentView docView = null;
248        DocumentViewCodecManager docViewService = getDocumentViewCodecService();
249        if (docViewService != null) {
250            docView = docViewService.getDocumentViewFromUrl(codecName, url, desc.getNeedBaseURL(),
251                    BaseURL.getLocalBaseURL(request));
252        }
253        if (docView != null) {
254            // set pattern name
255            docView.setPatternName(patternName);
256            // set other parameters as set in the url pattern if docView does
257            // not hold them already
258            Map<String, String> docViewParameters = docView.getParameters();
259            Map<String, String> requestParameters = URIUtils.getRequestParameters(queryString);
260            if (requestParameters != null) {
261                ValueBindingDescriptor[] bindings = desc.getValueBindings();
262                for (ValueBindingDescriptor binding : bindings) {
263                    String paramName = binding.getName();
264                    if (!docViewParameters.containsKey(paramName)) {
265                        Object paramValue = requestParameters.get(paramName);
266                        if (paramValue == null || paramValue instanceof String) {
267                            docView.addParameter(paramName, (String) paramValue);
268                        }
269                    }
270                }
271            }
272        }
273
274        return docView;
275    }
276
277    protected URLPatternDescriptor getURLPatternDescriptor(DocumentView docView) {
278        URLPatternDescriptor res = null;
279        if (docView != null) {
280            String patternName = docView.getPatternName();
281            try {
282                res = getURLPatternDescriptor(patternName);
283            } catch (IllegalArgumentException e) {
284            }
285        }
286        // if (res == null && log.isDebugEnabled()) {
287        // log.debug("Could not get url pattern for document view");
288        // }
289        return res;
290    }
291
292    @Override
293    public String getUrlFromDocumentView(DocumentView docView, String baseUrl) {
294        String url = null;
295        String patternName = docView.getPatternName();
296        if (patternName != null) {
297            // try with original document view pattern
298            URLPatternDescriptor desc = getURLPatternDescriptor(patternName);
299            if (desc != null) {
300                // return corresponding url
301                url = getUrlFromDocumentView(desc.getName(), docView, baseUrl);
302            }
303        }
304        if (url == null) {
305            // take first matching pattern
306            List<URLPatternDescriptor> descs = getURLPatternDescriptors();
307            for (URLPatternDescriptor desc : descs) {
308                url = getUrlFromDocumentView(desc.getName(), docView, baseUrl);
309                if (url != null) {
310                    break;
311                }
312            }
313        }
314        // if (url == null && log.isDebugEnabled()) {
315        // log.debug("Could not get url from document view");
316        // }
317        return url;
318    }
319
320    /**
321     * Returns patterns sorted according to:
322     * <ul>
323     * <li>First patterns holding the given view id</li>
324     * <li>The default pattern if it does not hold this view id</li>
325     * <li>Other patterns not holding this view id</li>
326     * </ul>
327     *
328     * @since 5.4.2
329     * @param viewId
330     * @deprecated since 5.5
331     */
332    @Deprecated
333    protected List<URLPatternDescriptor> getSortedURLPatternDescriptorsFor(String viewId) {
334        List<URLPatternDescriptor> sortedDescriptors = new ArrayList<URLPatternDescriptor>();
335        List<URLPatternDescriptor> nonMatchingViewIdDescriptors = new ArrayList<URLPatternDescriptor>();
336
337        List<URLPatternDescriptor> descriptors = getURLPatternDescriptors();
338        for (URLPatternDescriptor descriptor : descriptors) {
339            List<String> handledViewIds = descriptor.getViewIds();
340            if (handledViewIds != null && handledViewIds.contains(viewId)) {
341                sortedDescriptors.add(descriptor);
342            } else {
343                nonMatchingViewIdDescriptors.add(descriptor);
344            }
345        }
346        sortedDescriptors.addAll(nonMatchingViewIdDescriptors);
347        return sortedDescriptors;
348    }
349
350    @Override
351    public String getUrlFromDocumentView(String patternName, DocumentView docView, String baseUrl) {
352        if (DOWNLOADFILE_PATTERN.equals(patternName)) {
353            // this used to be a codec but now delegates to DownloadService
354            DownloadService downloadService = Framework.getService(DownloadService.class);
355            DocumentLocation docLoc = docView.getDocumentLocation();
356            String repositoryName = docLoc.getServerName();
357            String docId = docLoc.getDocRef().toString();
358            String xpath = docView.getParameter(DocumentFileCodec.FILE_PROPERTY_PATH_KEY);
359            String filename = docView.getParameter(DocumentFileCodec.FILENAME_KEY);
360            String url = downloadService.getDownloadUrl(repositoryName, docId, xpath, filename);
361            if (!StringUtils.isBlank(baseUrl)) {
362                if (!baseUrl.endsWith("/")) {
363                    baseUrl += "/";
364                }
365                url = baseUrl + url;
366            }
367            return url;
368        }
369        DocumentViewCodecManager docViewService = getDocumentViewCodecService();
370        URLPatternDescriptor desc = getURLPatternDescriptor(patternName);
371        String codecName = desc.getDocumentViewCodecName();
372        return docViewService.getUrlFromDocumentView(codecName, docView, desc.getNeedBaseURL(), baseUrl);
373    }
374
375    @Override
376    public void applyRequestParameters(FacesContext facesContext) {
377        // try to set document view
378        ExpressionFactory ef = facesContext.getApplication().getExpressionFactory();
379        ELContext context = facesContext.getELContext();
380
381        HttpServletRequest httpRequest = (HttpServletRequest) facesContext.getExternalContext().getRequest();
382
383        URLPatternDescriptor pattern = getURLPatternDescriptor(httpRequest);
384        if (pattern == null) {
385            return;
386        }
387
388        DocumentView docView = getDocumentViewFromRequest(pattern.getName(), httpRequest);
389        // pattern applies => document view will not be null
390        if (docView != null) {
391            String documentViewBinding = pattern.getDocumentViewBinding();
392            if (documentViewBinding != null && !"".equals(documentViewBinding)) {
393                // try to set it from custom mapping
394                ValueExpression ve = ef.createValueExpression(context, pattern.getDocumentViewBinding(), Object.class);
395                ve.setValue(context, docView);
396            }
397        }
398
399        Map<String, String> docViewParameters = null;
400        if (docView != null) {
401            docViewParameters = docView.getParameters();
402        }
403        ValueBindingDescriptor[] bindings = pattern.getValueBindings();
404        if (bindings != null && httpRequest.getAttribute(URLPolicyService.DISABLE_ACTION_BINDING_KEY) == null) {
405            for (ValueBindingDescriptor binding : bindings) {
406                if (!binding.getCallSetter()) {
407                    continue;
408                }
409                String paramName = binding.getName();
410                // try doc view parameters
411                Object value = null;
412                if (docViewParameters != null && docViewParameters.containsKey(paramName)) {
413                    value = docView.getParameter(paramName);
414                } else {
415                    // try request attributes
416                    value = httpRequest.getAttribute(paramName);
417                }
418                String expr = binding.getExpression();
419                if (ComponentTagUtils.isValueReference(expr)) {
420                    ValueExpression ve = ef.createValueExpression(context, expr, Object.class);
421                    try {
422                        ve.setValue(context, value);
423                    } catch (ELException e) {
424                        log.error("Could not apply request parameter '" + value + "' to expression '" + expr + "'", e);
425                    }
426                }
427            }
428        }
429    }
430
431    @Override
432    public void appendParametersToRequest(FacesContext facesContext) {
433        appendParametersToRequest(facesContext, null);
434    }
435
436    public void appendParametersToRequest(FacesContext facesContext, String pattern) {
437        // try to get doc view from custom mapping
438        DocumentView docView = null;
439        ExpressionFactory ef = facesContext.getApplication().getExpressionFactory();
440        ELContext context = facesContext.getELContext();
441        HttpServletRequest httpRequest = (HttpServletRequest) facesContext.getExternalContext().getRequest();
442
443        // get existing document view from given pattern, else create it
444        URLPatternDescriptor patternDesc = null;
445        if (pattern != null && !"".equals(pattern)) {
446            patternDesc = getURLPatternDescriptor(pattern);
447        } else {
448            // iterate over pattern descriptors, and take the first one that
449            // applies, or use the default one
450            List<URLPatternDescriptor> descs = getURLPatternDescriptors();
451            boolean applies = false;
452            for (URLPatternDescriptor desc : descs) {
453                String documentViewAppliesExpr = desc.getDocumentViewBindingApplies();
454                if (!StringUtils.isBlank(documentViewAppliesExpr)) {
455                    // TODO: maybe put view id to the request to help writing
456                    // the EL expression
457                    ValueExpression ve = ef.createValueExpression(context, documentViewAppliesExpr, Object.class);
458                    try {
459                        Object res = ve.getValue(context);
460                        if (Boolean.TRUE.equals(res)) {
461                            applies = true;
462                        }
463                    } catch (ELException e) {
464                        if (log.isDebugEnabled()) {
465                            log.debug(String.format("Error executing expression '%s' for " + "url pattern '%s': %s",
466                                    documentViewAppliesExpr, desc.getName(), e.getMessage()));
467                        }
468                    }
469                }
470                if (applies) {
471                    patternDesc = desc;
472                    break;
473                }
474            }
475            if (patternDesc == null) {
476                // default on the default pattern desc
477                patternDesc = getDefaultPatternDescriptor();
478            }
479        }
480        if (patternDesc != null) {
481            // resolved doc view values thanks to bindings
482            Object docViewValue = null;
483            String documentViewBinding = patternDesc.getDocumentViewBinding();
484            if (!StringUtils.isBlank(documentViewBinding)) {
485                ValueExpression ve = ef.createValueExpression(context, documentViewBinding, Object.class);
486                docViewValue = ve.getValue(context);
487            }
488            if (docViewValue == null) {
489                documentViewBinding = patternDesc.getNewDocumentViewBinding();
490                if (!StringUtils.isBlank(documentViewBinding)) {
491                    ValueExpression ve = ef.createValueExpression(context, documentViewBinding, Object.class);
492                    docViewValue = ve.getValue(context);
493                }
494            }
495            if (docViewValue instanceof DocumentView) {
496                docView = (DocumentView) docViewValue;
497                // set pattern name in case it was just created
498                docView.setPatternName(patternDesc.getName());
499                ValueBindingDescriptor[] bindings = patternDesc.getValueBindings();
500                if (bindings != null) {
501                    for (ValueBindingDescriptor binding : bindings) {
502                        if (!binding.getCallGetter()) {
503                            continue;
504                        }
505                        String paramName = binding.getName();
506                        String expr = binding.getExpression();
507                        try {
508                            Object value;
509                            if (ComponentTagUtils.isValueReference(expr)) {
510                                ValueExpression ve = ef.createValueExpression(context, expr, Object.class);
511                                value = ve.getValue(context);
512                            } else {
513                                value = expr;
514                            }
515                            if (docView != null) {
516                                // do not set attributes on the request as
517                                // document view will be put in the request
518                                // anyway
519                                docView.addParameter(paramName, (String) value);
520                            } else {
521                                httpRequest.setAttribute(paramName, value);
522                            }
523                        } catch (ELException e) {
524                            log.error(String.format("Could not get parameter %s from expression %s", paramName, expr),
525                                    e);
526                        }
527                    }
528                }
529            }
530        }
531
532        // save document view to the request
533        setDocumentViewInRequest(httpRequest, docView);
534    }
535
536    @Override
537    public String navigate(FacesContext facesContext) {
538        HttpServletRequest httpRequest = (HttpServletRequest) facesContext.getExternalContext().getRequest();
539
540        URLPatternDescriptor pattern = getURLPatternDescriptor(httpRequest);
541        if (pattern == null) {
542            return null;
543        }
544
545        DocumentView docView = getDocumentViewFromRequest(pattern.getName(), httpRequest);
546        ExpressionFactory ef = facesContext.getApplication().getExpressionFactory();
547        ELContext context = facesContext.getELContext();
548        String actionBinding = pattern.getActionBinding();
549
550        if (actionBinding != null && !"".equals(actionBinding)
551                && httpRequest.getAttribute(URLPolicyService.DISABLE_ACTION_BINDING_KEY) == null) {
552            MethodExpression action = ef.createMethodExpression(context, actionBinding, String.class,
553                    new Class[] { DocumentView.class });
554            return (String) action.invoke(context, new Object[] { docView });
555        }
556        return null;
557    }
558
559    // registries management
560
561    @Override
562    public void addPatternDescriptor(URLPatternDescriptor pattern) {
563        String name = pattern.getName();
564        if (descriptors.containsKey(name)) {
565            // no merging right now
566            descriptors.remove(name);
567        }
568        descriptors.put(pattern.getName(), pattern);
569        log.debug("Added URLPatternDescriptor: " + name);
570    }
571
572    @Override
573    public void removePatternDescriptor(URLPatternDescriptor pattern) {
574        String name = pattern.getName();
575        descriptors.remove(name);
576        log.debug("Removed URLPatternDescriptor: " + name);
577    }
578
579    @Override
580    public void initViewIdManager(ServletContext context, HttpServletRequest request, HttpServletResponse response) {
581        if (viewIdManager == null) {
582            viewIdManager = new StaticNavigationHandler(context, request, response);
583        }
584    }
585
586    StaticNavigationHandler getViewIdManager() {
587        if (viewIdManager == null) {
588            throw new RuntimeException("View id manager is not initialized: "
589                    + "URLPolicyService#initViewIdManager should " + "have been called first");
590        }
591        return viewIdManager;
592    }
593
594    @Override
595    public String getOutcomeFromViewId(String viewId, HttpServletRequest httpRequest) {
596        return getViewIdManager().getOutcomeFromViewId(viewId);
597    }
598
599    @Override
600    public String getOutcomeFromUrl(String url, HttpServletRequest request) {
601        String baseUrl = BaseURL.getBaseURL(request);
602        // parse url to get outcome from view id
603        String viewId = url;
604        String webAppName = "/" + VirtualHostHelper.getWebAppName(request);
605        if (viewId.startsWith(baseUrl)) {
606            // url is absolute
607            viewId = '/' + viewId.substring(baseUrl.length());
608        } else if (viewId.startsWith(webAppName)) {
609            // url is relative to the web app
610            viewId = viewId.substring(webAppName.length());
611        }
612        int index = viewId.indexOf('?');
613        if (index != -1) {
614            viewId = viewId.substring(0, index);
615        }
616        return getOutcomeFromViewId(viewId, request);
617    }
618
619    @Override
620    public String getViewIdFromOutcome(String outcome, HttpServletRequest httpRequest) {
621        return getViewIdManager().getViewIdFromOutcome(outcome);
622    }
623
624    @Override
625    public void clear() {
626        descriptors.clear();
627    }
628
629    @Override
630    public void flushCache() {
631        viewIdManager = null;
632    }
633
634}