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.Collections; 020import java.util.HashMap; 021import java.util.Map; 022import java.util.Optional; 023 024import javax.jcr.RepositoryException; 025import javax.jcr.Session; 026 027import org.apache.avalon.framework.context.Context; 028import org.apache.avalon.framework.context.ContextException; 029import org.apache.avalon.framework.context.Contextualizable; 030import org.apache.avalon.framework.logger.AbstractLogEnabled; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.avalon.framework.service.Serviceable; 034import org.apache.cocoon.components.ContextHelper; 035import org.apache.cocoon.environment.Request; 036 037import org.ametys.cms.URIPrefixHandler; 038import org.ametys.cms.data.Resource; 039import org.ametys.cms.data.RichText; 040import org.ametys.cms.repository.Content; 041import org.ametys.cms.transformation.ConsistencyChecker.CHECK; 042import org.ametys.core.util.FilenameUtils; 043import org.ametys.core.util.ImageResolverHelper; 044import org.ametys.core.util.URIUtils; 045import org.ametys.plugins.repository.AmetysObjectResolver; 046import org.ametys.plugins.repository.AmetysRepositoryException; 047import org.ametys.plugins.repository.UnknownAmetysObjectException; 048import org.ametys.plugins.repository.version.VersionAwareAmetysObject; 049import org.ametys.runtime.i18n.I18nizableText; 050 051/** 052 * {@link URIResolver} for resources local to a rich text. 053 */ 054public class LocalURIResolver extends AbstractLogEnabled implements URIResolver, Contextualizable, Serviceable 055{ 056 /** The context */ 057 protected Context _context; 058 /** The ametys object resolver */ 059 protected AmetysObjectResolver _ametysObjectResolver; 060 061 private URIPrefixHandler _prefixHandler; 062 063 @Override 064 public void service(ServiceManager manager) throws ServiceException 065 { 066 _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 067 _prefixHandler = (URIPrefixHandler) manager.lookup(URIPrefixHandler.ROLE); 068 } 069 070 @Override 071 public void contextualize(Context context) throws ContextException 072 { 073 _context = context; 074 } 075 076 @Override 077 public String getType() 078 { 079 return "local"; 080 } 081 082 @Override 083 public String resolve(String uri, boolean download, boolean absolute, boolean internal) 084 { 085 URIInfo infos = getInfos(uri, false, null); 086 087 StringBuilder resultPath = new StringBuilder(); 088 089 resultPath.append(_prefixHandler.computeUriPrefix(absolute, internal)) 090 .append("/plugins/cms/richText-file/") 091 .append(FilenameUtils.encodeName(infos.getFilename())); 092 093 Map<String, String> params = new HashMap<>(); 094 params.put("contentId", infos.getContentId()); 095 params.put("attribute", infos.getAttribute()); 096 097 if (download) 098 { 099 params.put("download", "true"); 100 } 101 102 infos.getContentVersion() 103 .ifPresent(version -> params.put("contentVersion", version)); 104 105 // Encode twice 106 return URIUtils.encodeURI(resultPath.toString(), params); 107 } 108 109 /** 110 * Parses the uri. 111 * @param uri the incoming uri. 112 * @param resolveContent true if the Content should be actually resolved if not found in the request. 113 * @param session the JCR {@link Session} to use, or null to use the current Session. 114 * @return an object containing all parsed infos. 115 */ 116 protected URIInfo getInfos(String uri, boolean resolveContent, Session session) 117 { 118 // uri are like <contentId>[|<contentVersion>]@<attribute>;<file.ext> 119 int i = Math.max(uri.indexOf('|'), 0); 120 int j = uri.indexOf('@', i); 121 int k = uri.indexOf(';', j); 122 String id = uri.substring(0, i > 0 ? i : j); 123 Optional<String> contentVersion = i > 0 ? Optional.of(uri.substring(i + 1, j)) : Optional.empty(); 124 String attribute = uri.substring(j + 1, k); 125 String filename = uri.substring(k + 1); 126 127 Content content = null; 128 129 try 130 { 131 Request request = ContextHelper.getRequest(_context); 132 content = (Content) request.getAttribute(Content.class.getName()); 133 } 134 catch (Exception e) 135 { 136 // there's no request, thus no "current" content 137 } 138 139 if (content == null || !id.equals(content.getId())) 140 { 141 // Some time (such as frontoffice edition) the image is rendered with no content in attribute 142 // The content should be resolved against the given id, but in that case, getRevision will be always the head on 143 try 144 { 145 content = resolveContent ? session == null ? _ametysObjectResolver.resolveById(id) : _ametysObjectResolver.resolveById(id, session) : null; 146 } 147 catch (RepositoryException e) 148 { 149 throw new AmetysRepositoryException(e); 150 } 151 } 152 153 URIInfo infos = new URIInfo(); 154 infos.setContentId(id); 155 infos.setContentVersion(contentVersion); 156 infos.setContent(content); 157 infos.setAttribute(attribute); 158 infos.setFilename(filename); 159 160 return infos; 161 } 162 163 @Override 164 public String resolveImage(String uri, int height, int width, boolean download, boolean absolute, boolean internal) 165 { 166 return resolve(uri, download, absolute, internal); 167 } 168 169 @Override 170 public String resolveImageAsBase64(String uri, int height, int width) 171 { 172 return resolveImageAsBase64(uri, height, width, 0, 0, 0, 0); 173 } 174 175 @Override 176 public String resolveBoundedImage(String uri, int maxHeight, int maxWidth, boolean download, boolean absolute, boolean internal) 177 { 178 return resolve(uri, download, absolute, internal); 179 } 180 181 @Override 182 public String resolveBoundedImageAsBase64(String uri, int maxHeight, int maxWidth) 183 { 184 return resolveImageAsBase64(uri, 0, 0, maxHeight, maxWidth, 0, 0); 185 } 186 187 @Override 188 public String resolveCroppedImage(String uri, int cropHeight, int cropWidth, boolean download, boolean absolute, boolean internal) 189 { 190 return resolve(uri, download, absolute, internal); 191 } 192 193 @Override 194 public String resolveCroppedImageAsBase64(String uri, int cropHeight, int cropWidth) 195 { 196 return resolveImageAsBase64(uri, 0, 0, 0, 0, cropHeight, cropWidth); 197 } 198 199 public String getMimeType(String uri) 200 { 201 URIInfo infos = getInfos(uri, true, null); 202 203 ContentAndRevision contentAndRevision = _getContentAndRevisionFromAttributeInfo(infos); 204 Content content = contentAndRevision.content(); 205 206 try 207 { 208 RichText richText = content.getValue(infos.getAttribute()); 209 Resource resource = richText.getAttachment(infos.getFilename()); 210 return resource.getMimeType(); 211 } 212 catch (Exception e) 213 { 214 throw new IllegalStateException(e); 215 } 216 finally 217 { 218 _resetContentRevisionIfNeeded(contentAndRevision); 219 } 220 } 221 222 private record ContentAndRevision (Content content, boolean hasSwitched, Optional<String> revision) { /* empty */ } 223 224 /** 225 * Get an image's bytes encoded as base64, optionally resized. 226 * @param uri the image URI. 227 * @param height the specified height. Ignored if negative. 228 * @param width the specified width. Ignored if negative. 229 * @param maxHeight the maximum image height. Ignored if height or width is specified. 230 * @param maxWidth the maximum image width. Ignored if height or width is specified. 231 * @param cropHeight The cropping height. Ignored if negative. 232 * @param cropWidth The cropping width. Ignored if negative. 233 * @return the image bytes encoded as base64. 234 */ 235 protected String resolveImageAsBase64(String uri, int height, int width, int maxHeight, int maxWidth, int cropHeight, int cropWidth) 236 { 237 URIInfo infos = getInfos(uri, true, null); 238 239 ContentAndRevision contentAndRevision = _getContentAndRevisionFromAttributeInfo(infos); 240 Content content = contentAndRevision.content(); 241 242 try 243 { 244 RichText richText = content.getValue(infos.getAttribute()); 245 Resource resource = richText.getAttachment(infos.getFilename()); 246 247 try (InputStream dataIs = resource.getInputStream()) 248 { 249 return ImageResolverHelper.resolveImageAsBase64(dataIs, resource.getMimeType(), height, width, maxHeight, maxWidth, cropHeight, cropWidth); 250 } 251 } 252 catch (Exception e) 253 { 254 throw new IllegalStateException(e); 255 } 256 finally 257 { 258 _resetContentRevisionIfNeeded(contentAndRevision); 259 } 260 } 261 262 @Override 263 public CHECK checkLink(String uri, boolean shortTest) 264 { 265 URIInfo infos = getInfos(uri, true, null); 266 267 ContentAndRevision contentAndRevision = _getContentAndRevisionFromAttributeInfo(infos); 268 Content content = contentAndRevision.content(); 269 270 try 271 { 272 RichText richText = content.getValue(infos.getAttribute()); 273 return richText != null 274 ? richText.hasAttachment(infos.getFilename()) 275 ? CHECK.SUCCESS 276 : CHECK.NOT_FOUND 277 : CHECK.NOT_FOUND; 278 } 279 catch (Exception e) 280 { 281 throw new RuntimeException("Cannot check the uri '" + uri + "'", e); 282 } 283 finally 284 { 285 _resetContentRevisionIfNeeded(contentAndRevision); 286 } 287 } 288 289 private ContentAndRevision _getContentAndRevisionFromAttributeInfo(URIInfo info) 290 { 291 Content content = info.getContent(); 292 293 boolean hasToSwitch = _hasToSwitchRevision(info); 294 Optional<String> currentRevision = Optional.empty(); 295 if (hasToSwitch && content instanceof VersionAwareAmetysObject versionAwareAO) 296 { 297 // Keep current revision to switch again later 298 currentRevision = Optional.ofNullable(versionAwareAO.getRevision()); 299 300 // Switch to targeted revision 301 versionAwareAO.switchToRevision(info.getContentVersion().orElse(null)); 302 } 303 304 return new ContentAndRevision(content, hasToSwitch, currentRevision); 305 } 306 307 /** 308 * Checks in {@link URIInfo} if the content has the same revision than the targeted one 309 * @param info the {@link URIInfo} containing the content and the targeted revision (=contentVersion) 310 * @return <code>true</code> if a switch is needed, <code>false</code> otherwise 311 */ 312 private boolean _hasToSwitchRevision(URIInfo info) 313 { 314 Content content = info.getContent(); 315 316 if (content instanceof VersionAwareAmetysObject versionAwareAO) 317 { 318 String currentRevision = versionAwareAO.getRevision(); 319 Optional<String> targetRevision = info.getContentVersion(); 320 321 return currentRevision == null && targetRevision.isPresent() // content is at latest revision but another one has been targeted 322 || targetRevision.isEmpty() // content is at specific revision but latest has been targeted 323 || !targetRevision.get().equals(currentRevision); // content is at specific revision but another one has been targeted 324 } 325 326 return false; 327 } 328 329 private void _resetContentRevisionIfNeeded(ContentAndRevision contentAndRevision) 330 { 331 if (contentAndRevision.hasSwitched() && contentAndRevision.content() instanceof VersionAwareAmetysObject versionAwareAO) 332 { 333 // Switch again the content revision 334 Optional<String> oldRevision = contentAndRevision.revision(); 335 versionAwareAO.switchToRevision(oldRevision.orElse(null)); 336 } 337 } 338 339 @Override 340 public I18nizableText getLabel(String uri) 341 { 342 try 343 { 344 URIInfo infos = getInfos(uri, false, null); 345 346 return new I18nizableText("plugin.cms", "PLUGINS_CMS_LINK_LOCAL_LABEL", Collections.singletonList(infos.getFilename())); 347 } 348 catch (UnknownAmetysObjectException e) 349 { 350 return new I18nizableText("plugin.cms", "PLUGINS_CMS_LINK_LOCAL_UNKNOWN"); 351 } 352 } 353 354 /** 355 * Helper class containg all infos parsed from URI. 356 */ 357 protected static class URIInfo 358 { 359 /** The content id. */ 360 private String _contentId; 361 /** The relevant attribute */ 362 private String _attribute; 363 /** The resource name */ 364 private String _filename; 365 /** The content version, if any. */ 366 private Optional<String> _contentVersion = Optional.empty(); 367 /** The resolved content, if any. */ 368 private Content _content; 369 370 /** 371 * Returns the content id. 372 * @return the content id. 373 */ 374 public String getContentId() 375 { 376 return _contentId; 377 } 378 379 /** 380 * Set the content id. 381 * @param contentId the content id. 382 */ 383 public void setContentId(String contentId) 384 { 385 _contentId = contentId; 386 } 387 388 /** 389 * Returns the attribute. 390 * @return the attribute. 391 */ 392 public String getAttribute() 393 { 394 return _attribute; 395 } 396 397 /** 398 * Set the attribute. 399 * @param attribute the attribute. 400 */ 401 public void setAttribute(String attribute) 402 { 403 _attribute = attribute; 404 } 405 406 /** 407 * Returns the resource name. 408 * @return the name 409 */ 410 public String getFilename() 411 { 412 return _filename; 413 } 414 415 /** 416 * Set the resource name. 417 * @param name the name. 418 */ 419 public void setFilename(String name) 420 { 421 _filename = name; 422 } 423 424 /** 425 * Returns the content version, if any. 426 * @return the content version. 427 */ 428 public Optional<String> getContentVersion() 429 { 430 return _contentVersion; 431 } 432 433 /** 434 * Set the content version. 435 * @param contentVersion the content version. 436 */ 437 public void setContentVersion(Optional<String> contentVersion) 438 { 439 _contentVersion = contentVersion; 440 } 441 442 /** 443 * Returns the resolved content, if any. 444 * @return the content. 445 */ 446 public Content getContent() 447 { 448 return _content; 449 } 450 451 /** 452 * Set the content. 453 * @param content the content. 454 */ 455 public void setContent(Content content) 456 { 457 _content = content; 458 } 459 } 460}