mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
V1 merge (#5193)
# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Signed-off-by: dependabot[bot] <[email protected]> Signed-off-by: Balázs Szücs <[email protected]> Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com> Co-authored-by: ConnorYoh <[email protected]> Co-authored-by: Connor Yoh <[email protected]> Co-authored-by: OUNZAR Aymane <[email protected]> Co-authored-by: YAOU Reda <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> Co-authored-by: Balázs Szücs <[email protected]> Co-authored-by: Ludy <[email protected]> Co-authored-by: tkymmm <[email protected]> Co-authored-by: Peter Dave Hello <[email protected]> Co-authored-by: albanobattistella <[email protected]> Co-authored-by: PingLin8888 <[email protected]> Co-authored-by: FdaSilvaYY <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: OteJlo <[email protected]> Co-authored-by: Angel <[email protected]> Co-authored-by: Ricardo Catarino <[email protected]> Co-authored-by: Luis Antonio Argüelles González <[email protected]> Co-authored-by: Dawid Urbański <[email protected]> Co-authored-by: Stephan Paternotte <[email protected]> Co-authored-by: Leonardo Santos Paulucio <[email protected]> Co-authored-by: hamza khalem <[email protected]> Co-authored-by: IT Creativity + Art Team <[email protected]> Co-authored-by: Reece Browne <[email protected]> Co-authored-by: James Brunton <[email protected]> Co-authored-by: Victor Villarreal <[email protected]>
This commit is contained in:
co-authored by
ConnorYoh
Connor Yoh
OUNZAR Aymane
YAOU Reda
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Balázs Szücs
Ludy
tkymmm
Peter Dave Hello
albanobattistella
PingLin8888
FdaSilvaYY
Copilot
OteJlo
Angel
Ricardo Catarino
Luis Antonio Argüelles González
Dawid Urbański
Stephan Paternotte
Leonardo Santos Paulucio
hamza khalem
IT Creativity + Art Team
Reece Browne
James Brunton
Victor Villarreal
parent
a5dcdd5bd9
commit
68ed54e398
@@ -5,6 +5,7 @@ import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
@@ -307,8 +308,8 @@ public class AuditUtils {
|
||||
|
||||
// For HTTP methods, infer based on controller and path
|
||||
if (httpMethod != null && path != null) {
|
||||
String cls = controller.getSimpleName().toLowerCase();
|
||||
String pkg = controller.getPackage().getName().toLowerCase();
|
||||
String cls = controller.getSimpleName().toLowerCase(Locale.ROOT);
|
||||
String pkg = controller.getPackage().getName().toLowerCase(Locale.ROOT);
|
||||
|
||||
if ("GET".equals(httpMethod)) return AuditEventType.HTTP_REQUEST;
|
||||
|
||||
@@ -374,8 +375,8 @@ public class AuditUtils {
|
||||
}
|
||||
|
||||
// Otherwise infer from controller and path
|
||||
String cls = controller.getSimpleName().toLowerCase();
|
||||
String pkg = controller.getPackage().getName().toLowerCase();
|
||||
String cls = controller.getSimpleName().toLowerCase(Locale.ROOT);
|
||||
String pkg = controller.getPackage().getName().toLowerCase(Locale.ROOT);
|
||||
|
||||
if ("GET".equals(httpMethod)) return AuditEventType.HTTP_REQUEST;
|
||||
|
||||
|
||||
+1
-1
@@ -134,7 +134,7 @@ public class ProprietaryUIDataController {
|
||||
Security securityProps = applicationProperties.getSecurity();
|
||||
|
||||
// Add enableLogin flag so frontend doesn't need to call /app-config
|
||||
data.setEnableLogin(securityProps.getEnableLogin());
|
||||
data.setEnableLogin(securityProps.isEnableLogin());
|
||||
|
||||
// Check if this is first-time setup with default credentials
|
||||
// The isFirstLogin flag captures: default username/password usage and unchanged state
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@ import java.security.cert.X509Certificate;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
@@ -150,7 +151,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
registrationId = oAuthToken.getAuthorizedClientRegistrationId();
|
||||
|
||||
// Redirect based on OAuth2 provider
|
||||
switch (registrationId.toLowerCase()) {
|
||||
switch (registrationId.toLowerCase(Locale.ROOT)) {
|
||||
case "keycloak" -> {
|
||||
KeycloakProvider keycloak = oauth.getClient().getKeycloak();
|
||||
|
||||
|
||||
+1
-1
@@ -151,7 +151,7 @@ public class InitialSecuritySetup {
|
||||
if (internalApiUserOpt.isPresent()) {
|
||||
User internalApiUser = internalApiUserOpt.get();
|
||||
// move to team internal API user
|
||||
if (!internalApiUser.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||
if (!TeamService.INTERNAL_TEAM_NAME.equals(internalApiUser.getTeam().getName())) {
|
||||
log.info(
|
||||
"Moving internal API user to team: {}", TeamService.INTERNAL_TEAM_NAME);
|
||||
Team internalTeam = teamService.getOrCreateInternalTeam();
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
package stirling.software.proprietary.security.configuration;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
@@ -133,7 +135,7 @@ public class DatabaseConfig {
|
||||
private String getDriverClassName(String driverName) throws UnsupportedProviderException {
|
||||
try {
|
||||
ApplicationProperties.Driver driver =
|
||||
ApplicationProperties.Driver.valueOf(driverName.toUpperCase());
|
||||
ApplicationProperties.Driver.valueOf(driverName.toUpperCase(Locale.ROOT));
|
||||
|
||||
return switch (driver) {
|
||||
case H2 -> {
|
||||
|
||||
+6
-3
@@ -5,6 +5,7 @@ import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
|
||||
import org.bouncycastle.crypto.signers.Ed25519Signer;
|
||||
@@ -185,7 +186,7 @@ public class KeygenLicenseVerifier {
|
||||
byte[] signatureBytes = Base64.getDecoder().decode(encodedSignature);
|
||||
|
||||
// Create the signing data format - prefix with "license/"
|
||||
String signingData = String.format("license/%s", encryptedData);
|
||||
String signingData = String.format(Locale.ROOT, "license/%s", encryptedData);
|
||||
byte[] signingDataBytes = signingData.getBytes();
|
||||
|
||||
log.info("Signing data length: {} bytes", signingDataBytes.length);
|
||||
@@ -348,7 +349,7 @@ public class KeygenLicenseVerifier {
|
||||
.decode(encodedSignature.replace('-', '+').replace('_', '/'));
|
||||
|
||||
// For ED25519_SIGN format, the signing data is "key/" + encodedPayload
|
||||
String signingData = String.format("key/%s", encodedPayload);
|
||||
String signingData = String.format(Locale.ROOT, "key/%s", encodedPayload);
|
||||
byte[] dataBytes = signingData.getBytes();
|
||||
|
||||
byte[] publicKeyBytes = Hex.decode(PUBLIC_KEY);
|
||||
@@ -526,8 +527,10 @@ public class KeygenLicenseVerifier {
|
||||
String licenseKey, String machineFingerprint, LicenseContext context) throws Exception {
|
||||
String requestBody =
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"{\"meta\":{\"key\":\"%s\",\"scope\":{\"fingerprint\":\"%s\"}}}",
|
||||
licenseKey, machineFingerprint);
|
||||
licenseKey,
|
||||
machineFingerprint);
|
||||
HttpRequest request =
|
||||
HttpRequest.newBuilder()
|
||||
.uri(
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package stirling.software.proprietary.security.database;
|
||||
|
||||
public interface DatabaseNotificationServiceInterface {
|
||||
void notifyBackupsSuccess(String subject, String message);
|
||||
|
||||
void notifyBackupsFailure(String subject, String message);
|
||||
|
||||
void notifyImportsSuccess(String subject, String message);
|
||||
|
||||
void notifyImportsFailure(String subject, String message);
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package stirling.software.proprietary.security.database.service;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import jakarta.mail.MessagingException;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.Premium.EnterpriseFeatures.DatabaseNotifications;
|
||||
import stirling.software.proprietary.security.database.DatabaseNotificationServiceInterface;
|
||||
import stirling.software.proprietary.security.service.EmailService;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class DatabaseNotificationService implements DatabaseNotificationServiceInterface {
|
||||
|
||||
private final Optional<EmailService> emailService;
|
||||
private final ApplicationProperties props;
|
||||
private final boolean runningEE;
|
||||
private DatabaseNotifications notifications;
|
||||
|
||||
DatabaseNotificationService(
|
||||
Optional<EmailService> emailService,
|
||||
ApplicationProperties props,
|
||||
@Qualifier("runningEE") boolean runningEE) {
|
||||
this.emailService = emailService;
|
||||
this.props = props;
|
||||
this.runningEE = runningEE;
|
||||
notifications = props.getPremium().getEnterpriseFeatures().getDatabaseNotifications();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyBackupsSuccess(String subject, String message) {
|
||||
if (notifications.getBackups().isSuccessful() && runningEE) {
|
||||
sendMail(subject, message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyBackupsFailure(String subject, String message) {
|
||||
if (notifications.getBackups().isFailed() && runningEE) {
|
||||
sendMail(subject, message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyImportsSuccess(String subject, String message) {
|
||||
if (notifications.getImports().isSuccessful() && runningEE) {
|
||||
sendMail(subject, message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyImportsFailure(String subject, String message) {
|
||||
if (notifications.getImports().isFailed() && runningEE) {
|
||||
sendMail(subject, message);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMail(String subject, String message) {
|
||||
emailService.ifPresent(
|
||||
service -> {
|
||||
try {
|
||||
String to = props.getMail().getFrom();
|
||||
service.sendSimpleMail(to, subject, message);
|
||||
} catch (MessagingException e) {
|
||||
log.error("Error sending notification email: {}", e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -18,7 +18,8 @@ public class AttemptCounter {
|
||||
}
|
||||
|
||||
public boolean shouldReset(long attemptIncrementTime) {
|
||||
return System.currentTimeMillis() - lastAttemptTime > attemptIncrementTime;
|
||||
long elapsed = System.currentTimeMillis() - lastAttemptTime;
|
||||
return elapsed >= attemptIncrementTime;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@ import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -129,7 +130,7 @@ public class User implements UserDetails, Serializable {
|
||||
}
|
||||
|
||||
public void setAuthenticationType(AuthenticationType authenticationType) {
|
||||
this.authenticationType = authenticationType.toString().toLowerCase();
|
||||
this.authenticationType = authenticationType.toString().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
public void addAuthorities(Set<Authority> authorities) {
|
||||
|
||||
+4
-2
@@ -7,6 +7,7 @@ import static stirling.software.common.util.ValidationUtils.isStringEmpty;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -197,7 +198,7 @@ public class OAuth2Configuration {
|
||||
|
||||
String name = oauth.getProvider();
|
||||
String firstChar = String.valueOf(name.charAt(0));
|
||||
String clientName = name.replaceFirst(firstChar, firstChar.toUpperCase());
|
||||
String clientName = name.replaceFirst(firstChar, firstChar.toUpperCase(Locale.ROOT));
|
||||
|
||||
Provider oidcProvider =
|
||||
new Provider(
|
||||
@@ -207,7 +208,8 @@ public class OAuth2Configuration {
|
||||
oauth.getClientId(),
|
||||
oauth.getClientSecret(),
|
||||
oauth.getScopes(),
|
||||
UsernameAttribute.valueOf(oauth.getUseAsUsername().toUpperCase()),
|
||||
UsernameAttribute.valueOf(
|
||||
oauth.getUseAsUsername().toUpperCase(Locale.ROOT)),
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ class AppUpdateAuthService implements ShowAdminInterface {
|
||||
if (!showUpdate) {
|
||||
return showUpdate;
|
||||
}
|
||||
boolean showUpdateOnlyAdmin = applicationProperties.getSystem().getShowUpdateOnlyAdmin();
|
||||
boolean showUpdateOnlyAdmin = applicationProperties.getSystem().isShowUpdateOnlyAdmin();
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
return !showUpdateOnlyAdmin;
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
package stirling.software.proprietary.security.service;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
@@ -60,7 +62,7 @@ public class CustomUserDetailsService implements UserDetailsService {
|
||||
}
|
||||
|
||||
AuthenticationType userAuthenticationType =
|
||||
AuthenticationType.valueOf(authTypeStr.toUpperCase());
|
||||
AuthenticationType.valueOf(authTypeStr.toUpperCase(Locale.ROOT));
|
||||
if (!user.hasPassword() && userAuthenticationType == AuthenticationType.WEB) {
|
||||
throw new IllegalArgumentException("Password must not be null");
|
||||
}
|
||||
|
||||
+63
-1
@@ -7,7 +7,10 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
@@ -32,6 +35,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.FileInfo;
|
||||
import stirling.software.proprietary.security.database.DatabaseNotificationServiceInterface;
|
||||
import stirling.software.proprietary.security.model.exception.BackupNotFoundException;
|
||||
|
||||
@Slf4j
|
||||
@@ -44,12 +48,16 @@ public class DatabaseService implements DatabaseServiceInterface {
|
||||
|
||||
private final ApplicationProperties.Datasource datasourceProps;
|
||||
private final DataSource dataSource;
|
||||
private final DatabaseNotificationServiceInterface backupNotificationService;
|
||||
|
||||
public DatabaseService(
|
||||
ApplicationProperties.Datasource datasourceProps, DataSource dataSource) {
|
||||
ApplicationProperties.Datasource datasourceProps,
|
||||
DataSource dataSource,
|
||||
DatabaseNotificationServiceInterface backupNotificationService) {
|
||||
this.BACKUP_DIR = Paths.get(InstallationPathConfig.getBackupPath()).normalize();
|
||||
this.datasourceProps = datasourceProps;
|
||||
this.dataSource = dataSource;
|
||||
this.backupNotificationService = backupNotificationService;
|
||||
moveBackupFiles();
|
||||
}
|
||||
|
||||
@@ -172,6 +180,8 @@ public class DatabaseService implements DatabaseServiceInterface {
|
||||
public boolean importDatabaseFromUI(String fileName) {
|
||||
try {
|
||||
importDatabaseFromUI(getBackupFilePath(fileName));
|
||||
backupNotificationService.notifyImportsSuccess(
|
||||
"Database import completed", "Import file: " + fileName);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
log.error(
|
||||
@@ -179,6 +189,9 @@ public class DatabaseService implements DatabaseServiceInterface {
|
||||
fileName,
|
||||
e.getMessage(),
|
||||
e.getCause());
|
||||
backupNotificationService.notifyImportsFailure(
|
||||
"Database import failed",
|
||||
"Import file: " + fileName + " Message: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -219,16 +232,59 @@ public class DatabaseService implements DatabaseServiceInterface {
|
||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
stmt.setString(1, insertOutputFilePath.toString());
|
||||
stmt.execute();
|
||||
backupNotificationService.notifyBackupsSuccess(
|
||||
"Database backup export completed",
|
||||
"Backup file: " + insertOutputFilePath.getFileName());
|
||||
} catch (SQLException e) {
|
||||
log.error("Error during database export: {}", e.getMessage(), e);
|
||||
backupNotificationService.notifyBackupsFailure(
|
||||
"Database backup export failed",
|
||||
"Backup file: "
|
||||
+ insertOutputFilePath.getFileName()
|
||||
+ " Message: "
|
||||
+ e.getMessage());
|
||||
} catch (CannotReadScriptException e) {
|
||||
log.error("Error during database export: File {} not found", insertOutputFilePath);
|
||||
backupNotificationService.notifyBackupsFailure(
|
||||
"Database backup export failed",
|
||||
"Error during database export: File "
|
||||
+ insertOutputFilePath.getFileName()
|
||||
+ " not found. Message: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
|
||||
log.info("Database export completed: {}", insertOutputFilePath);
|
||||
verifyBackup(insertOutputFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean verifyBackup(Path backupPath) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] content = Files.readAllBytes(backupPath);
|
||||
String checksum = bytesToHex(digest.digest(content));
|
||||
log.info("Checksum for {}: {}", backupPath.getFileName(), checksum);
|
||||
|
||||
try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:backupVerify");
|
||||
PreparedStatement stmt = conn.prepareStatement("RUNSCRIPT FROM ?")) {
|
||||
stmt.setString(1, backupPath.toString());
|
||||
stmt.execute();
|
||||
}
|
||||
return true;
|
||||
} catch (IOException | NoSuchAlgorithmException | SQLException e) {
|
||||
log.error("Backup verification failed for {}: {}", backupPath, e.getMessage(), e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String bytesToHex(byte[] hash) {
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
hexString.append(String.format("%02x", b));
|
||||
}
|
||||
return hexString.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Pair<FileInfo, Boolean>> deleteAllBackups() {
|
||||
List<FileInfo> backupList = this.getBackupList();
|
||||
@@ -384,6 +440,12 @@ public class DatabaseService implements DatabaseServiceInterface {
|
||||
*/
|
||||
private void executeDatabaseScript(Path scriptPath) {
|
||||
if (isH2Database()) {
|
||||
|
||||
if (!verifyBackup(scriptPath)) {
|
||||
log.error("Backup verification failed for: {}", scriptPath);
|
||||
throw new IllegalArgumentException("Backup verification failed for: " + scriptPath);
|
||||
}
|
||||
|
||||
String query = "RUNSCRIPT from ?;";
|
||||
|
||||
try (Connection conn = dataSource.getConnection();
|
||||
|
||||
+36
@@ -11,6 +11,7 @@ import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.proprietary.security.model.api.Email;
|
||||
@@ -20,6 +21,7 @@ import stirling.software.proprietary.security.model.api.Email;
|
||||
* JavaMailSender to send the email and is designed to handle both the message content and file
|
||||
* attachments.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(value = "mail.enabled", havingValue = "true", matchIfMissing = false)
|
||||
@@ -72,6 +74,40 @@ public class EmailService {
|
||||
|
||||
// Sends the email via the configured mail sender
|
||||
mailSender.send(message);
|
||||
log.debug(
|
||||
"Email sent successfully to {} with subject: {} body: {}",
|
||||
email.getTo(),
|
||||
email.getSubject(),
|
||||
email.getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a simple email without attachments asynchronously.
|
||||
*
|
||||
* @param to the recipient address
|
||||
* @param subject subject line
|
||||
* @param body message body
|
||||
* @throws MessagingException if sending fails or address is invalid
|
||||
*/
|
||||
@Async
|
||||
public void sendSimpleMail(String to, String subject, String body) throws MessagingException {
|
||||
if (to == null || to.trim().isEmpty()) {
|
||||
throw new MessagingException("Invalid Addresses");
|
||||
}
|
||||
|
||||
ApplicationProperties.Mail mailProperties = applicationProperties.getMail();
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, false);
|
||||
helper.addTo(to);
|
||||
helper.setSubject(subject);
|
||||
helper.setText(body, false);
|
||||
helper.setFrom(mailProperties.getFrom());
|
||||
mailSender.send(message);
|
||||
log.debug(
|
||||
"Simple email sent successfully to {} with subject: {} body: {}",
|
||||
to,
|
||||
subject,
|
||||
body);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+10
-5
@@ -1,5 +1,6 @@
|
||||
package stirling.software.proprietary.security.service;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -45,17 +46,19 @@ public class LoginAttemptService {
|
||||
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
attemptsCache.remove(key.toLowerCase());
|
||||
String normalizedKey = key.toLowerCase(Locale.ROOT);
|
||||
attemptsCache.remove(normalizedKey);
|
||||
}
|
||||
|
||||
public void loginFailed(String key) {
|
||||
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
||||
String normalizedKey = key.toLowerCase(Locale.ROOT);
|
||||
AttemptCounter attemptCounter = attemptsCache.get(normalizedKey);
|
||||
if (attemptCounter == null) {
|
||||
attemptCounter = new AttemptCounter();
|
||||
attemptsCache.put(key.toLowerCase(), attemptCounter);
|
||||
attemptsCache.put(normalizedKey, attemptCounter);
|
||||
} else {
|
||||
if (attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) {
|
||||
attemptCounter.reset();
|
||||
@@ -68,7 +71,8 @@ public class LoginAttemptService {
|
||||
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
||||
String normalizedKey = key.toLowerCase(Locale.ROOT);
|
||||
AttemptCounter attemptCounter = attemptsCache.get(normalizedKey);
|
||||
if (attemptCounter == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -80,7 +84,8 @@ public class LoginAttemptService {
|
||||
// Arbitrarily high number if tracking is disabled
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
||||
String normalizedKey = key.toLowerCase(Locale.ROOT);
|
||||
AttemptCounter attemptCounter = attemptsCache.get(normalizedKey);
|
||||
if (attemptCounter == null) {
|
||||
return MAX_ATTEMPT;
|
||||
}
|
||||
|
||||
+7
-2
@@ -5,6 +5,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
@@ -145,6 +146,7 @@ public class UserService implements UserServiceInterface {
|
||||
return addApiKeyToUser(username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getApiKeyForUser(String username) {
|
||||
User user =
|
||||
findByUsernameIgnoreCase(username)
|
||||
@@ -581,9 +583,10 @@ public class UserService implements UserServiceInterface {
|
||||
.matches();
|
||||
|
||||
List<String> notAllowedUserList = new ArrayList<>();
|
||||
notAllowedUserList.add("ALL_USERS".toLowerCase());
|
||||
notAllowedUserList.add("ALL_USERS".toLowerCase(Locale.ROOT));
|
||||
notAllowedUserList.add("anonymoususer");
|
||||
boolean notAllowedUser = notAllowedUserList.contains(username.toLowerCase());
|
||||
String normalizedUsername = username.toLowerCase(Locale.ROOT);
|
||||
boolean notAllowedUser = notAllowedUserList.contains(normalizedUsername);
|
||||
return (isValidSimpleUsername || isValidEmail) && !notAllowedUser;
|
||||
}
|
||||
|
||||
@@ -631,6 +634,7 @@ public class UserService implements UserServiceInterface {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCurrentUsername() {
|
||||
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
|
||||
@@ -717,6 +721,7 @@ public class UserService implements UserServiceInterface {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTotalUsersCount() {
|
||||
// Count all users in the database
|
||||
long userCount = userRepository.count();
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
package stirling.software.proprietary.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDResources;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDTerminalField;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class FormCopyUtils {
|
||||
|
||||
public boolean hasAnyRotatedPage(PDDocument document) {
|
||||
try {
|
||||
for (PDPage page : document.getPages()) {
|
||||
int rot = page.getRotation();
|
||||
int norm = ((rot % 360) + 360) % 360;
|
||||
if (norm != 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to inspect page rotations: {}", e.getMessage(), e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void copyAndTransformFormFields(
|
||||
PDDocument sourceDocument,
|
||||
PDDocument newDocument,
|
||||
int totalPages,
|
||||
int pagesPerSheet,
|
||||
int cols,
|
||||
int rows,
|
||||
float cellWidth,
|
||||
float cellHeight)
|
||||
throws IOException {
|
||||
|
||||
PDDocumentCatalog sourceCatalog = sourceDocument.getDocumentCatalog();
|
||||
PDAcroForm sourceAcroForm = sourceCatalog.getAcroForm();
|
||||
|
||||
if (sourceAcroForm == null || sourceAcroForm.getFields().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PDDocumentCatalog newCatalog = newDocument.getDocumentCatalog();
|
||||
PDAcroForm newAcroForm = new PDAcroForm(newDocument);
|
||||
newCatalog.setAcroForm(newAcroForm);
|
||||
|
||||
PDResources dr = new PDResources();
|
||||
PDType1Font helvetica = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
|
||||
PDType1Font zapfDingbats = new PDType1Font(Standard14Fonts.FontName.ZAPF_DINGBATS);
|
||||
dr.put(COSName.getPDFName("Helv"), helvetica);
|
||||
dr.put(COSName.getPDFName("ZaDb"), zapfDingbats);
|
||||
newAcroForm.setDefaultResources(dr);
|
||||
newAcroForm.setDefaultAppearance("/Helv 12 Tf 0 g");
|
||||
|
||||
// Temporarily set NeedAppearances to true during field creation
|
||||
newAcroForm.setNeedAppearances(true);
|
||||
|
||||
Map<String, Integer> fieldNameCounters = new HashMap<>();
|
||||
|
||||
// Build widget -> field map once for efficient lookups
|
||||
Map<PDAnnotationWidget, PDField> widgetFieldMap = buildWidgetFieldMap(sourceAcroForm);
|
||||
|
||||
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||
PDPage sourcePage = sourceDocument.getPage(pageIndex);
|
||||
List<PDAnnotation> annotations = sourcePage.getAnnotations();
|
||||
|
||||
if (annotations.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int destinationPageIndex = pageIndex / pagesPerSheet;
|
||||
int adjustedPageIndex = pageIndex % pagesPerSheet;
|
||||
int rowIndex = adjustedPageIndex / cols;
|
||||
int colIndex = adjustedPageIndex % cols;
|
||||
|
||||
if (rowIndex >= rows) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (destinationPageIndex >= newDocument.getNumberOfPages()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PDPage destinationPage = newDocument.getPage(destinationPageIndex);
|
||||
PDRectangle sourceRect = sourcePage.getMediaBox();
|
||||
|
||||
float scaleWidth = cellWidth / sourceRect.getWidth();
|
||||
float scaleHeight = cellHeight / sourceRect.getHeight();
|
||||
float scale = Math.min(scaleWidth, scaleHeight);
|
||||
|
||||
float x = colIndex * cellWidth + (cellWidth - sourceRect.getWidth() * scale) / 2;
|
||||
float y =
|
||||
destinationPage.getMediaBox().getHeight()
|
||||
- ((rowIndex + 1) * cellHeight
|
||||
- (cellHeight - sourceRect.getHeight() * scale) / 2);
|
||||
|
||||
copyBasicFormFields(
|
||||
sourceAcroForm,
|
||||
newAcroForm,
|
||||
sourcePage,
|
||||
destinationPage,
|
||||
x,
|
||||
y,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters,
|
||||
widgetFieldMap);
|
||||
}
|
||||
|
||||
// Generate appearance streams and embed them authoritatively
|
||||
boolean appearancesGenerated = false;
|
||||
try {
|
||||
newAcroForm.refreshAppearances();
|
||||
appearancesGenerated = true;
|
||||
} catch (NoSuchMethodError nsme) {
|
||||
log.warn(
|
||||
"AcroForm.refreshAppearances() not available in this PDFBox version; "
|
||||
+ "leaving NeedAppearances=true for viewer-side rendering.");
|
||||
} catch (Exception t) {
|
||||
log.warn(
|
||||
"Failed to refresh field appearances via AcroForm: {}. "
|
||||
+ "Leaving NeedAppearances=true as fallback.",
|
||||
t.getMessage(),
|
||||
t);
|
||||
}
|
||||
|
||||
// After successful appearance generation, set NeedAppearances to false
|
||||
// to signal that appearance streams are now embedded authoritatively
|
||||
if (appearancesGenerated) {
|
||||
try {
|
||||
newAcroForm.setNeedAppearances(false);
|
||||
} catch (Exception e) {
|
||||
log.debug(
|
||||
"Failed to set NeedAppearances to false: {}. "
|
||||
+ "Appearances were generated but flag could not be updated.",
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void copyBasicFormFields(
|
||||
PDAcroForm sourceAcroForm,
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage sourcePage,
|
||||
PDPage destinationPage,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters,
|
||||
Map<PDAnnotationWidget, PDField> widgetFieldMap) {
|
||||
|
||||
try {
|
||||
List<PDAnnotation> sourceAnnotations = sourcePage.getAnnotations();
|
||||
List<PDAnnotation> destinationAnnotations = destinationPage.getAnnotations();
|
||||
|
||||
for (PDAnnotation annotation : sourceAnnotations) {
|
||||
if (annotation instanceof PDAnnotationWidget widgetAnnotation) {
|
||||
if (widgetAnnotation.getRectangle() == null) {
|
||||
continue;
|
||||
}
|
||||
PDField sourceField =
|
||||
widgetFieldMap != null ? widgetFieldMap.get(widgetAnnotation) : null;
|
||||
if (sourceField == null) {
|
||||
continue; // skip widgets without a matching field
|
||||
}
|
||||
if (!(sourceField instanceof PDTerminalField terminalField)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
FormFieldTypeSupport handler = FormFieldTypeSupport.forField(terminalField);
|
||||
if (handler == null) {
|
||||
log.debug(
|
||||
"Skipping unsupported field type '{}' for widget '{}'",
|
||||
sourceField.getClass().getSimpleName(),
|
||||
Optional.ofNullable(sourceField.getFullyQualifiedName())
|
||||
.orElseGet(sourceField::getPartialName));
|
||||
continue;
|
||||
}
|
||||
|
||||
copyFieldUsingHandler(
|
||||
handler,
|
||||
terminalField,
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
widgetAnnotation,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to copy basic form fields for page {}: {}",
|
||||
pageIndex,
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyFieldUsingHandler(
|
||||
FormFieldTypeSupport handler,
|
||||
PDTerminalField sourceField,
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
List<PDAnnotation> destinationAnnotations,
|
||||
PDAnnotationWidget sourceWidget,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters) {
|
||||
|
||||
try {
|
||||
PDTerminalField newField = handler.createField(newAcroForm);
|
||||
boolean initialized =
|
||||
initializeFieldWithWidget(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
newField,
|
||||
sourceField.getPartialName(),
|
||||
handler.fallbackWidgetName(),
|
||||
sourceWidget,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
handler.copyFromOriginal(sourceField, newField);
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to copy {} field '{}': {}",
|
||||
handler.typeName(),
|
||||
Optional.ofNullable(sourceField.getFullyQualifiedName())
|
||||
.orElseGet(sourceField::getPartialName),
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends PDTerminalField> boolean initializeFieldWithWidget(
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
List<PDAnnotation> destinationAnnotations,
|
||||
T newField,
|
||||
String originalName,
|
||||
String fallbackName,
|
||||
PDAnnotationWidget sourceWidget,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters) {
|
||||
|
||||
String baseName = (originalName != null) ? originalName : fallbackName;
|
||||
String newFieldName = generateUniqueFieldName(baseName, pageIndex, fieldNameCounters);
|
||||
newField.setPartialName(newFieldName);
|
||||
|
||||
PDAnnotationWidget newWidget = new PDAnnotationWidget();
|
||||
PDRectangle sourceRect = sourceWidget.getRectangle();
|
||||
if (sourceRect == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float newX = (sourceRect.getLowerLeftX() * scale) + offsetX;
|
||||
float newY = (sourceRect.getLowerLeftY() * scale) + offsetY;
|
||||
float newWidth = sourceRect.getWidth() * scale;
|
||||
float newHeight = sourceRect.getHeight() * scale;
|
||||
newWidget.setRectangle(new PDRectangle(newX, newY, newWidth, newHeight));
|
||||
newWidget.setPage(destinationPage);
|
||||
|
||||
newField.getWidgets().add(newWidget);
|
||||
newWidget.setParent(newField);
|
||||
newAcroForm.getFields().add(newField);
|
||||
destinationAnnotations.add(newWidget);
|
||||
return true;
|
||||
}
|
||||
|
||||
private String generateUniqueFieldName(
|
||||
String originalName, int pageIndex, Map<String, Integer> fieldNameCounters) {
|
||||
String baseName = "page" + pageIndex + "_" + originalName;
|
||||
|
||||
Integer counter = fieldNameCounters.get(baseName);
|
||||
if (counter == null) {
|
||||
counter = 0;
|
||||
} else {
|
||||
counter++;
|
||||
}
|
||||
fieldNameCounters.put(baseName, counter);
|
||||
|
||||
return counter == 0 ? baseName : baseName + "_" + counter;
|
||||
}
|
||||
|
||||
private Map<PDAnnotationWidget, PDField> buildWidgetFieldMap(PDAcroForm acroForm) {
|
||||
Map<PDAnnotationWidget, PDField> map = new HashMap<>();
|
||||
if (acroForm == null) {
|
||||
return map;
|
||||
}
|
||||
try {
|
||||
for (PDField field : acroForm.getFieldTree()) {
|
||||
List<PDAnnotationWidget> widgets = field.getWidgets();
|
||||
if (widgets == null) {
|
||||
continue;
|
||||
}
|
||||
for (PDAnnotationWidget widget : widgets) {
|
||||
if (widget != null) {
|
||||
map.put(widget, field);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to build widget->field map: {}", e.getMessage(), e);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -1690,9 +1690,9 @@ public class FormUtils {
|
||||
acroForm.getFields().add(field);
|
||||
}
|
||||
|
||||
// Delegation methods to FormCopyUtils for form field transformation
|
||||
// Delegation methods to GeneralFormCopyUtils for form field transformation
|
||||
public boolean hasAnyRotatedPage(PDDocument document) {
|
||||
return FormCopyUtils.hasAnyRotatedPage(document);
|
||||
return stirling.software.common.util.GeneralFormCopyUtils.hasAnyRotatedPage(document);
|
||||
}
|
||||
|
||||
public void copyAndTransformFormFields(
|
||||
@@ -1705,7 +1705,7 @@ public class FormUtils {
|
||||
float cellWidth,
|
||||
float cellHeight)
|
||||
throws IOException {
|
||||
FormCopyUtils.copyAndTransformFormFields(
|
||||
stirling.software.common.util.GeneralFormCopyUtils.copyAndTransformFormFields(
|
||||
sourceDocument,
|
||||
newDocument,
|
||||
totalPages,
|
||||
|
||||
Reference in New Issue
Block a user