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}