Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file renamed lib/build/db-1.0.12.jar → lib/build/db-1.0.13.jar
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
18 changes: 9 additions & 9 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.rajkowski</groupId>
<artifactId>cms-platform</artifactId>
<version>20260801.10000-SNAPSHOT</version>
<version>20260918.10000-SNAPSHOT</version>
<name>CMS Platform</name>
<url>https://github.com/rajkowski/cms-platform</url>
<description>Agile, Enterprise, Open Source Content Management System (CMS) and Web Portal</description>
Expand Down Expand Up @@ -47,7 +47,7 @@
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j_jdk17-core</artifactId>
<version>8.19.0</version>
<version>8.20.0</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
Expand Down Expand Up @@ -127,7 +127,7 @@
<dependency>
<groupId>com.github.rajkowski</groupId>
<artifactId>db</artifactId>
<version>1.0.12</version>
<version>1.0.13</version>
</dependency>
<dependency>
<groupId>com.github.rajkowski</groupId>
Expand Down Expand Up @@ -259,7 +259,7 @@
<dependency>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber</artifactId>
<version>9.0.38</version>
<version>9.0.39</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
Expand Down Expand Up @@ -334,17 +334,17 @@
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.18</version>
<version>2.0.19</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.18</version>
<version>2.0.19</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>2.0.18</version>
<version>2.0.19</version>
</dependency>
<dependency>
<groupId>org.apache.taglibs</groupId>
Expand All @@ -359,13 +359,13 @@
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jsp-api</artifactId>
<version>9.0.121</version>
<version>9.0.122</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-servlet-api</artifactId>
<version>9.0.121</version>
<version>9.0.122</version>
<scope>provided</scope>
</dependency>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import com.simisinc.platform.domain.model.analytics.PerformanceMetric;
import com.simisinc.platform.domain.model.dashboard.ActiveSessionData;
import com.simisinc.platform.domain.model.dashboard.StatisticsData;
import com.simisinc.platform.infrastructure.persistence.SessionRepository;
import com.simisinc.platform.infrastructure.persistence.UserRepository;
import com.simisinc.platform.infrastructure.persistence.analytics.PerformanceMetricRepository;
import com.simisinc.platform.infrastructure.persistence.cms.FileItemRepository;
import com.simisinc.platform.infrastructure.persistence.cms.FileVersionRepository;
Expand All @@ -41,8 +43,6 @@
import com.simisinc.platform.infrastructure.persistence.datasets.DatasetRepository;
import com.simisinc.platform.infrastructure.persistence.items.ItemFileItemRepository;
import com.simisinc.platform.infrastructure.persistence.login.UserLoginRepository;
import com.simisinc.platform.infrastructure.persistence.SessionRepository;
import com.simisinc.platform.infrastructure.persistence.UserRepository;

