diff --git a/lib/build/SparseBitSet-1.3.jar b/lib/build/SparseBitSet-1.3.jar new file mode 100644 index 00000000..5a435441 Binary files /dev/null and b/lib/build/SparseBitSet-1.3.jar differ diff --git a/lib/build/commons-math3-3.6.1.jar b/lib/build/commons-math3-3.6.1.jar new file mode 100644 index 00000000..0ff582cf Binary files /dev/null and b/lib/build/commons-math3-3.6.1.jar differ diff --git a/lib/build/curvesapi-1.08.jar b/lib/build/curvesapi-1.08.jar new file mode 100644 index 00000000..18fb71b1 Binary files /dev/null and b/lib/build/curvesapi-1.08.jar differ diff --git a/lib/build/log4j-api-2.24.3.jar b/lib/build/log4j-api-2.24.3.jar new file mode 100644 index 00000000..4af1681f Binary files /dev/null and b/lib/build/log4j-api-2.24.3.jar differ diff --git a/lib/build/poi-5.5.1.jar b/lib/build/poi-5.5.1.jar new file mode 100644 index 00000000..702effe8 Binary files /dev/null and b/lib/build/poi-5.5.1.jar differ diff --git a/lib/build/poi-ooxml-5.5.1.jar b/lib/build/poi-ooxml-5.5.1.jar new file mode 100644 index 00000000..dcf1044b Binary files /dev/null and b/lib/build/poi-ooxml-5.5.1.jar differ diff --git a/lib/build/poi-ooxml-lite-5.5.1.jar b/lib/build/poi-ooxml-lite-5.5.1.jar new file mode 100644 index 00000000..a9d53b76 Binary files /dev/null and b/lib/build/poi-ooxml-lite-5.5.1.jar differ diff --git a/lib/build/xmlbeans-5.3.0.jar b/lib/build/xmlbeans-5.3.0.jar new file mode 100644 index 00000000..2ba57e55 Binary files /dev/null and b/lib/build/xmlbeans-5.3.0.jar differ diff --git a/pom.xml b/pom.xml index 131be167..eda198fb 100644 --- a/pom.xml +++ b/pom.xml @@ -113,6 +113,11 @@ pdfbox 3.0.8 + + org.apache.poi + poi-ooxml + 5.5.1 + commons-validator commons-validator diff --git a/src/main/java/com/simisinc/platform/application/cms/PublishContentCommand.java b/src/main/java/com/simisinc/platform/application/cms/PublishContentCommand.java index 2f3151b8..ce78981d 100644 --- a/src/main/java/com/simisinc/platform/application/cms/PublishContentCommand.java +++ b/src/main/java/com/simisinc/platform/application/cms/PublishContentCommand.java @@ -26,10 +26,10 @@ import com.simisinc.platform.domain.model.cms.WebPage; import com.simisinc.platform.infrastructure.persistence.cms.ContentRepository; import com.simisinc.platform.infrastructure.persistence.cms.WebPageRepository; -import com.simisinc.platform.infrastructure.scheduler.cms.RefreshWebPageTextIndexJob; import com.simisinc.platform.infrastructure.workflow.WorkflowManager; import com.zeroio.platform.domain.model.cms.ContentVersion; import com.zeroio.platform.infrastructure.persistence.cms.ContentVersionRepository; +import com.zeroio.platform.infrastructure.scheduler.cms.RefreshWebPageTextIndexJob; /** * Publishes draft content to live diff --git a/src/main/java/com/simisinc/platform/application/cms/SaveContentCommand.java b/src/main/java/com/simisinc/platform/application/cms/SaveContentCommand.java index c6156d46..9bf06832 100644 --- a/src/main/java/com/simisinc/platform/application/cms/SaveContentCommand.java +++ b/src/main/java/com/simisinc/platform/application/cms/SaveContentCommand.java @@ -24,9 +24,9 @@ import com.simisinc.platform.application.DataException; import com.simisinc.platform.domain.model.cms.Content; import com.simisinc.platform.infrastructure.persistence.cms.ContentRepository; -import com.simisinc.platform.infrastructure.scheduler.cms.RefreshWebPageTextIndexJob; import com.zeroio.platform.domain.model.cms.ContentVersion; import com.zeroio.platform.infrastructure.persistence.cms.ContentVersionRepository; +import com.zeroio.platform.infrastructure.scheduler.cms.RefreshWebPageTextIndexJob; /** * Validates and saves content objects diff --git a/src/main/java/com/simisinc/platform/domain/model/cms/FileItem.java b/src/main/java/com/simisinc/platform/domain/model/cms/FileItem.java index 7b65b14a..7e9e6ff6 100644 --- a/src/main/java/com/simisinc/platform/domain/model/cms/FileItem.java +++ b/src/main/java/com/simisinc/platform/domain/model/cms/FileItem.java @@ -1,4 +1,5 @@ /* + * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski) * Copyright 2022 SimIS Inc. (https://www.simiscms.com) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -44,6 +45,7 @@ public class FileItem extends Entity { private String fileType = null; private String mimeType = null; private String fileHash = null; + private String documentText = null; private String webPath = null; private int width = -1; private int height = -1; @@ -175,6 +177,14 @@ public void setFileHash(String fileHash) { this.fileHash = fileHash; } + public String getDocumentText() { + return documentText; + } + + public void setDocumentText(String documentText) { + this.documentText = documentText; + } + public int getWidth() { return width; } diff --git a/src/main/java/com/simisinc/platform/domain/model/items/ItemFileItem.java b/src/main/java/com/simisinc/platform/domain/model/items/ItemFileItem.java index c0c77ffd..ce154e08 100644 --- a/src/main/java/com/simisinc/platform/domain/model/items/ItemFileItem.java +++ b/src/main/java/com/simisinc/platform/domain/model/items/ItemFileItem.java @@ -1,4 +1,5 @@ /* + * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski) * Copyright 2022 SimIS Inc. (https://www.simiscms.com) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -44,6 +45,7 @@ public class ItemFileItem extends Entity { private String fileType = null; private String mimeType = null; private String fileHash = null; + private String documentText = null; private String webPath = null; private int width = -1; private int height = -1; @@ -183,6 +185,14 @@ public void setFileHash(String fileHash) { this.fileHash = fileHash; } + public String getDocumentText() { + return documentText; + } + + public void setDocumentText(String documentText) { + this.documentText = documentText; + } + public int getWidth() { return width; } @@ -302,7 +312,8 @@ public String getWebPath() { public void setWebPath(String webPath) { this.webPath = webPath; } -public String getUrl() { + + public String getUrl() { return webPath + "-" + id + "/" + UrlCommand.encodeUri(filename); - } + } } diff --git a/src/main/java/com/simisinc/platform/infrastructure/database/DB.java b/src/main/java/com/simisinc/platform/infrastructure/database/DB.java index a34288c3..6aca88be 100644 --- a/src/main/java/com/simisinc/platform/infrastructure/database/DB.java +++ b/src/main/java/com/simisinc/platform/infrastructure/database/DB.java @@ -419,6 +419,29 @@ public static DataResult selectAllFrom(String tableName, SqlUtils select, SqlWhe public static DataResult selectAllFrom(String tableName, SqlUtils select, SqlJoins joins, SqlWhere where, SqlUtils orderBy, DataConstraints constraints, Function buildRecord) { + // This method creates a TABLE_NAME.* column automatically (but may be deprecated in the future) + if (select == null) { + select = DB.SELECT(); + } + // At the front of the columns, add the table's full columns for backwards compatibility + select.getValues().add(0, new SqlValue(tableName + ".*")); + return selectFrom(tableName, select, joins, where, orderBy, constraints, buildRecord); + } + + /** + * Select records from the database with the specified parameters. + * + * @param tableName the name of the table + * @param columns the columns values + * @param joins the join clauses + * @param where the where clause + * @param orderBy the order by clause + * @param constraints the data constraints + * @param buildRecord the function to build an entity from the result set + * @return the data result containing the records + */ + public static DataResult selectFrom(String tableName, SqlUtils columns, SqlJoins joins, SqlWhere where, + SqlUtils orderBy, DataConstraints constraints, Function buildRecord) { // Determine the max records based on the where conditions DataResult dataResult = new DataResult(); @@ -462,10 +485,8 @@ public static DataResult selectAllFrom(String tableName, SqlUtils select, SqlJoi // Prepare the query StringBuilder sb = new StringBuilder(); - sb.append("SELECT ").append(tableName).append(".*"); - if (select != null) { - sb.append(createAdditionalSelectFields(select)); - } + sb.append("SELECT "); + sb.append(createSelectFields(columns)); sb.append(" FROM ").append(tableName); sb.append(joinsSb); sb.append(whereSb); @@ -489,7 +510,7 @@ public static DataResult selectAllFrom(String tableName, SqlUtils select, SqlJoi List records = null; long startQueryTime = System.currentTimeMillis(); try (Connection connection = getConnection(); - PreparedStatement pst = createPreparedStatement(connection, sb.toString(), select, where, orderBy); + PreparedStatement pst = createPreparedStatement(connection, sb.toString(), columns, where, orderBy); ResultSet rs = pst.executeQuery()) { records = new ArrayList<>(); while (rs.next()) { diff --git a/src/main/java/com/simisinc/platform/infrastructure/database/SqlWhere.java b/src/main/java/com/simisinc/platform/infrastructure/database/SqlWhere.java index 42394e07..f6fd4a65 100644 --- a/src/main/java/com/simisinc/platform/infrastructure/database/SqlWhere.java +++ b/src/main/java/com/simisinc/platform/infrastructure/database/SqlWhere.java @@ -35,6 +35,8 @@ public class SqlWhere { public static final String AND_OPERATOR = "AND"; public static final String OR_OPERATOR = "OR"; + public static final String NOT_AND_OPERATOR = "NOT AND"; + public static final String NOT_OR_OPERATOR = "OR"; private List values = new ArrayList<>(); @@ -296,10 +298,20 @@ private static String arrayToSqlStatementList(String jsonbColumnName, String[] a if (StringUtils.isBlank(jsonbColumnName) || arrayValues == null || arrayValues.length == 0) { return null; } + StringBuilder sql = new StringBuilder(); // Default to AND if operator is null or invalid - String op = OR_OPERATOR.equalsIgnoreCase(operator) ? OR_OPERATOR : AND_OPERATOR; - StringBuilder sql = new StringBuilder("("); + String op = (OR_OPERATOR.equalsIgnoreCase(operator) || NOT_OR_OPERATOR.equalsIgnoreCase(operator)) + ? OR_OPERATOR + : AND_OPERATOR; + + // If the operator is a NOT variant, prepend "NOT " to the condition + if (NOT_AND_OPERATOR.equalsIgnoreCase(operator) || NOT_OR_OPERATOR.equalsIgnoreCase(operator)) { + sql.append("NOT "); + } + + // Build the condition with one placeholder per value + sql.append("("); for (int i = 0; i < arrayValues.length; i++) { if (i > 0) { sql.append(" ").append(op).append(" "); diff --git a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/FileItemRepository.java b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/FileItemRepository.java index fe376e4e..e958f783 100644 --- a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/FileItemRepository.java +++ b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/FileItemRepository.java @@ -61,11 +61,51 @@ public class FileItemRepository { private static String[] PRIMARY_KEY = new String[] { "file_id" }; private static DataResult query(FileSpecification specification, DataConstraints constraints) { - SqlUtils select = new SqlUtils(); + + SqlUtils select = DB.SELECT( + TABLE_NAME + ".file_id", + TABLE_NAME + ".folder_id", + TABLE_NAME + ".filename", + TABLE_NAME + ".title", + TABLE_NAME + ".barcode", + TABLE_NAME + ".version", + TABLE_NAME + ".extension", + TABLE_NAME + ".path", + TABLE_NAME + ".file_length", + TABLE_NAME + ".file_type", + TABLE_NAME + ".mime_type", + TABLE_NAME + ".file_hash", + TABLE_NAME + ".width", + TABLE_NAME + ".height", + TABLE_NAME + ".summary", + TABLE_NAME + ".created_by", + TABLE_NAME + ".created", + TABLE_NAME + ".modified_by", + TABLE_NAME + ".modified", + TABLE_NAME + ".processed", + TABLE_NAME + ".expiration_date", + TABLE_NAME + ".privacy_type", + TABLE_NAME + ".default_token", + TABLE_NAME + ".version_count", + TABLE_NAME + ".download_count", + TABLE_NAME + ".sub_folder_id", + TABLE_NAME + ".category_id", + TABLE_NAME + ".web_path", + TABLE_NAME + ".tags"); + SqlJoins joins = new SqlJoins(); SqlWhere where = DB.WHERE(); SqlUtils orderBy = new SqlUtils(); + + final boolean includeDocumentText = specification != null && specification.getIncludeDocumentText(); + if (specification != null) { + + // Only include this potentially-large field when requested + if (includeDocumentText) { + select.add(TABLE_NAME + ".document_text"); + } + joins.add("LEFT JOIN folders ON (files.folder_id = folders.folder_id)"); where .andAddIfHasValue("file_id = ?", specification.getId(), -1) @@ -79,6 +119,10 @@ private static DataResult query(FileSpecification specification, DataConstraints where.AND("LOWER(files.file_type) = ANY(?)", Arrays.stream(specification.getFileType()).map(String::toLowerCase).toArray(String[]::new), Types.ARRAY); } + if (specification.getFileExtension() != null) { + where.AND("LOWER(files.extension) = ANY(?)", + Arrays.stream(specification.getFileExtension()).map(String::toLowerCase).toArray(String[]::new), Types.ARRAY); + } if (specification.getMatchesName() != null) { String likeValue = specification.getMatchesName().trim() .replace("!", "!!") @@ -97,6 +141,13 @@ private static DataResult query(FileSpecification specification, DataConstraints where.AND("sub_folder_id IS NULL"); } } + if (specification.getIsProcessed() != DataConstants.UNDEFINED) { + if (specification.getIsProcessed() == DataConstants.TRUE) { + where.AND("processed IS NOT NULL"); + } else { + where.AND("processed IS NULL"); + } + } // For user id // User must be in a user group with folder access @@ -143,6 +194,10 @@ private static DataResult query(FileSpecification specification, DataConstraints where.AND("tags", specification.getFilterTags(), SqlWhere.AND_OPERATOR); } + if (specification.getExcludeTags() != null && specification.getExcludeTags().length > 0) { + where.AND("tags", specification.getExcludeTags(), SqlWhere.NOT_OR_OPERATOR); + } + // Add modified date range filters // Some imported files can have a null modified date, so fall back to created. if (specification.getModifiedAfter() != null) { @@ -167,8 +222,10 @@ private static DataResult query(FileSpecification specification, DataConstraints where.AND(userCondition.toString(), (Object[]) userIdsBoxed); } } - return DB.selectAllFrom( - TABLE_NAME, select, joins, where, orderBy, constraints, FileItemRepository::buildRecord); + + return DB.selectFrom( + TABLE_NAME, select, joins, where, orderBy, constraints, + rs -> buildRecord(rs, includeDocumentText)); } public static FileItem findById(long id) { @@ -369,6 +426,21 @@ public static FileItem saveVersion(FileItem record) { return null; } + public static boolean updateDocumentText(FileItem record, String documentText) { + try (Connection connection = DB.getConnection()) { + SqlUtils updateValues = new SqlUtils() + .add("document_text", documentText) + .add("processed", new Timestamp(System.currentTimeMillis())); + if (DB.update(connection, TABLE_NAME, updateValues, DB.WHERE("file_id = ?", record.getId()))) { + return true; + } + } catch (SQLException se) { + LOG.error("SQLException: " + se.getMessage()); + } + LOG.error("The document text update failed!"); + return false; + } + public static boolean remove(FileItem record) { try (Connection connection = DB.getConnection(); AutoStartTransaction a = new AutoStartTransaction(connection); @@ -427,6 +499,10 @@ public static long findTotalFileSize() { } private static FileItem buildRecord(ResultSet rs) { + return buildRecord(rs, false); + } + + private static FileItem buildRecord(ResultSet rs, boolean includeDocumentText) { try { FileItem record = new FileItem(); record.setId(rs.getLong("file_id")); @@ -463,6 +539,9 @@ private static FileItem buildRecord(ResultSet rs) { record.setCategoryId(DB.getLong(rs, "category_id", -1L)); record.setWebPath(rs.getString("web_path")); record.setTags(JsonCommand.fromJsonArray(rs.getString("tags"))); + if (includeDocumentText) { + record.setDocumentText(rs.getString("document_text")); + } return record; } catch (SQLException se) { LOG.error("buildRecord", se); diff --git a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/FileSpecification.java b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/FileSpecification.java index 032c1af9..0391525f 100644 --- a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/FileSpecification.java +++ b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/FileSpecification.java @@ -37,15 +37,19 @@ public class FileSpecification extends Entity { private String barcode = null; private long createdBy = -1; private String[] fileType = null; + private String[] fileExtension = null; private Long forUserId = -1L; private String matchesName = null; private String searchName = null; private String searchContent = null; + private boolean includeDocumentText = false; private int withinLastDays = -1; private int inASubFolder = DataConstants.UNDEFINED; + private int isProcessed = DataConstants.UNDEFINED; private String versionWebPath = null; private String[] regionTags = null; private String[] filterTags = null; + private String[] excludeTags = null; private Timestamp modifiedAfter = null; private Timestamp modifiedBefore = null; private long[] modifiedByUserIds = null; @@ -129,6 +133,14 @@ public void setFileType(String value) { } } + public String[] getFileExtension() { + return fileExtension; + } + + public void setFileExtension(String[] fileExtension) { + this.fileExtension = fileExtension; + } + public Long getForUserId() { return forUserId; } @@ -161,6 +173,14 @@ public void setSearchContent(String searchContent) { this.searchContent = searchContent; } + public boolean getIncludeDocumentText() { + return includeDocumentText; + } + + public void setIncludeDocumentText(boolean includeDocumentText) { + this.includeDocumentText = includeDocumentText; + } + public int getWithinLastDays() { return withinLastDays; } @@ -181,6 +201,18 @@ public void setInASubFolder(boolean inASubFolder) { this.inASubFolder = (inASubFolder ? DataConstants.TRUE : DataConstants.FALSE); } + public int getIsProcessed() { + return isProcessed; + } + + public void setIsProcessed(int isProcessed) { + this.isProcessed = isProcessed; + } + + public void setIsProcessed(boolean isProcessed) { + this.isProcessed = (isProcessed ? DataConstants.TRUE : DataConstants.FALSE); + } + public String[] getFilterTags() { return filterTags; } @@ -189,6 +221,14 @@ public void setFilterTags(String[] filterTags) { this.filterTags = filterTags; } + public String[] getExcludeTags() { + return excludeTags; + } + + public void setExcludeTags(String[] excludeTags) { + this.excludeTags = excludeTags; + } + public String[] getRegionTags() { return regionTags; } diff --git a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageHitRepository.java b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageHitRepository.java index aef07f5f..af0e2c91 100644 --- a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageHitRepository.java +++ b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageHitRepository.java @@ -87,7 +87,8 @@ public static boolean remove(WebPageHit record) { return false; } - private static PreparedStatement createPreparedStatementForDelete(Connection connection, WebPageHit record) throws SQLException { + private static PreparedStatement createPreparedStatementForDelete(Connection connection, WebPageHit record) + throws SQLException { String SQL_QUERY = "DELETE FROM web_page_hits " + "WHERE hit_id = ?"; int i = 0; @@ -177,8 +178,10 @@ public static List findDailySessions(int daysToLimit) { } public static List findMonthlySessions(int monthsLimit) { - String SQL_QUERY = "SELECT DATE_TRUNC('month', month)::VARCHAR(10) AS date_column, SUM(unique_sessions) AS monthly_count " + - "FROM (SELECT generate_series(NOW() - INTERVAL '" + monthsLimit + " months', NOW(), INTERVAL '1 month')::date) d(month) " + + String SQL_QUERY = "SELECT DATE_TRUNC('month', month)::VARCHAR(10) AS date_column, SUM(unique_sessions) AS monthly_count " + + + "FROM (SELECT generate_series(NOW() - INTERVAL '" + monthsLimit + + " months', NOW(), INTERVAL '1 month')::date) d(month) " + "LEFT JOIN web_page_hit_snapshots ON DATE_TRUNC('month', snapshot_date) = DATE_TRUNC('month', month) " + "GROUP BY d.month " + "ORDER BY d.month"; @@ -230,7 +233,8 @@ public static List findTopPaths(int value, char intervalType, in "FROM web_page_hits " + "WHERE hit_date > NOW() - INTERVAL '" + value + " " + (intervalType == 'y' ? "years" - : (intervalType == 'm' ? "months" : (intervalType == 'w' ? "weeks" : (intervalType == 'h' ? "hours" : "days")))) + : (intervalType == 'm' ? "months" + : (intervalType == 'w' ? "weeks" : (intervalType == 'h' ? "hours" : "days")))) + "' " + "AND page_path NOT LIKE '/admin%' " + @@ -346,7 +350,8 @@ public static List findTopAssets(int days, int recordLimit, String a sqlQuery.append(" page_path LIKE '%.jpg' OR page_path LIKE '%.png' OR page_path LIKE '%.gif' OR "); sqlQuery.append(" page_path LIKE '%.zip' OR page_path LIKE '%.exe' OR page_path LIKE '%.ppt%' OR "); sqlQuery.append(" page_path LIKE '%.drawio' OR page_path LIKE '%.vsdx') "); - sqlQuery.append("AND NOT EXISTS (SELECT 1 FROM sessions WHERE session_id = web_page_hits.session_id AND is_bot = TRUE) "); + sqlQuery.append( + "AND NOT EXISTS (SELECT 1 FROM sessions WHERE session_id = web_page_hits.session_id AND is_bot = TRUE) "); // Add asset type filter if specified if (assetType != null && !assetType.trim().isEmpty()) { @@ -368,7 +373,8 @@ public static List findTopAssets(int days, int recordLimit, String a sqlQuery.append("page_path LIKE '%.drawio' OR page_path LIKE '%.vsdx'"); break; case "Image": - sqlQuery.append("page_path LIKE '%.jpg' OR page_path LIKE '%.jpeg' OR page_path LIKE '%.png' OR page_path LIKE '%.gif'"); + sqlQuery.append( + "page_path LIKE '%.jpg' OR page_path LIKE '%.jpeg' OR page_path LIKE '%.png' OR page_path LIKE '%.gif'"); break; case "Archive": sqlQuery.append("page_path LIKE '%.zip'"); @@ -578,24 +584,24 @@ public static List findDailyWebHitsForPage(String pagePath, java return records; } - public static List findAuthenticatedUserVisitsForPage(String pagePath, int days, int recordLimit) { + public static List findAuthenticatedUserVisitsForPage(String pagePath, int days) { java.time.LocalDate endDate = java.time.LocalDate.now(); java.time.LocalDate startDate = endDate.minusDays(Math.max(0, days - 1)); - return findAuthenticatedUserVisitsForPageRange(pagePath, startDate, endDate, recordLimit); + return findAuthenticatedUserVisitsForPageRange(pagePath, startDate, endDate); } public static List findAuthenticatedUserVisitsForPage(String pagePath, java.time.LocalDate fromDate, - java.time.LocalDate toDate, int recordLimit) { + java.time.LocalDate toDate) { java.time.LocalDate startDate = fromDate != null ? fromDate : toDate; java.time.LocalDate endDate = toDate != null ? toDate : fromDate; if (startDate == null || endDate == null) { return null; } - return findAuthenticatedUserVisitsForPageRange(pagePath, startDate, endDate, recordLimit); + return findAuthenticatedUserVisitsForPageRange(pagePath, startDate, endDate); } - private static List findAuthenticatedUserVisitsForPageRange(String pagePath, java.time.LocalDate startDate, - java.time.LocalDate endDate, int recordLimit) { + private static List findAuthenticatedUserVisitsForPageRange(String pagePath, + java.time.LocalDate startDate, java.time.LocalDate endDate) { String normalizedPagePath = normalizePagePath(pagePath); if (normalizedPagePath == null) { return null; @@ -622,15 +628,15 @@ private static List findAuthenticatedUserVisitsForPageRange(String p " AND page_path = ? " + " AND is_logged_in = TRUE " + " AND session_id IS NOT NULL " + - " AND NOT EXISTS (SELECT 1 FROM sessions s WHERE s.session_id = web_page_hits.session_id AND s.is_bot = TRUE) " + + " AND NOT EXISTS (SELECT 1 FROM sessions s WHERE s.session_id = web_page_hits.session_id AND s.is_bot = TRUE) " + + " GROUP BY page_path, session_id " + " ) page_visits " + " JOIN user_logins ul ON ul.session_id = page_visits.session_id " + " JOIN users u ON u.user_id = ul.user_id " + " GROUP BY page_visits.page_path, ul.user_id, u.first_name, u.last_name, u.email, u.username " + ") member_visits " + - "ORDER BY last_hit DESC " + - "LIMIT " + recordLimit; + "ORDER BY last_hit DESC"; List records = null; try (Connection connection = DB.getConnection(); PreparedStatement pst = connection.prepareStatement(SQL_QUERY)) { diff --git a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageRepository.java b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageRepository.java index c9d227a5..d777d41e 100644 --- a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageRepository.java +++ b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageRepository.java @@ -90,6 +90,10 @@ private static DataResult query(WebPageSpecification specification, DataConstrai where.AND("web_pages.tags", specification.getFilterTags(), SqlWhere.AND_OPERATOR); } + if (specification.getExcludeTags() != null && specification.getExcludeTags().length > 0) { + where.AND("web_pages.tags", specification.getExcludeTags(), SqlWhere.NOT_OR_OPERATOR); + } + // Add modified date range filters if (specification.getModifiedAfter() != null) { where.AND("web_pages.modified >= ?", specification.getModifiedAfter()); @@ -331,6 +335,45 @@ public static void markAsModifiedAndFindable(WebPage record, long userId) { } } + public static boolean archivePage(WebPage record, long userId) { + if (record == null || record.getId() == -1) { + return false; + } + // Explicitly set enabled to false (archived) + SqlUtils updateValues = new SqlUtils() + .add("enabled", false) + .add("modified", new Timestamp(System.currentTimeMillis())) + .add("modified_by", userId); + boolean updated = DB.update(TABLE_NAME, updateValues, DB.WHERE("web_page_id = ?", record.getId())); + if (updated) { + record.setEnabled(false); + record.setModifiedBy(userId); + // Note: We do NOT remove the page from cache when archiving + // The page layout/content hasn't changed, only the enabled status + // This allows admins/content-managers to still view the archived page + } + return updated; + } + + public static boolean unarchivePage(WebPage record, long userId) { + if (record == null || record.getId() == -1) { + return false; + } + // Explicitly set enabled to true (unarchived) + SqlUtils updateValues = new SqlUtils() + .add("enabled", true) + .add("modified", new Timestamp(System.currentTimeMillis())) + .add("modified_by", userId); + boolean updated = DB.update(TABLE_NAME, updateValues, DB.WHERE("web_page_id = ?", record.getId())); + if (updated) { + record.setEnabled(true); + record.setModifiedBy(userId); + // Note: We do NOT remove the page from cache when unarchiving + // The page layout/content hasn't changed, only the enabled status + } + return updated; + } + public static void removeDraft(WebPage record) { if (record == null || record.getId() == -1) { return; diff --git a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageSpecification.java b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageSpecification.java index ba9431be..9d6e2226 100644 --- a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageSpecification.java +++ b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageSpecification.java @@ -38,6 +38,7 @@ public class WebPageSpecification extends Entity { private int hasRedirect = DataConstants.UNDEFINED; private String[] regionTags = null; private String[] filterTags = null; + private String[] excludeTags = null; private Timestamp modifiedAfter = null; private Timestamp modifiedBefore = null; private long[] modifiedByUserIds = null; @@ -122,6 +123,14 @@ public void setFilterTags(String[] filterTags) { this.filterTags = filterTags; } + public String[] getExcludeTags() { + return excludeTags; + } + + public void setExcludeTags(String[] excludeTags) { + this.excludeTags = excludeTags; + } + public String[] getRegionTags() { return regionTags; } diff --git a/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemFileItemRepository.java b/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemFileItemRepository.java index a273cdea..53a2ae92 100644 --- a/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemFileItemRepository.java +++ b/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemFileItemRepository.java @@ -61,12 +61,52 @@ public class ItemFileItemRepository { private static String[] PRIMARY_KEY = new String[] { "file_id" }; private static DataResult query(ItemFileSpecification specification, DataConstraints constraints) { - SqlUtils select = new SqlUtils(); + + SqlUtils select = DB.SELECT( + TABLE_NAME + ".file_id", + TABLE_NAME + ".item_id", + TABLE_NAME + ".folder_id", + TABLE_NAME + ".filename", + TABLE_NAME + ".title", + TABLE_NAME + ".barcode", + TABLE_NAME + ".version", + TABLE_NAME + ".extension", + TABLE_NAME + ".path", + TABLE_NAME + ".file_length", + TABLE_NAME + ".file_type", + TABLE_NAME + ".mime_type", + TABLE_NAME + ".file_hash", + TABLE_NAME + ".width", + TABLE_NAME + ".height", + TABLE_NAME + ".summary", + TABLE_NAME + ".created_by", + TABLE_NAME + ".created", + TABLE_NAME + ".modified_by", + TABLE_NAME + ".modified", + TABLE_NAME + ".processed", + TABLE_NAME + ".expiration_date", + TABLE_NAME + ".privacy_type", + TABLE_NAME + ".default_token", + TABLE_NAME + ".version_count", + TABLE_NAME + ".download_count", + TABLE_NAME + ".sub_folder_id", + TABLE_NAME + ".category_id", + TABLE_NAME + ".web_path", + TABLE_NAME + ".tags"); + SqlJoins joins = new SqlJoins(); SqlWhere where = DB.WHERE(); SqlUtils orderBy = new SqlUtils(); + + final boolean includeDocumentText = specification != null && specification.getIncludeDocumentText(); + if (specification != null) { + // Only include this potentially-large field when requested + if (includeDocumentText) { + select.add("item_files.document_text"); + } + joins.add("LEFT JOIN item_folders ON (item_files.folder_id = item_folders.folder_id)"); if (specification.getCollectionId() > -1) { joins.add("LEFT JOIN items ON (item_files.item_id = items.item_id)"); @@ -86,6 +126,10 @@ private static DataResult query(ItemFileSpecification specification, DataConstra where.AND("LOWER(item_files.file_type) = ANY(?)", Arrays.stream(specification.getFileType()).map(String::toLowerCase).toArray(String[]::new), Types.ARRAY); } + if (specification.getFileExtension() != null) { + where.AND("LOWER(item_files.extension) = ANY(?)", + Arrays.stream(specification.getFileExtension()).map(String::toLowerCase).toArray(String[]::new), Types.ARRAY); + } if (specification.getMatchesName() != null) { String likeValue = specification.getMatchesName().trim() .replace("!", "!!") @@ -104,6 +148,13 @@ private static DataResult query(ItemFileSpecification specification, DataConstra where.AND("sub_folder_id IS NULL"); } } + if (specification.getIsProcessed() != DataConstants.UNDEFINED) { + if (specification.getIsProcessed() == DataConstants.TRUE) { + where.AND("processed IS NOT NULL"); + } else { + where.AND("processed IS NULL"); + } + } // For user id // User must be in a user group with folder access @@ -134,8 +185,11 @@ private static DataResult query(ItemFileSpecification specification, DataConstra orderBy.add("rank DESC, file_id"); } } - return DB.selectAllFrom( - TABLE_NAME, select, joins, where, orderBy, constraints, ItemFileItemRepository::buildRecord); + + return DB.selectFrom( + TABLE_NAME, select, joins, where, orderBy, constraints, + rs -> buildRecord(rs, includeDocumentText)); + } public static ItemFileItem findById(long id) { @@ -205,6 +259,12 @@ public static List findAll(ItemFileSpecification specification, Da return (List) result.getRecords(); } + public static long fileCount(long itemId) { + SqlWhere where = DB.WHERE(); + where.AND("item_id = ?", itemId); + return DB.selectCountFrom(TABLE_NAME, where); + } + public static ItemFileItem save(ItemFileItem record) { if (record.getId() > -1) { return update(record); @@ -365,6 +425,21 @@ public static ItemFileItem saveVersion(ItemFileItem record) { return null; } + public static boolean updateDocumentText(ItemFileItem record, String documentText) { + try (Connection connection = DB.getConnection()) { + SqlUtils updateValues = new SqlUtils() + .add("document_text", documentText) + .add("processed", new Timestamp(System.currentTimeMillis())); + if (DB.update(connection, TABLE_NAME, updateValues, DB.WHERE("file_id = ?", record.getId()))) { + return true; + } + } catch (SQLException se) { + LOG.error("SQLException: " + se.getMessage()); + } + LOG.error("The document text update failed!"); + return false; + } + public static boolean remove(ItemFileItem record) { try (Connection connection = DB.getConnection(); AutoStartTransaction a = new AutoStartTransaction(connection); @@ -426,6 +501,10 @@ public static long findTotalFileSize() { } private static ItemFileItem buildRecord(ResultSet rs) { + return buildRecord(rs, false); + } + + private static ItemFileItem buildRecord(ResultSet rs, boolean includeDocumentText) { try { ItemFileItem record = new ItemFileItem(); record.setId(rs.getLong("file_id")); @@ -463,6 +542,9 @@ private static ItemFileItem buildRecord(ResultSet rs) { record.setCategoryId(DB.getLong(rs, "category_id", -1L)); record.setWebPath(rs.getString("web_path")); record.setTags(JsonCommand.fromJsonArray(rs.getString("tags"))); + if (includeDocumentText) { + record.setDocumentText(rs.getString("document_text")); + } return record; } catch (SQLException se) { LOG.error("buildRecord", se); diff --git a/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemFileSpecification.java b/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemFileSpecification.java index e4f4b24b..09406cf7 100644 --- a/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemFileSpecification.java +++ b/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemFileSpecification.java @@ -37,12 +37,15 @@ public class ItemFileSpecification extends Entity { private String barcode = null; private long createdBy = -1; private String[] fileType = null; + private String[] fileExtension = null; private Long forUserId = -1L; private String matchesName = null; private String searchName = null; private String searchContent = null; + private boolean includeDocumentText = false; private int withinLastDays = -1; private int inASubFolder = DataConstants.UNDEFINED; + private int isProcessed = DataConstants.UNDEFINED; public ItemFileSpecification() { } @@ -131,6 +134,14 @@ public void setFileType(String value) { } } + public String[] getFileExtension() { + return fileExtension; + } + + public void setFileExtension(String[] fileExtension) { + this.fileExtension = fileExtension; + } + public Long getForUserId() { return forUserId; } @@ -163,6 +174,14 @@ public void setSearchContent(String searchContent) { this.searchContent = searchContent; } + public boolean getIncludeDocumentText() { + return includeDocumentText; + } + + public void setIncludeDocumentText(boolean includeDocumentText) { + this.includeDocumentText = includeDocumentText; + } + public int getWithinLastDays() { return withinLastDays; } @@ -182,4 +201,16 @@ public void setInASubFolder(int inASubFolder) { public void setInASubFolder(boolean inASubFolder) { this.inASubFolder = (inASubFolder ? DataConstants.TRUE : DataConstants.FALSE); } + + public int getIsProcessed() { + return isProcessed; + } + + public void setIsProcessed(int isProcessed) { + this.isProcessed = isProcessed; + } + + public void setIsProcessed(boolean isProcessed) { + this.isProcessed = (isProcessed ? DataConstants.TRUE : DataConstants.FALSE); + } } diff --git a/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemRepository.java b/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemRepository.java index 9eb1fe46..b76750f4 100644 --- a/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemRepository.java +++ b/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemRepository.java @@ -580,6 +580,10 @@ private static DataResult query(ItemSpecification specification, DataConstraints where.AND("tags", specification.getFilterTags(), SqlWhere.AND_OPERATOR); } + if (specification.getExcludeTags() != null && specification.getExcludeTags().length > 0) { + where.AND("tags", specification.getExcludeTags(), SqlWhere.NOT_OR_OPERATOR); + } + if (specification.getFieldInFilters() != null) { for (Map.Entry> entry : specification.getFieldInFilters().entrySet()) { String fieldName = entry.getKey(); diff --git a/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemSpecification.java b/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemSpecification.java index 00620621..0efa56d7 100644 --- a/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemSpecification.java +++ b/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemSpecification.java @@ -56,6 +56,7 @@ public class ItemSpecification { private long datasetId = -1L; private Timestamp datasetSyncTimestampThreshold = null; private String[] filterTags = null; + private String[] excludeTags = null; private String[] regionTags = null; private List customFieldFilters = null; private Map> fieldInFilters = null; @@ -266,6 +267,14 @@ public void setFilterTags(String[] filterTags) { this.filterTags = filterTags; } + public String[] getExcludeTags() { + return excludeTags; + } + + public void setExcludeTags(String[] excludeTags) { + this.excludeTags = excludeTags; + } + public String[] getRegionTags() { return regionTags; } diff --git a/src/main/java/com/simisinc/platform/infrastructure/scheduler/SchedulerManager.java b/src/main/java/com/simisinc/platform/infrastructure/scheduler/SchedulerManager.java index 9425731b..9e741c66 100644 --- a/src/main/java/com/simisinc/platform/infrastructure/scheduler/SchedulerManager.java +++ b/src/main/java/com/simisinc/platform/infrastructure/scheduler/SchedulerManager.java @@ -42,7 +42,6 @@ import com.simisinc.platform.infrastructure.scheduler.analytics.RecordPerformanceMetricJob; import com.simisinc.platform.infrastructure.scheduler.cms.LoadSystemFilesJob; import com.simisinc.platform.infrastructure.scheduler.cms.RecordWebPageHitJob; -import com.simisinc.platform.infrastructure.scheduler.cms.RefreshAllWebPageTextIndexesJob; import com.simisinc.platform.infrastructure.scheduler.cms.WebPageHitSnapshotJob; import com.simisinc.platform.infrastructure.scheduler.cms.WebPageHitsCleanupJob; import com.simisinc.platform.infrastructure.scheduler.ecommerce.OrderManagementProcessNewOrders; @@ -51,6 +50,8 @@ import com.simisinc.platform.infrastructure.scheduler.login.UserTokensCleanupJob; import com.simisinc.platform.infrastructure.scheduler.medicine.ProcessMedicineSchedulesJob; import com.simisinc.platform.infrastructure.scheduler.socialmedia.InstagramMediaSnapshotJob; +import com.zeroio.platform.infrastructure.scheduler.cms.RefreshAllDocumentTextIndexesJob; +import com.zeroio.platform.infrastructure.scheduler.cms.RefreshAllWebPageTextIndexesJob; /** * Initializes background jobs to be run on a schedule @@ -80,6 +81,7 @@ public class SchedulerManager { public static final String ORDER_MANAGEMENT_PROCESS_SHIPPING_UPDATES_JOB = "OrderManagementProcessShippingUpdates"; public static final String PROCESS_MEDICINE_SCHEDULES_JOB = "ProcessMedicineSchedules"; public static final String REFRESH_ALL_WEB_PAGE_TEXT_INDEXES_JOB = "RefreshAllWebPageTextIndexes"; + public static final String REFRESH_ALL_DOCUMENT_TEXT_INDEXES_JOB = "RefreshAllDocumentTextIndexes"; // Jobs which can be run by multiple clients public static final String DATASETS_DOWNLOAD_AND_SYNC_JOB = "DatasetsDownloadAndSync"; @@ -164,6 +166,8 @@ public static void startup(ServletContext servletContext1) { BackgroundJob.scheduleRecurrently(OAUTH_STATE_CLEANUP_JOB, Cron.every5minutes(), OAuthStateCleanupJob::execute); BackgroundJob.scheduleRecurrently(REFRESH_ALL_WEB_PAGE_TEXT_INDEXES_JOB, Cron.yearly(month, day), RefreshAllWebPageTextIndexesJob::execute); + BackgroundJob.scheduleRecurrently(REFRESH_ALL_DOCUMENT_TEXT_INDEXES_JOB, Cron.yearly(month, day), + RefreshAllDocumentTextIndexesJob::execute); BackgroundJob.scheduleRecurrently(INSTAGRAM_MEDIA_SNAPSHOT_JOB, Cron.hourly(), InstagramMediaSnapshotJob::execute); BackgroundJob.scheduleRecurrently(DATASETS_DOWNLOAD_AND_SYNC_JOB, Cron.minutely(), DatasetsDownloadAndSyncJob::execute); BackgroundJob.scheduleRecurrently(ORDER_MANAGEMENT_PROCESS_NEW_ORDERS_JOB, Cron.minutely(), diff --git a/src/main/java/com/simisinc/platform/presentation/controller/PageServlet.java b/src/main/java/com/simisinc/platform/presentation/controller/PageServlet.java index e393af73..335e33c6 100644 --- a/src/main/java/com/simisinc/platform/presentation/controller/PageServlet.java +++ b/src/main/java/com/simisinc/platform/presentation/controller/PageServlet.java @@ -364,16 +364,9 @@ public void service(HttpServletRequest request, HttpServletResponse response) { // Always access the WebPage record so it can be used downstream WebPage webPage = LoadWebPageCommand.loadByLink(pageRequest.getPagePath()); + boolean showArchivedMessage = false; if (webPage != null) { - // Determine if this is a draft page - if (webPage.getDraft()) { - if (!userSession.hasRole("admin") && !userSession.hasRole("content-manager")) { - LOG.error("DRAFT FOUND, no access: " + pageRequest.getPagePath() + " " + pageRequest.getRemoteAddr()); - controllerSession.clearAllWidgetData(); - response.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - } + // Determine if this is a redirect String redirectLocation = webPage.getRedirectUrl(); if (StringUtils.isNotBlank(redirectLocation)) { @@ -387,12 +380,44 @@ public void service(HttpServletRequest request, HttpServletResponse response) { response.setStatus(SC_MOVED_PERMANENTLY); return; } - // @todo Not needed once pageRenderInfo has calculated title, keywords, description + + // Determine if this is a draft page (and unavailable to general users) + if (webPage.getDraft()) { + if (!userSession.hasRole("admin") && !userSession.hasRole("content-manager")) { + LOG.debug("DRAFT FOUND, no access: " + pageRequest.getPagePath()); + controllerSession.clearAllWidgetData(); + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + } + + // Determine if this page is archived + if (!webPage.isEnabled()) { + // Found an archived page, so for logged out users show a 404 + if (!userSession.isLoggedIn()) { + LOG.debug("ARCHIVED FOUND, no access: " + pageRequest.getPagePath()); + controllerSession.clearAllWidgetData(); + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + // Found an archived page, so for normal users show a message instead of the page + if (!userSession.hasRole("admin") && !userSession.hasRole("content-manager")) { + showArchivedMessage = true; + } + } + + // @todo Probably not needed once pageRenderInfo has calculated title, keywords, description request.setAttribute(MASTER_WEB_PAGE, webPage); } // Determine the Page XML Layout for this request - Page pageRef = WebPageXmlLayoutCommand.retrievePageForRequest(webPage, pageRequest.getPagePath()); + Page pageRef; + if (showArchivedMessage) { + // Load archived page template for normal users + pageRef = WebPageXmlLayoutCommand.retrievePage("_page_archived_"); + } else { + pageRef = WebPageXmlLayoutCommand.retrievePageForRequest(webPage, pageRequest.getPagePath()); + } // Load site properties early (needed for online/draft checks and hit tracking) Map sitePropertyMap = LoadSitePropertyCommand.loadAsMap("site"); diff --git a/src/main/java/com/simisinc/platform/presentation/widgets/admin/cms/WebPageFormWidget.java b/src/main/java/com/simisinc/platform/presentation/widgets/admin/cms/WebPageFormWidget.java index 92ea628d..913f3b12 100644 --- a/src/main/java/com/simisinc/platform/presentation/widgets/admin/cms/WebPageFormWidget.java +++ b/src/main/java/com/simisinc/platform/presentation/widgets/admin/cms/WebPageFormWidget.java @@ -181,12 +181,52 @@ public WidgetContext action(WidgetContext context) { context.setRedirect(webPage.getLink()); String action = context.getParameter("action"); if ("deletePage".equals(action)) { + // Only admins can delete pages + if (!context.hasRole("admin")) { + context.setErrorMessage("You do not have permission to delete pages"); + return context; + } try { WebPageRepository.remove(webPage); context.setSuccessMessage("Page was deleted"); + context.setRedirect(webPage.getLink()); } catch (Exception e) { context.setErrorMessage("The page could not be deleted: " + e.getMessage()); } + } else if ("archivePage".equals(action)) { + // Content managers and admins can archive pages + if (!context.hasRole("admin") && !context.hasRole("content-manager")) { + context.setErrorMessage("You do not have permission to archive pages"); + return context; + } + boolean success = WebPageRepository.archivePage(webPage, context.getUserId()); + if (success) { + context.setWarningMessage("Page was archived"); + // Stay on the form page + context.setRedirect(context.getUri() + "?webPageId=" + webPageId); + } else { + context.setErrorMessage("The page could not be archived"); + // Redirect back to form on error + context.setRedirect(context.getUri() + "?webPageId=" + webPageId); + } + } else if ("unarchivePage".equals(action)) { + // Content managers and admins can unarchive pages + if (!context.hasRole("admin") && !context.hasRole("content-manager")) { + context.setErrorMessage("You do not have permission to unarchive pages"); + return context; + } + boolean success = WebPageRepository.unarchivePage(webPage, context.getUserId()); + if (success) { + context.setSuccessMessage("Page was unarchived"); + // Stay on the form page + context.setRedirect(context.getUri() + "?webPageId=" + webPageId); + } else { + context.setErrorMessage("The page could not be unarchived"); + // Redirect back to form on error + context.setRedirect(context.getUri() + "?webPageId=" + webPageId); + } + } else { + context.setRedirect(webPage.getLink()); } return context; } diff --git a/src/main/java/com/simisinc/platform/presentation/widgets/items/ItemFieldsWidget.java b/src/main/java/com/simisinc/platform/presentation/widgets/items/ItemFieldsWidget.java index ed812f6c..d85522e9 100644 --- a/src/main/java/com/simisinc/platform/presentation/widgets/items/ItemFieldsWidget.java +++ b/src/main/java/com/simisinc/platform/presentation/widgets/items/ItemFieldsWidget.java @@ -28,7 +28,7 @@ import com.simisinc.platform.presentation.widgets.cms.PreferenceEntriesList; /** - * Description + * Displays the fields for an item * * @author matt rajkowski * @created 4/20/18 2:23 PM @@ -47,15 +47,11 @@ public WidgetContext execute(WidgetContext context) { return null; } - PreferenceEntriesList entriesList = context.getPreferenceAsDataList("fields"); - if (entriesList.isEmpty()) { - return context; - } - // Use the fields preference to determine the object properties to be shown + PreferenceEntriesList entriesList = context.getPreferenceAsDataList("fields"); List fieldList = ItemCustomFieldCommand.renderDisplayValues(entriesList, item); - // Show the custom fields if requested + // Append the custom fields if requested boolean showAllCustomFields = "true".equals(context.getPreferences().get("showAllCustomFields")); if (showAllCustomFields && item.getCustomFieldList() != null && !item.getCustomFieldList().isEmpty()) { fieldList.addAll(item.getCustomFieldList().values()); diff --git a/src/main/java/com/simisinc/platform/presentation/widgets/items/ItemsListWidget.java b/src/main/java/com/simisinc/platform/presentation/widgets/items/ItemsListWidget.java index 7f3f774e..32d21e5f 100644 --- a/src/main/java/com/simisinc/platform/presentation/widgets/items/ItemsListWidget.java +++ b/src/main/java/com/simisinc/platform/presentation/widgets/items/ItemsListWidget.java @@ -209,15 +209,23 @@ public WidgetContext execute(WidgetContext context) { // Handle the case where the filter value is a reference to another field (e.g., ${item.custom.Code}) LOG.debug("Processing custom field filter: " + filterFieldName + " = " + filterFieldValue); if (filterFieldValue.startsWith("${item.custom.") && filterFieldValue.endsWith("}")) { - String referencedFieldName = filterFieldValue.substring("${item.custom.".length(), filterFieldValue.length() - 1); + String referencedFieldName = filterFieldValue.substring("${item.custom.".length(), + filterFieldValue.length() - 1); + // Load the authorized item that this widget is embedded on Item currentItem = LoadItemCommand.loadItemByUniqueId(context.getCoreData().get("itemUniqueId")); if (currentItem == null) { // If the current item is not found, we cannot apply the filter, so we return null to indicate that the widget should not render any items return null; } + // Retrieve the value of the referenced custom field from the current item in the request context - String referencedFieldValue = currentItem.getCustomField(referencedFieldName).getValue(); + CustomField referencedField = currentItem.getCustomField(referencedFieldName); + if (referencedField == null) { + // If the referenced field is not found, we cannot apply the filter, so we return null to indicate that the widget should not render any items + return null; + } + String referencedFieldValue = referencedField.getValue(); if (StringUtils.isBlank(referencedFieldValue)) { // If the referenced field value is blank, we cannot apply the filter, so we return null to indicate that the widget should not render any items return null; diff --git a/src/main/java/com/simisinc/platform/rest/controller/RestRequestFilter.java b/src/main/java/com/simisinc/platform/rest/controller/RestRequestFilter.java index c22193c0..a35da618 100644 --- a/src/main/java/com/simisinc/platform/rest/controller/RestRequestFilter.java +++ b/src/main/java/com/simisinc/platform/rest/controller/RestRequestFilter.java @@ -251,6 +251,12 @@ public void doFilter(ServletRequest request, ServletResponse servletResponse, Fi return; } + // At this point a guest may only use GET or HEAD + if (!"get".equals(requestMethod) && !"head".equals(requestMethod)) { + doUnauthorized(servletResponse); + return; + } + // Limit the number of hits per minute based on the successful use of the api key if (!isLocal && !RateLimitCommand.isAppAllowedRightNow(thisApp, useLenientRateLimit)) { do429(servletResponse); diff --git a/src/main/java/com/simisinc/platform/rest/controller/RestServlet.java b/src/main/java/com/simisinc/platform/rest/controller/RestServlet.java index 2833f237..24d01399 100644 --- a/src/main/java/com/simisinc/platform/rest/controller/RestServlet.java +++ b/src/main/java/com/simisinc/platform/rest/controller/RestServlet.java @@ -23,6 +23,8 @@ import java.io.PrintWriter; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -63,6 +65,20 @@ public class RestServlet extends HttpServlet { // Services Cache private Map serviceInstances = new ConcurrentHashMap(); + private static class RouteMatch { + private final Object classRef; + private final String endpoint; + private final String pathParam; + private final String pathParam2; + + private RouteMatch(Object classRef, String endpoint, String pathParam, String pathParam2) { + this.classRef = classRef; + this.endpoint = endpoint; + this.pathParam = pathParam; + this.pathParam2 = pathParam2; + } + } + @Override public void init(ServletConfig config) throws ServletException { @@ -111,6 +127,7 @@ public void service(HttpServletRequest request, HttpServletResponse response) { long startRequestTime = System.currentTimeMillis(); + LOG.debug("-----------------------------------------------------------------------"); LOG.debug("Service processor..."); // Determine request values @@ -143,21 +160,11 @@ public void service(HttpServletRequest request, HttpServletResponse response) { // Determine the resource try { // Get the cached class reference for processing - Object classRef = serviceInstances.get(endpoint); - String pathEndpoint = null; - if (classRef == null) { - LOG.debug("Could not find endpoint: " + endpoint); - if (endpoint.contains("/")) { - // Try as a pathParam - pathEndpoint = endpoint.substring(0, endpoint.indexOf("/")); - pathParam = endpoint.substring(endpoint.indexOf("/") + 1); - if (pathParam.contains("/")) { - pathParam2 = pathParam.substring(pathParam.indexOf("/") + 1); - pathParam = pathParam.substring(0, pathParam.indexOf("/")); - } - classRef = serviceInstances.get(pathEndpoint); - } - } + RouteMatch routeMatch = findRoute(endpoint); + Object classRef = (routeMatch != null ? routeMatch.classRef : null); + String pathEndpoint = (routeMatch != null ? routeMatch.endpoint : null); + pathParam = (routeMatch != null ? routeMatch.pathParam : null); + pathParam2 = (routeMatch != null ? routeMatch.pathParam2 : null); if (classRef == null) { LOG.error("Class not found for service: " + endpoint); sendError(response, SC_NOT_FOUND, "Endpoint not found"); @@ -187,13 +194,8 @@ public void service(HttpServletRequest request, HttpServletResponse response) { // Execute the service ServiceResponse result = null; try { - LOG.debug("-----------------------------------------------------------------------"); - if (pathEndpoint != null) { - LOG.debug("Executing service: " + pathEndpoint); - } else { - LOG.debug("Executing service: " + endpoint); - } RestService service = (RestService) classRef; + LOG.debug("Executing service: " + service.getClass().getName()); if ("post".equals(requestMethod)) { result = service.post(serviceContext); } else if ("put".equals(requestMethod)) { @@ -260,4 +262,120 @@ public static void sendError(HttpServletResponse response, int code, String erro out.flush(); } } + + private RouteMatch findRoute(String requestEndpoint) { + if (requestEndpoint != null && requestEndpoint.contains("?")) { + requestEndpoint = requestEndpoint.substring(0, requestEndpoint.indexOf("?")); + } + Object classRef = serviceInstances.get(requestEndpoint); + if (classRef != null) { + return new RouteMatch(classRef, requestEndpoint, null, null); + } + + if (LOG.isDebugEnabled()) { + LOG.debug("Could not find exact endpoint: " + requestEndpoint + ", trying path pattern match"); + } + + List requestParts = splitPath(requestEndpoint); + RouteMatch bestExactPatternMatch = null; + RouteMatch bestPrefixPatternMatch = null; + for (Map.Entry entry : serviceInstances.entrySet()) { + String endpointPattern = entry.getKey(); + if (!endpointPattern.contains("{")) { + continue; + } + + List patternParts = splitPath(endpointPattern); + if (patternParts.size() > requestParts.size()) { + continue; + } + + RouteMatch routeMatch = matchPattern(entry.getValue(), endpointPattern, patternParts, requestParts); + if (routeMatch == null) { + continue; + } + + if (patternParts.size() == requestParts.size()) { + if (bestExactPatternMatch == null || splitPath(bestExactPatternMatch.endpoint).size() < patternParts.size()) { + bestExactPatternMatch = routeMatch; + } + } else if (bestPrefixPatternMatch == null + || splitPath(bestPrefixPatternMatch.endpoint).size() < patternParts.size()) { + bestPrefixPatternMatch = routeMatch; + } + } + + if (bestExactPatternMatch != null) { + return bestExactPatternMatch; + } + if (bestPrefixPatternMatch != null) { + return bestPrefixPatternMatch; + } + + String bestLiteralPrefix = null; + for (String endpoint : serviceInstances.keySet()) { + if (endpoint.contains("{")) { + continue; + } + if (requestEndpoint.equals(endpoint) || requestEndpoint.startsWith(endpoint + "/")) { + if (bestLiteralPrefix == null || splitPath(bestLiteralPrefix).size() < splitPath(endpoint).size()) { + bestLiteralPrefix = endpoint; + } + } + } + if (bestLiteralPrefix != null) { + return new RouteMatch(serviceInstances.get(bestLiteralPrefix), bestLiteralPrefix, null, null); + } + return null; + } + + private RouteMatch matchPattern(Object classRef, String endpointPattern, List patternParts, + List requestParts) { + String matchedPathParam = null; + String matchedPathParam2 = null; + int matchedParamCount = 0; + + for (int i = 0; i < patternParts.size(); i++) { + String patternPart = patternParts.get(i); + String requestPart = requestParts.get(i); + + if (patternPart.startsWith("{") && patternPart.endsWith("}")) { + matchedParamCount++; + if (matchedParamCount == 1) { + matchedPathParam = requestPart; + } else if (matchedParamCount == 2) { + matchedPathParam2 = requestPart; + } + continue; + } + + if (!patternPart.equals(requestPart)) { + return null; + } + } + + if (requestParts.size() > patternParts.size()) { + if (matchedPathParam2 == null && patternParts.size() == 1 && !requestParts.isEmpty()) { + matchedPathParam = requestParts.get(0); + matchedPathParam2 = requestParts.get(1); + } else if (matchedPathParam2 == null && patternParts.size() < requestParts.size()) { + matchedPathParam2 = requestParts.get(patternParts.size()); + } + } + + return new RouteMatch(classRef, endpointPattern, matchedPathParam, matchedPathParam2); + } + + private List splitPath(String value) { + List parts = new ArrayList<>(); + if (StringUtils.isBlank(value)) { + return parts; + } + for (String part : value.split("/")) { + if (StringUtils.isNotBlank(part)) { + parts.add(part); + } + } + return parts; + } } diff --git a/src/main/java/com/simisinc/platform/rest/controller/ServiceContext.java b/src/main/java/com/simisinc/platform/rest/controller/ServiceContext.java index c0ff6be8..b4d37623 100644 --- a/src/main/java/com/simisinc/platform/rest/controller/ServiceContext.java +++ b/src/main/java/com/simisinc/platform/rest/controller/ServiceContext.java @@ -1,4 +1,5 @@ /* + * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski) * Copyright 2022 SimIS Inc. (https://www.simiscms.com) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,14 +17,16 @@ package com.simisinc.platform.rest.controller; -import com.simisinc.platform.domain.model.App; -import com.simisinc.platform.domain.model.User; -import org.apache.commons.lang3.StringUtils; +import java.io.Serializable; +import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.io.Serializable; -import java.util.Map; + +import org.apache.commons.lang3.StringUtils; + +import com.simisinc.platform.domain.model.App; +import com.simisinc.platform.domain.model.User; /** * Common object for context objects available during a service request @@ -155,6 +158,10 @@ public int getParameterAsInt(String name, int defaultValue) { return defaultValue; } + public String[] getParameterAsArray(String name) { + return parameterMap.get(name); + } + public String getPathParam2() { return pathParam2; } diff --git a/src/main/java/com/simisinc/platform/rest/controller/XMLServiceLoader.java b/src/main/java/com/simisinc/platform/rest/controller/XMLServiceLoader.java index a719e59c..81983cd4 100644 --- a/src/main/java/com/simisinc/platform/rest/controller/XMLServiceLoader.java +++ b/src/main/java/com/simisinc/platform/rest/controller/XMLServiceLoader.java @@ -1,4 +1,5 @@ /* + * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski) * Copyright 2022 SimIS Inc. (https://www.simiscms.com) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -87,12 +88,12 @@ private void addServices(Document document) { NodeList objectTags = document.getElementsByTagName("service"); for (int i = 0; i < objectTags.getLength(); i++) { Element objectTag = (Element) objectTags.item(i); - String endpoint = objectTag.getAttribute("endpoint"); String endpointValue = objectTag.getAttribute("endpoint"); + String endpoint = endpointValue; String serviceClass = objectTag.getAttribute("class"); String method = objectTag.getAttribute("method"); - if (endpoint.contains("/{")) { - endpoint = endpoint.substring(0, endpoint.indexOf("/{")); + if (endpoint.contains("?")) { + endpoint = endpoint.substring(0, endpoint.indexOf("?")); } Map service = new HashMap<>(); service.put("endpoint", endpoint); diff --git a/src/main/java/com/simisinc/platform/rest/services/items/ItemFilesService.java b/src/main/java/com/simisinc/platform/rest/services/items/ItemFilesService.java new file mode 100644 index 00000000..dba05746 --- /dev/null +++ b/src/main/java/com/simisinc/platform/rest/services/items/ItemFilesService.java @@ -0,0 +1,124 @@ +/* + * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.simisinc.platform.rest.services.items; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.simisinc.platform.application.items.LoadCollectionCommand; +import com.simisinc.platform.application.items.LoadItemCommand; +import com.simisinc.platform.domain.model.items.Collection; +import com.simisinc.platform.domain.model.items.Item; +import com.simisinc.platform.domain.model.items.ItemFileItem; +import com.simisinc.platform.infrastructure.database.DataConstraints; +import com.simisinc.platform.infrastructure.persistence.items.ItemFileItemRepository; +import com.simisinc.platform.infrastructure.persistence.items.ItemFileSpecification; +import com.simisinc.platform.rest.controller.GenericRestService; +import com.simisinc.platform.rest.controller.ServiceContext; +import com.simisinc.platform.rest.controller.ServiceResponse; +import com.simisinc.platform.rest.controller.ServiceResponseCommand; + +/** + * Returns a list of item files + * + * @author matt rajkowski + * @created 7/28/26 10:15 AM + */ +public class ItemFilesService extends GenericRestService { + + private static Log LOG = LogFactory.getLog(ItemFilesService.class); + + private static final String EXPAND_DOCUMENT_TEXT = "documentText"; + + // GET /item/{itemUniqueId}/files + @Override + public ServiceResponse get(ServiceContext context) { + + // Determine the item + String itemUniqueId = context.getPathParam(); + Item item = LoadItemCommand.loadItemByUniqueIdForAuthorizedUser(itemUniqueId, context.getUserId()); + if (item == null) { + ServiceResponse response = new ServiceResponse(404); + response.getError().put("title", "Item was not found"); + return response; + } + + // Validate access to the collection + Collection collection = LoadCollectionCommand.loadCollectionByIdForAuthorizedUser(item.getCollectionId(), + context.getUserId()); + if (collection == null) { + LOG.warn("User does not have access to this collection"); + ServiceResponse response = new ServiceResponse(400); + response.getError().put("title", "Item was not found"); + return response; + } + + // Return the ItemDetailResponse if requested + boolean expandDocumentText = false; + String[] expand = context.getParameterAsArray("expand"); + if (expand != null) { + for (String expandValue : expand) { + if (EXPAND_DOCUMENT_TEXT.equals(expandValue)) { + expandDocumentText = true; + } + } + } + + // Retrieve all files for the item within user visibility + ItemFileSpecification specification = new ItemFileSpecification(); + specification.setItemId(item.getId()); + specification.setForUserId(context.getUserId()); + specification.setIncludeDocumentText(expandDocumentText); + + // Enable paging + int pageNumber = context.getParameterAsInt("page", 1); + int pageSize = context.getParameterAsInt("size", 50); + + DataConstraints constraints = new DataConstraints(pageNumber, pageSize); + constraints.setColumnToSortBy("created", "desc"); + + List itemFileList = ItemFileItemRepository.findAll(specification, constraints); + List> responseList = new ArrayList<>(); + for (ItemFileItem itemFile : itemFileList) { + // Include filename, fileType, mimeType, fileLength, fileHash, webPath, created, modified + Map entry = new LinkedHashMap<>(); + entry.put("uniqueId", itemFile.getWebPath()); + entry.put("filename", itemFile.getFilename()); + entry.put("fileType", itemFile.getFileType()); + entry.put("mimeType", itemFile.getMimeType()); + entry.put("fileLength", itemFile.getFileLength()); + entry.put("fileHash", itemFile.getFileHash()); + if (expandDocumentText) { + entry.put("documentText", itemFile.getDocumentText()); + } + entry.put("created", itemFile.getCreated() != null ? itemFile.getCreated().toInstant().toString() : null); + entry.put("modified", itemFile.getModified() != null ? itemFile.getModified().toInstant().toString() : null); + responseList.add(entry); + } + + // Prepare the response + ServiceResponse response = new ServiceResponse(200); + ServiceResponseCommand.addMeta(response, "item-file", responseList, null); + response.setData(responseList); + return response; + } +} diff --git a/src/main/java/com/simisinc/platform/rest/services/items/ItemListService.java b/src/main/java/com/simisinc/platform/rest/services/items/ItemListService.java index ce136c34..dd78d72a 100644 --- a/src/main/java/com/simisinc/platform/rest/services/items/ItemListService.java +++ b/src/main/java/com/simisinc/platform/rest/services/items/ItemListService.java @@ -47,7 +47,21 @@ public class ItemListService extends GenericRestService { private static Log LOG = LogFactory.getLog(ItemListService.class); - // GET /items/{collectionUniqueId}?category=${categoryUniqueId}&query=value + private static final String PARAM_QUERY = "query"; + private static final String PARAM_CATEGORY = "category"; + private static final String PARAM_TAGS = "tags"; + + private static final String PARAM_FILTER = "filter"; + private static final String FILTER_BY_NAME = "name"; + private static final String FILTER_BY_UNIQUE_ID = "uniqueId"; + + private static final String PARAM_EXPAND = "expand"; + private static final String PARAM_PAGE = "page"; + private static final String PARAM_SIZE = "size"; + + private static final String EXPAND_DETAILS = "details"; + + // GET /items/{collectionUniqueId}?category=${categoryUniqueId}&query=value&expand=details @Override public ServiceResponse get(ServiceContext context) { @@ -69,7 +83,7 @@ public ServiceResponse get(ServiceContext context) { } // Check for a specific category - String categoryUniqueId = context.getParameter("category"); + String categoryUniqueId = context.getParameter(PARAM_CATEGORY); Category category = null; if (!StringUtils.isBlank(categoryUniqueId)) { category = LoadCategoryCommand.loadCategoryByUniqueIdWithinCollection(categoryUniqueId, collection.getId()); @@ -81,8 +95,8 @@ public ServiceResponse get(ServiceContext context) { } // Determine the constraints - int pageNumber = context.getParameterAsInt("page", 1); - int pageSize = context.getParameterAsInt("size", 20); + int pageNumber = context.getParameterAsInt(PARAM_PAGE, 1); + int pageSize = context.getParameterAsInt(PARAM_SIZE, 20); if (pageNumber < 1 || pageSize < 1) { ServiceResponse response = new ServiceResponse(400); response.getError().put("title", "Required query params: page (>=1) and size (>=1)"); @@ -99,7 +113,7 @@ public ServiceResponse get(ServiceContext context) { } // Check for tags - String tagsValue = context.getParameter("tags"); + String tagsValue = context.getParameter(PARAM_TAGS); if (StringUtils.isNotBlank(tagsValue)) { List filterTagList = new ArrayList<>(); String[] tagsArray = tagsValue.split(","); @@ -112,25 +126,78 @@ public ServiceResponse get(ServiceContext context) { } // Check for a search query - String query = context.getParameter("query"); + String query = context.getParameter(PARAM_QUERY); if (StringUtils.isNotBlank(query)) { specification.setSearchName(query); } + // Determine any filters + boolean validFilters = true; + String[] filterValues = context.getParameterAsArray(PARAM_FILTER); + if (filterValues != null) { + for (String filterValue : filterValues) { + // Handles an 'equals' filter in the form of "fieldName=fieldValue" + String[] parts = filterValue.split("="); + if (parts.length == 2) { + String fieldName = parts[0].trim(); + String fieldValue = parts[1].trim(); + if (StringUtils.isNotBlank(fieldName) && StringUtils.isNotBlank(fieldValue)) { + if (fieldName.equals(FILTER_BY_NAME)) { + // Name filter + specification.setName(fieldValue); + } else if (fieldName.equals(FILTER_BY_UNIQUE_ID)) { + // Unique ID filter + specification.setUniqueId(fieldValue); + } else { + LOG.debug("Unknown field filter: " + fieldName + "=" + fieldValue); + validFilters = false; + } + } + } + } + } + if (!validFilters) { + ServiceResponse response = new ServiceResponse(400); + response.getError().put("title", "Invalid filter query param"); + return response; + } + // Retrieve the records DataConstraints constraints = new DataConstraints(pageNumber, pageSize); List itemList = ItemRepository.findAll(specification, constraints); - // Set the fields to return - List recordList = new ArrayList<>(); - for (Item item : itemList) { - recordList.add(new ItemResponse(item)); - } - // Prepare the response ServiceResponse response = new ServiceResponse(200); ServiceResponseCommand.addMeta(response, "item", itemList, constraints); - response.setData(recordList); + + // Return the ItemDetailResponse if requested + boolean expandItemDetails = false; + String[] expand = context.getParameterAsArray(PARAM_EXPAND); + if (expand != null) { + for (String expandValue : expand) { + if (EXPAND_DETAILS.equals(expandValue)) { + expandItemDetails = true; + } + } + } + + // Determine the response type + if (expandItemDetails) { + // Set the fields to return + List recordList = new ArrayList<>(); + for (Item item : itemList) { + recordList.add(new ItemDetailsResponse(item, collection)); + } + response.setData(recordList); + } else { + // Set the fields to return + List recordList = new ArrayList<>(); + for (Item item : itemList) { + recordList.add(new ItemResponse(item)); + } + response.setData(recordList); + } + return response; } diff --git a/src/main/java/com/zeroio/platform/application/analytics/PageAnalyticsDataService.java b/src/main/java/com/zeroio/platform/application/analytics/PageAnalyticsDataService.java index b6a1d8ee..152b26f9 100644 --- a/src/main/java/com/zeroio/platform/application/analytics/PageAnalyticsDataService.java +++ b/src/main/java/com/zeroio/platform/application/analytics/PageAnalyticsDataService.java @@ -52,12 +52,12 @@ public static ObjectNode loadPageAnalytics(String pagePath, LocalDate fromDate, if (fromDate != null && toDate != null) { dailyViews = WebPageHitRepository.findDailyWebHitsForPage(resolvedPagePath, fromDate, toDate); - members = WebPageHitRepository.findAuthenticatedUserVisitsForPage(resolvedPagePath, fromDate, toDate, 100); + members = WebPageHitRepository.findAuthenticatedUserVisitsForPage(resolvedPagePath, fromDate, toDate); response.put("fromDate", fromDate.toString()); response.put("toDate", toDate.toString()); } else { dailyViews = WebPageHitRepository.findDailyWebHitsForPage(resolvedPagePath, days); - members = WebPageHitRepository.findAuthenticatedUserVisitsForPage(resolvedPagePath, days, 100); + members = WebPageHitRepository.findAuthenticatedUserVisitsForPage(resolvedPagePath, days); response.put("days", days); } @@ -116,7 +116,7 @@ private static String resolvePagePath(String pagePath) { if (toDateIndex > -1) { normalizedPagePath = normalizedPagePath.substring(0, toDateIndex); } - if (normalizedPagePath.endsWith("/")) { + if (normalizedPagePath.endsWith("/") && normalizedPagePath.length() > 1) { normalizedPagePath = normalizedPagePath.substring(0, normalizedPagePath.length() - 1); } return normalizedPagePath; diff --git a/src/main/java/com/zeroio/platform/application/cms/ExtractTextFromPDFCommand.java b/src/main/java/com/zeroio/platform/application/cms/ExtractTextFromPDFCommand.java new file mode 100644 index 00000000..42d4c780 --- /dev/null +++ b/src/main/java/com/zeroio/platform/application/cms/ExtractTextFromPDFCommand.java @@ -0,0 +1,54 @@ +/* + * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.zeroio.platform.application.cms; + +import java.io.File; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.text.PDFTextStripper; + +/** + * Command for extracting text from PDF files + * + * @author matt rajkowski + * @created 8/6/26 5:00 PM + */ +public class ExtractTextFromPDFCommand { + + private static Log LOG = LogFactory.getLog(ExtractTextFromPDFCommand.class); + + /** + * Extracts text from a PDF file + * + * @param pdfFile the PDF file + * @return the extracted text, or null if unsuccessful + */ + public static String textFromFile(File pdfFile) { + try (PDDocument document = Loader.loadPDF(pdfFile)) { + PDFTextStripper pdfStripper = new PDFTextStripper(); + String text = pdfStripper.getText(document); + LOG.debug("Extracted text from PDF file: " + pdfFile.getPath()); + return text; + } catch (Exception e) { + LOG.warn(e.getMessage()); + return null; + } + } +} diff --git a/src/main/java/com/zeroio/platform/application/cms/ExtractTextFromPowerPointCommand.java b/src/main/java/com/zeroio/platform/application/cms/ExtractTextFromPowerPointCommand.java new file mode 100644 index 00000000..8d1e3a0b --- /dev/null +++ b/src/main/java/com/zeroio/platform/application/cms/ExtractTextFromPowerPointCommand.java @@ -0,0 +1,55 @@ +/* + * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.zeroio.platform.application.cms; + +import java.io.File; +import java.io.FileInputStream; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.poi.xslf.extractor.XSLFExtractor; +import org.apache.poi.xslf.usermodel.XMLSlideShow; + +/** + * Command for extracting text from PowerPoint files + * + * @author matt rajkowski + * @created 8/6/26 5:00 PM + */ +public class ExtractTextFromPowerPointCommand { + + private static Log LOG = LogFactory.getLog(ExtractTextFromPowerPointCommand.class); + + /** + * Extracts text from a PowerPoint file + * + * @param pptFile the PowerPoint file + * @return the extracted text, or null if unsuccessful + */ + public static String textFromFile(File pptFile) { + try (FileInputStream fis = new FileInputStream(pptFile); + XMLSlideShow ppt = new XMLSlideShow(fis); + XSLFExtractor extractor = new XSLFExtractor(ppt)) { + String text = extractor.getText(); + LOG.debug("Extracted text from PowerPoint file: " + pptFile.getPath()); + return text; + } catch (Exception e) { + LOG.warn(e.getMessage()); + return null; + } + } +} diff --git a/src/main/java/com/zeroio/platform/application/cms/ExtractTextFromWordCommand.java b/src/main/java/com/zeroio/platform/application/cms/ExtractTextFromWordCommand.java new file mode 100644 index 00000000..22c3c61f --- /dev/null +++ b/src/main/java/com/zeroio/platform/application/cms/ExtractTextFromWordCommand.java @@ -0,0 +1,55 @@ +/* + * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.zeroio.platform.application.cms; + +import java.io.File; +import java.io.FileInputStream; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.poi.xwpf.extractor.XWPFWordExtractor; +import org.apache.poi.xwpf.usermodel.XWPFDocument; + +/** + * Command for extracting text from Word files + * + * @author matt rajkowski + * @created 8/6/26 5:00 PM + */ +public class ExtractTextFromWordCommand { + + private static Log LOG = LogFactory.getLog(ExtractTextFromWordCommand.class); + + /** + * Extracts text from a Word file + * + * @param wordFile the Word file + * @return the extracted text, or null if unsuccessful + */ + public static String textFromFile(File wordFile) { + try (FileInputStream fis = new FileInputStream(wordFile); + XWPFDocument doc = new XWPFDocument(fis); + XWPFWordExtractor extractor = new XWPFWordExtractor(doc)) { + String text = extractor.getText(); + LOG.debug("Extracted text from Word file: " + wordFile.getPath()); + return text; + } catch (Exception e) { + LOG.warn(e.getMessage()); + return null; + } + } +} diff --git a/src/main/java/com/zeroio/platform/application/cms/FileItemDocumentContentCommand.java b/src/main/java/com/zeroio/platform/application/cms/FileItemDocumentContentCommand.java new file mode 100644 index 00000000..e101cbc8 --- /dev/null +++ b/src/main/java/com/zeroio/platform/application/cms/FileItemDocumentContentCommand.java @@ -0,0 +1,72 @@ +/* + * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.zeroio.platform.application.cms; + +import java.io.File; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.simisinc.platform.application.filesystem.FileSystemCommand; +import com.simisinc.platform.domain.model.cms.FileItem; +import com.simisinc.platform.infrastructure.persistence.cms.FileItemRepository; + +/** + * Command for extracting text from documents + * + * @author matt rajkowski + * @created 8/6/26 5:00 PM + */ +public class FileItemDocumentContentCommand { + + private static Log LOG = LogFactory.getLog(FileItemDocumentContentCommand.class); + + /** + * Updates the text index for the given file item + * + * @param fileItem the file item + * @return true if successful, false otherwise + */ + public static boolean updateTextIndex(FileItem fileItem) { + if (fileItem == null) { + return false; + } + + File file = FileSystemCommand.getFileServerRootPath(fileItem.getFileServerPath()); + if (!file.exists()) { + LOG.warn("File does not exist: " + file.getPath()); + return false; + } + + // Process PDF files + if (fileItem.getExtension().equalsIgnoreCase("pdf")) { + String text = ExtractTextFromPDFCommand.textFromFile(file); + FileItemRepository.updateDocumentText(fileItem, text); + return true; + } else if (fileItem.getExtension().equalsIgnoreCase("docx")) { + String text = ExtractTextFromWordCommand.textFromFile(file); + FileItemRepository.updateDocumentText(fileItem, text); + return true; + } else if (fileItem.getExtension().equalsIgnoreCase("pptx")) { + String text = ExtractTextFromPowerPointCommand.textFromFile(file); + FileItemRepository.updateDocumentText(fileItem, text); + return true; + } + return false; + } + +} diff --git a/src/main/java/com/zeroio/platform/application/cms/ItemFileItemDocumentContentCommand.java b/src/main/java/com/zeroio/platform/application/cms/ItemFileItemDocumentContentCommand.java new file mode 100644 index 00000000..d20eded6 --- /dev/null +++ b/src/main/java/com/zeroio/platform/application/cms/ItemFileItemDocumentContentCommand.java @@ -0,0 +1,69 @@ +/* + * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.zeroio.platform.application.cms; + +import java.io.File; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.simisinc.platform.application.filesystem.FileSystemCommand; +import com.simisinc.platform.domain.model.items.ItemFileItem; +import com.simisinc.platform.infrastructure.persistence.items.ItemFileItemRepository; + +/** + * Command for extracting text from documents + * + * @author matt rajkowski + * @created 8/6/26 5:00 PM + */ +public class ItemFileItemDocumentContentCommand { + + private static Log LOG = LogFactory.getLog(ItemFileItemDocumentContentCommand.class); + + /** + * Updates the text index for the given item file item + * + * @param itemFileItem the item file item + * @return true if successful, false otherwise + */ + public static boolean updateTextIndex(ItemFileItem fileItem) { + if (fileItem == null) { + return false; + } + File file = FileSystemCommand.getFileServerRootPath(fileItem.getFileServerPath()); + if (!file.exists()) { + LOG.warn("File does not exist: " + file.getPath()); + return false; + } + // Process PDF files + if (fileItem.getExtension().equalsIgnoreCase("pdf")) { + String text = ExtractTextFromPDFCommand.textFromFile(file); + ItemFileItemRepository.updateDocumentText(fileItem, text); + return true; + } else if (fileItem.getExtension().equalsIgnoreCase("docx")) { + String text = ExtractTextFromWordCommand.textFromFile(file); + ItemFileItemRepository.updateDocumentText(fileItem, text); + return true; + } else if (fileItem.getExtension().equalsIgnoreCase("pptx")) { + String text = ExtractTextFromPowerPointCommand.textFromFile(file); + ItemFileItemRepository.updateDocumentText(fileItem, text); + return true; + } + return false; + } +} diff --git a/src/main/java/com/zeroio/platform/infrastructure/permission/Permission.java b/src/main/java/com/zeroio/platform/infrastructure/permission/Permission.java index 45884f07..9ba52031 100644 --- a/src/main/java/com/zeroio/platform/infrastructure/permission/Permission.java +++ b/src/main/java/com/zeroio/platform/infrastructure/permission/Permission.java @@ -88,6 +88,13 @@ public static synchronized void load(List groups) { * @return {@code true} if permitted (or if the action is not governed by any policy) */ public static boolean check(String action, UserSession userSession) { + if (StringUtils.isBlank(action)) { + return false; + } + if (userSession == null) { + return false; + } + // Look up the governing permission group for this action PermissionGroup group = actionMap.get(action); if (group == null) { // Not registered — disallow by default diff --git a/src/main/java/com/zeroio/platform/infrastructure/scheduler/cms/RefreshAllDocumentTextIndexesJob.java b/src/main/java/com/zeroio/platform/infrastructure/scheduler/cms/RefreshAllDocumentTextIndexesJob.java new file mode 100644 index 00000000..e475120d --- /dev/null +++ b/src/main/java/com/zeroio/platform/infrastructure/scheduler/cms/RefreshAllDocumentTextIndexesJob.java @@ -0,0 +1,79 @@ +/* + * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.zeroio.platform.infrastructure.scheduler.cms; + +import java.time.Duration; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jobrunr.jobs.annotations.Job; + +import com.simisinc.platform.domain.model.cms.FileItem; +import com.simisinc.platform.domain.model.items.ItemFileItem; +import com.simisinc.platform.infrastructure.distributedlock.LockManager; +import com.simisinc.platform.infrastructure.persistence.cms.FileItemRepository; +import com.simisinc.platform.infrastructure.persistence.cms.FileSpecification; +import com.simisinc.platform.infrastructure.persistence.items.ItemFileItemRepository; +import com.simisinc.platform.infrastructure.persistence.items.ItemFileSpecification; +import com.simisinc.platform.infrastructure.scheduler.SchedulerManager; +import com.zeroio.platform.application.cms.FileItemDocumentContentCommand; +import com.zeroio.platform.application.cms.ItemFileItemDocumentContentCommand; + +import lombok.NoArgsConstructor; + +/** + * Async job for updating all document text indexes. + * + * @author matt rajkowski + * @created 8/6/26 5:00 PM + */ +@NoArgsConstructor +public class RefreshAllDocumentTextIndexesJob { + + private static Log LOG = LogFactory.getLog(RefreshAllDocumentTextIndexesJob.class); + + @Job(name = "Refresh all document text indexes for content") + public static void execute() { + // Distributed lock + String lock = LockManager.lock(SchedulerManager.REFRESH_ALL_DOCUMENT_TEXT_INDEXES_JOB, Duration.ofMinutes(30)); + if (lock == null) { + return; + } + + try { + // Page through all files and update text indexes + FileSpecification fileSpecification = new FileSpecification(); + fileSpecification.setIsProcessed(false); + fileSpecification.setFileExtension(new String[] { "pdf", "docx", "pptx" }); + List allFiles = FileItemRepository.findAll(fileSpecification, null); + allFiles.forEach(fileItem -> FileItemDocumentContentCommand.updateTextIndex(fileItem)); + + // Page through all item files and update text indexes + ItemFileSpecification itemFileSpecification = new ItemFileSpecification(); + itemFileSpecification.setIsProcessed(false); + itemFileSpecification.setFileExtension(new String[] { "pdf", "docx", "pptx" }); + List allItemFiles = ItemFileItemRepository.findAll(itemFileSpecification, null); + allItemFiles.forEach(itemFileItem -> ItemFileItemDocumentContentCommand.updateTextIndex(itemFileItem)); + } catch (Exception e) { + LOG.warn("Error refreshing all document text indexes: " + e.getMessage(), e); + } finally { + // Finish up + LockManager.unlock(SchedulerManager.REFRESH_ALL_DOCUMENT_TEXT_INDEXES_JOB, lock); + } + } +} diff --git a/src/main/java/com/simisinc/platform/infrastructure/scheduler/cms/RefreshAllWebPageTextIndexesJob.java b/src/main/java/com/zeroio/platform/infrastructure/scheduler/cms/RefreshAllWebPageTextIndexesJob.java similarity index 96% rename from src/main/java/com/simisinc/platform/infrastructure/scheduler/cms/RefreshAllWebPageTextIndexesJob.java rename to src/main/java/com/zeroio/platform/infrastructure/scheduler/cms/RefreshAllWebPageTextIndexesJob.java index ae7ba80b..440a4157 100644 --- a/src/main/java/com/simisinc/platform/infrastructure/scheduler/cms/RefreshAllWebPageTextIndexesJob.java +++ b/src/main/java/com/zeroio/platform/infrastructure/scheduler/cms/RefreshAllWebPageTextIndexesJob.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.simisinc.platform.infrastructure.scheduler.cms; +package com.zeroio.platform.infrastructure.scheduler.cms; import java.time.Duration; import java.util.List; diff --git a/src/main/java/com/simisinc/platform/infrastructure/scheduler/cms/RefreshWebPageTextIndexJob.java b/src/main/java/com/zeroio/platform/infrastructure/scheduler/cms/RefreshWebPageTextIndexJob.java similarity index 98% rename from src/main/java/com/simisinc/platform/infrastructure/scheduler/cms/RefreshWebPageTextIndexJob.java rename to src/main/java/com/zeroio/platform/infrastructure/scheduler/cms/RefreshWebPageTextIndexJob.java index 1c47bc78..09ee3bb3 100644 --- a/src/main/java/com/simisinc/platform/infrastructure/scheduler/cms/RefreshWebPageTextIndexJob.java +++ b/src/main/java/com/zeroio/platform/infrastructure/scheduler/cms/RefreshWebPageTextIndexJob.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.simisinc.platform.infrastructure.scheduler.cms; +package com.zeroio.platform.infrastructure.scheduler.cms; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; diff --git a/src/main/java/com/zeroio/platform/presentation/tags/PermissionTag.java b/src/main/java/com/zeroio/platform/presentation/tags/PermissionTag.java new file mode 100644 index 00000000..32c085a7 --- /dev/null +++ b/src/main/java/com/zeroio/platform/presentation/tags/PermissionTag.java @@ -0,0 +1,48 @@ +/* + * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.zeroio.platform.presentation.tags; + +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.tagext.BodyTagSupport; + +import com.simisinc.platform.presentation.controller.SessionConstants; +import com.simisinc.platform.presentation.controller.UserSession; +import com.zeroio.platform.infrastructure.permission.Permission; + +public class PermissionTag extends BodyTagSupport { + + private static final long serialVersionUID = 7372839409034423539L; + + private String action; + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + + @Override + public int doStartTag() throws JspException { + UserSession userSession = (UserSession) pageContext.getSession().getAttribute(SessionConstants.USER); + if (Permission.check(action, userSession)) { + return EVAL_BODY_BUFFERED; + } + return SKIP_BODY; + } +} \ No newline at end of file diff --git a/src/main/java/com/zeroio/platform/rest/RawItemFileService.java b/src/main/java/com/zeroio/platform/rest/RawItemFileService.java index 181c378c..ca27166a 100644 --- a/src/main/java/com/zeroio/platform/rest/RawItemFileService.java +++ b/src/main/java/com/zeroio/platform/rest/RawItemFileService.java @@ -16,7 +16,9 @@ package com.zeroio.platform.rest; import java.io.File; +import java.io.FileInputStream; import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; @@ -24,6 +26,7 @@ import java.util.Map; import java.util.concurrent.Semaphore; +import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Part; import org.apache.commons.lang3.StringUtils; @@ -31,6 +34,8 @@ import org.apache.commons.logging.LogFactory; import com.simisinc.platform.application.filesystem.FileSystemCommand; +import com.simisinc.platform.application.items.LoadItemCommand; +import com.simisinc.platform.domain.model.items.Item; import com.simisinc.platform.domain.model.items.ItemFileItem; import com.simisinc.platform.infrastructure.persistence.items.ItemFileItemRepository; import com.simisinc.platform.rest.controller.GenericRestService; @@ -49,7 +54,89 @@ public class RawItemFileService extends GenericRestService { private static final String ERROR_TITLE_KEY = "title"; private static final Semaphore REQUEST_LOCK = new Semaphore(1); - // POST /rawItemFile + /** + * GET /rawItemFile/{webPath} + * Streams the item file data + * + */ + @Override + public ServiceResponse get(ServiceContext context) { + // A webPath is required to locate the file + String webPath = StringUtils.trimToNull(context.getPathParam()); + if (webPath == null || !isSafeWebPath(webPath)) { + ServiceResponse response = new ServiceResponse(400); + response.getError().put(ERROR_TITLE_KEY, "A valid webPath is required"); + return response; + } + ItemFileItem itemFile = ItemFileItemRepository.findByWebPath(webPath); + if (itemFile == null) { + ServiceResponse response = new ServiceResponse(404); + response.getError().put(ERROR_TITLE_KEY, "File not found"); + return response; + } + + // Check permissions of the Item record + Item item = LoadItemCommand.loadItemById(itemFile.getItemId()); + if (item != null) { + item = LoadItemCommand.loadItemForAuthorizedUser(item, context.getUser()); + } + if (item == null) { + ServiceResponse response = new ServiceResponse(404); + response.getError().put(ERROR_TITLE_KEY, "Item was not found"); + return response; + } + + // Resolve the target path and check if the file exists + Path targetPath = resolveTargetPath(itemFile); + if (targetPath == null) { + ServiceResponse response = new ServiceResponse(400); + response.getError().put(ERROR_TITLE_KEY, "File not found"); + return response; + } + File existingFile = targetPath.toFile(); + if (!existingFile.isFile()) { + ServiceResponse response = new ServiceResponse(404); + response.getError().put(ERROR_TITLE_KEY, "File not found"); + return response; + } + + // Check for a last-modified header and return 304 if possible + long lastModified = itemFile.getModified().getTime(); + long headerValue = context.getRequest().getDateHeader("If-Modified-Since"); + if (lastModified <= headerValue + 1000) { + ServiceResponse response = new ServiceResponse(HttpServletResponse.SC_NOT_MODIFIED); + response.setHandledResponse(true); + context.getResponse().setStatus(HttpServletResponse.SC_NOT_MODIFIED); + return response; + } + + // Set response headers + context.getResponse().setDateHeader("Last-Modified", lastModified); + context.getResponse().setContentType(itemFile.getMimeType()); + context.getResponse().setContentLength((int) existingFile.length()); + + // Stream the binary data + try (FileInputStream in = new FileInputStream(existingFile); + OutputStream out = context.getResponse().getOutputStream()) { + byte[] buf = new byte[8192]; + int count; + while ((count = in.read(buf)) >= 0) { + out.write(buf, 0, count); + } + out.flush(); + } catch (Exception e) { + log.debug("Stream error: " + e.getMessage()); + } + + ServiceResponse response = new ServiceResponse(200); + response.setHandledResponse(true); + return response; + } + + /** + * POST /rawItemFile + * Restores missing item-files on the file server from a posted multipart file and web path. + */ @Override public ServiceResponse post(ServiceContext context) { diff --git a/src/main/resources/database/upgrade/2026/UPGRADE_20260810.1000__web_page_archive.sql b/src/main/resources/database/upgrade/2026/UPGRADE_20260810.1000__web_page_archive.sql new file mode 100644 index 00000000..5278e7b2 --- /dev/null +++ b/src/main/resources/database/upgrade/2026/UPGRADE_20260810.1000__web_page_archive.sql @@ -0,0 +1 @@ +UPDATE web_pages SET enabled = true WHERE draft = false AND created < '2026-07-28 00:00:00'; diff --git a/src/main/webapp/WEB-INF/jsp/admin.jsp b/src/main/webapp/WEB-INF/jsp/admin.jsp index 5929311f..8fd690f4 100644 --- a/src/main/webapp/WEB-INF/jsp/admin.jsp +++ b/src/main/webapp/WEB-INF/jsp/admin.jsp @@ -80,7 +80,7 @@