001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.xml.xml2json;
029
030import org.opencms.cache.CmsVfsMemoryObjectCache;
031import org.opencms.configuration.CmsParameterConfiguration;
032import org.opencms.configuration.I_CmsNeedsAdminCmsObject;
033import org.opencms.file.CmsFile;
034import org.opencms.file.CmsObject;
035import org.opencms.file.CmsResource;
036import org.opencms.flex.CmsFlexController;
037import org.opencms.json.JSONObject;
038import org.opencms.main.CmsException;
039import org.opencms.main.CmsLog;
040import org.opencms.main.CmsResourceInitException;
041import org.opencms.main.I_CmsResourceInit;
042import org.opencms.main.OpenCms;
043import org.opencms.relations.I_CmsCustomLinkRenderer;
044import org.opencms.security.CmsSecurityException;
045import org.opencms.util.CmsFileUtil;
046import org.opencms.util.CmsStringUtil;
047import org.opencms.xml.xml2json.handler.CmsExceptionSafeHandlerWrapper;
048import org.opencms.xml.xml2json.handler.CmsJsonHandlerContext;
049import org.opencms.xml.xml2json.handler.I_CmsJsonHandler;
050
051import java.io.IOException;
052import java.io.PrintWriter;
053import java.util.ArrayList;
054import java.util.List;
055import java.util.Map;
056import java.util.ServiceLoader;
057import java.util.TreeMap;
058import java.util.stream.Collectors;
059
060import javax.servlet.http.HttpServletRequest;
061import javax.servlet.http.HttpServletResponse;
062
063import org.apache.commons.logging.Log;
064
065/**
066 * Handles /json requests.
067 */
068public class CmsJsonResourceHandler implements I_CmsResourceInit, I_CmsNeedsAdminCmsObject {
069
070    /** Request attribute for storing the JSON handler context. */
071    public static final String ATTR_CONTEXT = "jsonHandlerContext";
072
073    /** Configuration parameter that determines which authorization method to use. */
074    public static final String PARAM_AUTHORIZATION = "authorization";
075
076    /** URL prefix. */
077    public static final String PREFIX = "/json";
078
079    /** Logger instance for this class. */
080    private static final Log LOG = CmsLog.getLog(CmsJsonResourceHandler.class);
081
082    /** Parameter to reference the link rewriting strategy defined elsewhere. */
083    public static final Object PARAM_LINKREWRITE_REFID = "linkrewrite.refid";
084
085    /** The Admin CMS context. */
086    private CmsObject m_adminCms;
087
088    /** Configuration from config file. */
089    private CmsParameterConfiguration m_config = new CmsParameterConfiguration();
090
091    /** Service loader used to load external JSON handler classes. */
092    private ServiceLoader<I_CmsJsonHandlerProvider> m_serviceLoader = ServiceLoader.load(
093        I_CmsJsonHandlerProvider.class);
094
095    /**
096     * Creates a new instance.
097     */
098    public CmsJsonResourceHandler() {
099
100        CmsFlexController.registerUncacheableAttribute(ATTR_CONTEXT);
101    }
102
103    /**
104     * Gets the link renderer for the current CMS context.
105     *
106     * @param cms the current CMS context
107     * @return the link renderer for the context, or null if there is none
108     */
109    public static I_CmsCustomLinkRenderer getLinkRenderer(CmsObject cms) {
110
111        Object context = cms.getRequestContext().getAttribute(ATTR_CONTEXT);
112        if (context instanceof CmsJsonHandlerContext) {
113            String linkRewriterKey = ((CmsJsonHandlerContext)context).getHandlerConfig().get(
114                CmsJsonResourceHandler.PARAM_LINKREWRITE_REFID);
115            if (linkRewriterKey != null) {
116                Object linkRewriterObj = OpenCms.getRuntimeProperty(linkRewriterKey);
117                if (linkRewriterObj instanceof I_CmsCustomLinkRenderer) {
118                    return (I_CmsCustomLinkRenderer)linkRewriterObj;
119                }
120            }
121        }
122        return null;
123    }
124
125    /**
126     * Produces a link to the given resource, using the link renderer from the current CMS context if it is set.
127     *
128     * @param cms the CMS context
129     * @param res the resource to link to
130     * @return the link to the resource
131     */
132    public static String link(CmsObject cms, CmsResource res) {
133
134        I_CmsCustomLinkRenderer linkRenderer = getLinkRenderer(cms);
135        if (linkRenderer != null) {
136            String result = linkRenderer.getLink(cms, res);
137            if (result != null) {
138                return result;
139            }
140        }
141        return OpenCms.getLinkManager().substituteLink(cms, res);
142    }
143
144    /**
145     * Helper method for authorizing requests based on a comma-separated list of API authorization handler names.
146     *
147     * <p>This will evaluate each authorization handler from authChain and return the first non-null CmsObject returned.
148     * A special case is authChain contains the word 'default', this is not u
149     *
150     * <p>Returns null if the authorization failed.
151     *
152     * @param adminCms the Admin CmsObject
153     * @param defaultCms the current CmsObject with the default user data from the request
154     * @param request the current request
155     * @param authChain a comma-separated list of API authorization handler names
156     *
157     * @return the initialized CmsObject
158     */
159    private static CmsObject authorize(
160        CmsObject adminCms,
161        CmsObject defaultCms,
162        HttpServletRequest request,
163        String authChain) {
164
165        if (authChain == null) {
166            return defaultCms;
167        }
168        for (String token : authChain.split(",")) {
169            token = token.trim();
170            if ("default".equals(token)) {
171                LOG.info("Using default CmsObject");
172                return defaultCms;
173            } else if ("guest".equals(token)) {
174                try {
175                    return OpenCms.initCmsObject(OpenCms.getDefaultUsers().getUserGuest());
176                } catch (CmsException e) {
177                    LOG.error(e.getLocalizedMessage(), e);
178                    return null;
179                }
180            } else {
181                I_CmsApiAuthorizationHandler handler = OpenCms.getApiAuthorization(token);
182                if (handler == null) {
183                    LOG.error("Could not find API authorization handler " + token);
184                    return null;
185                } else {
186                    try {
187                        CmsObject cms = handler.initCmsObject(adminCms, request);
188                        if (cms != null) {
189                            LOG.info("Succeeded with authorization handler: " + token);
190                            return cms;
191                        }
192                    } catch (CmsException e) {
193                        LOG.error("Error evaluating authorization handler " + token);
194                        return null;
195                    }
196                }
197            }
198        }
199        LOG.info("Authentication unsusccessful");
200        return null;
201    }
202
203    /**
204     * Gets the list of sub-handlers, sorted by ascending order.
205     *
206     * @return the sorted list of sub-handlers
207     */
208    public List<I_CmsJsonHandler> getSubHandlers() {
209
210        List<I_CmsJsonHandler> result = new ArrayList<>(CmsDefaultJsonHandlers.getHandlers());
211        for (I_CmsJsonHandlerProvider provider : m_serviceLoader) {
212            try {
213                result.addAll(provider.getJsonHandlers());
214            } catch (Exception e) {
215                LOG.error(e.getLocalizedMessage(), e);
216            }
217        }
218        result.sort((h1, h2) -> Double.compare(h1.getOrder(), h2.getOrder()));
219        result = result.stream().map(h -> new CmsExceptionSafeHandlerWrapper(h)).collect(Collectors.toList());
220        return result;
221    }
222
223    /**
224     * @see org.opencms.main.I_CmsResourceInit#initParameters(org.opencms.configuration.CmsParameterConfiguration)
225     */
226    public void initParameters(CmsParameterConfiguration params) {
227
228        m_config = CmsParameterConfiguration.unmodifiableVersion(params);
229    }
230
231    /**
232     * @see org.opencms.main.I_CmsResourceInit#initResource(org.opencms.file.CmsResource, org.opencms.file.CmsObject, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
233     */
234    public CmsResource initResource(CmsResource origRes, CmsObject cms, HttpServletRequest req, HttpServletResponse res)
235    throws CmsResourceInitException {
236
237        String uri = cms.getRequestContext().getUri();
238
239        if (origRes != null) {
240            return origRes;
241        }
242        if (res == null) {
243            // called from locale handler
244            return origRes;
245        }
246        if (!CmsStringUtil.isPrefixPath(PREFIX, uri)) {
247            return null;
248        }
249        String path = uri.substring(PREFIX.length());
250        if (path.isEmpty()) {
251            path = "/";
252        } else if (path.length() > 1) {
253            path = CmsFileUtil.removeTrailingSeparator(path);
254        }
255
256        Map<String, String> singleParams = new TreeMap<>();
257        // we don't care about multiple parameter values, single parameter values are easier to work with
258        for (Map.Entry<String, String[]> entry : req.getParameterMap().entrySet()) {
259            String[] data = entry.getValue();
260            String value = null;
261            if (data.length > 0) {
262                value = data[0];
263            }
264            singleParams.put(entry.getKey(), value);
265        }
266
267        String authorizationParam = m_config.get(PARAM_AUTHORIZATION);
268        CmsObject origCms = cms;
269        cms = authorize(m_adminCms, origCms, req, authorizationParam);
270        if ((cms != null) && (cms != origCms)) {
271            origCms.getRequestContext().setAttribute(I_CmsResourceInit.ATTR_ALTERNATIVE_CMS_OBJECT, cms);
272            cms.getRequestContext().setSiteRoot(origCms.getRequestContext().getSiteRoot());
273            cms.getRequestContext().setUri(origCms.getRequestContext().getUri());
274        }
275
276        int status = HttpServletResponse.SC_OK;
277        String output = "";
278        CmsJsonAccessPolicy accessPolicy = null;
279        try {
280            if (cms == null) {
281                status = HttpServletResponse.SC_UNAUTHORIZED;
282                output = JSONObject.quote("unauthorized");
283            } else {
284                CmsObject rootCms = OpenCms.initCmsObject(cms);
285                rootCms.getRequestContext().setSiteRoot("");
286                boolean resourcePermissionDenied = false;
287                CmsResource resource = null;
288                try {
289                    resource = rootCms.readResource(path);
290                } catch (CmsSecurityException e) {
291                    LOG.info(
292                        "Read permission denied for "
293                            + path
294                            + ", user="
295                            + rootCms.getRequestContext().getCurrentUser().getName());
296                    resourcePermissionDenied = true;
297                } catch (CmsException e) {
298                    // ignore
299                }
300
301                accessPolicy = getAccessPolicy(rootCms);
302                CmsJsonHandlerContext context = new CmsJsonHandlerContext(
303                    cms,
304                    rootCms,
305                    path,
306                    resource,
307                    singleParams,
308                    m_config,
309                    accessPolicy);
310                cms.getRequestContext().setAttribute(ATTR_CONTEXT, context);
311                String encoding = "UTF-8";
312                res.setContentType("application/json; charset=" + encoding);
313                if (!accessPolicy.checkAccess(context.getCms(), context.getPath())) {
314                    LOG.info("JSON access to path'" + context.getPath() + "' denied by access policy.");
315                    status = HttpServletResponse.SC_FORBIDDEN;
316                    output = JSONObject.quote("forbidden");
317                } else if (resourcePermissionDenied) {
318                    if (cms.getRequestContext().getCurrentUser().getName().equals(
319                        OpenCms.getDefaultUsers().getUserGuest())) {
320                        status = HttpServletResponse.SC_UNAUTHORIZED;
321                        output = JSONObject.quote("unauthorized");
322                    } else {
323                        status = HttpServletResponse.SC_FORBIDDEN;
324                        output = JSONObject.quote("forbidden");
325                    }
326                } else {
327                    boolean foundHandler = false;
328                    for (I_CmsJsonHandler handler : getSubHandlers()) {
329                        if (handler.matches(context)) {
330                            CmsJsonResult result = handler.renderJson(context);
331                            if (result.getNextResource() != null) {
332                                req.setAttribute(ATTR_CONTEXT, context);
333                                return result.getNextResource();
334                            } else {
335                                try {
336                                    status = result.getStatus();
337                                    output = JSONObject.valueToString(result.getJson(), 4, 0);
338                                } catch (Exception e) {
339                                    status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
340                                    output = JSONObject.quote(e.getLocalizedMessage());
341                                }
342                                foundHandler = true;
343                                break;
344
345                            }
346                        }
347                    }
348                    if (!foundHandler) {
349                        LOG.info("No JSON handler found for path: " + path);
350                        status = HttpServletResponse.SC_NOT_FOUND;
351                        output = JSONObject.quote("");
352                    }
353                }
354            }
355        } catch (Exception e) {
356            LOG.error(e.getLocalizedMessage(), e);
357            status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
358            output = JSONObject.quote(e.getLocalizedMessage());
359        }
360        res.setStatus(status);
361        if (accessPolicy != null) {
362            accessPolicy.setCorsHeaders(res);
363        }
364        try {
365            PrintWriter writer = res.getWriter();
366            writer.write(output);
367            writer.flush();
368        } catch (IOException ioe) {
369            LOG.error(ioe.getLocalizedMessage(), ioe);
370        }
371        CmsResourceInitException ex = new CmsResourceInitException(CmsJsonResourceHandler.class);
372        ex.setClearErrors(true);
373        throw ex;
374
375    }
376
377    /**
378     * @see org.opencms.configuration.I_CmsNeedsAdminCmsObject#setAdminCmsObject(org.opencms.file.CmsObject)
379     */
380    public void setAdminCmsObject(CmsObject adminCms) {
381
382        m_adminCms = adminCms;
383    }
384
385    /**
386     * Reads JSON access policy from cache or loads it if necessary.
387     *
388     * @param cms the CMS context used to load the access policy
389     * @return the access policy
390     */
391    protected CmsJsonAccessPolicy getAccessPolicy(CmsObject cms) {
392
393        String accessConfigPath = m_config.getString("access-policy", null);
394        if (accessConfigPath == null) {
395            return new CmsJsonAccessPolicy(true);
396        }
397        CmsVfsMemoryObjectCache cache = CmsVfsMemoryObjectCache.getVfsMemoryObjectCache();
398        CmsJsonAccessPolicy result = (CmsJsonAccessPolicy)cache.loadVfsObject(cms, accessConfigPath, obj -> {
399            try {
400                CmsFile file = cms.readFile(accessConfigPath);
401                CmsJsonAccessPolicy policy = CmsJsonAccessPolicy.parse(file.getContents());
402                return policy;
403            } catch (Exception e) {
404                // If access policy is configured, but can't be read, disable everything
405                LOG.error(e.getLocalizedMessage(), e);
406                return new CmsJsonAccessPolicy(false);
407            }
408        });
409        return result;
410    }
411
412}