From 21dda9fe9bf61efa020d029e7028ab6e7dc15b89 Mon Sep 17 00:00:00 2001 From: RiccardoRuocco Date: Wed, 15 Apr 2026 14:15:43 +0200 Subject: [PATCH 1/3] block-folder-delete-with-live-content --- .../folders/action/EditFolderAction.java | 100 ++++++++++++++++-- .../resources/dotmarketing-config.properties | 4 + .../WEB-INF/messages/Language.properties | 1 + 3 files changed, 95 insertions(+), 10 deletions(-) diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/folders/action/EditFolderAction.java b/dotCMS/src/main/java/com/dotmarketing/portlets/folders/action/EditFolderAction.java index 9b1d7a65d708..e70170d1e864 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/folders/action/EditFolderAction.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/folders/action/EditFolderAction.java @@ -30,6 +30,7 @@ import com.dotmarketing.portlets.folders.exception.InvalidFolderNameException; import com.dotmarketing.portlets.folders.model.Folder; import com.dotmarketing.portlets.folders.struts.FolderForm; +import com.dotmarketing.portlets.htmlpageasset.business.HTMLPageAssetAPI; import com.dotmarketing.portlets.links.model.Link; import com.dotmarketing.portlets.structure.model.Structure; import com.dotmarketing.util.Config; @@ -62,9 +63,12 @@ public class EditFolderAction extends DotPortletAction { private FolderAPI folderAPI = APILocator.getFolderAPI(); private HostAPI hostAPI = APILocator.getHostAPI(); + private HTMLPageAssetAPI htmlPageAssetAPI = APILocator.getHTMLPageAssetAPI(); private static final int MAX_FOLDER_PATH_LENGTH = 255; private static final int MAX_FOLDER_NAME_LENGTH = 255; + private static final String DELETE_FOLDER_WITH_LIVE_CONTENT_PROPERTY = "no.delete.folder.with.live.content"; + private static final String DELETE_FOLDER_WITH_LIVE_CONTENT_MESSAGE_KEY = "message.folder.delete.live.content"; /** * @@ -438,26 +442,102 @@ public void _deleteFolder(ActionRequest req, ActionResponse res, // gets the session object for the messages HttpSession session = httpReq.getSession(); - String selectedFolder = ((String) session - .getAttribute(com.dotmarketing.util.WebKeys.FOLDER_SELECTED) != null) ? (String) session - .getAttribute(com.dotmarketing.util.WebKeys.FOLDER_SELECTED) - : ""; - - - - + session.removeAttribute(com.dotmarketing.util.WebKeys.FOLDER_SELECTED); + if (isDeleteFolderWithLiveContentProtectionEnabled() && containsLiveAssetsRecursively(f)) { + SessionMessages.add(req, "message", DELETE_FOLDER_WITH_LIVE_CONTENT_MESSAGE_KEY); + return; + } - session.removeAttribute(com.dotmarketing.util.WebKeys.FOLDER_SELECTED); User user = _getUser(req); folderAPI.delete(f, user,false); - // For messages to be displayed on messages page SessionMessages.add(req, "message", "message.folder.delete"); } + /** + * Determines whether the protection that blocks folder deletion when live + * content exists is enabled through configuration. + * + * @return {@code true} if the protection is enabled, otherwise {@code false}. + */ + private boolean isDeleteFolderWithLiveContentProtectionEnabled() { + return Config.getBooleanProperty(DELETE_FOLDER_WITH_LIVE_CONTENT_PROPERTY, false); + } + + /** + * Recursively checks whether the specified folder, or one of its descendants, + * contains live assets that must prevent deletion. + * + * @param folder + * the folder to inspect. + * @return {@code true} if at least one live asset is found. + * @throws DotDataException + * if a data access error occurs. + * @throws DotStateException + * if a folder is in an invalid state. + * @throws DotSecurityException + * if access to the APIs is not authorized. + */ + private boolean containsLiveAssetsRecursively(final Folder folder) + throws DotDataException, DotStateException, DotSecurityException { + return containsLiveAssetsRecursively(folder, APILocator.getUserAPI().getSystemUser()); + } + + /** + * Performs the recursive check while reusing the same system user for all + * queries executed against the folder tree. + * + * @param folder + * the current folder to inspect. + * @param user + * the user used to execute the internal checks. + * @return {@code true} as soon as a live asset is found. + * @throws DotDataException + * if a data access error occurs. + * @throws DotStateException + * if a folder is in an invalid state. + * @throws DotSecurityException + * if access to the APIs is not authorized. + */ + private boolean containsLiveAssetsRecursively(final Folder folder, final User user) + throws DotDataException, DotStateException, DotSecurityException { + if (containsLiveAssets(folder, user)) { + return true; + } + + for (final Folder childFolder : folderAPI.findSubFolders(folder, user, false)) { + if (containsLiveAssetsRecursively(childFolder, user)) { + return true; + } + } + + return false; + } + + /** + * Determines whether the current folder directly contains at least one live + * contentlet, link, or HTML page. + * + * @param folder + * the folder to inspect. + * @param user + * the user used to query the internal APIs. + * @return {@code true} if the folder contains direct live assets. + * @throws DotDataException + * if a data access error occurs. + * @throws DotSecurityException + * if access to the APIs is not authorized. + */ + private boolean containsLiveAssets(final Folder folder, final User user) + throws DotDataException, DotSecurityException { + return !folderAPI.getLiveContent(folder, user, false).isEmpty() + || !folderAPI.getLiveLinks(folder, user, false).isEmpty() + || !htmlPageAssetAPI.getLiveHTMLPages(folder, user, false).isEmpty(); + } + /** * * @param folder diff --git a/dotCMS/src/main/resources/dotmarketing-config.properties b/dotCMS/src/main/resources/dotmarketing-config.properties index d66f392d19a6..297a1cf9e461 100644 --- a/dotCMS/src/main/resources/dotmarketing-config.properties +++ b/dotCMS/src/main/resources/dotmarketing-config.properties @@ -30,6 +30,10 @@ DEFAULT_FILE_TO_DEFAULT_LANGUAGE = true ## it is similar to DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE but only applies to Persona DEFAULT_PERSONA_TO_DEFAULT_LANGUAGE = true +## Prevents folder deletion when the folder or one of its descendants contains live content. +## Environment variable: DOT_NO_DELETE_FOLDER_WITH_LIVE_CONTENT +no.delete.folder.with.live.content=false + PER_PAGE = 40 ## in minutes diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties index dd2f7189b56e..42d4dc84feee 100644 --- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties +++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties @@ -2551,6 +2551,7 @@ message.folder.admin.doesnotallow=The Site cannot have folder with the name &quo message.folder.alreadyexists=The folder already exists; please choose a different folder name. message.folder.defaultfiletype.required=A default File Asset type is required message.folder.delete=Folder deleted +message.folder.delete.live.content=This folder cannot be deleted because it contains live content. message.folder.hostname.required=A Site name is required. message.folder.ischildfolder=The destination folder is a sub-folder of the source folder; please select another destination folder. message.folder.ishostfolder=The destination folder is a Site; please select another destination folder. From 0634ac6fbaa8b3d7d729605b287c8c3253260e10 Mon Sep 17 00:00:00 2001 From: RiccardoRuocco Date: Mon, 20 Apr 2026 21:35:15 +0200 Subject: [PATCH 2/3] Issue 35310: block folder delete when not empty --- .../folders/action/EditFolderAction.java | 45 ++++++++++--------- .../resources/dotmarketing-config.properties | 6 +-- .../WEB-INF/messages/Language.properties | 2 +- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/folders/action/EditFolderAction.java b/dotCMS/src/main/java/com/dotmarketing/portlets/folders/action/EditFolderAction.java index e70170d1e864..592d7c1b87b1 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/folders/action/EditFolderAction.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/folders/action/EditFolderAction.java @@ -67,8 +67,8 @@ public class EditFolderAction extends DotPortletAction { private static final int MAX_FOLDER_PATH_LENGTH = 255; private static final int MAX_FOLDER_NAME_LENGTH = 255; - private static final String DELETE_FOLDER_WITH_LIVE_CONTENT_PROPERTY = "no.delete.folder.with.live.content"; - private static final String DELETE_FOLDER_WITH_LIVE_CONTENT_MESSAGE_KEY = "message.folder.delete.live.content"; + private static final String DELETE_NOT_EMPTY_FOLDER_PROPERTY = "no.delete.notempty.folder"; + private static final String DELETE_NOT_EMPTY_FOLDER_MESSAGE_KEY = "message.folder.delete.not.empty"; /** * @@ -444,8 +444,8 @@ public void _deleteFolder(ActionRequest req, ActionResponse res, session.removeAttribute(com.dotmarketing.util.WebKeys.FOLDER_SELECTED); - if (isDeleteFolderWithLiveContentProtectionEnabled() && containsLiveAssetsRecursively(f)) { - SessionMessages.add(req, "message", DELETE_FOLDER_WITH_LIVE_CONTENT_MESSAGE_KEY); + if (isDeleteNotEmptyFolderProtectionEnabled() && isNotEmpty(f)) { + SessionMessages.add(req, "message", DELETE_NOT_EMPTY_FOLDER_MESSAGE_KEY); return; } @@ -458,22 +458,22 @@ public void _deleteFolder(ActionRequest req, ActionResponse res, } /** - * Determines whether the protection that blocks folder deletion when live - * content exists is enabled through configuration. + * Determines whether the protection that blocks the deletion of non-empty + * folders is enabled through configuration. * * @return {@code true} if the protection is enabled, otherwise {@code false}. */ - private boolean isDeleteFolderWithLiveContentProtectionEnabled() { - return Config.getBooleanProperty(DELETE_FOLDER_WITH_LIVE_CONTENT_PROPERTY, false); + private boolean isDeleteNotEmptyFolderProtectionEnabled() { + return Config.getBooleanProperty(DELETE_NOT_EMPTY_FOLDER_PROPERTY, false); } /** * Recursively checks whether the specified folder, or one of its descendants, - * contains live assets that must prevent deletion. + * contains content that makes the branch non-empty. * * @param folder * the folder to inspect. - * @return {@code true} if at least one live asset is found. + * @return {@code true} if at least one relevant asset is found. * @throws DotDataException * if a data access error occurs. * @throws DotStateException @@ -481,9 +481,9 @@ private boolean isDeleteFolderWithLiveContentProtectionEnabled() { * @throws DotSecurityException * if access to the APIs is not authorized. */ - private boolean containsLiveAssetsRecursively(final Folder folder) + private boolean isNotEmpty(final Folder folder) throws DotDataException, DotStateException, DotSecurityException { - return containsLiveAssetsRecursively(folder, APILocator.getUserAPI().getSystemUser()); + return isNotEmpty(folder, APILocator.getUserAPI().getSystemUser()); } /** @@ -494,7 +494,7 @@ private boolean containsLiveAssetsRecursively(final Folder folder) * the current folder to inspect. * @param user * the user used to execute the internal checks. - * @return {@code true} as soon as a live asset is found. + * @return {@code true} as soon as content is found in the current branch. * @throws DotDataException * if a data access error occurs. * @throws DotStateException @@ -502,14 +502,14 @@ private boolean containsLiveAssetsRecursively(final Folder folder) * @throws DotSecurityException * if access to the APIs is not authorized. */ - private boolean containsLiveAssetsRecursively(final Folder folder, final User user) + private boolean isNotEmpty(final Folder folder, final User user) throws DotDataException, DotStateException, DotSecurityException { - if (containsLiveAssets(folder, user)) { + if (containsRelevantAssets(folder, user)) { return true; } for (final Folder childFolder : folderAPI.findSubFolders(folder, user, false)) { - if (containsLiveAssetsRecursively(childFolder, user)) { + if (isNotEmpty(childFolder, user)) { return true; } } @@ -518,24 +518,25 @@ private boolean containsLiveAssetsRecursively(final Folder folder, final User us } /** - * Determines whether the current folder directly contains at least one live + * Determines whether the current folder directly contains at least one * contentlet, link, or HTML page. * * @param folder * the folder to inspect. * @param user * the user used to query the internal APIs. - * @return {@code true} if the folder contains direct live assets. + * @return {@code true} if the folder contains direct relevant assets. * @throws DotDataException * if a data access error occurs. * @throws DotSecurityException * if access to the APIs is not authorized. */ - private boolean containsLiveAssets(final Folder folder, final User user) + private boolean containsRelevantAssets(final Folder folder, final User user) throws DotDataException, DotSecurityException { - return !folderAPI.getLiveContent(folder, user, false).isEmpty() - || !folderAPI.getLiveLinks(folder, user, false).isEmpty() - || !htmlPageAssetAPI.getLiveHTMLPages(folder, user, false).isEmpty(); + return !folderAPI.getContent(folder, user, false).isEmpty() + || !folderAPI.getLinks(folder, user, false).isEmpty() + || !htmlPageAssetAPI.getHTMLPages(folder, false, false, user, false).isEmpty() + || !htmlPageAssetAPI.getHTMLPages(folder, true, false, user, false).isEmpty(); } /** diff --git a/dotCMS/src/main/resources/dotmarketing-config.properties b/dotCMS/src/main/resources/dotmarketing-config.properties index 297a1cf9e461..92ee3db004d3 100644 --- a/dotCMS/src/main/resources/dotmarketing-config.properties +++ b/dotCMS/src/main/resources/dotmarketing-config.properties @@ -30,9 +30,9 @@ DEFAULT_FILE_TO_DEFAULT_LANGUAGE = true ## it is similar to DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE but only applies to Persona DEFAULT_PERSONA_TO_DEFAULT_LANGUAGE = true -## Prevents folder deletion when the folder or one of its descendants contains live content. -## Environment variable: DOT_NO_DELETE_FOLDER_WITH_LIVE_CONTENT -no.delete.folder.with.live.content=false +## Prevents folder deletion when the folder or one of its descendants is not empty. +## Environment variable: DOT_NO_DELETE_NOTEMPTY_FOLDER +no.delete.notempty.folder=false PER_PAGE = 40 diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties index 42d4dc84feee..52192d6aaf33 100644 --- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties +++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties @@ -2551,7 +2551,7 @@ message.folder.admin.doesnotallow=The Site cannot have folder with the name &quo message.folder.alreadyexists=The folder already exists; please choose a different folder name. message.folder.defaultfiletype.required=A default File Asset type is required message.folder.delete=Folder deleted -message.folder.delete.live.content=This folder cannot be deleted because it contains live content. +message.folder.delete.not.empty=This folder cannot be deleted because it is not empty. message.folder.hostname.required=A Site name is required. message.folder.ischildfolder=The destination folder is a sub-folder of the source folder; please select another destination folder. message.folder.ishostfolder=The destination folder is a Site; please select another destination folder. From a27de5174ca70a64924138663e7c89f378ac1286 Mon Sep 17 00:00:00 2001 From: RiccardoRuocco Date: Thu, 23 Apr 2026 09:49:35 +0200 Subject: [PATCH 3/3] Use Lazy for folder delete config flag --- .../portlets/folders/action/EditFolderAction.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/folders/action/EditFolderAction.java b/dotCMS/src/main/java/com/dotmarketing/portlets/folders/action/EditFolderAction.java index 592d7c1b87b1..63086fd2810f 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/folders/action/EditFolderAction.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/folders/action/EditFolderAction.java @@ -43,6 +43,7 @@ import com.liferay.portal.util.Constants; import com.liferay.portlet.ActionRequestImpl; import com.liferay.util.servlet.SessionMessages; +import io.vavr.Lazy; import java.util.List; import java.util.Set; import javax.servlet.http.HttpServletRequest; @@ -69,6 +70,8 @@ public class EditFolderAction extends DotPortletAction { private static final int MAX_FOLDER_NAME_LENGTH = 255; private static final String DELETE_NOT_EMPTY_FOLDER_PROPERTY = "no.delete.notempty.folder"; private static final String DELETE_NOT_EMPTY_FOLDER_MESSAGE_KEY = "message.folder.delete.not.empty"; + private static final Lazy DELETE_NOT_EMPTY_FOLDER_PROTECTION = + Lazy.of(() -> Config.getBooleanProperty(DELETE_NOT_EMPTY_FOLDER_PROPERTY, false)); /** * @@ -464,7 +467,7 @@ public void _deleteFolder(ActionRequest req, ActionResponse res, * @return {@code true} if the protection is enabled, otherwise {@code false}. */ private boolean isDeleteNotEmptyFolderProtectionEnabled() { - return Config.getBooleanProperty(DELETE_NOT_EMPTY_FOLDER_PROPERTY, false); + return DELETE_NOT_EMPTY_FOLDER_PROTECTION.get(); } /**