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}