001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.gwt.shared;
029
030import org.opencms.util.CmsStringUtil;
031
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.Collections;
035import java.util.HashMap;
036import java.util.HashSet;
037import java.util.List;
038import java.util.Map;
039import java.util.Objects;
040import java.util.Set;
041
042import com.google.common.base.Joiner;
043import com.google.gwt.user.client.rpc.IsSerializable;
044
045/**
046 * Contains information about which folders should restrict uploads.
047 */
048public class CmsUploadRestrictionInfo implements IsSerializable {
049
050    /**
051     * Helper class for building a new CmsUploadRestrictionInfo object.
052     */
053    public static class Builder {
054
055        /** The tree node corresponding to the root directory. */
056        private Node m_root = new Node();
057
058        /** Map used to 'uniquify' equivalent node data objects. */
059        private Map<NodeData, NodeData> m_nodeDataCache = new HashMap<>();
060
061        /**
062         * Adds a new entry with the given options for the given path.
063         *
064         * @param path the path
065         * @param enabled upload enabled (TRUE / FALSE / null)
066         * @param extensions (set of file extensions or null)
067         *
068         * @return the builder instance
069         */
070        public Builder add(String path, Boolean enabled, Set<String> extensions) {
071
072            Node node = findOrCreateNode(m_root, path);
073            NodeData data = new NodeData();
074            data.setEnabled(enabled);
075            data.setExtensions(extensions);
076            data = m_nodeDataCache.computeIfAbsent(data, dataParam -> dataParam);
077            node.setData(data);
078            return this;
079        }
080
081        /**
082         * Adds a new entry.
083         *
084         * <p>The info string has the format 'key1:value1|key2:value2|....'. Currently the keys 'types' and 'enabled' are supported;
085         * 'enabled' (format: 'enabled:true') enables/disables uploads, and 'types' (format: 'types:jpg,png' sets the allowed file extensions.
086         *
087         * @param path the path
088         * @param info the upload info entry
089         * @return the builder instance
090         */
091        public Builder add(String path, String info) {
092
093            Node node = findOrCreateNode(m_root, path);
094            NodeData data = new NodeData();
095            data.parse(info);
096            data = m_nodeDataCache.computeIfAbsent(data, dataParam -> dataParam);
097            node.setData(data);
098            return this;
099
100        }
101
102        /**
103         * Creates a new upload restriction info object.
104         *
105         * @return the new object
106         */
107        @SuppressWarnings("synthetic-access")
108        public CmsUploadRestrictionInfo build() {
109
110            CmsUploadRestrictionInfo result = new CmsUploadRestrictionInfo();
111            result.m_root = m_root;
112            return result;
113        }
114
115    }
116
117    /**
118     * Tree node that stores the settings for a single folder.
119     */
120    public static class Node implements IsSerializable {
121
122        /** Map of child nodes by name. */
123        private Map<String, Node> m_children = new HashMap<>();
124
125        /** The stored node data (may be null). */
126        private NodeData m_data;
127
128        /**
129         * Creates a new instance.
130         */
131        public Node() {}
132
133        /**
134         * Gets the children.
135         *
136         * @return the children by name
137         */
138        public Map<String, Node> getChildren() {
139
140            return m_children;
141        }
142
143        /**
144         * Gets the node data
145         *
146         * @return the node data
147         */
148        public NodeData getData() {
149
150            return m_data;
151        }
152
153        /**
154         * Sets the node data
155         *
156         * @param data the node data
157         */
158        public void setData(NodeData data) {
159
160            m_data = data;
161        }
162    }
163
164    /**
165     * The data for a single node.
166     *
167     * <p>Contains information on which file extensions are uploadable and whether uploads are enabled at all.
168     */
169    public static class NodeData implements IsSerializable {
170
171        /** True if upload enabled (may be null). */
172        private Boolean m_enabled;
173
174        /** Set of allowed extensions (without leading '.'). May be null. */
175        private Set<String> m_extensions;
176
177        /** Creates a new instance. */
178        public NodeData() {}
179
180        /**
181         * @see java.lang.Object#equals(java.lang.Object)
182         */
183        public boolean equals(Object other) {
184
185            if (!(other instanceof NodeData)) {
186                return false;
187            }
188            NodeData data = (NodeData)other;
189            return Objects.equals(m_enabled, data.getEnabled()) && Objects.equals(m_extensions, data.getExtensions());
190
191        }
192
193        /**
194         * @see java.lang.Object#hashCode()
195         */
196        @Override
197        public int hashCode() {
198
199            return Objects.hash(m_enabled, m_extensions);
200        }
201
202        /**
203         * Merges this node with a child node, where if the child node has attributes set, they override the corresponding attributes of this node.
204         *
205         * @param child the child node
206         * @return the merged node
207         */
208        public NodeData merge(NodeData child) {
209
210            if (child == null) {
211                return this;
212            }
213            if ((child.getExtensions() != null) && (child.getEnabled() != null)) {
214                // everything is overwritten, so we don't need a merged version
215                return child;
216            }
217            NodeData result = new NodeData();
218            if (child.getExtensions() != null) {
219                result.setExtensions(child.getExtensions());
220            } else {
221                result.setExtensions(getExtensions());
222            }
223            if (child.getEnabled() != null) {
224                result.setEnabled(child.getEnabled());
225            } else {
226                result.setEnabled(getEnabled());
227            }
228            return result;
229        }
230
231        public void parse(String info) {
232
233            Map<String, String> parsedInfo = CmsStringUtil.splitAsMap(info, "|", ":");
234            String enabledStr = parsedInfo.get(KEY_ENABLED);
235            if (enabledStr != null) {
236                setEnabled(Boolean.valueOf(enabledStr));
237            }
238            String typesStr = parsedInfo.get(KEY_TYPES);
239            if (typesStr != null) {
240                Set<String> types = new HashSet<>();
241                for (String type : typesStr.split(",")) {
242                    type = type.trim().toLowerCase();
243                    if (type.startsWith(".")) {
244                        type = type.substring(1);
245                    }
246                    if (type.length() > 0) {
247                        types.add(type);
248                    }
249                }
250                setExtensions(types);
251            }
252        }
253
254        /**
255         * Gets the 'upload enabled' status (may be null).
256         *
257         * @return the 'upload enabled' status
258         */
259        private Boolean getEnabled() {
260
261            return m_enabled;
262        }
263
264        /**
265         * Gets the allowed file extensions for uploading (may be null).
266         *
267         * @return the set of allowed file extensions for uploads
268         */
269        private Set<String> getExtensions() {
270
271            return m_extensions;
272        }
273
274        /**
275         * Sets the 'upload enabled' status.
276         *
277         * @param enabled the 'upload enabled' status
278         */
279        private void setEnabled(Boolean enabled) {
280
281            m_enabled = enabled;
282        }
283
284        /**
285         * Sets the allowed file extensions.
286         *
287         * @param extensions the set of allowed file extensions
288         */
289        private void setExtensions(Set<String> extensions) {
290
291            m_extensions = extensions;
292        }
293
294    }
295
296    /** The 'enabled' key. */
297    public static final String KEY_ENABLED = "enabled";
298
299    /** The 'types' key. */
300    public static final String KEY_TYPES = "types";
301
302    /** The default upload restriction that allows everything. */
303    public static final String UNRESTRICTED_UPLOADS = "enabled:true|types:*";
304
305    protected Node m_root;
306
307    /**
308     * Creates a new instance.
309     */
310    protected CmsUploadRestrictionInfo() {}
311
312    /**
313     * Helper method for collecting and merging the node data valid for a particular path.
314     *
315     * @param root the root node
316     * @param path the path along which to collect and merge the node data
317     *
318     * @return the merged node data
319     */
320    public static NodeData collectNodeData(Node root, String path) {
321
322        NodeData empty = new NodeData();
323        NodeData currentData = empty.merge(root.getData()); // root.getData() may be null, so we merge it with an empty NodeData instance
324        Node current = root;
325        List<String> pathComponents = Arrays.asList(path.split("/"));
326        for (String part : pathComponents) {
327            if ("".equals(part)) {
328                continue;
329            }
330            Node child = current.getChildren().get(part);
331            if (child != null) {
332                currentData = currentData.merge(child.getData());
333                current = child;
334            } else {
335                break;
336            }
337        }
338        return currentData;
339
340    }
341
342    /**
343     * Finds or creates the node corresponding to a given path from a root node (also creating any required intermediate nodes).
344     *
345     * @param root the root node of the tree
346     * @param path the path
347     * @return the node for the given path
348     */
349    public static Node findOrCreateNode(Node root, String path) {
350
351        Node current = root;
352        if ("/".equals(path) || "".equals(path)) {
353            // empty list of path components is OK
354        } else {
355            List<String> pathComponents = Arrays.asList(path.split("/"));
356            for (String part : pathComponents) {
357                if ("".equals(part)) {
358                    // trailing or duplicate slashes
359                    continue;
360                }
361                Node child = current.getChildren().computeIfAbsent(part, k -> new Node());
362                current = child;
363            }
364        }
365        return current;
366    }
367
368    /**
369     * Normalizes a path.
370     *
371     * @param path the path
372     * @return the normalized path
373     */
374    static String normalizePath(String path) {
375
376        if (!path.endsWith("/")) {
377            path = path + "/";
378        }
379        return path;
380    }
381
382    /**
383     * Check if a given file extension is allowed for the given upload path.
384     *
385     * @param path the root path of the upload folder
386     * @param extension the file extension to check
387     * @return true if the file extension is valid for uploads to the folder
388     */
389    public boolean checkTypeAllowed(String path, String extension) {
390
391        Set<String> types = getTypes(path);
392        if (extension.startsWith(".")) {
393            extension = extension.substring(1);
394        }
395        extension = extension.toLowerCase();
396        boolean result = types.contains(extension) || types.contains("*");
397        return result;
398    }
399
400    /**
401     * Gets the 'accept' attribute to use for the file input element for the given upload folder.
402     *
403     * @param path the upload folder root path
404     * @return the 'accept' attribute that should be used for the file input
405     */
406    public String getAcceptAttribute(String path) {
407
408        Set<String> types = getTypes(path);
409        List<String> suffixes = new ArrayList<>();
410        for (String type : types) {
411            if ("*".equals(type)) {
412                return "";
413            } else {
414                suffixes.add("." + type);
415            }
416        }
417        String result = Joiner.on(",").join(suffixes);
418        return result;
419    }
420
421    /**
422     * Checks if the upload should be enabled for the given upload path.
423     *
424     * @param originalPath the upload root path
425     * @return true if the upload is enabled
426     */
427    public boolean isUploadEnabled(String originalPath) {
428
429        NodeData data = collectNodeData(m_root, originalPath);
430        return (data.getEnabled() == null) || data.getEnabled().booleanValue();
431    }
432
433    /**
434     * Gets the valid extensions for uploads to a given upload folder
435     *
436     * @param path the upload folder root path
437     * @return the valid extensions
438     */
439    protected Set<String> getTypes(String path) {
440
441        NodeData data = collectNodeData(m_root, path);
442        if (data.getExtensions() == null) {
443            return Collections.emptySet();
444        } else {
445            return data.getExtensions();
446        }
447    }
448
449}