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