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.version.VersionableAmetysObject;
046import org.ametys.runtime.i18n.I18nizableText;
047
048/**
049 * {@link URIResolver} for resources local to a rich text.
050 */
051public class LocalURIResolver extends AbstractLogEnabled implements URIResolver, Contextualizable, Serviceable
052{
053    /** The context */
054    protected Context _context;
055    /** The ametys object resolver */
056    protected AmetysObjectResolver _ametysObjectResolver;
057    
058    private URIPrefixHandler _prefixHandler;
059
060    @Override
061    public void service(ServiceManager manager) throws ServiceException
062    {
063        _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
064        _prefixHandler = (URIPrefixHandler) manager.lookup(URIPrefixHandler.ROLE);
065    }
066    
067    @Override
068    public void contextualize(Context context) throws ContextException
069    {
070        _context = context;
071    }
072    
073    @Override
074    public String getType()
075    {
076        return "local";
077    }
078
079    @Override
080    public String resolve(String uri, boolean download, boolean absolute, boolean internal)
081    {
082        URIInfo infos = getInfos(uri, false, null);
083        
084        StringBuilder resultPath = new StringBuilder();
085        
086        resultPath.append(_prefixHandler.computeUriPrefix(absolute, internal))
087                  .append("/plugins/cms/richText-file/")
088                  .append(FilenameUtils.encodeName(infos.getFilename()));
089        
090        Map<String, String> params = new HashMap<>();
091        params.put("contentId", infos.getContentId());
092        params.put("attribute", infos.getAttribute());
093        
094        if (download)
095        {
096            params.put("download", "true");
097        }
098        
099        if (infos.getContentVersion() != null)
100        {
101            params.put("contentVersion", infos.getContentVersion());
102        }
103        
104        // Encode twice
105        return URIUtils.encodeURI(resultPath.toString(), params);
106    }
107    
108    /**
109     * Parses the uri.
110     * @param uri the incoming uri.
111     * @param resolveContent true if the Content should be actually resolved if not found in the request.
112     * @param session the JCR {@link Session} to use, or null to use the current Session.
113     * @return an object containing all parsed infos.
114     */
115    protected URIInfo getInfos(String uri, boolean resolveContent, Session session)
116    {
117        // uri are like <contentId>@<attribute>;<file.ext>
118        int i = uri.indexOf('@');
119        int j = uri.indexOf(';', i);
120        String id = uri.substring(0, i);
121        String attribute = uri.substring(i + 1, j);
122        String filename = uri.substring(j + 1);
123
124        Content content = null;
125        
126        try
127        {
128            Request request  = ContextHelper.getRequest(_context);
129            content = (Content) request.getAttribute(Content.class.getName());
130        }
131        catch (Exception e)
132        {
133            // there's no request, thus no "current" content 
134        }
135
136        String contentVersion = null;
137         
138        if (content == null || !id.equals(content.getId()))
139        {
140            // Some time (such as frontoffice edition) the image is rendered with no content in attribute
141            // The content should be resolved against the given id, but in that case, getRevision will be always the head on
142            try
143            {
144                content = resolveContent ? session == null ? _ametysObjectResolver.resolveById(id) : _ametysObjectResolver.resolveById(id, session) : null;
145            }
146            catch (RepositoryException e)
147            {
148                throw new AmetysRepositoryException(e);
149            }
150        }
151        else
152        {
153            if (content instanceof VersionableAmetysObject)
154            {
155                contentVersion = ((VersionableAmetysObject) content).getRevision();
156            }
157        }
158        
159        URIInfo infos = new URIInfo();
160        infos.setContentId(id);
161        infos.setContentVersion(contentVersion);
162        infos.setContent(content);
163        infos.setAttribute(attribute);
164        infos.setFilename(filename);
165        
166        return infos;
167    }
168    
169    @Override
170    public String resolveImage(String uri, int height, int width, boolean download, boolean absolute, boolean internal)
171    {
172        return resolve(uri, download, absolute, internal);
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        return resolve(uri, download, absolute, internal);
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        return resolve(uri, download, absolute, internal);
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, null);
219        
220        try
221        {
222            RichText richText = infos.getContent().getValue(infos.getAttribute());
223            Resource resource = richText.getAttachment(infos.getFilename());
224            
225            try (InputStream dataIs = resource.getInputStream())
226            {
227                return ImageResolverHelper.resolveImageAsBase64(dataIs, resource.getMimeType(), height, width, maxHeight, maxWidth, cropHeight, cropWidth);
228            }
229        }
230        catch (Exception e)
231        {
232            throw new IllegalStateException(e);
233        }
234    }
235    
236    @Override
237    public CHECK checkLink(String uri, boolean shortTest)
238    {
239        try
240        {
241            URIInfo infos = getInfos(uri, true, null);
242            
243            Content content = infos.getContent();
244            RichText richText = content.getValue(infos.getAttribute());
245            return richText != null
246                 ? richText.hasAttachment(infos.getFilename())
247                         ? CHECK.SUCCESS
248                         : CHECK.NOT_FOUND
249                 : CHECK.NOT_FOUND;
250        }
251        catch (Exception e)
252        {
253            throw new RuntimeException("Cannot check the uri '" + uri + "'", e);
254        }
255    }
256    
257    @Override
258    public I18nizableText getLabel(String uri)
259    {
260        URIInfo infos = getInfos(uri, false, null);
261        
262        return new I18nizableText("plugin.cms", "PLUGINS_CMS_LINK_LOCAL_LABEL", Collections.singletonList(infos.getFilename()));
263    }
264    
265    /**
266     * Helper class containg all infos parsed from URI.
267     */
268    protected static class URIInfo
269    {
270        /** The content id. */
271        private String _contentId;
272        /** The relevant attribute */
273        private String _attribute;
274        /** The resource name */
275        private String _filename;
276        /** The content version, if any. */
277        private String _contentVersion;
278        /** The resolved content, if any. */
279        private Content _content;
280        
281        /**
282         * Returns the content id.
283         * @return the content id.
284         */
285        public String getContentId()
286        {
287            return _contentId;
288        }
289        
290        /**
291         * Set the content id.
292         * @param contentId the content id.
293         */
294        public void setContentId(String contentId)
295        {
296            _contentId = contentId;
297        }
298        
299        /**
300         * Returns the metadata.
301         * @return the metadata.
302         */
303        public String getAttribute()
304        {
305            return _attribute;
306        }
307        
308        /**
309         * Set the metadata.
310         * @param attribute the metadata.
311         */
312        public void setAttribute(String attribute)
313        {
314            _attribute = attribute;
315        }
316        
317        /**
318         * Returns the resource name.
319         * @return the name
320         */
321        public String getFilename()
322        {
323            return _filename;
324        }
325        
326        /**
327         * Set the resource name.
328         * @param name the name.
329         */
330        public void setFilename(String name)
331        {
332            _filename = name;
333        }
334        
335        /**
336         * Returns the content version, if any.
337         * @return the content version.
338         */
339        public String getContentVersion()
340        {
341            return _contentVersion;
342        }
343        
344        /**
345         * Set the content version.
346         * @param contentVersion the content version.
347         */
348        public void setContentVersion(String contentVersion)
349        {
350            _contentVersion = contentVersion;
351        }
352        
353        /**
354         * Returns the resolved content, if any.
355         * @return the content.
356         */
357        public Content getContent()
358        {
359            return _content;
360        }
361        
362        /**
363         * Set the content.
364         * @param content the content.
365         */
366        public void setContent(Content content)
367        {
368            _content = content;
369        }
370    }
371}