001/* 002 * Copyright 2019 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.cms.transformation; 017 018import java.io.InputStream; 019import java.util.Arrays; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.Iterator; 023import java.util.Map; 024 025import org.apache.avalon.framework.context.Context; 026import org.apache.avalon.framework.context.ContextException; 027import org.apache.avalon.framework.context.Contextualizable; 028import org.apache.avalon.framework.logger.AbstractLogEnabled; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.avalon.framework.service.Serviceable; 032import org.apache.cocoon.components.ContextHelper; 033import org.apache.cocoon.environment.Request; 034 035import org.ametys.cms.repository.Content; 036import org.ametys.cms.transformation.ConsistencyChecker.CHECK; 037import org.ametys.core.util.URLEncoder; 038import org.ametys.plugins.repository.AmetysObjectResolver; 039import org.ametys.plugins.repository.metadata.CompositeMetadata; 040import org.ametys.plugins.repository.metadata.File; 041import org.ametys.plugins.repository.metadata.Folder; 042import org.ametys.plugins.repository.metadata.Resource; 043import org.ametys.plugins.repository.metadata.RichText; 044import org.ametys.plugins.repository.metadata.UnknownMetadataException; 045import org.ametys.plugins.repository.version.VersionableAmetysObject; 046import org.ametys.runtime.i18n.I18nizableText; 047import org.ametys.runtime.workspace.WorkspaceMatcher; 048 049/** 050 * {@link URIResolver} for resources local to a Content. 051 */ 052public class LocalURIResolver extends AbstractLogEnabled implements URIResolver, Contextualizable, Serviceable 053{ 054 /** The context */ 055 protected Context _context; 056 /** The ametys object resolver */ 057 protected AmetysObjectResolver _ametysObjectResolver; 058 059 @Override 060 public void service(ServiceManager manager) throws ServiceException 061 { 062 _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 063 } 064 065 @Override 066 public void contextualize(Context context) throws ContextException 067 { 068 _context = context; 069 } 070 071 @Override 072 public String getType() 073 { 074 return "local"; 075 } 076 077 @Override 078 public String resolve(String uri, boolean download, boolean absolute, boolean internal) 079 { 080 URIInfo infos = getInfos(uri, false); 081 082 StringBuilder resultPath = new StringBuilder(); 083 084 resultPath.append(getUriPrefix(download, absolute, internal)) 085 .append("/plugins/cms" + (download ? "/download/" : "/view/")) 086 .append(infos.getPath()); 087 088 Map<String, String> params = new HashMap<>(); 089 params.put("contentId", infos.getContentId()); 090 params.put("metadata", infos.getMetadata()); 091 if (infos.getContentVersion() != null) 092 { 093 params.put("contentVersion", infos.getContentVersion()); 094 } 095 096 // Encode twice 097 String encodedPath = URLEncoder.encodePath(resultPath.toString()); 098 return URLEncoder.encodeURI(encodedPath, params); 099 } 100 101 /** 102 * Parses the uri. 103 * @param uri the incoming uri. 104 * @param resolveContent true if the Content should be actually resolved if not found in the request. 105 * @return an object containing all parsed infos. 106 */ 107 protected URIInfo getInfos(String uri, boolean resolveContent) 108 { 109 // uri are like content://UUID@metadata;data/file.ext 110 int i = uri.indexOf('@'); 111 int j = uri.indexOf(';', i); 112 String id = uri.substring(0, i); 113 String metadata = uri.substring(i + 1, j); 114 String path = uri.substring(j + 1); 115 116 Request request = ContextHelper.getRequest(_context); 117 118 String contentVersion = null; 119 // The content should be the one from request (UUID) but in that case, getRevision will be always the head on 120 Content content = (Content) request.getAttribute(Content.class.getName()); 121 if (content == null || !id.equals(content.getId())) 122 { 123 // Some time (such as frontoffice edition) the image is rendered with no content in attrbiute 124 content = resolveContent ? _ametysObjectResolver.resolveById(id) : null; 125 } 126 else 127 { 128 contentVersion = ((VersionableAmetysObject) content).getRevision(); 129 } 130 131 URIInfo infos = new URIInfo(); 132 infos.setContentId(id); 133 infos.setContentVersion(contentVersion); 134 infos.setMetadata(metadata); 135 infos.setPath(path); 136 infos.setContent(content); 137 138 return infos; 139 } 140 141 /** 142 * Get the URI prefix 143 * @param download true if the pointed resource is to be downloaded. 144 * @param absolute true if the url must be absolute 145 * @param internal true to get an internal URI. 146 * @return the URI prefix 147 */ 148 protected String getUriPrefix (boolean download, boolean absolute, boolean internal) 149 { 150 if (internal) 151 { 152 return "cocoon://"; 153 } 154 else 155 { 156 Request request = ContextHelper.getRequest(_context); 157 String workspaceURI = (String) request.getAttribute(WorkspaceMatcher.WORKSPACE_URI); 158 String uriPrefix = request.getContextPath() + workspaceURI; 159 160 if (absolute && !uriPrefix.startsWith(request.getScheme())) 161 { 162 uriPrefix = request.getScheme() + "://" + request.getServerName() + (request.getServerPort() != 80 ? ":" + request.getServerPort() : "") + uriPrefix; 163 } 164 165 return uriPrefix; 166 } 167 } 168 169 @Override 170 public String resolveImage(String uri, int height, int width, boolean download, boolean absolute, boolean internal) 171 { 172 throw new UnsupportedOperationException("resolveImage"); 173 } 174 175 @Override 176 public String resolveImageAsBase64(String uri, int height, int width) 177 { 178 return resolveImageAsBase64(uri, height, width, 0, 0, 0, 0); 179 } 180 181 @Override 182 public String resolveBoundedImage(String uri, int maxHeight, int maxWidth, boolean download, boolean absolute, boolean internal) 183 { 184 throw new UnsupportedOperationException("resolveBoundedImage"); 185 } 186 187 @Override 188 public String resolveBoundedImageAsBase64(String uri, int maxHeight, int maxWidth) 189 { 190 return resolveImageAsBase64(uri, 0, 0, maxHeight, maxWidth, 0, 0); 191 } 192 193 @Override 194 public String resolveCroppedImage(String uri, int cropHeight, int cropWidth, boolean download, boolean absolute, boolean internal) 195 { 196 throw new UnsupportedOperationException("resolveCroppedImage"); 197 } 198 199 @Override 200 public String resolveCroppedImageAsBase64(String uri, int cropHeight, int cropWidth) 201 { 202 return resolveImageAsBase64(uri, 0, 0, 0, 0, cropHeight, cropWidth); 203 } 204 205 /** 206 * Get an image's bytes encoded as base64, optionally resized. 207 * @param uri the image URI. 208 * @param height the specified height. Ignored if negative. 209 * @param width the specified width. Ignored if negative. 210 * @param maxHeight the maximum image height. Ignored if height or width is specified. 211 * @param maxWidth the maximum image width. Ignored if height or width is specified. 212 * @param cropHeight The cropping height. Ignored if negative. 213 * @param cropWidth The cropping width. Ignored if negative. 214 * @return the image bytes encoded as base64. 215 */ 216 protected String resolveImageAsBase64(String uri, int height, int width, int maxHeight, int maxWidth, int cropHeight, int cropWidth) 217 { 218 URIInfo infos = getInfos(uri, true); 219 220 try 221 { 222 RichText richText = _getMeta(infos.getContent().getMetadataHolder(), infos.getMetadata()); 223 File file = _getFile(richText.getAdditionalDataFolder(), infos.getPath()); 224 Resource resource = file.getResource(); 225 226 try (InputStream dataIs = resource.getInputStream()) 227 { 228 return ImageResolverHelper.resolveImageAsBase64(dataIs, resource.getMimeType(), height, width, maxHeight, maxWidth, cropHeight, cropWidth); 229 } 230 } 231 catch (Exception e) 232 { 233 throw new IllegalStateException(e); 234 } 235 } 236 237 @Override 238 public CHECK checkLink(String uri, boolean shortTest) 239 { 240 try 241 { 242 int i = uri.indexOf('@'); 243 int j = uri.indexOf(';', i); 244 245 if (i == -1 || j == -1) 246 { 247 getLogger().warn("Failed to check local URI: '" + uri + " does not respect the excepted format 'content://UUID@metadata;data/file.ext'"); 248 return CHECK.SERVER_ERROR; 249 } 250 251 String id = uri.substring(0, i); 252 String metadata = uri.substring(i + 1, j); 253 String fileName = uri.substring(j + 1); 254 255 Content content = _ametysObjectResolver.resolveById(id); 256 RichText richText = _getMeta(content.getMetadataHolder(), metadata); 257 258 richText.getAdditionalDataFolder().getFile(fileName); 259 260 return CHECK.SUCCESS; 261 } 262 catch (UnknownMetadataException e) 263 { 264 return CHECK.NOT_FOUND; 265 } 266 catch (Exception e) 267 { 268 throw new RuntimeException("Cannot check the uri '" + uri + "'", e); 269 } 270 } 271 272 @Override 273 public I18nizableText getLabel(String uri) 274 { 275 int i = uri.indexOf('@'); 276 int j = uri.indexOf(';', i); 277 278 String fileName = uri.substring(j + 1); 279 280 return new I18nizableText("plugin.cms", "PLUGINS_CMS_LINK_LOCAL_LABEL", Collections.singletonList(fileName)); 281 } 282 283 /** 284 * Get the rich text meta 285 * @param meta The composite meta 286 * @param metadataName The metadata name (with /) 287 * @return The rich text meta 288 */ 289 protected RichText _getMeta(CompositeMetadata meta, String metadataName) 290 { 291 int pos = metadataName.indexOf("/"); 292 if (pos == -1) 293 { 294 return meta.getRichText(metadataName); 295 } 296 else 297 { 298 return _getMeta(meta.getCompositeMetadata(metadataName.substring(0, pos)), metadataName.substring(pos + 1)); 299 } 300 } 301 302 /** 303 * Get the file at the specified path in the given folder. 304 * @param folder The folder to search in. 305 * @param path The file path in the folder (can contain slashes). 306 * @return The file if found, null otherwise. 307 */ 308 protected File _getFile(Folder folder, String path) 309 { 310 File file = null; 311 312 Iterator<String> it = Arrays.asList(path.split("/")).iterator(); 313 314 Folder browsedFolder = folder; 315 while (it.hasNext()) 316 { 317 String pathElement = it.next(); 318 319 if (it.hasNext()) 320 { 321 // not the last segment : it is a composite 322 browsedFolder = browsedFolder.getFolder(pathElement); 323 } 324 else 325 { 326 file = browsedFolder.getFile(pathElement); 327 } 328 } 329 330 return file; 331 } 332 333 /** 334 * Helper class containg all infos parsed from URI. 335 */ 336 protected static class URIInfo 337 { 338 /** The content id. */ 339 private String _contentId; 340 /** The relevant attribute */ 341 private String _metadata; 342 /** The path to the resource */ 343 private String _path; 344 /** The content version, if any. */ 345 private String _contentVersion; 346 /** The resolved content, if any. */ 347 private Content _content; 348 349 /** 350 * Returns the content id. 351 * @return the content id. 352 */ 353 public String getContentId() 354 { 355 return _contentId; 356 } 357 358 /** 359 * Set the content id. 360 * @param contentId the content id. 361 */ 362 public void setContentId(String contentId) 363 { 364 _contentId = contentId; 365 } 366 367 /** 368 * Returns the metadata. 369 * @return the metadata. 370 */ 371 public String getMetadata() 372 { 373 return _metadata; 374 } 375 376 /** 377 * Set the metadata. 378 * @param metadata the metadata. 379 */ 380 public void setMetadata(String metadata) 381 { 382 _metadata = metadata; 383 } 384 385 /** 386 * Returns the resource path. 387 * @return the path 388 */ 389 public String getPath() 390 { 391 return _path; 392 } 393 394 /** 395 * Set the resource path. 396 * @param path the path. 397 */ 398 public void setPath(String path) 399 { 400 _path = path; 401 } 402 403 /** 404 * Returns the content version, if any. 405 * @return the content version. 406 */ 407 public String getContentVersion() 408 { 409 return _contentVersion; 410 } 411 412 /** 413 * Set the content version. 414 * @param contentVersion the content version. 415 */ 416 public void setContentVersion(String contentVersion) 417 { 418 _contentVersion = contentVersion; 419 } 420 421 /** 422 * Returns the resolved content, if any. 423 * @return the content. 424 */ 425 public Content getContent() 426 { 427 return _content; 428 } 429 430 /** 431 * Set the content. 432 * @param content the content. 433 */ 434 public void setContent(Content content) 435 { 436 _content = content; 437 } 438 } 439}