/**
* Service for aggregating and providing analytics data
Expand All @@ -65,25 +65,26 @@ public static ObjectNode loadOverview(String rangeStart, String rangeEnd) {
LocalDate end = LocalDate.parse(rangeEnd);

// The queries will need to be updated to handle different date ranges

// This method currently assumes daily data from based on date amounts
int days = (int) ChronoUnit.DAYS.between(start, end) + 1;

// Get data from repositories for current period
List<StatisticsData> dailySessions = WebPageHitRepository.findDailySessions(days);
List<StatisticsData> dailyLogins = UserLoginRepository.findUniqueDailyLogins(days);
List<StatisticsData> dailyHits = WebPageHitRepository.findDailyWebHits(days);

// Calculate KPIs for current period
long totalSessions = dailySessions.stream().mapToLong(d -> Long.parseLong(d.getValue())).sum();
long totalHits = dailyHits.stream().mapToLong(d -> Long.parseLong(d.getValue())).sum();
long totalUsers = dailyLogins.stream().mapToLong(d -> Long.parseLong(d.getValue())).sum();

long totalUniqueLogins = UserLoginRepository.countUniqueLogins(
Timestamp.valueOf(start.atStartOfDay()),
Timestamp.valueOf(end.plusDays(1).atStartOfDay()));

// Calculate new users based on user creation dates within range
long newUsersCount = UserRepository.countNewUsers(
Timestamp.valueOf(start.atStartOfDay()),
Timestamp.valueOf(end.plusDays(1).atStartOfDay())
);
Timestamp.valueOf(end.plusDays(1).atStartOfDay()));

// Calculate bounce rate based on single-page sessions
double bounceRate = SessionRepository.findBounceRate(days) / 100.0;
Expand All @@ -94,14 +95,12 @@ public static ObjectNode loadOverview(String rangeStart, String rangeEnd) {
// Get data for previous period to calculate trends
// Query the previous equivalent period
List<StatisticsData> prevDailySessions = WebPageHitRepository.findDailySessions(days * 2);
List<StatisticsData> prevDailyLogins = UserLoginRepository.findUniqueDailyLogins(days * 2);
List<StatisticsData> prevDailyHits = WebPageHitRepository.findDailyWebHits(days * 2);

// Extract previous period data (older half of the data)
long prevTotalSessions = 0;
long prevTotalHits = 0;
long prevTotalUsers = 0;


if (prevDailySessions != null && prevDailySessions.size() > days) {
prevTotalSessions = prevDailySessions.stream()
.skip(Math.max(0, prevDailySessions.size() - days * 2))
Expand All @@ -116,13 +115,6 @@ public static ObjectNode loadOverview(String rangeStart, String rangeEnd) {
.mapToLong(d -> Long.parseLong(d.getValue()))
.sum();
}
if (prevDailyLogins != null && prevDailyLogins.size() > days) {
prevTotalUsers = prevDailyLogins.stream()
.skip(Math.max(0, prevDailyLogins.size() - days * 2))
.limit(days)
.mapToLong(d -> Long.parseLong(d.getValue()))
.sum();
}

double prevAvgSessionDuration = SessionRepository.findAverageSessionDuration(days * 2);

Expand All @@ -132,13 +124,17 @@ public static ObjectNode loadOverview(String rangeStart, String rangeEnd) {
// Calculate previous period new users
LocalDate prevStart = start.minusDays(days);
LocalDate prevEnd = end.minusDays(days);

long prevNewUsersCount = UserRepository.countNewUsers(
Timestamp.valueOf(prevStart.atStartOfDay()),
Timestamp.valueOf(prevEnd.plusDays(1).atStartOfDay())
);
Timestamp.valueOf(prevEnd.plusDays(1).atStartOfDay()));

long prevUniqueLogins = UserLoginRepository.countUniqueLogins(
Timestamp.valueOf(prevStart.atStartOfDay()),
Timestamp.valueOf(prevEnd.plusDays(1).atStartOfDay()));

// Calculate trend percentages
double usersTrend = calculateTrendPercentage(prevTotalUsers, totalUsers);
double usersTrend = calculateTrendPercentage(prevUniqueLogins, totalUniqueLogins);
double sessionsTrend = calculateTrendPercentage(prevTotalSessions, totalSessions);
double pageViewsTrend = calculateTrendPercentage(prevTotalHits, totalHits);
double avgDurationTrend = calculateTrendPercentage(prevAvgSessionDuration, avgSessionDuration);
Expand All @@ -153,7 +149,7 @@ public static ObjectNode loadOverview(String rangeStart, String rangeEnd) {

// KPIs with trend values
ObjectNode kpis = response.putObject("kpis");
kpis.put("activeUsers", totalUsers);
kpis.put("activeUsers", totalUniqueLogins);
kpis.put("activeUsersTrend", Math.round(usersTrend * 10.0) / 10.0);
kpis.put("sessions", totalSessions);
kpis.put("sessionsTrend", Math.round(sessionsTrend * 10.0) / 10.0);
Expand Down Expand Up @@ -240,7 +236,7 @@ public static ObjectNode loadContent(String rangeStart, String rangeEnd, int day

// @todo additional data
response.putArray("searchQueries");

response.putArray("referrers");

return response;
Expand Down Expand Up @@ -270,7 +266,7 @@ public static ObjectNode loadAudience(String rangeStart, String rangeEnd) {
// Get devices and browsers data from sessions
List<StatisticsData> devices = SessionRepository.findTopDevices(days);
List<StatisticsData> browsers = SessionRepository.findTopBrowsers(days);

// Get average session duration
double avgSessionDuration = SessionRepository.findAverageSessionDuration(days);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class FontCommand {
private static Log LOG = LogFactory.getLog(FontCommand.class);

private static String version = "free";
// private static String version = "pro";
// private static String version = "pro";

public static final String REGULAR = "regular";
public static final String LIGHT = "light";
Expand All @@ -41,9 +41,9 @@ public class FontCommand {

public static String fontawesome() {
if ("pro".equals(version)) {
return "fontawesome-pro-7.1.0-web";
return "fontawesome-pro-7.3.1-web";
}
return "fontawesome-free-7.1.0-web";
return "fontawesome-free-7.3.1-web";
}

private static String fa(String type) {
Expand All @@ -69,36 +69,36 @@ private static String fa(String type) {
}

// Font Awesome Free
if (REGULAR.equals(type)) {
return "fa-regular";
}
if (BRANDS.equals(type)) {
return "fa-brands";
}
return "fa-solid";
}

public static String far() {
if ("free".equals(version)) {
return fa(SOLID);
}
return fa(REGULAR);
}

public static String fal() {
if ("free".equals(version)) {
return fa(SOLID);
return fa(REGULAR);
}
return fa(LIGHT);
}

public static String fad() {
if ("free".equals(version)) {
return fa(SOLID);
return fa(REGULAR);
}
return fa(DUOTONE);
}

public static String fat() {
if ("free".equals(version)) {
return fa(SOLID);
return fa(REGULAR);
}
return fa(THIN);
}
Expand Down
59 changes: 43 additions & 16 deletions src/main/java/com/simisinc/platform/application/cms/UrlCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
Expand All @@ -36,22 +37,21 @@ public class UrlCommand {

private static Log LOG = LogFactory.getLog(UrlCommand.class);

public static String encode(String url) {
if (StringUtils.isBlank(url)) {
LOG.debug("URL is blank");
return "#";
}
// URL characters that cannot break out of an href/src attribute: no quotes, angle brackets,
// backslash, backtick, or whitespace.
private static final Pattern SAFE_URL = Pattern.compile("^[A-Za-z0-9/?&=#%._~:@!$()*+,;-]*$");

// Validate first
String[] schemes = { "http", "https" };
UrlValidator urlValidator = new UrlValidator(schemes);
if (!urlValidator.isValid(url)) {
// Site-relative path + optional query/fragment built from URL-safe
// characters only: no quotes, angle brackets, backslash, colon, or whitespace
private static final Pattern SAFE_RETURN_PAGE = Pattern.compile("^/[A-Za-z0-9/?&=#%._~+,;-]*$");

public static String encode(String url) {
String sanitizedUrl = sanitizeUrl(url);
if (StringUtils.isBlank(sanitizedUrl)) {
LOG.debug("URL is not acceptable");
return "#";
}

// @todo handle invalid urls

return url;
return sanitizedUrl;
}

public static String encodeUri(String uri) {
Expand Down Expand Up @@ -94,12 +94,39 @@ public static String getValidReturnPage(String returnPage) {
if (StringUtils.isBlank(returnPage)) {
return null;
}
if (returnPage.contains(":")) {
if (returnPage.startsWith("//")) {
return null;
}
if (returnPage.startsWith("/")) {
return returnPage;
if (!SAFE_RETURN_PAGE.matcher(returnPage).matches()) {
return null;
}
return returnPage;
}

/**
* Returns the url when it is safe to place in an href/src attribute -- a site-relative path,
* anchor, or an http(s)/mailto/tel absolute url with no attribute-breakout characters -- otherwise
* null. Active schemes such as javascript: and data: are rejected, as are protocol-relative "//"
* targets.
*/
public static String sanitizeUrl(String url) {
if (StringUtils.isBlank(url)) {
return null;
}
String value = url.trim();
if (value.startsWith("//") || !SAFE_URL.matcher(value).matches()) {
return null;
}
// If the value carries a scheme, allow only the safe ones
String lower = value.toLowerCase();
if (lower.matches("^[a-z][a-z0-9+.-]*:.*")) {
if (lower.startsWith("http://") || lower.startsWith("https://")
|| lower.startsWith("mailto:") || lower.startsWith("tel:")) {
return value;
}
return null;
}
// No scheme: a site-relative path, anchor, or query
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,24 @@
*/
public class HttpGetCommand {

private static Log LOG = LogFactory.getLog(HttpDownloadFileCommand.class);
private static Log LOG = LogFactory.getLog(HttpGetCommand.class);

public static final int GET = 1;
public static final int DELETE = 2;

/**
* Validates that {@code url} is SSRF-safe, then fetches it. Returns null and logs a
* warning if the guard rejects the URL. Use this for any URL derived from untrusted input.
*/
public static String executeUserUrl(String url) {
RemoteUrlValidationCommand.ValidationResult validation = RemoteUrlValidationCommand.validate(url);
if (!validation.isAllowed()) {
LOG.warn("Blocked an SSRF-unsafe user-supplied url: " + url);
return null;
}
return execute(url, GET);
}

public static String execute(String url) {
return execute(url, GET);
}
Expand Down
Loading
Loading