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}