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;
022
023import javax.jcr.RepositoryException;
024import javax.jcr.Session;
025
026import org.apache.avalon.framework.context.Context;
027import org.apache.avalon.framework.context.ContextException;
028import org.apache.avalon.framework.context.Contextualizable;
029import org.apache.avalon.framework.logger.AbstractLogEnabled;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.cocoon.components.ContextHelper;
034import org.apache.cocoon.environment.Request;
035
036import org.ametys.cms.URIPrefixHandler;
037import org.ametys.cms.data.Resource;
038import org.ametys.cms.data.RichText;
039import org.ametys.cms.repository.Content;
040import org.ametys.cms.transformation.ConsistencyChecker.CHECK;
041import org.ametys.core.util.FilenameUtils;
042import org.ametys.core.util.URIUtils;
043import org.ametys.plugins.repository.AmetysObjectResolver;
044import org.ametys.plugins.repository.AmetysRepositoryException;
045import org.ametys.plugins.repository.UnknownAmetysObjectException;
046import org.ametys.plugins.repository.version.VersionableAmetysObject;
047import org.ametys.runtime.i18n.I18nizableText;
048
049/**
050 * {@link URIResolver} for resources local to a rich text.
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    private URIPrefixHandler _prefixHandler;
060
061    @Override
062    public void service(ServiceManager manager) throws ServiceException
063    {
064        _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
065        _prefixHandler = (URIPrefixHandler) manager.lookup(URIPrefixHandler.ROLE);
066    }
067    
068    @Override
069    public void contextualize(Context context) throws ContextException
070    {
071        _context = context;
072    }
073    
074    @Override
075    public String getType()
076    {
077        return "local";
078    }
079
080    @Override
081    public String resolve(String uri, boolean download, boolean absolute, boolean internal)
082    {
083        URIInfo infos = getInfos(uri, false, null);
084        
085        StringBuilder resultPath = new StringBuilder();
086        
087        resultPath.append(_prefixHandler.computeUriPrefix(absolute, internal))
088                  .append("/plugins/cms/richText-file/")
089                  .append(FilenameUtils.encodeName(infos.getFilename()));
090        
091        Map<String, String> params = new HashMap<>();
092        params.put("contentId", infos.getContentId());
093        params.put("attribute", infos.getAttribute());
094        
095        if (download)
096        {
097            params.put("download", "true");
098        }
099        
100        if (infos.getContentVersion() != null)
101        {
102            params.put("contentVersion", infos.getContentVersion());
103        }
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>@<attribute>;<file.ext>
119        int i = uri.indexOf('@');
120        int j = uri.indexOf(';', i);
121        String id = uri.substring(0, i);
122        String attribute = uri.substring(i + 1, j);
123        String filename = uri.substring(j + 1);
124
125        Content content = null;
126        
127        try
128        {
129            Request request  = ContextHelper.getRequest(_context);
130            content = (Content) request.getAttribute(Content.class.getName());
131        }
132        catch (Exception e)
133        {
134            // there's no request, thus no "current" content 
135        }
136
137        String contentVersion = null;
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        else
153        {
154            if (content instanceof VersionableAmetysObject)
155            {
156                contentVersion = ((VersionableAmetysObject) content).getRevision();
157            }
158        }
159        
160        URIInfo infos = new URIInfo();
161        infos.setContentId(id);
162        infos.setContentVersion(contentVersion);
163        infos.setContent(content);
164        infos.setAttribute(attribute);
165        infos.setFilename(filename);
166        
167        return infos;
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, null);
220        
221        try
222        {
223            RichText richText = infos.getContent().getValue(infos.getAttribute());
224            Resource resource = richText.getAttachment(infos.getFilename());
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            URIInfo infos = getInfos(uri, true, null);
243            
244            Content content = infos.getContent();
245            RichText richText = content.getValue(infos.getAttribute());
246            return richText != null
247                 ? richText.hasAttachment(infos.getFilename())
248                         ? CHECK.SUCCESS
249                         : CHECK.NOT_FOUND
250                 : CHECK.NOT_FOUND;
251        }
252        catch (Exception e)
253        {
254            throw new RuntimeException("Cannot check the uri '" + uri + "'", e);
255        }
256    }
257    
258    @Override
259    public I18nizableText getLabel(String uri)
260    {
261        try
262        {
263            URIInfo infos = getInfos(uri, false, null);
264            
265            return new I18nizableText("plugin.cms", "PLUGINS_CMS_LINK_LOCAL_LABEL", Collections.singletonList(infos.getFilename()));
266        }
267        catch (UnknownAmetysObjectException e)
268        {
269            return new I18nizableText("plugin.cms", "PLUGINS_CMS_LINK_LOCAL_UNKNOWN");
270        }
271    }
272    
273    /**
274     * Helper class containg all infos parsed from URI.
275     */
276    protected static class URIInfo
277    {
278        /** The content id. */
279        private String _contentId;
280        /** The relevant attribute */
281        private String _attribute;
282        /** The resource name */
283        private String _filename;
284        /** The content version, if any. */
285        private String _contentVersion;
286        /** The resolved content, if any. */
287        private Content _content;
288        
289        /**
290         * Returns the content id.
291         * @return the content id.
292         */
293        public String getContentId()
294        {
295            return _contentId;
296        }
297        
298        /**
299         * Set the content id.
300         * @param contentId the content id.
301         */
302        public void setContentId(String contentId)
303        {
304            _contentId = contentId;
305        }
306        
307        /**
308         * Returns the metadata.
309         * @return the metadata.
310         */
311        public String getAttribute()
312        {
313            return _attribute;
314        }
315        
316        /**
317         * Set the metadata.
318         * @param attribute the metadata.
319         */
320        public void setAttribute(String attribute)
321        {
322            _attribute = attribute;
323        }
324        
325        /**
326         * Returns the resource name.
327         * @return the name
328         */
329        public String getFilename()
330        {
331            return _filename;
332        }
333        
334        /**
335         * Set the resource name.
336         * @param name the name.
337         */
338        public void setFilename(String name)
339        {
340            _filename = name;
341        }
342        
343        /**
344         * Returns the content version, if any.
345         * @return the content version.
346         */
347        public String getContentVersion()
348        {
349            return _contentVersion;
350        }
351        
352        /**
353         * Set the content version.
354         * @param contentVersion the content version.
355         */
356        public void setContentVersion(String contentVersion)
357        {
358            _contentVersion = contentVersion;
359        }
360        
361        /**
362         * Returns the resolved content, if any.
363         * @return the content.
364         */
365        public Content getContent()
366        {
367            return _content;
368        }
369        
370        /**
371         * Set the content.
372         * @param content the content.
373         */
374        public void setContent(Content content)
375        {
376            _content = content;
377        }
378    }
379}