Merge remote-tracking branch 'origin/main' into cucumber

This commit is contained in:
Anthony Stirling 2024-05-26 15:32:34 +01:00
commit 48bd060d6e
57 changed files with 1668 additions and 817 deletions

2
.github/FUNDING.yml vendored
View file

@ -10,4 +10,4 @@ liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: ['https://paypal.me/froodleplex?country.x=GB&locale.x=en_GB'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] custom: ['https://www.paypal.com/donate/?hosted_button_id=MN7JPG5G6G3JL'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View file

@ -29,7 +29,7 @@ If you would like to add or modify a translation, please see [How to add new lan
## Docs ## Docs
Documentation for Stirling-PDF is handled in a seperate repository. Please see [Docs repository](https://github.com/Stirling-Tools/Stirling-Tools.github.io) or use "edit this page"-button at the bottom of each page at [https://stirlingtools.com/docs/](https://stirlingtools.com/docs/). Documentation for Stirling-PDF is handled in a separate repository. Please see [Docs repository](https://github.com/Stirling-Tools/Stirling-Tools.github.io) or use "edit this page"-button at the bottom of each page at [https://stirlingtools.com/docs/](https://stirlingtools.com/docs/).
## Fixing Bugs or Adding a New Feature ## Fixing Bugs or Adding a New Feature

View file

@ -159,38 +159,39 @@ Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR
## Supported Languages ## Supported Languages
Stirling PDF currently supports 27! Stirling PDF currently supports 28!
| Language | Progress | | Language | Progress |
| ------------------------------------------- | -------------------------------------- | | ------------------------------------------- | -------------------------------------- |
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) | | English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) | | English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| Arabic (العربية) (ar_AR) | ![41%](https://geps.dev/progress/41) | | Arabic (العربية) (ar_AR) | ![41%](https://geps.dev/progress/41) |
| German (Deutsch) (de_DE) | ![97%](https://geps.dev/progress/97) | | German (Deutsch) (de_DE) | ![100%](https://geps.dev/progress/100) |
| French (Français) (fr_FR) | ![94%](https://geps.dev/progress/94) | | French (Français) (fr_FR) | ![94%](https://geps.dev/progress/94) |
| Spanish (Español) (es_ES) | ![97%](https://geps.dev/progress/97) | | Spanish (Español) (es_ES) | ![96%](https://geps.dev/progress/96) |
| Simplified Chinese (简体中文) (zh_CN) | ![96%](https://geps.dev/progress/96) | | Simplified Chinese (简体中文) (zh_CN) | ![96%](https://geps.dev/progress/96) |
| Traditional Chinese (繁體中文) (zh_TW) | ![96%](https://geps.dev/progress/96) | | Traditional Chinese (繁體中文) (zh_TW) | ![95%](https://geps.dev/progress/95) |
| Catalan (Català) (ca_CA) | ![50%](https://geps.dev/progress/50) | | Catalan (Català) (ca_CA) | ![49%](https://geps.dev/progress/49) |
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) | | Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) |
| Swedish (Svenska) (sv_SE) | ![41%](https://geps.dev/progress/41) | | Swedish (Svenska) (sv_SE) | ![41%](https://geps.dev/progress/41) |
| Polish (Polski) (pl_PL) | ![43%](https://geps.dev/progress/43) | | Polish (Polski) (pl_PL) | ![43%](https://geps.dev/progress/43) |
| Romanian (Română) (ro_RO) | ![40%](https://geps.dev/progress/40) | | Romanian (Română) (ro_RO) | ![40%](https://geps.dev/progress/40) |
| Korean (한국어) (ko_KR) | ![89%](https://geps.dev/progress/89) | | Korean (한국어) (ko_KR) | ![88%](https://geps.dev/progress/88) |
| Portuguese Brazilian (Português) (pt_BR) | ![62%](https://geps.dev/progress/62) | | Portuguese Brazilian (Português) (pt_BR) | ![62%](https://geps.dev/progress/62) |
| Russian (Русский) (ru_RU) | ![89%](https://geps.dev/progress/89) | | Russian (Русский) (ru_RU) | ![88%](https://geps.dev/progress/88) |
| Basque (Euskara) (eu_ES) | ![65%](https://geps.dev/progress/65) | | Basque (Euskara) (eu_ES) | ![64%](https://geps.dev/progress/64) |
| Japanese (日本語) (ja_JP) | ![89%](https://geps.dev/progress/89) | | Japanese (日本語) (ja_JP) | ![88%](https://geps.dev/progress/88) |
| Dutch (Nederlands) (nl_NL) | ![86%](https://geps.dev/progress/86) | | Dutch (Nederlands) (nl_NL) | ![86%](https://geps.dev/progress/86) |
| Greek (Ελληνικά) (el_GR) | ![87%](https://geps.dev/progress/87) | | Greek (Ελληνικά) (el_GR) | ![86%](https://geps.dev/progress/86) |
| Turkish (Türkçe) (tr_TR) | ![99%](https://geps.dev/progress/99) | | Turkish (Türkçe) (tr_TR) | ![99%](https://geps.dev/progress/99) |
| Indonesia (Bahasa Indonesia) (id_ID) | ![80%](https://geps.dev/progress/80) | | Indonesia (Bahasa Indonesia) (id_ID) | ![79%](https://geps.dev/progress/79) |
| Hindi (हिंदी) (hi_IN) | ![81%](https://geps.dev/progress/81) | | Hindi (हिंदी) (hi_IN) | ![80%](https://geps.dev/progress/80) |
| Hungarian (Magyar) (hu_HU) | ![79%](https://geps.dev/progress/79) | | Hungarian (Magyar) (hu_HU) | ![79%](https://geps.dev/progress/79) |
| Bulgarian (Български) (bg_BG) | ![96%](https://geps.dev/progress/96) | | Bulgarian (Български) (bg_BG) | ![96%](https://geps.dev/progress/96) |
| Sebian Latin alphabet (Srpski) (sr_LATN_RS) | ![82%](https://geps.dev/progress/82) | | Sebian Latin alphabet (Srpski) (sr_LATN_RS) | ![81%](https://geps.dev/progress/81) |
| Ukrainian (Українська) (uk_UA) | ![88%](https://geps.dev/progress/88) | | Ukrainian (Українська) (uk_UA) | ![87%](https://geps.dev/progress/87) |
| Slovakian (Slovensky) (sk_SK) | ![96%](https://geps.dev/progress/96) | | Slovakian (Slovensky) (sk_SK) | ![96%](https://geps.dev/progress/96) |
| Czech (Česky) (cs_CZ) | ![94%](https://geps.dev/progress/94) |
## Contributing (creating issues, translations, fixing bugs, etc.) ## Contributing (creating issues, translations, fixing bugs, etc.)
@ -226,7 +227,7 @@ security:
loginAttemptCount: 5 # lock user account after 5 tries loginAttemptCount: 5 # lock user account after 5 tries
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
# initialLogin: # initialLogin:
# username: "admin" # Initial username for the first login (these are defaulted) # username: "admin" # Initial username for the first login
# password: "stirling" # Initial password for the first login # password: "stirling" # Initial password for the first login
# oauth2: # oauth2:
# enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work) # enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
@ -237,6 +238,23 @@ security:
# useAsUsername: "email" # Default is 'email'; custom fields can be used as the username # useAsUsername: "email" # Default is 'email'; custom fields can be used as the username
# scopes: "openid, profile, email" # Specify the scopes for which the application will request permissions # scopes: "openid, profile, email" # Specify the scopes for which the application will request permissions
# provider: "google" # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak' # provider: "google" # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
# client:
# google:
# clientId: "" # Client ID for Google OAuth2
# clientSecret: "" # Client Secret for Google OAuth2
# scopes: "https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile" # Scopes for Google OAuth2
# useAsUsername: "email" # Field to use as the username for Google OAuth2
# github:
# clientId: "" # Client ID for GitHub OAuth2
# clientSecret: "" # Client Secret for GitHub OAuth2
# scopes: "read:user" # Scope for GitHub OAuth2
# useAsUsername: "login" # Field to use as the username for GitHub OAuth2
# keycloak:
# issuer: "http://192.168.0.123:8888/realms/stirling-pdf" # URL of the Keycloak realm's OpenID Connect Discovery endpoint
# clientId: "stirling-pdf" # Client ID for Keycloak OAuth2
# clientSecret: "" # Client Secret for Keycloak OAuth2
# scopes: "openid, profile, email" # Scopes for Keycloak OAuth2
# useAsUsername: "email" # Field to use as the username for Keycloak OAuth2
system: system:
defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc) defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)

View file

@ -12,7 +12,7 @@ plugins {
import com.github.jk1.license.render.* import com.github.jk1.license.render.*
group = 'stirling.software' group = 'stirling.software'
version = '0.24.5' version = '0.24.6'
//17 is lowest but we support and recommend 21 //17 is lowest but we support and recommend 21
sourceCompatibility = '17' sourceCompatibility = '17'

View file

@ -1,5 +1,5 @@
apiVersion: v2 apiVersion: v2
appVersion: 0.24.5 appVersion: 0.24.6
description: locally hosted web application that allows you to perform various operations description: locally hosted web application that allows you to perform various operations
on PDF files on PDF files
home: https://github.com/Stirling-Tools/Stirling-PDF home: https://github.com/Stirling-Tools/Stirling-PDF

View file

@ -13,6 +13,11 @@ ignore = [
'language.direction', 'language.direction',
] ]
[cs_CZ]
ignore = [
'language.direction',
]
[de_DE] [de_DE]
ignore = [ ignore = [
'AddStampRequest.alphabet', 'AddStampRequest.alphabet',

View file

@ -58,7 +58,7 @@ public class ConfigInitializer
Files.exists(userPath) ? Files.readAllLines(userPath) : new ArrayList<>(); Files.exists(userPath) ? Files.readAllLines(userPath) : new ArrayList<>();
List<String> resultLines = new ArrayList<>(); List<String> resultLines = new ArrayList<>();
int position = 0;
for (String templateLine : templateLines) { for (String templateLine : templateLines) {
// Check if the line is a comment // Check if the line is a comment
if (templateLine.trim().startsWith("#")) { if (templateLine.trim().startsWith("#")) {
@ -66,7 +66,7 @@ public class ConfigInitializer
if (!entry.isEmpty()) { if (!entry.isEmpty()) {
// Check if this comment has been uncommented in userLines // Check if this comment has been uncommented in userLines
String key = entry.split(":")[0].trim(); String key = entry.split(":")[0].trim();
addLine(resultLines, userLines, templateLine, key); addLine(resultLines, userLines, templateLine, key, position);
} else { } else {
resultLines.add(templateLine); resultLines.add(templateLine);
} }
@ -74,12 +74,13 @@ public class ConfigInitializer
// Check if the line is a key-value pair // Check if the line is a key-value pair
else if (templateLine.contains(":")) { else if (templateLine.contains(":")) {
String key = templateLine.split(":")[0].trim(); String key = templateLine.split(":")[0].trim();
addLine(resultLines, userLines, templateLine, key); addLine(resultLines, userLines, templateLine, key, position);
} }
// Handle empty lines // Handle empty lines
else if (templateLine.trim().length() == 0) { else if (templateLine.trim().length() == 0) {
resultLines.add(""); resultLines.add("");
} }
position++;
} }
// Write the result to the user settings file // Write the result to the user settings file
@ -94,11 +95,16 @@ public class ConfigInitializer
// TODO check parent value instead of just indent lines for duplicate keys (like enabled etc) // TODO check parent value instead of just indent lines for duplicate keys (like enabled etc)
private static void addLine( private static void addLine(
List<String> resultLines, List<String> userLines, String templateLine, String key) { List<String> resultLines,
List<String> userLines,
String templateLine,
String key,
int position) {
boolean added = false; boolean added = false;
int templateIndentationLevel = getIndentationLevel(templateLine); int templateIndentationLevel = getIndentationLevel(templateLine);
int pos = 0;
for (String settingsLine : userLines) { for (String settingsLine : userLines) {
if (settingsLine.trim().startsWith(key + ":")) { if (settingsLine.trim().startsWith(key + ":") && position == pos) {
int settingsIndentationLevel = getIndentationLevel(settingsLine); int settingsIndentationLevel = getIndentationLevel(settingsLine);
// Check if it is correct settingsLine and has the same parent as templateLine // Check if it is correct settingsLine and has the same parent as templateLine
if (settingsIndentationLevel == templateIndentationLevel) { if (settingsIndentationLevel == templateIndentationLevel) {
@ -107,6 +113,7 @@ public class ConfigInitializer
break; break;
} }
} }
pos++;
} }
if (!added) { if (!added) {
resultLines.add(templateLine); resultLines.add(templateLine);

View file

@ -2,6 +2,8 @@ package stirling.software.SPDF.config.security;
import java.util.*; import java.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -36,7 +38,11 @@ import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationS
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.GithubProvider;
import stirling.software.SPDF.model.ApplicationProperties.GoogleProvider;
import stirling.software.SPDF.model.ApplicationProperties.KeycloakProvider;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.JPATokenRepositoryImpl; import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
@ -47,6 +53,8 @@ public class SecurityConfiguration {
@Autowired private CustomUserDetailsService userDetailsService; @Autowired private CustomUserDetailsService userDetailsService;
private static final Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); return new BCryptPasswordEncoder();
@ -140,6 +148,7 @@ public class SecurityConfiguration {
|| trimmedUri.startsWith("/images/") || trimmedUri.startsWith("/images/")
|| trimmedUri.startsWith("/public/") || trimmedUri.startsWith("/public/")
|| trimmedUri.startsWith("/css/") || trimmedUri.startsWith("/css/")
|| trimmedUri.startsWith("/fonts/")
|| trimmedUri.startsWith("/js/") || trimmedUri.startsWith("/js/")
|| trimmedUri.startsWith( || trimmedUri.startsWith(
"/api/v1/info/status"); "/api/v1/info/status");
@ -150,7 +159,8 @@ public class SecurityConfiguration {
.authenticationProvider(authenticationProvider()); .authenticationProvider(authenticationProvider());
// Handle OAUTH2 Logins // Handle OAUTH2 Logins
if (applicationProperties.getSecurity().getOAUTH2().getEnabled()) { if (applicationProperties.getSecurity().getOAUTH2() != null
&& applicationProperties.getSecurity().getOAUTH2().getEnabled()) {
http.oauth2Login( http.oauth2Login(
oauth2 -> oauth2 ->
@ -181,9 +191,10 @@ public class SecurityConfiguration {
.logout( .logout(
logout -> logout ->
logout.logoutSuccessHandler( logout.logoutSuccessHandler(
new CustomOAuth2LogoutSuccessHandler( new CustomOAuth2LogoutSuccessHandler(
this.applicationProperties, this.applicationProperties,
sessionRegistry()))); sessionRegistry()))
.invalidateHttpSession(true));
} }
} else { } else {
http.csrf(csrf -> csrf.disable()) http.csrf(csrf -> csrf.disable())
@ -200,19 +211,127 @@ public class SecurityConfiguration {
havingValue = "true", havingValue = "true",
matchIfMissing = false) matchIfMissing = false)
public ClientRegistrationRepository clientRegistrationRepository() { public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(this.oidcClientRegistration()); List<ClientRegistration> registrations = new ArrayList<>();
githubClientRegistration().ifPresent(registrations::add);
oidcClientRegistration().ifPresent(registrations::add);
googleClientRegistration().ifPresent(registrations::add);
keycloakClientRegistration().ifPresent(registrations::add);
if (registrations.isEmpty()) {
logger.error("At least one OAuth2 provider must be configured");
System.exit(1);
}
return new InMemoryClientRegistrationRepository(registrations);
} }
private ClientRegistration oidcClientRegistration() { private Optional<ClientRegistration> googleClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2(); OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
return ClientRegistrations.fromIssuerLocation(oauth.getIssuer()) if (oauth == null || !oauth.getEnabled()) {
.registrationId("oidc") return Optional.empty();
.clientId(oauth.getClientId()) }
.clientSecret(oauth.getClientSecret()) Client client = oauth.getClient();
.scope(oauth.getScopes()) if (client == null) {
.userNameAttributeName(oauth.getUseAsUsername()) return Optional.empty();
.clientName("OIDC") }
.build(); GoogleProvider google = client.getGoogle();
return google != null && google.isSettingsValid()
? Optional.of(
ClientRegistration.withRegistrationId("google")
.clientId(google.getClientId())
.clientSecret(google.getClientSecret())
.scope(google.getScopes())
.authorizationUri(google.getAuthorizationuri())
.tokenUri(google.getTokenuri())
.userInfoUri(google.getUserinfouri())
.userNameAttributeName(google.getUseAsUsername())
.clientName("Google")
.redirectUri("{baseUrl}/login/oauth2/code/google")
.authorizationGrantType(
org.springframework.security.oauth2.core
.AuthorizationGrantType.AUTHORIZATION_CODE)
.build())
: Optional.empty();
}
private Optional<ClientRegistration> keycloakClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
KeycloakProvider keycloak = client.getKeycloak();
return keycloak != null && keycloak.isSettingsValid()
? Optional.of(
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
.registrationId("keycloak")
.clientId(keycloak.getClientId())
.clientSecret(keycloak.getClientSecret())
.scope(keycloak.getScopes())
.userNameAttributeName(keycloak.getUseAsUsername())
.clientName("Keycloak")
.build())
: Optional.empty();
}
private Optional<ClientRegistration> githubClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
GithubProvider github = client.getGithub();
return github != null && github.isSettingsValid()
? Optional.of(
ClientRegistration.withRegistrationId("github")
.clientId(github.getClientId())
.clientSecret(github.getClientSecret())
.scope(github.getScopes())
.authorizationUri(github.getAuthorizationuri())
.tokenUri(github.getTokenuri())
.userInfoUri(github.getUserinfouri())
.userNameAttributeName(github.getUseAsUsername())
.clientName("GitHub")
.redirectUri("{baseUrl}/login/oauth2/code/github")
.authorizationGrantType(
org.springframework.security.oauth2.core
.AuthorizationGrantType.AUTHORIZATION_CODE)
.build())
: Optional.empty();
}
private Optional<ClientRegistration> oidcClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
if (oauth == null
|| oauth.getIssuer() == null
|| oauth.getIssuer().isEmpty()
|| oauth.getClientId() == null
|| oauth.getClientId().isEmpty()
|| oauth.getClientSecret() == null
|| oauth.getClientSecret().isEmpty()
|| oauth.getScopes() == null
|| oauth.getScopes().isEmpty()
|| oauth.getUseAsUsername() == null
|| oauth.getUseAsUsername().isEmpty()) {
return Optional.empty();
}
return Optional.of(
ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
.registrationId("oidc")
.clientId(oauth.getClientId())
.clientSecret(oauth.getClientSecret())
.scope(oauth.getScopes())
.userNameAttributeName(oauth.getUseAsUsername())
.clientName("OIDC")
.build());
} }
/* /*

View file

@ -101,6 +101,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
contextPath + "/images/", contextPath + "/images/",
contextPath + "/public/", contextPath + "/public/",
contextPath + "/css/", contextPath + "/css/",
contextPath + "/fonts/",
contextPath + "/js/", contextPath + "/js/",
contextPath + "/pdfjs/", contextPath + "/pdfjs/",
contextPath + "/api/v1/info/status", contextPath + "/api/v1/info/status",

View file

@ -41,6 +41,7 @@ public class CustomOAuth2AuthenticationFailureHandler
} else if (exception instanceof LockedException) { } else if (exception instanceof LockedException) {
logger.error("Account locked: ", exception); logger.error("Account locked: ", exception);
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked"); getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
return;
} else { } else {
logger.error("Unhandled authentication exception", exception); logger.error("Unhandled authentication exception", exception);
super.onAuthenticationFailure(request, response, exception); super.onAuthenticationFailure(request, response, exception);

View file

@ -6,6 +6,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
@ -14,6 +15,8 @@ import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.Provider;
import stirling.software.SPDF.utils.UrlUtils;
public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
@ -33,11 +36,30 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
public void onLogoutSuccess( public void onLogoutSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication) HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException { throws IOException, ServletException {
String param = "logout=true"; String param = "logout=true";
String registrationId = null;
String issuer = null;
String clientId = null;
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2(); OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
String provider = oauth.getProvider() != null ? oauth.getProvider() : "";
if (authentication instanceof OAuth2AuthenticationToken) {
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
registrationId = oauthToken.getAuthorizedClientRegistrationId();
try {
Provider provider = oauth.getClient().get(registrationId);
issuer = provider.getIssuer();
clientId = provider.getClientId();
} catch (Exception e) {
e.printStackTrace();
}
} else {
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
issuer = oauth.getIssuer();
clientId = oauth.getClientId();
}
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) { if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
param = "erroroauth=oauth2AuthenticationErrorWeb"; param = "erroroauth=oauth2AuthenticationErrorWeb";
@ -49,36 +71,46 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
param = "error=oauth2AutoCreateDisabled"; param = "error=oauth2AutoCreateDisabled";
} }
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
HttpSession session = request.getSession(false); HttpSession session = request.getSession(false);
if (session != null) { if (session != null) {
String sessionId = session.getId(); String sessionId = session.getId();
sessionRegistry.removeSessionInformation(sessionId); sessionRegistry.removeSessionInformation(sessionId);
session.invalidate(); session.invalidate();
logger.debug("Session invalidated: " + sessionId); logger.info("Session invalidated: " + sessionId);
} }
switch (provider) { switch (registrationId) {
case "keycloak": case "keycloak":
// Add Keycloak specific logout URL if needed
String logoutUrl = String logoutUrl =
oauth.getIssuer() issuer
+ "/protocol/openid-connect/logout" + "/protocol/openid-connect/logout"
+ "?client_id=" + "?client_id="
+ oauth.getClientId() + clientId
+ "&post_logout_redirect_uri=" + "&post_logout_redirect_uri="
+ response.encodeRedirectURL( + response.encodeRedirectURL(redirect_url);
request.getScheme() logger.info("Redirecting to Keycloak logout URL: " + logoutUrl);
+ "://"
+ request.getHeader("host")
+ "/login?"
+ param);
logger.debug("Redirecting to Keycloak logout URL: " + logoutUrl);
response.sendRedirect(logoutUrl); response.sendRedirect(logoutUrl);
break; break;
case "github":
// Add GitHub specific logout URL if needed
String githubLogoutUrl = "https://github.com/logout";
logger.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
response.sendRedirect(githubLogoutUrl);
break;
case "google": case "google":
// Add Google specific logout URL if needed // Add Google specific logout URL if needed
// String googleLogoutUrl =
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
// + response.encodeRedirectURL(redirect_url);
// logger.info("Redirecting to Google logout URL: " + googleLogoutUrl);
// response.sendRedirect(googleLogoutUrl);
// break;
default: default:
String redirectUrl = request.getContextPath() + "/login?" + param; String redirectUrl = request.getContextPath() + "/login?" + param;
logger.debug("Redirecting to default logout URL: " + redirectUrl); logger.info("Redirecting to default logout URL: " + redirectUrl);
response.sendRedirect(redirectUrl); response.sendRedirect(redirectUrl);
break; break;
} }

View file

@ -1,57 +0,0 @@
package stirling.software.SPDF.config.security.oauth2;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import stirling.software.SPDF.model.ApplicationProperties;
public class CustomOAuthUserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
private static final Logger logger = LoggerFactory.getLogger(CustomOAuthUserService.class);
private final OidcUserService delegate = new OidcUserService();
private ApplicationProperties applicationProperties;
public CustomOAuthUserService(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
String usernameAttribute =
applicationProperties.getSecurity().getOAUTH2().getUseAsUsername();
try {
OidcUser user = delegate.loadUser(userRequest);
Map<String, Object> attributes = new HashMap<>(user.getAttributes());
// Ensure the preferred username attribute is present
if (!attributes.containsKey(usernameAttribute)) {
attributes.put(usernameAttribute, attributes.getOrDefault("email", ""));
usernameAttribute = "email";
logger.info("Adjusted username attribute to use email");
}
// Return a new OidcUser with adjusted attributes
return new DefaultOidcUser(
user.getAuthorities(),
userRequest.getIdToken(),
user.getUserInfo(),
usernameAttribute);
} catch (java.lang.IllegalArgumentException e) {
throw new OAuth2AuthenticationException(
new OAuth2Error(e.getMessage()), e.getMessage(), e);
}
}
}

View file

@ -1,10 +1,13 @@
package stirling.software.SPDF.controller.web; package stirling.software.SPDF.controller.web;
import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@ -21,6 +24,11 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.GithubProvider;
import stirling.software.SPDF.model.ApplicationProperties.GoogleProvider;
import stirling.software.SPDF.model.ApplicationProperties.KeycloakProvider;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.Authority; import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
@ -31,6 +39,7 @@ import stirling.software.SPDF.repository.UserRepository;
public class AccountWebController { public class AccountWebController {
@Autowired ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
private static final Logger logger = LoggerFactory.getLogger(AccountWebController.class);
@GetMapping("/login") @GetMapping("/login")
public String login(HttpServletRequest request, Model model, Authentication authentication) { public String login(HttpServletRequest request, Model model, Authentication authentication) {
@ -38,6 +47,33 @@ public class AccountWebController {
return "redirect:/"; return "redirect:/";
} }
Map<String, String> providerList = new HashMap<>();
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
if (oauth != null) {
if (oauth.isSettingsValid()) {
providerList.put("oidc", "OpenID Connect");
}
Client client = oauth.getClient();
if (client != null) {
GoogleProvider google = client.getGoogle();
if (google.isSettingsValid()) {
providerList.put("google", "Google");
}
GithubProvider github = client.getGithub();
if (github.isSettingsValid()) {
providerList.put("github", "Github");
}
KeycloakProvider keycloak = client.getKeycloak();
if (keycloak.isSettingsValid()) {
providerList.put("keycloak", "Keycloak");
}
}
}
model.addAttribute("providerlist", providerList);
model.addAttribute( model.addAttribute(
"oAuth2Enabled", applicationProperties.getSecurity().getOAUTH2().getEnabled()); "oAuth2Enabled", applicationProperties.getSecurity().getOAUTH2().getEnabled());
@ -80,6 +116,19 @@ public class AccountWebController {
break; break;
case "invalid_token_response": case "invalid_token_response":
erroroauth = "login.oauth2InvalidTokenResponse"; erroroauth = "login.oauth2InvalidTokenResponse";
break;
case "authorization_request_not_found":
erroroauth = "login.oauth2RequestNotFound";
break;
case "access_denied":
erroroauth = "login.oauth2AccessDenied";
break;
case "invalid_user_info_response":
erroroauth = "login.oauth2InvalidUserInfoResponse";
break;
case "invalid_request":
erroroauth = "login.oauth2invalidRequest";
break;
default: default:
break; break;
} }

View file

@ -6,6 +6,8 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource; import org.springframework.context.annotation.PropertySource;
@ -23,6 +25,7 @@ public class ApplicationProperties {
private Metrics metrics; private Metrics metrics;
private AutomaticallyGenerated automaticallyGenerated; private AutomaticallyGenerated automaticallyGenerated;
private AutoPipeline autoPipeline; private AutoPipeline autoPipeline;
private static final Logger logger = LoggerFactory.getLogger(ApplicationProperties.class);
public AutoPipeline getAutoPipeline() { public AutoPipeline getAutoPipeline() {
return autoPipeline != null ? autoPipeline : new AutoPipeline(); return autoPipeline != null ? autoPipeline : new AutoPipeline();
@ -182,13 +185,12 @@ public class ApplicationProperties {
+ oauth2 + oauth2
+ ", initialLogin=" + ", initialLogin="
+ initialLogin + initialLogin
+ ", csrfDisabled=" + ", csrfDisabled="
+ csrfDisabled + csrfDisabled
+ "]"; + "]";
} }
public static class InitialLogin { public static class InitialLogin {
private String username; private String username;
private String password; private String password;
@ -219,22 +221,21 @@ public class ApplicationProperties {
} }
public static class OAUTH2 { public static class OAUTH2 {
private Boolean enabled = false;
private boolean enabled;
private String issuer; private String issuer;
private String clientId; private String clientId;
private String clientSecret; private String clientSecret;
private boolean autoCreateUser; private Boolean autoCreateUser = false;
private String useAsUsername; private String useAsUsername;
private Collection<String> scopes = new ArrayList<>();
private String provider; private String provider;
private Client client = new Client();
private Collection<String> scopes = new ArrayList<String>(); public Boolean getEnabled() {
public boolean getEnabled() {
return enabled; return enabled;
} }
public void setEnabled(boolean enabled) { public void setEnabled(Boolean enabled) {
this.enabled = enabled; this.enabled = enabled;
} }
@ -262,19 +263,16 @@ public class ApplicationProperties {
this.clientSecret = clientSecret; this.clientSecret = clientSecret;
} }
public boolean getAutoCreateUser() { public Boolean getAutoCreateUser() {
return autoCreateUser; return autoCreateUser;
} }
public void setAutoCreateUser(boolean autoCreateUser) { public void setAutoCreateUser(Boolean autoCreateUser) {
this.autoCreateUser = autoCreateUser; this.autoCreateUser = autoCreateUser;
} }
public String getUseAsUsername() { public String getUseAsUsername() {
if (useAsUsername != null && useAsUsername.trim().length() > 0) { return useAsUsername;
return useAsUsername;
}
return "email";
} }
public void setUseAsUsername(String useAsUsername) { public void setUseAsUsername(String useAsUsername) {
@ -293,14 +291,44 @@ public class ApplicationProperties {
return scopes; return scopes;
} }
public void setScopes(String scpoes) { public void setScopes(String scopes) {
List<String> scopesList = List<String> scopesList =
Arrays.stream(scpoes.split(",")) Arrays.stream(scopes.split(","))
.map(String::trim) .map(String::trim)
.collect(Collectors.toList()); .collect(Collectors.toList());
this.scopes.addAll(scopesList); this.scopes.addAll(scopesList);
} }
public Client getClient() {
return client;
}
public void setClient(Client client) {
this.client = client;
}
protected boolean isValid(String value, String name) {
if (value != null && !value.trim().isEmpty()) {
return true;
}
return false;
}
protected boolean isValid(Collection<String> value, String name) {
if (value != null && !value.isEmpty()) {
return true;
}
return false;
}
public boolean isSettingsValid() {
return isValid(this.getIssuer(), "issuer")
&& isValid(this.getClientId(), "clientId")
&& isValid(this.getClientSecret(), "clientSecret")
&& isValid(this.getScopes(), "scopes")
&& isValid(this.getUseAsUsername(), "useAsUsername");
}
@Override @Override
public String toString() { public String toString() {
return "OAUTH2 [enabled=" return "OAUTH2 [enabled="
@ -315,12 +343,353 @@ public class ApplicationProperties {
+ autoCreateUser + autoCreateUser
+ ", useAsUsername=" + ", useAsUsername="
+ useAsUsername + useAsUsername
+ ", provider" + ", provider="
+ provider + provider
+ ", scopes=" + ", scopes="
+ scopes + scopes
+ "]"; + "]";
} }
public static class Client {
private GoogleProvider google = new GoogleProvider();
private GithubProvider github = new GithubProvider();
private KeycloakProvider keycloak = new KeycloakProvider();
public Provider get(String registrationId) throws Exception {
switch (registrationId) {
case "gogole":
return getGoogle();
case "github":
return getGithub();
case "keycloak":
return getKeycloak();
default:
break;
}
throw new Exception("Provider not supported, use custom setting.");
}
public GoogleProvider getGoogle() {
return google;
}
public void setGoogle(GoogleProvider google) {
this.google = google;
}
public GithubProvider getGithub() {
return github;
}
public void setGithub(GithubProvider github) {
this.github = github;
}
public KeycloakProvider getKeycloak() {
return keycloak;
}
public void setKeycloak(KeycloakProvider keycloak) {
this.keycloak = keycloak;
}
@Override
public String toString() {
return "Client [google="
+ google
+ ", github="
+ github
+ ", keycloak="
+ keycloak
+ "]";
}
}
}
}
public static class GoogleProvider extends Provider {
private static final String authorizationUri =
"https://accounts.google.com/o/oauth2/v2/auth";
private static final String tokenUri = "https://www.googleapis.com/oauth2/v4/token";
private static final String userInfoUri =
"https://www.googleapis.com/oauth2/v3/userinfo?alt=json";
public String getAuthorizationuri() {
return authorizationUri;
}
public String getTokenuri() {
return tokenUri;
}
public String getUserinfouri() {
return userInfoUri;
}
private String clientId;
private String clientSecret;
private Collection<String> scopes = new ArrayList<>();
private String useAsUsername = "email";
@Override
public String getClientId() {
return this.clientId;
}
@Override
public void setClientId(String clientId) {
this.clientId = clientId;
}
@Override
public String getClientSecret() {
return this.clientSecret;
}
@Override
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
@Override
public Collection<String> getScopes() {
if (scopes == null || scopes.isEmpty()) {
scopes.add("https://www.googleapis.com/auth/userinfo.email");
scopes.add("https://www.googleapis.com/auth/userinfo.profile");
}
return scopes;
}
@Override
public void setScopes(String scopes) {
this.scopes =
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
}
@Override
public String getUseAsUsername() {
return this.useAsUsername;
}
@Override
public void setUseAsUsername(String useAsUsername) {
this.useAsUsername = useAsUsername;
}
@Override
public String toString() {
return "Google [clientId="
+ clientId
+ ", clientSecret="
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
+ ", scopes="
+ scopes
+ ", useAsUsername="
+ useAsUsername
+ "]";
}
@Override
public String getName() {
return "google";
}
public boolean isSettingsValid() {
return super.isValid(this.getClientId(), "clientId")
&& super.isValid(this.getClientSecret(), "clientSecret")
&& super.isValid(this.getScopes(), "scopes")
&& isValid(this.getUseAsUsername(), "useAsUsername");
}
}
public static class GithubProvider extends Provider {
private static final String authorizationUri = "https://github.com/login/oauth/authorize";
private static final String tokenUri = "https://github.com/login/oauth/access_token";
private static final String userInfoUri = "https://api.github.com/user";
public String getAuthorizationuri() {
return authorizationUri;
}
public String getTokenuri() {
return tokenUri;
}
public String getUserinfouri() {
return userInfoUri;
}
private String clientId;
private String clientSecret;
private Collection<String> scopes = new ArrayList<>();
private String useAsUsername = "login";
@Override
public String getIssuer() {
return new String();
}
@Override
public void setIssuer(String issuer) {}
@Override
public String getClientId() {
return this.clientId;
}
@Override
public void setClientId(String clientId) {
this.clientId = clientId;
}
@Override
public String getClientSecret() {
return this.clientSecret;
}
@Override
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
public Collection<String> getScopes() {
if (scopes == null || scopes.isEmpty()) {
scopes.add("read:user");
}
return scopes;
}
@Override
public void setScopes(String scopes) {
this.scopes =
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
}
@Override
public String getUseAsUsername() {
return this.useAsUsername;
}
@Override
public void setUseAsUsername(String useAsUsername) {
this.useAsUsername = useAsUsername;
}
@Override
public String toString() {
return "GitHub [clientId="
+ clientId
+ ", clientSecret="
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
+ ", scopes="
+ scopes
+ ", useAsUsername="
+ useAsUsername
+ "]";
}
@Override
public String getName() {
return "github";
}
public boolean isSettingsValid() {
return super.isValid(this.getClientId(), "clientId")
&& super.isValid(this.getClientSecret(), "clientSecret")
&& super.isValid(this.getScopes(), "scopes")
&& isValid(this.getUseAsUsername(), "useAsUsername");
}
}
public static class KeycloakProvider extends Provider {
private String issuer;
private String clientId;
private String clientSecret;
private Collection<String> scopes = new ArrayList<>();
private String useAsUsername = "email";
@Override
public String getIssuer() {
return this.issuer;
}
@Override
public void setIssuer(String issuer) {
this.issuer = issuer;
}
@Override
public String getClientId() {
return this.clientId;
}
@Override
public void setClientId(String clientId) {
this.clientId = clientId;
}
@Override
public String getClientSecret() {
return this.clientSecret;
}
@Override
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
@Override
public Collection<String> getScopes() {
if (scopes == null || scopes.isEmpty()) {
scopes.add("openid");
scopes.add("profile");
scopes.add("email");
}
return scopes;
}
public void setScopes(String scopes) {
this.scopes =
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
}
@Override
public String getUseAsUsername() {
return this.useAsUsername;
}
@Override
public void setUseAsUsername(String useAsUsername) {
this.useAsUsername = useAsUsername;
}
@Override
public String toString() {
return "Keycloak [issuer="
+ issuer
+ ", clientId="
+ clientId
+ ", clientSecret="
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
+ ", scopes="
+ scopes
+ ", useAsUsername="
+ useAsUsername
+ "]";
}
@Override
public String getName() {
return "keycloak";
}
public boolean isSettingsValid() {
return isValid(this.getIssuer(), "issuer")
&& isValid(this.getClientId(), "clientId")
&& isValid(this.getClientSecret(), "clientSecret")
&& isValid(this.getScopes(), "scopes")
&& isValid(this.getUseAsUsername(), "useAsUsername");
} }
} }

View file

@ -0,0 +1,87 @@
package stirling.software.SPDF.model;
import java.util.Collection;
public class Provider implements ProviderInterface {
private String name;
public String getName() {
return name;
}
protected boolean isValid(String value, String name) {
if (value != null && !value.trim().isEmpty()) {
return true;
}
return false;
// throw new IllegalArgumentException(getName() + ": " + name + " is required!");
}
protected boolean isValid(Collection<String> value, String name) {
if (value != null && !value.isEmpty()) {
return true;
}
return false;
// throw new IllegalArgumentException(getName() + ": " + name + " is required!");
}
@Override
public Collection<String> getScopes() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getScope'");
}
@Override
public void setScopes(String scopes) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'setScope'");
}
@Override
public String getUseAsUsername() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getUseAsUsername'");
}
@Override
public void setUseAsUsername(String useAsUsername) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'setUseAsUsername'");
}
@Override
public String getIssuer() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getIssuer'");
}
@Override
public void setIssuer(String issuer) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'setIssuer'");
}
@Override
public String getClientSecret() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getClientSecret'");
}
@Override
public void setClientSecret(String clientSecret) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'setClientSecret'");
}
@Override
public String getClientId() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getClientId'");
}
@Override
public void setClientId(String clientId) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'setClientId'");
}
}

View file

@ -0,0 +1,26 @@
package stirling.software.SPDF.model;
import java.util.Collection;
public interface ProviderInterface {
public Collection<String> getScopes();
public void setScopes(String scopes);
public String getUseAsUsername();
public void setUseAsUsername(String useAsUsername);
public String getIssuer();
public void setIssuer(String issuer);
public String getClientSecret();
public void setClientSecret(String clientSecret);
public String getClientId();
public void setClientId(String clientId);
}

View file

@ -5,6 +5,7 @@ public class RequestUriUtils {
public static boolean isStaticResource(String requestURI) { public static boolean isStaticResource(String requestURI) {
return requestURI.startsWith("/css/") return requestURI.startsWith("/css/")
|| requestURI.startsWith("/fonts/")
|| requestURI.startsWith("/js/") || requestURI.startsWith("/js/")
|| requestURI.startsWith("/images/") || requestURI.startsWith("/images/")
|| requestURI.startsWith("/public/") || requestURI.startsWith("/public/")

View file

@ -0,0 +1,15 @@
package stirling.software.SPDF.utils;
import jakarta.servlet.http.HttpServletRequest;
public class UrlUtils {
public static String getOrigin(HttpServletRequest request) {
String scheme = request.getScheme(); // http or https
String serverName = request.getServerName(); // localhost
int serverPort = request.getServerPort(); // 8080
String contextPath = request.getContextPath(); // /myapp
return scheme + "://" + serverName + ":" + serverPort + contextPath;
}
}

View file

@ -452,6 +452,11 @@ login.locked=Your account has been locked.
login.signinTitle=Please sign in login.signinTitle=Please sign in
login.ssoSignIn=تسجيل الدخول عبر تسجيل الدخول الأحادي login.ssoSignIn=تسجيل الدخول عبر تسجيل الدخول الأحادي
login.oauth2AutoCreateDisabled=تم تعطيل مستخدم الإنشاء التلقائي لـ OAuth2 login.oauth2AutoCreateDisabled=تم تعطيل مستخدم الإنشاء التلقائي لـ OAuth2
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=Вашият акаунт е заключен.
login.signinTitle=Моля впишете се login.signinTitle=Моля впишете се
login.ssoSignIn=Влизане чрез еднократно влизане login.ssoSignIn=Влизане чрез еднократно влизане
login.oauth2AutoCreateDisabled=OAUTH2 Автоматично създаване на потребител е деактивирано login.oauth2AutoCreateDisabled=OAUTH2 Автоматично създаване на потребител е деактивирано
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=Compte bloquejat
login.signinTitle=Autenticat login.signinTitle=Autenticat
login.ssoSignIn=Inicia sessió mitjançant l'inici de sessió ún login.ssoSignIn=Inicia sessió mitjançant l'inici de sessió ún
login.oauth2AutoCreateDisabled=L'usuari de creació automàtica OAUTH2 està desactivat login.oauth2AutoCreateDisabled=L'usuari de creació automàtica OAUTH2 està desactivat
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

File diff suppressed because it is too large Load diff

View file

@ -59,8 +59,8 @@ deleteCurrentUserMessage=Der aktuell angemeldete Benutzer kann nicht gelöscht w
deleteUsernameExistsMessage=Der Benutzername existiert nicht und kann nicht gelöscht werden. deleteUsernameExistsMessage=Der Benutzername existiert nicht und kann nicht gelöscht werden.
downgradeCurrentUserMessage=Die Rolle des aktuellen Benutzers kann nicht herabgestuft werden downgradeCurrentUserMessage=Die Rolle des aktuellen Benutzers kann nicht herabgestuft werden
downgradeCurrentUserLongMessage=Die Rolle des aktuellen Benutzers kann nicht herabgestuft werden. Daher wird der aktuelle Benutzer nicht angezeigt. downgradeCurrentUserLongMessage=Die Rolle des aktuellen Benutzers kann nicht herabgestuft werden. Daher wird der aktuelle Benutzer nicht angezeigt.
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user. userAlreadyExistsOAuthMessage=Der Benutzer ist bereits als OAuth2-Benutzer vorhanden.
userAlreadyExistsWebMessage=The user already exists as an web user. userAlreadyExistsWebMessage=Der Benutzer ist bereits als Webbenutzer vorhanden.
error=Fehler error=Fehler
oops=Hoppla! oops=Hoppla!
help=Hilfe help=Hilfe
@ -71,7 +71,7 @@ visitGithub=GitHub-Repository besuchen
donate=Spenden donate=Spenden
color=Farbe color=Farbe
sponsor=Sponsor sponsor=Sponsor
info=Info info=Die Info
@ -105,18 +105,18 @@ pipelineOptions.validateButton=Validieren
############# #############
# NAVBAR # # NAVBAR #
############# #############
navbar.favorite=Favorites navbar.favorite=Favoriten
navbar.darkmode=Dunkler Modus navbar.darkmode=Dunkler Modus
navbar.language=Languages navbar.language=Sprachen
navbar.settings=Einstellungen navbar.settings=Einstellungen
navbar.allTools=Tools navbar.allTools=Werkzeuge
navbar.multiTool=Multi Tools navbar.multiTool=Multitools
navbar.sections.organize=Organize navbar.sections.organize=Organisieren
navbar.sections.convertTo=Convert to PDF navbar.sections.convertTo=In PDF konvertieren
navbar.sections.convertFrom=Convert from PDF navbar.sections.convertFrom=Konvertieren von PDF
navbar.sections.security=Sign & Security navbar.sections.security=Zeichen und Sicherheit
navbar.sections.advance=Advanced navbar.sections.advance=Fortschrittlich
navbar.sections.edit=View & Edit navbar.sections.edit=Anzeigen und Bearbeiten
############# #############
# SETTINGS # # SETTINGS #
@ -185,7 +185,7 @@ adminUserSettings.internalApiUser=Interner API-Benutzer
adminUserSettings.forceChange=Benutzer dazu zwingen, Benutzernamen/Passwort bei der Anmeldung zu ändern adminUserSettings.forceChange=Benutzer dazu zwingen, Benutzernamen/Passwort bei der Anmeldung zu ändern
adminUserSettings.submit=Benutzer speichern adminUserSettings.submit=Benutzer speichern
adminUserSettings.changeUserRole=Benutzerrolle ändern adminUserSettings.changeUserRole=Benutzerrolle ändern
adminUserSettings.authenticated=Authenticated adminUserSettings.authenticated=Authentifiziert
############# #############
# HOME-PAGE # # HOME-PAGE #
@ -452,6 +452,11 @@ login.locked=Ihr Konto wurde gesperrt.
login.signinTitle=Bitte melden Sie sich an. login.signinTitle=Bitte melden Sie sich an.
login.ssoSignIn=Anmeldung per Single Sign-On login.ssoSignIn=Anmeldung per Single Sign-On
login.oauth2AutoCreateDisabled=OAUTH2 Benutzer automatisch erstellen deaktiviert login.oauth2AutoCreateDisabled=OAUTH2 Benutzer automatisch erstellen deaktiviert
login.oauth2RequestNotFound=Autorisierungsanfrage nicht gefunden
login.oauth2InvalidUserInfoResponse=Ungültige Benutzerinformationsantwort
login.oauth2invalidRequest=ungültige Anfrage
login.oauth2AccessDenied=Zugriff abgelehnt
login.oauth2InvalidTokenResponse=Ungültige Token-Antwort
#auto-redact #auto-redact
@ -704,7 +709,7 @@ repair.submit=Reparieren
#flatten #flatten
flatten.title=Abflachen flatten.title=Abflachen
flatten.header=PDFs reduzieren flatten.header=PDFs reduzieren
flatten.flattenOnlyForms=Flatten only forms flatten.flattenOnlyForms=Nur Formulare abflachen
flatten.submit=Abflachen flatten.submit=Abflachen
@ -752,7 +757,7 @@ extractImages.submit=Extrahieren
fileToPDF.title=Datei in PDF fileToPDF.title=Datei in PDF
fileToPDF.header=Beliebige Dateien in PDF konvertieren fileToPDF.header=Beliebige Dateien in PDF konvertieren
fileToPDF.credit=Dieser Dienst verwendet LibreOffice und Unoconv für die Dateikonvertierung. fileToPDF.credit=Dieser Dienst verwendet LibreOffice und Unoconv für die Dateikonvertierung.
fileToPDF.supportedFileTypesInfo=Supported File types fileToPDF.supportedFileTypesInfo=Unterstützte Dateitypen
fileToPDF.supportedFileTypes=Unterstützte Dateitypen sollten die folgenden enthalten, eine vollständige aktualisierte Liste der unterstützten Formate finden Sie jedoch in der LibreOffice-Dokumentation fileToPDF.supportedFileTypes=Unterstützte Dateitypen sollten die folgenden enthalten, eine vollständige aktualisierte Liste der unterstützten Formate finden Sie jedoch in der LibreOffice-Dokumentation
fileToPDF.submit=In PDF konvertieren fileToPDF.submit=In PDF konvertieren
@ -805,7 +810,7 @@ pdfOrganiser.placeholder=(z.B. 1,3,2 oder 4-8,2,10-12 oder 2n-1)
#multiTool #multiTool
multiTool.title=PDF-Multitool multiTool.title=PDF-Multitool
multiTool.header=PDF-Multitool multiTool.header=PDF-Multitool
multiTool.uploadPrompts=File Name multiTool.uploadPrompts=Dateiname
#view pdf #view pdf
viewPdf.title=PDF anzeigen viewPdf.title=PDF anzeigen

View file

@ -452,6 +452,11 @@ login.locked=Ο λογαριασμός σας έχει κλειδωθεί.
login.signinTitle=Παρακαλώ, συνδεθείτε login.signinTitle=Παρακαλώ, συνδεθείτε
login.ssoSignIn=Σύνδεση μέσω μοναδικής σύνδεσης login.ssoSignIn=Σύνδεση μέσω μοναδικής σύνδεσης
login.oauth2AutoCreateDisabled=Απενεργοποιήθηκε ο χρήστης αυτόματης δημιουργίας OAUTH2 login.oauth2AutoCreateDisabled=Απενεργοποιήθηκε ο χρήστης αυτόματης δημιουργίας OAUTH2
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=Your account has been locked.
login.signinTitle=Please sign in login.signinTitle=Please sign in
login.ssoSignIn=Login via Single Sign-on login.ssoSignIn=Login via Single Sign-on
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=Your account has been locked.
login.signinTitle=Please sign in login.signinTitle=Please sign in
login.ssoSignIn=Login via Single Sign-on login.ssoSignIn=Login via Single Sign-on
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=Su cuenta se ha bloqueado.
login.signinTitle=Por favor, inicie sesión login.signinTitle=Por favor, inicie sesión
login.ssoSignIn=Iniciar sesión a través del inicio de sesión único login.ssoSignIn=Iniciar sesión a través del inicio de sesión único
login.oauth2AutoCreateDisabled=Usuario DE creación automática de OAUTH2 DESACTIVADO login.oauth2AutoCreateDisabled=Usuario DE creación automática de OAUTH2 DESACTIVADO
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=Zure kontua blokeatu egin da.
login.signinTitle=Mesedez, hasi saioa login.signinTitle=Mesedez, hasi saioa
login.ssoSignIn=Hasi saioa Saioa hasteko modu bakarraren bidez login.ssoSignIn=Hasi saioa Saioa hasteko modu bakarraren bidez
login.oauth2AutoCreateDisabled=OAUTH2 Sortu automatikoki erabiltzailea desgaituta dago login.oauth2AutoCreateDisabled=OAUTH2 Sortu automatikoki erabiltzailea desgaituta dago
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -114,7 +114,7 @@ navbar.multiTool=Multi Outils
navbar.sections.organize=Oragnisation navbar.sections.organize=Oragnisation
navbar.sections.convertTo=Convertir en PDF navbar.sections.convertTo=Convertir en PDF
navbar.sections.convertFrom=Convertir depuis PDF navbar.sections.convertFrom=Convertir depuis PDF
navbar.sections.security=Signalisation et sécurité navbar.sections.security=Signature et sécurité
navbar.sections.advance=Mode avancé navbar.sections.advance=Mode avancé
navbar.sections.edit=Voir la modification navbar.sections.edit=Voir la modification
@ -452,6 +452,11 @@ login.locked=Votre compte a été verrouillé.
login.signinTitle=Veuillez vous connecter login.signinTitle=Veuillez vous connecter
login.ssoSignIn=Se connecter via l'authentification unique login.ssoSignIn=Se connecter via l'authentification unique
login.oauth2AutoCreateDisabled=OAUTH2 Création automatique d'utilisateur désactivée login.oauth2AutoCreateDisabled=OAUTH2 Création automatique d'utilisateur désactivée
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=आपका खाता लॉक कर दिया गया
login.signinTitle=कृपया साइन इन करें login.signinTitle=कृपया साइन इन करें
login.ssoSignIn=सिंगल साइन - ऑन के ज़रिए लॉग इन करें login.ssoSignIn=सिंगल साइन - ऑन के ज़रिए लॉग इन करें
login.oauth2AutoCreateDisabled=OAUTH2 ऑटो - क्रिएट यूज़र अक्षम किया गया login.oauth2AutoCreateDisabled=OAUTH2 ऑटो - क्रिएट यूज़र अक्षम किया गया
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=A fiókja zárolva lett!
login.signinTitle=Kérjük, jelentkezzen be! login.signinTitle=Kérjük, jelentkezzen be!
login.ssoSignIn=Bejelentkezés egyszeri bejelentkezéssel login.ssoSignIn=Bejelentkezés egyszeri bejelentkezéssel
login.oauth2AutoCreateDisabled=OAUTH2 Felhasználó automatikus létrehozása letiltva login.oauth2AutoCreateDisabled=OAUTH2 Felhasználó automatikus létrehozása letiltva
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=Akun Anda telah dikunci.
login.signinTitle=Silakan masuk login.signinTitle=Silakan masuk
login.ssoSignIn=Masuk melalui Single Sign - on login.ssoSignIn=Masuk melalui Single Sign - on
login.oauth2AutoCreateDisabled=OAUTH2 Buat Otomatis Pengguna Dinonaktifkan login.oauth2AutoCreateDisabled=OAUTH2 Buat Otomatis Pengguna Dinonaktifkan
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=Il tuo account è stato bloccato.
login.signinTitle=Per favore accedi login.signinTitle=Per favore accedi
login.ssoSignIn=Accedi tramite Single Sign-on login.ssoSignIn=Accedi tramite Single Sign-on
login.oauth2AutoCreateDisabled=Creazione automatica utente OAUTH2 DISABILITATA login.oauth2AutoCreateDisabled=Creazione automatica utente OAUTH2 DISABILITATA
login.oauth2RequestNotFound=Richiesta di autorizzazione non trovata
login.oauth2InvalidUserInfoResponse=Risposta relativa alle informazioni utente non valida
login.oauth2invalidRequest=Richiesta non valida
login.oauth2AccessDenied=Accesso negato
login.oauth2InvalidTokenResponse=Risposta token non valida
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=あなたのアカウントはロックされています。
login.signinTitle=サインインしてください login.signinTitle=サインインしてください
login.ssoSignIn=シングルサインオンでログイン login.ssoSignIn=シングルサインオンでログイン
login.oauth2AutoCreateDisabled=OAuth 2自動作成ユーザーが無効 login.oauth2AutoCreateDisabled=OAuth 2自動作成ユーザーが無効
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=계정이 잠겼습니다.
login.signinTitle=로그인해 주세요. login.signinTitle=로그인해 주세요.
login.ssoSignIn=싱글사인온을 통한 로그인 login.ssoSignIn=싱글사인온을 통한 로그인
login.oauth2AutoCreateDisabled=OAUTH2 사용자 자동 생성 비활성화됨 login.oauth2AutoCreateDisabled=OAUTH2 사용자 자동 생성 비활성화됨
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=Je account is geblokkeerd.
login.signinTitle=Gelieve in te loggen login.signinTitle=Gelieve in te loggen
login.ssoSignIn=Inloggen via Single Sign-on login.ssoSignIn=Inloggen via Single Sign-on
login.oauth2AutoCreateDisabled=OAUTH2 Automatisch aanmaken gebruiker uitgeschakeld login.oauth2AutoCreateDisabled=OAUTH2 Automatisch aanmaken gebruiker uitgeschakeld
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=Your account has been locked.
login.signinTitle=Please sign in login.signinTitle=Please sign in
login.ssoSignIn=Zaloguj się za pomocą logowania jednokrotnego login.ssoSignIn=Zaloguj się za pomocą logowania jednokrotnego
login.oauth2AutoCreateDisabled=Wyłączono automatyczne tworzenie użytkownika OAUTH2 login.oauth2AutoCreateDisabled=Wyłączono automatyczne tworzenie użytkownika OAUTH2
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=Your account has been locked.
login.signinTitle=Please sign in login.signinTitle=Please sign in
login.ssoSignIn=Iniciar sessão através de início de sessão único login.ssoSignIn=Iniciar sessão através de início de sessão único
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Criar Usuário Desativado login.oauth2AutoCreateDisabled=OAUTH2 Auto-Criar Usuário Desativado
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=A sua conta foi bloqueada.
login.signinTitle=Introduza os seus dados de acesso login.signinTitle=Introduza os seus dados de acesso
login.ssoSignIn=Iniciar sessão através de início de sessão único login.ssoSignIn=Iniciar sessão através de início de sessão único
login.oauth2AutoCreateDisabled=OAUTH2 Criação Automática de Utilizador Desativada login.oauth2AutoCreateDisabled=OAUTH2 Criação Automática de Utilizador Desativada
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=Your account has been locked.
login.signinTitle=Please sign in login.signinTitle=Please sign in
login.ssoSignIn=Conectare prin conectare unică login.ssoSignIn=Conectare prin conectare unică
login.oauth2AutoCreateDisabled=OAUTH2 Creare automată utilizator dezactivată login.oauth2AutoCreateDisabled=OAUTH2 Creare automată utilizator dezactivată
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=Ваша учетная запись заблокирована.
login.signinTitle=Пожалуйста, войдите login.signinTitle=Пожалуйста, войдите
login.ssoSignIn=Вход через единый вход login.ssoSignIn=Вход через единый вход
login.oauth2AutoCreateDisabled=OAUTH2 Автоматическое создание пользователя отключено login.oauth2AutoCreateDisabled=OAUTH2 Автоматическое создание пользователя отключено
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=Váš účet bol uzamknutý.
login.signinTitle=Prosím, prihláste sa login.signinTitle=Prosím, prihláste sa
login.ssoSignIn=Prihlásiť sa cez Single Sign-on login.ssoSignIn=Prihlásiť sa cez Single Sign-on
login.oauth2AutoCreateDisabled=Vytváranie používateľa cez OAUTH2 je zakázané login.oauth2AutoCreateDisabled=Vytváranie používateľa cez OAUTH2 je zakázané
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=Vaš nalog je zaključan.
login.signinTitle=Molimo vas da se prijavite login.signinTitle=Molimo vas da se prijavite
login.ssoSignIn=Prijavite se putem jedinstvene prijave login.ssoSignIn=Prijavite se putem jedinstvene prijave
login.oauth2AutoCreateDisabled=OAUTH2 automatsko kreiranje korisnika je onemogućeno login.oauth2AutoCreateDisabled=OAUTH2 automatsko kreiranje korisnika je onemogućeno
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=Your account has been locked.
login.signinTitle=Please sign in login.signinTitle=Please sign in
login.ssoSignIn=Logga in via enkel inloggning login.ssoSignIn=Logga in via enkel inloggning
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User inaktiverad login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User inaktiverad
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=Hesabınız kilitlendi.
login.signinTitle=Lütfen giriş yapınız. login.signinTitle=Lütfen giriş yapınız.
login.ssoSignIn=Tek Oturum Açma ile Giriş Yap login.ssoSignIn=Tek Oturum Açma ile Giriş Yap
login.oauth2AutoCreateDisabled=OAUTH2 Otomatik Oluşturma Kullanıcı Devre Dışı Bırakıldı login.oauth2AutoCreateDisabled=OAUTH2 Otomatik Oluşturma Kullanıcı Devre Dışı Bırakıldı
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -452,6 +452,11 @@ login.locked=Ваш обліковий запис заблоковано.
login.signinTitle=Будь ласка, увійдіть login.signinTitle=Будь ласка, увійдіть
login.ssoSignIn=Увійти через єдиний вхід login.ssoSignIn=Увійти через єдиний вхід
login.oauth2AutoCreateDisabled=Автоматичне створення користувача OAUTH2 ВИМКНЕНО login.oauth2AutoCreateDisabled=Автоматичне створення користувача OAUTH2 ВИМКНЕНО
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -105,18 +105,18 @@ pipelineOptions.validateButton=验证
############# #############
# NAVBAR # # NAVBAR #
############# #############
navbar.favorite=Favorites navbar.favorite=收藏
navbar.darkmode=模式 navbar.darkmode=模式
navbar.language=Languages navbar.language=语言
navbar.settings=设置 navbar.settings=设置
navbar.allTools=Tools navbar.allTools=工具箱
navbar.multiTool=Multi Tools navbar.multiTool=多功能工具
navbar.sections.organize=Organize navbar.sections.organize=组织
navbar.sections.convertTo=Convert to PDF navbar.sections.convertTo=转换成PDF
navbar.sections.convertFrom=Convert from PDF navbar.sections.convertFrom=从PDF转换
navbar.sections.security=Sign & Security navbar.sections.security=签名和安全
navbar.sections.advance=Advanced navbar.sections.advance=高级功能
navbar.sections.edit=View & Edit navbar.sections.edit=查看和编辑
############# #############
# SETTINGS # # SETTINGS #
@ -452,6 +452,11 @@ login.locked=您的账户已被锁定。
login.signinTitle=请登录 login.signinTitle=请登录
login.ssoSignIn=通过单点登录登录 login.ssoSignIn=通过单点登录登录
login.oauth2AutoCreateDisabled=OAUTH2自动创建用户已禁用 login.oauth2AutoCreateDisabled=OAUTH2自动创建用户已禁用
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact
@ -805,7 +810,7 @@ pdfOrganiser.placeholder=(例如 1,3,2 或 4-8,2,10-12 或 2n-1
#multiTool #multiTool
multiTool.title=PDF多功能工具 multiTool.title=PDF多功能工具
multiTool.header=PDF多功能工具 multiTool.header=PDF多功能工具
multiTool.uploadPrompts=File Name multiTool.uploadPrompts=文件名
#view pdf #view pdf
viewPdf.title=浏览PDF viewPdf.title=浏览PDF

View file

@ -452,6 +452,11 @@ login.locked=您的帳戶已被鎖定。
login.signinTitle=請登入 login.signinTitle=請登入
login.ssoSignIn=透過織網單一簽入 login.ssoSignIn=透過織網單一簽入
login.oauth2AutoCreateDisabled=OAUTH2自動建立使用者已停用 login.oauth2AutoCreateDisabled=OAUTH2自動建立使用者已停用
login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
#auto-redact #auto-redact

View file

@ -19,6 +19,23 @@ security:
# useAsUsername: "email" # Default is 'email'; custom fields can be used as the username # useAsUsername: "email" # Default is 'email'; custom fields can be used as the username
# scopes: "openid, profile, email" # Specify the scopes for which the application will request permissions # scopes: "openid, profile, email" # Specify the scopes for which the application will request permissions
# provider: "google" # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak' # provider: "google" # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
# client:
# google:
# clientId: "" # Client ID for Google OAuth2
# clientSecret: "" # Client Secret for Google OAuth2
# scopes: "https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile" # Scopes for Google OAuth2
# useAsUsername: "email" # Field to use as the username for Google OAuth2
# github:
# clientId: "" # Client ID for GitHub OAuth2
# clientSecret: "" # Client Secret for GitHub OAuth2
# scopes: "read:user" # Scope for GitHub OAuth2
# useAsUsername: "login" # Field to use as the username for GitHub OAuth2
# keycloak:
# issuer: "http://192.168.0.123:8888/realms/stirling-pdf" # URL of the Keycloak realm's OpenID Connect Discovery endpoint
# clientId: "stirling-pdf" # Client ID for Keycloak OAuth2
# clientSecret: "" # Client Secret for Keycloak OAuth2
# scopes: "openid, profile, email" # Scopes for Keycloak OAuth2
# useAsUsername: "email" # Field to use as the username for Keycloak OAuth2
system: system:
defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc) defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)

View file

@ -60,22 +60,22 @@ function handleDropdownItemClick(event) {
} }
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll(".col-lg-2.col-sm-6").forEach((element) => { document.querySelectorAll(".col-lg-2.col-sm-6").forEach((element) => {
const dropdownItems = element.querySelectorAll(".dropdown-item"); const dropdownItems = element.querySelectorAll(".dropdown-item");
const items = Array.from(dropdownItems).filter(item => !item.querySelector("hr.dropdown-divider")); const items = Array.from(dropdownItems).filter(item => !item.querySelector("hr.dropdown-divider"));
if (items.length <= 2) { if (items.length <= 2) {
if ( if (
element.previousElementSibling && element.previousElementSibling &&
element.previousElementSibling.classList.contains("col-lg-2") && element.previousElementSibling.classList.contains("col-lg-2") &&
element.previousElementSibling.classList.contains("nav-item-separator") element.previousElementSibling.classList.contains("nav-item-separator")
) { ) {
element.previousElementSibling.remove(); element.previousElementSibling.remove();
} }
element.remove(); element.remove();
} }
}); });
//Sort languages by alphabet //Sort languages by alphabet
const list = Array.from(document.querySelector('.dropdown-menu[aria-labelledby="languageDropdown"]').children).filter( const list = Array.from(document.querySelector('.dropdown-menu[aria-labelledby="languageDropdown"]').children).filter(

View file

@ -40,7 +40,6 @@ document.querySelector("#navbarSearchInput").addEventListener("input", function
var itemHref = item.getAttribute("href"); var itemHref = item.getAttribute("href");
var tags = item.getAttribute("data-bs-tags") || ""; // If no tags, default to empty string var tags = item.getAttribute("data-bs-tags") || ""; // If no tags, default to empty string
if (titleElement && iconElement && itemHref !== "#") { if (titleElement && iconElement && itemHref !== "#") {
var title = titleElement.innerText; var title = titleElement.innerText;
if ( if (

View file

@ -20,13 +20,13 @@
<p th:text="#{pdfToPDFA.tip}"></p> <p th:text="#{pdfToPDFA.tip}"></p>
<form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/pdf/pdfa}"> <form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/pdf/pdfa}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="mb-3"> <div class="mb-3">
<label th:text="#{pdfToPDFA.outputFormat}"></label> <label th:text="#{pdfToPDFA.outputFormat}"></label>
<select class="form-control" name="outputFormat"> <select class="form-control" name="outputFormat">
<option value="pdfa-1">PDF/A-1b</option> <option value="pdfa-1">PDF/A-1b</option>
<option value="pdfa">PDF/A-2b</option> <option value="pdfa">PDF/A-2b</option>
</select> </select>
</div> </div>
<br> <br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pdfToPDFA.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pdfToPDFA.submit}"></button>
</form> </form>

View file

@ -6,10 +6,10 @@
<button type="button" class="btn btn-secondary" onclick="copytrace()" th:text="#{error.copyStack}"></button> <button type="button" class="btn btn-secondary" onclick="copytrace()" th:text="#{error.copyStack}"></button>
<button type="button" class="btn btn-info" onclick="showHelp()" th:text="#{help}"></button> <button type="button" class="btn btn-info" onclick="showHelp()" th:text="#{help}"></button>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close" onclick="dismissError()"> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close" onclick="dismissError()">
<span class="material-symbols-rounded"> <span class="material-symbols-rounded">
close close
</span> </span>
</button> </button>
<!-- Stack trace section --> <!-- Stack trace section -->
<div id="trace" style="max-height: 0; overflow: hidden;"> <div id="trace" style="max-height: 0; overflow: hidden;">
<div> <div>
@ -23,11 +23,11 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="helpModalLabel" th:text="#{help}"></h5> <h5 class="modal-title" id="helpModalLabel" th:text="#{help}"></h5>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close" onclick="dismissError()"> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close" onclick="dismissError()">
<span class="material-symbols-rounded"> <span class="material-symbols-rounded">
close close
</span> </span>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="container"> <div class="container">

View file

@ -233,7 +233,6 @@
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#" th:href="@{multi-tool}" <a class="nav-link" href="#" th:href="@{multi-tool}"
th:classappend="${currentPage}=='multi-tool' ? 'active' : ''" th:title="#{home.multiTool.desc}"> th:classappend="${currentPage}=='multi-tool' ? 'active' : ''" th:title="#{home.multiTool.desc}">
@ -254,7 +253,6 @@
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#" title="#{home.compressPdfs.title}" th:href="@{compress-pdf}" <a class="nav-link" href="#" title="#{home.compressPdfs.title}" th:href="@{compress-pdf}"
th:classappend="${currentPage}=='compress-pdf' ? 'active' : ''" th:title="#{home.compressPdfs.desc}"> th:classappend="${currentPage}=='compress-pdf' ? 'active' : ''" th:title="#{home.compressPdfs.desc}">
@ -276,8 +274,7 @@
</a> </a>
</li> </li>
<!-- <!-- <li class="nav-item">
<li class="nav-item">
<a class="nav-link" href="#" th:href="@{merge-pdfs}" <a class="nav-link" href="#" th:href="@{merge-pdfs}"
th:classappend="${currentPage}=='merge-pdfs' ? 'active' : ''" th:title="#{home.merge.desc}"> th:classappend="${currentPage}=='merge-pdfs' ? 'active' : ''" th:title="#{home.merge.desc}">
<span class="material-symbols-rounded"> <span class="material-symbols-rounded">
@ -285,8 +282,8 @@
</span> </span>
<span class="icon-text" th:data-text="#{home.merge.title}" th:text="#{home.merge.title}"></span> <span class="icon-text" th:data-text="#{home.merge.title}" th:text="#{home.merge.title}"></span>
</a> </a>
</li> </li> -->
-->
</ul> </ul>
<ul class="navbar-nav flex-nowrap"> <ul class="navbar-nav flex-nowrap">
<li class="nav-item dropdown"> <li class="nav-item dropdown">
@ -329,24 +326,22 @@
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link" href="#" id="searchDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link" href="#" id="searchDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="material-symbols-rounded"> <span class="material-symbols-rounded">
search search
</span> </span>
<span class="icon-text icon-hide">Search</span> <span class="icon-text icon-hide">Search</span>
</a> </a>
<div class="dropdown-menu dropdown-menu-tp" aria-labelledby="searchDropdown"> <div class="dropdown-menu dropdown-menu-tp" aria-labelledby="searchDropdown">
<div class="dropdown-menu-wrapper px-xl-2 px-2"> <div class="dropdown-menu-wrapper px-xl-2 px-2">
<form class="d-flex p-2 search-form" id="searchForm"> <form class="d-flex p-2 search-form" id="searchForm">
<input class="form-control search-input" type="search" placeholder="Search" aria-label="Search" id="navbarSearchInput"> <input class="form-control search-input" type="search" placeholder="Search" aria-label="Search" id="navbarSearchInput">
</form> </form>
<!-- Search Results --> <!-- Search Results -->
<div id="searchResults" class="search-results scrollable-y dropdown-mw-20"></div> <div id="searchResults" class="search-results scrollable-y dropdown-mw-20"></div>
</div> </div>
</div> </div>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<!-- Settings Button --> <!-- Settings Button -->
@ -405,9 +400,6 @@
target="_blank"></a> target="_blank"></a>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="downloadOption" th:utext="#{settings.downloadOption.title}"></label> <label for="downloadOption" th:utext="#{settings.downloadOption.title}"></label>
<select class="form-control" id="downloadOption"> <select class="form-control" id="downloadOption">

View file

@ -13,8 +13,6 @@
document.addEventListener('modeChanged', function(e) { document.addEventListener('modeChanged', function(e) {
var mode = e.detail; var mode = e.detail;
setInputMode("username", mode);
setInputMode("password", mode);
document.body.classList.remove("light-mode", "dark-mode", "rainbow-mode"); // remove all mode classes first document.body.classList.remove("light-mode", "dark-mode", "rainbow-mode"); // remove all mode classes first
switch (mode) { switch (mode) {
@ -117,7 +115,7 @@
<h1 class="h1 mb-3 fw-normal" th:text="${@appName}">Stirling-PDF</h1> <h1 class="h1 mb-3 fw-normal" th:text="${@appName}">Stirling-PDF</h1>
<div th:if="${oAuth2Enabled}"> <div th:if="${oAuth2Enabled}">
<a href="oauth2/authorization/oidc" class="w-100 btn btn-lg btn-primary" th:text="#{login.ssoSignIn}">Login Via SSO</a> <a href="#" class="w-100 btn btn-lg btn-primary" data-bs-toggle="modal" data-bs-target="#loginsModal" th:text="#{login.ssoSignIn}">Login Via SSO</a>
<br> <br>
<br> <br>
<div th:if="${erroroauth}" class="alert alert-danger text-center"> <div th:if="${erroroauth}" class="alert alert-danger text-center">
@ -126,7 +124,7 @@
<hr /> <hr />
</div> </div>
<div th:if="${error}" class="alert alert-danger text-danger text-center"> <div th:if="${error}" class="alert alert-danger text-center">
<div th:if="${error}" th:text="#{${error}}">OAuth2: Error Message</div> <div th:if="${error}" th:text="#{${error}}">OAuth2: Error Message</div>
</div> </div>
<div class="text-danger text-center"> <div class="text-danger text-center">
@ -170,5 +168,27 @@
</main> </main>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block> <th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
<div th:if="${oAuth2Enabled}" class="modal fade" id="loginsModal" tabindex="-1" role="dialog" aria-labelledby="loginsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="loginsModalLabel" th:text="#{login.ssoSignIn}"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">
close
</span>
</button>
</div>
<div class="modal-body">
<div class="mb-3" th:each="provider : ${providerlist}">
<a th:href="@{|/oauth2/authorization/${provider.key}|}" th:text="${provider.value}" class="w-100 btn btn-lg btn-primary">OpenID Connect</a>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button>
</div>
</div>
</div>
</div>
</body> </body>
</html> </html>

View file

@ -25,7 +25,7 @@
<input type="checkbox" id="flattenOnlyForms" name="flattenOnlyForms"> <input type="checkbox" id="flattenOnlyForms" name="flattenOnlyForms">
<label for="flattenOnlyForms" th:text="#{flatten.flattenOnlyForms}" ></label> <label for="flattenOnlyForms" th:text="#{flatten.flattenOnlyForms}" ></label>
</div> </div>
</div> </div>
<br> <br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{flatten.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{flatten.submit}"></button>
</form> </form>

View file

@ -19,8 +19,8 @@
<div class="card mb-3"> <div class="card mb-3">
<div class="card-body"> <div class="card-body">
<h4 th:text="#{printFile.selectText.1}">Select Printer</h4> <!-- Assuming the message code printFile.selectText.3 corresponds to "Select Printer" --> <h4 th:text="#{printFile.selectText.1}">Select Printer</h4> <!-- Assuming the message code printFile.selectText.3 corresponds to "Select Printer" -->
<label for="printerName" th:text="#{printFile.selectText.2}">Printer Name:</label> <!-- Assuming the message code printFile.selectText.4 corresponds to "Printer Name:" --> <label for="printerName" th:text="#{printFile.selectText.2}">Printer Name:</label> <!-- Assuming the message code printFile.selectText.4 corresponds to "Printer Name:" -->
<input type="text" name="printerName" id="printerName" class="form-control"> <input type="text" name="printerName" id="printerName" class="form-control">
</div> </div>
</div> </div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{printFile.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{printFile.submit}"></button>