001/* 002 * Copyright 2016 Anyware Services 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 */ 016package org.ametys.plugins.workspaces.requests; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.util.Arrays; 021import java.util.Enumeration; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.Map.Entry; 026import java.util.function.Function; 027import java.util.stream.Collectors; 028 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.cocoon.ProcessingException; 032import org.apache.cocoon.ResourceNotFoundException; 033import org.apache.cocoon.environment.ObjectModelHelper; 034import org.apache.cocoon.environment.Request; 035import org.apache.cocoon.generation.ServiceableGenerator; 036import org.apache.cocoon.util.location.LocatedException; 037import org.apache.cocoon.xml.AttributesImpl; 038import org.apache.cocoon.xml.XMLUtils; 039import org.apache.commons.io.IOUtils; 040import org.apache.commons.lang3.StringUtils; 041import org.apache.commons.lang3.exception.ExceptionUtils; 042import org.apache.excalibur.source.Source; 043import org.apache.excalibur.xml.sax.SAXParser; 044import org.xml.sax.InputSource; 045import org.xml.sax.SAXException; 046 047import org.ametys.core.authentication.AuthenticateAction; 048import org.ametys.core.ui.dispatcher.DispatchGenerator; 049import org.ametys.core.ui.dispatcher.DispatchGenerator.ResponseHandler; 050import org.ametys.core.ui.dispatcher.DispatchProcessExtensionPoint; 051import org.ametys.core.ui.dispatcher.DispatchRequestProcess; 052import org.ametys.core.user.CurrentUserProvider; 053import org.ametys.core.util.JSONUtils; 054import org.ametys.core.util.URIUtils; 055import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 056import org.ametys.runtime.authentication.AccessDeniedException; 057import org.ametys.runtime.workspace.WorkspaceMatcher; 058 059/** 060 * The purpose of this action is to handle front office requests. 061 * These requests are usually AJAX requests coming from services. 062 * The processing is loosely based on the {@link DispatchGenerator} 063 */ 064public class HandleWorkspacesFoRequestGenerator extends ServiceableGenerator 065{ 066 /** Dispatch process EP */ 067 private DispatchProcessExtensionPoint _dispatchProcessExtensionPoint; 068 069 /** JSON Utils */ 070 private JSONUtils _jsonUtils; 071 072 private CurrentUserProvider _currentUserProvider; 073 074 @Override 075 public void service(ServiceManager smanager) throws ServiceException 076 { 077 super.service(smanager); 078 _dispatchProcessExtensionPoint = (DispatchProcessExtensionPoint) manager.lookup(DispatchProcessExtensionPoint.ROLE); 079 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 080 _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE); 081 } 082 083 @Override 084 public void generate() throws IOException, SAXException, ProcessingException 085 { 086 Request request = ObjectModelHelper.getRequest(objectModel); 087 088 Map<String, Object> reqParameters = _jsonUtils.convertJsonToMap(request.getParameter("content")); 089 Map<String, Object> reqContext = _jsonUtils.convertJsonToMap(request.getParameter("context.parameters")); 090 Map<String, Object> uploadedFiles = _extractFiles(request); 091 092 contentHandler.startDocument(); 093 094 if (_currentUserProvider.getUser() != null) 095 { 096 // Save request attributes 097 Map<String, Object> savedReqAttributes = _saveRequestAttributes(request); 098 099 // WORKSPACES-147 Possible WorkspacesFoRequestGenerator request attributes 100 // FIXME Cannot remove request attributes, because necessary FO attributes like Web:FrontOffice:UserIdentity will be removed. 101 // _removeRequestAttributes(); 102 103 for (String extension : _dispatchProcessExtensionPoint.getExtensionsIds()) 104 { 105 DispatchRequestProcess processor = _dispatchProcessExtensionPoint.getExtension(extension); 106 processor.preProcess(ObjectModelHelper.getRequest(objectModel)); 107 } 108 109 reqContext.putAll(_transmitAttributes(savedReqAttributes)); 110 _setContextInRequestAttributes(request, reqContext); 111 112 String pluginOrWorkspace = (String) reqParameters.get("pluginOrWorkspace"); 113 String relativeUrl = (String) reqParameters.get("url"); 114 String responseType = (String) reqParameters.get("responseType"); 115 116 @SuppressWarnings("unchecked") 117 Map<String, Object> requestParameters = (Map<String, Object>) reqParameters.get("parameters"); 118 119 // add possible uploaded files to requestParameters 120 if (requestParameters != null) 121 { 122 @SuppressWarnings("unchecked") 123 List<Object> callableParameters = (List) requestParameters.get("parameters"); 124 125 if (callableParameters != null) 126 { 127 // Replace file parameters by real files 128 for (Entry<String, Object> entry : uploadedFiles.entrySet()) 129 { 130 int parameterIndex = Integer.parseInt(StringUtils.substringAfter(entry.getKey(), "file-")); 131 132 callableParameters.remove(parameterIndex); 133 callableParameters.add(parameterIndex, entry.getValue()); 134 } 135 } 136 } 137 138 Source response = null; 139 ResponseHandler responseHandler = null; 140 141 String currentWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 142 143 try 144 { 145 String url = _createUrl(pluginOrWorkspace, relativeUrl, requestParameters != null ? requestParameters : new HashMap<String, Object>()); 146 147 if (getLogger().isInfoEnabled()) 148 { 149 getLogger().info(String.format("Resolving front end request with url '%s'", url)); 150 } 151 152 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, "default"); 153 154 response = resolver.resolveURI(url, null, requestParameters); 155 156 // Workaround - use 0 as parameter key to reuse the ResponseHandler. 157 responseHandler = new ResponseHandler(contentHandler, "0", "200"); 158 159 SAXParser saxParser = null; 160 try (InputStream is = response.getInputStream()) 161 { 162 if ("xml".equalsIgnoreCase(responseType)) 163 { 164 // DO NOT USE SitemapSource.toSAX in this case 165 saxParser = (SAXParser) manager.lookup(SAXParser.ROLE); 166 saxParser.parse(new InputSource(is), responseHandler); 167 } 168 else 169 { 170 responseHandler.startDocument(); 171 172 String data = IOUtils.toString(is, "UTF-8"); 173 if ("xml2text".equalsIgnoreCase(responseType)) 174 { 175 // removing xml prolog and surrounding 'text' tag 176 data = data.substring(data.indexOf(">", data.indexOf("?>") + 2) + 1, data.lastIndexOf("<")); 177 } 178 XMLUtils.data(responseHandler, data); 179 180 responseHandler.endDocument(); 181 } 182 } 183 finally 184 { 185 manager.release(saxParser); 186 } 187 } 188 catch (Throwable e) 189 { 190 String message = String.format("Can not dispatch FO request : '%s' '%s' '%s'", pluginOrWorkspace, relativeUrl, requestParameters); 191 192 // Ensure SAXException are unrolled the right way 193 getLogger().error(message, new LocatedException(message, e)); 194 195 Throwable t = _unroll(e); 196 197 String code = "500"; 198 if (t instanceof ResourceNotFoundException || t.toString().startsWith("org.apache.cocoon.ResourceNotFoundException:")) 199 { 200 code = "404"; 201 } 202 // Specific workspaces case, where callable are restricted to workspaces plugin 203 if (t instanceof AccessDeniedException) 204 { 205 code = "403"; 206 } 207 208 AttributesImpl attrs = new AttributesImpl(); 209 attrs.addCDATAAttribute("id", "0"); 210 attrs.addCDATAAttribute("code", code); 211 212 String exceptionMessage = t.getMessage(); 213 214 XMLUtils.startElement(contentHandler, "response", attrs); 215 XMLUtils.createElement(contentHandler, "message", _escape(exceptionMessage != null ? exceptionMessage : "")); 216 XMLUtils.createElement(contentHandler, "stacktrace", _escape(ExceptionUtils.getStackTrace(t))); 217 XMLUtils.endElement(contentHandler, "response"); 218 } 219 finally 220 { 221 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWorkspace); 222 resolver.release(response); 223 for (String extension : _dispatchProcessExtensionPoint.getExtensionsIds()) 224 { 225 DispatchRequestProcess processor = _dispatchProcessExtensionPoint.getExtension(extension); 226 processor.postProcess(ObjectModelHelper.getRequest(objectModel)); 227 } 228 229 // Restore initial request attributes 230 _restoreRequestAttributes(request, savedReqAttributes); 231 } 232 } 233 else 234 { 235 XMLUtils.createElement(contentHandler, "NotConnected"); 236 } 237 238 contentHandler.endDocument(); 239 } 240 241 private Throwable _unroll(Throwable initial) 242 { 243 Throwable t = initial; 244 while (t.getCause() != null || t instanceof SAXException && ((SAXException) t).getException() != null) 245 { 246 if (t instanceof SAXException) 247 { 248 t = ((SAXException) t).getException(); 249 } 250 else 251 { 252 t = t.getCause(); 253 } 254 } 255 256 return t; 257 } 258 259 private Map<String, Object> _extractFiles(Request request) 260 { 261 Map<String, Object> files = new HashMap<>(); 262 263 // Extract the optional list of uploaded files 264 Enumeration paramNames = request.getParameterNames(); 265 while (paramNames.hasMoreElements()) 266 { 267 String paramName = (String) paramNames.nextElement(); 268 if (paramName.startsWith("file-")) 269 { 270 files.put(paramName, request.get(paramName)); 271 } 272 } 273 274 return files; 275 } 276 277 private void _setContextInRequestAttributes(Request request, Map<String, Object> reqContext) 278 { 279 reqContext.forEach((name, value) -> request.setAttribute(name, value)); 280 } 281 282 private Map<String, Object> _transmitAttributes(Map<String, Object> attributes) 283 { 284 List<String> attributesToTransmit = Arrays.asList( 285 AuthenticateAction.REQUEST_ATTRIBUTE_AUTHENTICATED, 286 WorkspaceMatcher.IN_WORKSPACE_URL, 287 WorkspaceMatcher.WORKSPACE_NAME, 288 WorkspaceMatcher.WORKSPACE_THEME, 289 WorkspaceMatcher.WORKSPACE_THEME_URL, 290 WorkspaceMatcher.WORKSPACE_URI 291 ); 292 293 // mapping each attributesToTransmit entry to (entry, value of entry in attributes) 294 return attributesToTransmit.stream().collect(Collectors.toMap(Function.identity(), attributes::get)); 295 } 296 297 /** 298 * Transforms the request attributes into a map and clean the attributes 299 * @param request The request 300 * @return A copy of all the request attributes 301 */ 302 private Map<String, Object> _saveRequestAttributes(Request request) 303 { 304 Map<String, Object> attrs = new HashMap<>(); 305 306 Enumeration<String> attrNames = request.getAttributeNames(); 307 308 while (attrNames.hasMoreElements()) 309 { 310 String attrName = attrNames.nextElement(); 311 attrs.put(attrName, request.getAttribute(attrName)); 312 } 313 314 return attrs; 315 } 316 317 /** 318 * Clean the requests attributes and add those in the map 319 * @param request The request 320 * @param attributes The attributes to restore 321 */ 322 private void _restoreRequestAttributes(Request request, Map<String, Object> attributes) 323 { 324 _removeRequestAttributes(); 325 326 for (String attrName : attributes.keySet()) 327 { 328 request.setAttribute(attrName, attributes.get(attrName)); 329 } 330 } 331 332 private void _removeRequestAttributes() 333 { 334 Request request = ObjectModelHelper.getRequest(objectModel); 335 Enumeration<String> attrNames = request.getAttributeNames(); 336 337 while (attrNames.hasMoreElements()) 338 { 339 String attrName = attrNames.nextElement(); 340 request.removeAttribute(attrName); 341 } 342 } 343 344 private String _escape(String value) 345 { 346 return value.replaceAll("&", "&").replaceAll("<", "<".replaceAll(">", ">")); 347 } 348 349 /** 350 * Create url to call 351 * @param pluginOrWorkspace the plugin or workspace name 352 * @param relativeUrl the relative url 353 * @param requestParameters the request parameters. Can not be null. 354 * @return the full url 355 */ 356 protected String _createUrl(String pluginOrWorkspace, String relativeUrl, Map<String, Object> requestParameters) 357 { 358 StringBuilder url = new StringBuilder(); 359 360 String urlPrefix = _getUrlPrefix(pluginOrWorkspace); 361 url.append(urlPrefix); 362 363 url.append(_getRelativePath(relativeUrl)); 364 365 int queryIndex = relativeUrl.indexOf("?"); 366 367 if (queryIndex == -1 && !requestParameters.isEmpty()) 368 { 369 // no existing parameters in request 370 url.append("?"); 371 372 for (String key : requestParameters.keySet()) 373 { 374 Object value = requestParameters.get(key); 375 if (value instanceof List) 376 { 377 @SuppressWarnings("unchecked") 378 List<Object> valueAsList = (List<Object>) value; 379 for (Object v : valueAsList) 380 { 381 if (v != null) 382 { 383 url.append(_buildQueryParameter(key, v)); 384 } 385 } 386 } 387 else if (value != null) 388 { 389 url.append(_buildQueryParameter(key, value)); 390 } 391 } 392 } 393 else if (queryIndex > 0) 394 { 395 url.append("?"); 396 397 String queryUrl = relativeUrl.substring(queryIndex + 1, relativeUrl.length()); 398 String[] queryParameters = queryUrl.split("&"); 399 400 for (String queryParameter : queryParameters) 401 { 402 if (StringUtils.isNotBlank(queryParameter)) 403 { 404 String[] part = queryParameter.split("="); 405 String key = part[0]; 406 String v = part.length > 1 ? part[1] : ""; 407 408 String value = URIUtils.decode(v); 409 url.append(_buildQueryParameter(key, value)); 410 411 if (!requestParameters.containsKey(key)) 412 { 413 requestParameters.put(key, value); 414 } 415 } 416 } 417 } 418 419 return url.toString(); 420 } 421 422 private StringBuilder _buildQueryParameter(String key, Object value) 423 { 424 StringBuilder queryParameter = new StringBuilder(); 425 queryParameter.append(key); 426 queryParameter.append("="); 427 queryParameter.append(String.valueOf(value).replaceAll("%", "%25").replaceAll("=", "%3D").replaceAll("&", "%26").replaceAll("\\+", "%2B")); 428 queryParameter.append("&"); 429 430 return queryParameter; 431 } 432 433 /** 434 * Get the url prefix 435 * @param pluginOrWorkspace the plugin or workspace name 436 * @return the url prefix 437 */ 438 protected String _getUrlPrefix (String pluginOrWorkspace) 439 { 440 StringBuffer url = new StringBuffer("cocoon://"); 441 442 if (!StringUtils.startsWith(pluginOrWorkspace, "_")) 443 { 444 url.append("_plugins/"); 445 } 446 447 if (StringUtils.isNotEmpty(pluginOrWorkspace)) 448 { 449 url.append(pluginOrWorkspace).append("/"); 450 } 451 452 return url.toString(); 453 } 454 455 private String _getRelativePath(String url) 456 { 457 int beginIndex = StringUtils.startsWith(url, "/") ? 1 : 0; 458 int endIndex = StringUtils.indexOf(url, "?"); 459 return StringUtils.substring(url, beginIndex, endIndex >= 0 ? endIndex : url.length()); 460 } 461}