Compare commits

...

14 Commits

Author SHA1 Message Date
Anthony Stirling
94dbb035cc Delete package-lock.json 2024-10-22 00:39:15 +01:00
Surya Karthikeyan Vijayalakshmi
e046172374 Fixed layering issue with z-index, and added smoother transitions for… (#1996)
Fixed layering issue with z-index, and added smoother transitions for signing

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-10-22 00:29:33 +01:00
github-actions[bot]
edd0ec9d23 Update 3rd Party Licenses (#2056)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-10-22 00:22:01 +01:00
dependabot[bot]
899f3d267b Bump org.commonmark:commonmark from 0.23.0 to 0.24.0 (#2054)
Bumps [org.commonmark:commonmark](https://github.com/commonmark/commonmark-java) from 0.23.0 to 0.24.0.
- [Release notes](https://github.com/commonmark/commonmark-java/releases)
- [Changelog](https://github.com/commonmark/commonmark-java/blob/main/CHANGELOG.md)
- [Commits](https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.23.0...commonmark-parent-0.24.0)

---
updated-dependencies:
- dependency-name: org.commonmark:commonmark
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-22 00:21:14 +01:00
dependabot[bot]
88c0a9e26b Bump org.springframework:spring-webmvc from 6.1.13 to 6.1.14 (#2053)
Bumps [org.springframework:spring-webmvc](https://github.com/spring-projects/spring-framework) from 6.1.13 to 6.1.14.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.13...v6.1.14)

---
updated-dependencies:
- dependency-name: org.springframework:spring-webmvc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-22 00:19:52 +01:00
dependabot[bot]
dc6cec9daf Bump org.commonmark:commonmark-ext-gfm-tables from 0.23.0 to 0.24.0 (#2055)
Bumps [org.commonmark:commonmark-ext-gfm-tables](https://github.com/commonmark/commonmark-java) from 0.23.0 to 0.24.0.
- [Release notes](https://github.com/commonmark/commonmark-java/releases)
- [Changelog](https://github.com/commonmark/commonmark-java/blob/main/CHANGELOG.md)
- [Commits](https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.23.0...commonmark-parent-0.24.0)

---
updated-dependencies:
- dependency-name: org.commonmark:commonmark-ext-gfm-tables
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-22 00:19:14 +01:00
github-actions[bot]
a64dd2e282 📝 Update README: Translation Progress Table (#2047)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-20 21:02:09 +01:00
github-actions[bot]
c9b7d848b4 Update translation files (#2048)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-10-20 21:01:50 +01:00
Anthony Stirling
89a9ba6ebc remove unused translation 2024-10-20 21:00:16 +01:00
Patryk Marszelewski
22249ef9bf Update messages_pl_PL.properties (#2042) 2024-10-20 20:34:39 +01:00
github-actions[bot]
619a863b99 Update 3rd Party Licenses (#2044)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-10-20 20:32:57 +01:00
Peter Dave Hello
e098b2999c Update and improve zh_TW Traditional Chinese locale (#2046)
This is a small follow-up to #2030, but it will significantly improve the user experience for Traditional Chinese users.
2024-10-20 20:32:40 +01:00
IT Creativity + Art Team
1149f2a30d Update messages_bg_BG.properties (#2045)
Update messages_bg_BG.properties
2024-10-20 20:32:18 +01:00
Ludy
eff1843061 Major Enhancements to SAML2 and OAuth2 Integration with Simplified Security Configurations (#2040)
* implement Saml2 login/logout

* changed: deprecation code

* relyingPartyRegistrations only enabled samle
2024-10-20 12:30:58 +01:00
74 changed files with 1718 additions and 1424 deletions

View File

@@ -172,18 +172,18 @@ Stirling PDF currently supports 38!
| Language | Progress | | Language | Progress |
| ------------------------------------------- | -------------------------------------- | | ------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![93%](https://geps.dev/progress/93) | | Arabic (العربية) (ar_AR) | ![94%](https://geps.dev/progress/94) |
| Basque (Euskara) (eu_ES) | ![57%](https://geps.dev/progress/57) | | Basque (Euskara) (eu_ES) | ![57%](https://geps.dev/progress/57) |
| Bulgarian (Български) (bg_BG) | ![86%](https://geps.dev/progress/86) | | Bulgarian (Български) (bg_BG) | ![99%](https://geps.dev/progress/99) |
| Catalan (Català) (ca_CA) | ![44%](https://geps.dev/progress/44) | | Catalan (Català) (ca_CA) | ![44%](https://geps.dev/progress/44) |
| Croatian (Hrvatski) (hr_HR) | ![87%](https://geps.dev/progress/87) | | Croatian (Hrvatski) (hr_HR) | ![87%](https://geps.dev/progress/87) |
| Czech (Česky) (cs_CZ) | ![82%](https://geps.dev/progress/82) | | Czech (Česky) (cs_CZ) | ![83%](https://geps.dev/progress/83) |
| Danish (Dansk) (da_DK) | ![91%](https://geps.dev/progress/91) | | Danish (Dansk) (da_DK) | ![91%](https://geps.dev/progress/91) |
| Dutch (Nederlands) (nl_NL) | ![88%](https://geps.dev/progress/88) | | Dutch (Nederlands) (nl_NL) | ![88%](https://geps.dev/progress/88) |
| 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) |
| French (Français) (fr_FR) | ![85%](https://geps.dev/progress/85) | | French (Français) (fr_FR) | ![85%](https://geps.dev/progress/85) |
| German (Deutsch) (de_DE) | ![93%](https://geps.dev/progress/93) | | German (Deutsch) (de_DE) | ![94%](https://geps.dev/progress/94) |
| Greek (Ελληνικά) (el_GR) | ![75%](https://geps.dev/progress/75) | | Greek (Ελληνικά) (el_GR) | ![75%](https://geps.dev/progress/75) |
| Hindi (हिंदी) (hi_IN) | ![72%](https://geps.dev/progress/72) | | Hindi (हिंदी) (hi_IN) | ![72%](https://geps.dev/progress/72) |
| Hungarian (Magyar) (hu_HU) | ![69%](https://geps.dev/progress/69) | | Hungarian (Magyar) (hu_HU) | ![69%](https://geps.dev/progress/69) |
@@ -193,7 +193,7 @@ Stirling PDF currently supports 38!
| Japanese (日本語) (ja_JP) | ![87%](https://geps.dev/progress/87) | | Japanese (日本語) (ja_JP) | ![87%](https://geps.dev/progress/87) |
| Korean (한국어) (ko_KR) | ![77%](https://geps.dev/progress/77) | | Korean (한국어) (ko_KR) | ![77%](https://geps.dev/progress/77) |
| Norwegian (Norsk) (no_NB) | ![90%](https://geps.dev/progress/90) | | Norwegian (Norsk) (no_NB) | ![90%](https://geps.dev/progress/90) |
| Polish (Polski) (pl_PL) | ![84%](https://geps.dev/progress/84) | | Polish (Polski) (pl_PL) | ![99%](https://geps.dev/progress/99) |
| Portuguese (Português) (pt_PT) | ![72%](https://geps.dev/progress/72) | | Portuguese (Português) (pt_PT) | ![72%](https://geps.dev/progress/72) |
| Portuguese Brazilian (Português) (pt_BR) | ![99%](https://geps.dev/progress/99) | | Portuguese Brazilian (Português) (pt_BR) | ![99%](https://geps.dev/progress/99) |
| Romanian (Română) (ro_RO) | ![92%](https://geps.dev/progress/92) | | Romanian (Română) (ro_RO) | ![92%](https://geps.dev/progress/92) |
@@ -207,7 +207,7 @@ Stirling PDF currently supports 38!
| Traditional Chinese (繁體中文) (zh_TW) | ![99%](https://geps.dev/progress/99) | | Traditional Chinese (繁體中文) (zh_TW) | ![99%](https://geps.dev/progress/99) |
| Turkish (Türkçe) (tr_TR) | ![94%](https://geps.dev/progress/94) | | Turkish (Türkçe) (tr_TR) | ![94%](https://geps.dev/progress/94) |
| Ukrainian (Українська) (uk_UA) | ![82%](https://geps.dev/progress/82) | | Ukrainian (Українська) (uk_UA) | ![82%](https://geps.dev/progress/82) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![90%](https://geps.dev/progress/90) | | Vietnamese (Tiếng Việt) (vi_VN) | ![91%](https://geps.dev/progress/91) |
## Contributing (creating issues, translations, fixing bugs, etc.) ## Contributing (creating issues, translations, fixing bugs, etc.)

View File

@@ -32,11 +32,9 @@ java {
repositories { repositories {
mavenCentral() mavenCentral()
maven { url "https://jitpack.io" } maven { url "https://jitpack.io" }
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
maven { maven {
url "https://build.shibboleth.net/nexus/content/repositories/releases/" url 'https://build.shibboleth.net/maven/releases'
}
maven {
url "https://build.shibboleth.net/maven/releases/"
} }
} }
@@ -121,7 +119,7 @@ configurations.all {
} }
dependencies { dependencies {
//security updates //security updates
implementation "org.springframework:spring-webmvc:6.1.13" implementation "org.springframework:spring-webmvc:6.1.14"
implementation("io.github.pixee:java-security-toolkit:1.2.0") implementation("io.github.pixee:java-security-toolkit:1.2.0")
@@ -148,6 +146,14 @@ dependencies {
//2.2.x requires rebuild of DB file.. need migration path //2.2.x requires rebuild of DB file.. need migration path
runtimeOnly "com.h2database:h2:2.1.214" runtimeOnly "com.h2database:h2:2.1.214"
// implementation "com.h2database:h2:2.2.224" // implementation "com.h2database:h2:2.2.224"
constraints {
implementation "org.opensaml:opensaml-core"
implementation "org.opensaml:opensaml-saml-api"
implementation "org.opensaml:opensaml-saml-impl"
}
implementation "org.springframework.security:spring-security-saml2-service-provider"
implementation 'com.coveo:saml-client:5.0.0'
} }
testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion" testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
@@ -198,8 +204,8 @@ dependencies {
implementation "io.micrometer:micrometer-core:1.13.6" implementation "io.micrometer:micrometer-core:1.13.6"
implementation group: "com.google.zxing", name: "core", version: "3.5.3" implementation group: "com.google.zxing", name: "core", version: "3.5.3"
// https://mvnrepository.com/artifact/org.commonmark/commonmark // https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation "org.commonmark:commonmark:0.23.0" implementation "org.commonmark:commonmark:0.24.0"
implementation "org.commonmark:commonmark-ext-gfm-tables:0.23.0" implementation "org.commonmark:commonmark-ext-gfm-tables:0.24.0"
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17 // https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
implementation "com.bucket4j:bucket4j_jdk17-core:8.14.0" implementation "com.bucket4j:bucket4j_jdk17-core:8.14.0"
implementation "com.fathzer:javaluator:3.0.5" implementation "com.fathzer:javaluator:3.0.5"

View File

@@ -0,0 +1,20 @@
#!/bin/bash
# Check if a key was provided
if [ $# -eq 0 ]; then
echo "Please provide a key to remove."
exit 1
fi
key_to_remove="$1"
for file in ../src/main/resources/messages_*.properties; do
# If the key ends with a dot, remove all keys starting with it
if [[ "$key_to_remove" == *. ]]; then
sed -i "/^${key_to_remove//./\\.}/d" "$file"
else
# Otherwise, remove only the exact key match
sed -i "/^${key_to_remove//./\\.}=/d" "$file"
fi
echo "Updated $file"
done

View File

@@ -33,8 +33,12 @@ public class SPdfApplication {
@Autowired private Environment env; @Autowired private Environment env;
@Autowired ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
private static String baseUrlStatic;
private static String serverPortStatic; private static String serverPortStatic;
@Value("${baseUrl:http://localhost}")
private String baseUrl;
@Value("${server.port:8080}") @Value("${server.port:8080}")
public void setServerPortStatic(String port) { public void setServerPortStatic(String port) {
if ("auto".equalsIgnoreCase(port)) { if ("auto".equalsIgnoreCase(port)) {
@@ -65,12 +69,13 @@ public class SPdfApplication {
@PostConstruct @PostConstruct
public void init() { public void init() {
baseUrlStatic = this.baseUrl;
// Check if the BROWSER_OPEN environment variable is set to true // Check if the BROWSER_OPEN environment variable is set to true
String browserOpenEnv = env.getProperty("BROWSER_OPEN"); String browserOpenEnv = env.getProperty("BROWSER_OPEN");
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv); boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
if (browserOpen) { if (browserOpen) {
try { try {
String url = "http://localhost:" + getStaticPort(); String url = baseUrl + ":" + getStaticPort();
String os = System.getProperty("os.name").toLowerCase(); String os = System.getProperty("os.name").toLowerCase();
Runtime rt = Runtime.getRuntime(); Runtime rt = Runtime.getRuntime();
@@ -138,10 +143,18 @@ public class SPdfApplication {
private static void printStartupLogs() { private static void printStartupLogs() {
logger.info("Stirling-PDF Started."); logger.info("Stirling-PDF Started.");
String url = "http://localhost:" + getStaticPort(); String url = baseUrlStatic + ":" + getStaticPort();
logger.info("Navigate to {}", url); logger.info("Navigate to {}", url);
} }
public static String getStaticBaseUrl() {
return baseUrlStatic;
}
public String getNonStaticBaseUrl() {
return baseUrlStatic;
}
public static String getStaticPort() { public static String getStaticPort() {
return serverPortStatic; return serverPortStatic;
} }

View File

@@ -1,27 +1,237 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.io.Resource;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import com.coveo.saml.SamlClient;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.config.security.saml2.CertificateUtils;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.Provider;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.UrlUtils;
@Slf4j
@AllArgsConstructor
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private final ApplicationProperties applicationProperties;
@Override @Override
public void onLogoutSuccess( public void onLogoutSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication) HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException { throws IOException, ServletException {
if (!response.isCommitted()) {
// Handle user logout due to disabled account
if (request.getParameter("userIsDisabled") != null) { if (request.getParameter("userIsDisabled") != null) {
getRedirectStrategy() response.sendRedirect(
.sendRedirect(request, response, "/login?erroroauth=userIsDisabled"); request.getContextPath() + "/login?erroroauth=userIsDisabled");
return; return;
} }
// Handle OAuth2 authentication error
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
response.sendRedirect(
request.getContextPath() + "/login?erroroauth=userAlreadyExistsWeb");
return;
}
if (authentication != null) {
// Handle SAML2 logout redirection
if (authentication instanceof Saml2Authentication) {
getRedirect_saml2(request, response, authentication);
return;
}
// Handle OAuth2 logout redirection
else if (authentication instanceof OAuth2AuthenticationToken) {
getRedirect_oauth2(request, response, authentication);
return;
}
// Handle Username/Password logout
else if (authentication instanceof UsernamePasswordAuthenticationToken) {
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
return;
}
// Handle unknown authentication types
else {
log.error(
"authentication class unknown: "
+ authentication.getClass().getSimpleName());
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
return;
}
} else {
// Redirect to login page after logout
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
return;
}
}
}
// Redirect for SAML2 authentication logout
private void getRedirect_saml2(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
String registrationId = samlConf.getRegistrationId();
Saml2Authentication samlAuthentication = (Saml2Authentication) authentication;
CustomSaml2AuthenticatedPrincipal principal =
(CustomSaml2AuthenticatedPrincipal) samlAuthentication.getPrincipal();
String nameIdValue = principal.getName();
try {
// Read certificate from the resource
Resource certificateResource = samlConf.getSpCert();
X509Certificate certificate = CertificateUtils.readCertificate(certificateResource);
List<X509Certificate> certificates = new ArrayList<>();
certificates.add(certificate);
// Construct URLs required for SAML configuration
String serverUrl =
SPdfApplication.getStaticBaseUrl() + ":" + SPdfApplication.getStaticPort();
String relyingPartyIdentifier =
serverUrl + "/saml2/service-provider-metadata/" + registrationId;
String assertionConsumerServiceUrl = serverUrl + "/login/saml2/sso/" + registrationId;
String idpUrl = samlConf.getIdpSingleLogoutUrl();
String idpIssuer = samlConf.getIdpIssuer();
// Create SamlClient instance for SAML logout
SamlClient samlClient =
new SamlClient(
relyingPartyIdentifier,
assertionConsumerServiceUrl,
idpUrl,
idpIssuer,
certificates,
SamlClient.SamlIdpBinding.POST);
// Read private key for service provider
Resource privateKeyResource = samlConf.getPrivateKey();
RSAPrivateKey privateKey = CertificateUtils.readPrivateKey(privateKeyResource);
// Set service provider keys for the SamlClient
samlClient.setSPKeys(certificate, privateKey);
// Redirect to identity provider for logout
samlClient.redirectToIdentityProvider(response, null, nameIdValue);
} catch (Exception e) {
log.error(nameIdValue, e);
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true"); getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
} }
}
// Redirect for OAuth2 authentication logout
private void getRedirect_oauth2(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
String param = "logout=true";
String registrationId = null;
String issuer = null;
String clientId = null;
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (authentication instanceof OAuth2AuthenticationToken) {
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
registrationId = oauthToken.getAuthorizedClientRegistrationId();
try {
// Get OAuth2 provider details from configuration
Provider provider = oauth.getClient().get(registrationId);
issuer = provider.getIssuer();
clientId = provider.getClientId();
} catch (UnsupportedProviderException e) {
log.error(e.getMessage());
}
} else {
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
issuer = oauth.getIssuer();
clientId = oauth.getClientId();
}
String errorMessage = "";
// Handle different error scenarios during logout
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
param = "erroroauth=oauth2AuthenticationErrorWeb";
} else if ((errorMessage = request.getParameter("error")) != null) {
param = "error=" + sanitizeInput(errorMessage);
} else if ((errorMessage = request.getParameter("erroroauth")) != null) {
param = "erroroauth=" + sanitizeInput(errorMessage);
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
param = "error=oauth2AutoCreateDisabled";
} else if (request.getParameter("oauth2_admin_blocked_user") != null) {
param = "erroroauth=oauth2_admin_blocked_user";
} else if (request.getParameter("userIsDisabled") != null) {
param = "erroroauth=userIsDisabled";
} else if (request.getParameter("badcredentials") != null) {
param = "error=badcredentials";
}
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
// Redirect based on OAuth2 provider
switch (registrationId.toLowerCase()) {
case "keycloak":
// Add Keycloak specific logout URL if needed
String logoutUrl =
issuer
+ "/protocol/openid-connect/logout"
+ "?client_id="
+ clientId
+ "&post_logout_redirect_uri="
+ response.encodeRedirectURL(redirect_url);
log.info("Redirecting to Keycloak logout URL: " + logoutUrl);
response.sendRedirect(logoutUrl);
break;
case "github":
// Add GitHub specific logout URL if needed
String githubLogoutUrl = "https://github.com/logout";
log.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
response.sendRedirect(githubLogoutUrl);
break;
case "google":
// 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);
log.info("Google does not have a specific logout URL");
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
// response.sendRedirect(googleLogoutUrl);
// break;
default:
String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
log.info("Redirecting to default logout URL: " + defaultRedirectUrl);
response.sendRedirect(defaultRedirectUrl);
break;
}
}
// Sanitize input to avoid potential security vulnerabilities
private String sanitizeInput(String input) {
return input.replaceAll("[^a-zA-Z0-9 ]", "");
}
} }

View File

@@ -1,22 +1,36 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.security.cert.X509Certificate;
import java.util.*;
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;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.core.io.Resource;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter; import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
@@ -28,13 +42,20 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
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.config.security.saml.ConvertResponseToAuthentication; import stirling.software.SPDF.config.security.saml2.CertificateUtils;
import stirling.software.SPDF.config.security.saml.CustomSAMLAuthenticationFailureHandler; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationFailureHandler;
import stirling.software.SPDF.config.security.saml.CustomSAMLAuthenticationSuccessHandler; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationSuccessHandler;
import stirling.software.SPDF.config.security.saml2.CustomSaml2ResponseAuthenticationConverter;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
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.Client;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.model.provider.GithubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider;
import stirling.software.SPDF.repository.JPATokenRepositoryImpl; import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
@Configuration @Configuration
@@ -45,12 +66,6 @@ public class SecurityConfiguration {
@Autowired private CustomUserDetailsService userDetailsService; @Autowired private CustomUserDetailsService userDetailsService;
@Autowired(required = false)
private GrantedAuthoritiesMapper userAuthoritiesMapper;
@Autowired(required = false)
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); return new BCryptPasswordEncoder();
@@ -71,11 +86,8 @@ public class SecurityConfiguration {
@Autowired private FirstLoginFilter firstLoginFilter; @Autowired private FirstLoginFilter firstLoginFilter;
@Autowired private SessionPersistentRegistry sessionRegistry; @Autowired private SessionPersistentRegistry sessionRegistry;
@Autowired private ConvertResponseToAuthentication convertResponseToAuthentication;
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authenticationManager(authenticationManager(http));
if (loginEnabledValue) { if (loginEnabledValue) {
http.addFilterBefore( http.addFilterBefore(
@@ -94,34 +106,23 @@ public class SecurityConfiguration {
.sessionRegistry(sessionRegistry) .sessionRegistry(sessionRegistry)
.expiredUrl("/login?logout=true")); .expiredUrl("/login?logout=true"));
http.formLogin( http.authenticationProvider(daoAuthenticationProvider());
formLogin -> http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()));
formLogin http.logout(
.loginPage("/login")
.successHandler(
new CustomAuthenticationSuccessHandler(
loginAttemptService, userService))
.defaultSuccessUrl("/")
.failureHandler(
new CustomAuthenticationFailureHandler(
loginAttemptService, userService))
.permitAll())
.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()))
.logout(
logout -> logout ->
logout.logoutRequestMatcher( logout.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
new AntPathRequestMatcher("/logout")) .logoutSuccessHandler(
.logoutSuccessHandler(new CustomLogoutSuccessHandler()) new CustomLogoutSuccessHandler(applicationProperties))
.invalidateHttpSession(true) // Invalidate session .invalidateHttpSession(true) // Invalidate session
.deleteCookies("JSESSIONID", "remember-me")) .deleteCookies("JSESSIONID", "remember-me"));
.rememberMe( http.rememberMe(
rememberMeConfigurer -> rememberMeConfigurer ->
rememberMeConfigurer // Use the configurator directly rememberMeConfigurer // Use the configurator directly
.key("uniqueAndSecret") .key("uniqueAndSecret")
.tokenRepository(persistentTokenRepository()) .tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(1209600) // 2 weeks .tokenValiditySeconds(1209600) // 2 weeks
) );
.authorizeHttpRequests( http.authorizeHttpRequests(
authz -> authz ->
authz.requestMatchers( authz.requestMatchers(
req -> { req -> {
@@ -132,16 +133,14 @@ public class SecurityConfiguration {
String trimmedUri = String trimmedUri =
uri.startsWith(contextPath) uri.startsWith(contextPath)
? uri.substring( ? uri.substring(
contextPath contextPath.length())
.length())
: uri; : uri;
return trimmedUri.startsWith("/login") return trimmedUri.startsWith("/login")
|| trimmedUri.startsWith("/oauth") || trimmedUri.startsWith("/oauth")
|| trimmedUri.startsWith("/saml2") || trimmedUri.startsWith("/saml2")
|| trimmedUri.endsWith(".svg") || trimmedUri.endsWith(".svg")
|| trimmedUri.startsWith( || trimmedUri.startsWith("/register")
"/register")
|| trimmedUri.startsWith("/error") || trimmedUri.startsWith("/error")
|| trimmedUri.startsWith("/images/") || trimmedUri.startsWith("/images/")
|| trimmedUri.startsWith("/public/") || trimmedUri.startsWith("/public/")
@@ -155,13 +154,24 @@ public class SecurityConfiguration {
.anyRequest() .anyRequest()
.authenticated()); .authenticated());
// Handle User/Password Logins
if (applicationProperties.getSecurity().isUserPass()) {
http.formLogin(
formLogin ->
formLogin
.loginPage("/login")
.successHandler(
new CustomAuthenticationSuccessHandler(
loginAttemptService, userService))
.failureHandler(
new CustomAuthenticationFailureHandler(
loginAttemptService, userService))
.defaultSuccessUrl("/")
.permitAll());
}
// Handle OAUTH2 Logins // Handle OAUTH2 Logins
if (applicationProperties.getSecurity().getOauth2() != null if (applicationProperties.getSecurity().isOauth2Activ()) {
&& applicationProperties.getSecurity().getOauth2().getEnabled()
&& !applicationProperties
.getSecurity()
.getLoginMethod()
.equalsIgnoreCase("normal")) {
http.oauth2Login( http.oauth2Login(
oauth2 -> oauth2 ->
@@ -188,34 +198,24 @@ public class SecurityConfiguration {
userService, userService,
loginAttemptService)) loginAttemptService))
.userAuthoritiesMapper( .userAuthoritiesMapper(
userAuthoritiesMapper))) userAuthoritiesMapper()))
.logout( .permitAll());
logout ->
logout.logoutSuccessHandler(
new CustomOAuth2LogoutSuccessHandler(
applicationProperties)));
} }
// Handle SAML // Handle SAML
if (applicationProperties.getSecurity().getSaml() != null if (applicationProperties.getSecurity().isSaml2Activ()) {
&& applicationProperties.getSecurity().getSaml().getEnabled() http.authenticationProvider(samlAuthenticationProvider());
&& !applicationProperties
.getSecurity()
.getLoginMethod()
.equalsIgnoreCase("normal")) {
http.saml2Login( http.saml2Login(
saml2 -> { saml2 ->
saml2.loginPage("/saml2") saml2.loginPage("/saml2")
.relyingPartyRegistrationRepository(
relyingPartyRegistrationRepository)
.successHandler( .successHandler(
new CustomSAMLAuthenticationSuccessHandler( new CustomSaml2AuthenticationSuccessHandler(
loginAttemptService, loginAttemptService,
userService, applicationProperties,
applicationProperties)) userService))
.failureHandler( .failureHandler(
new CustomSAMLAuthenticationFailureHandler()); new CustomSaml2AuthenticationFailureHandler())
}) .permitAll())
.addFilterBefore( .addFilterBefore(
userAuthenticationFilter, Saml2WebSsoAuthenticationFilter.class); userAuthenticationFilter, Saml2WebSsoAuthenticationFilter.class);
} }
@@ -231,39 +231,234 @@ public class SecurityConfiguration {
@Bean @Bean
@ConditionalOnProperty( @ConditionalOnProperty(
name = "security.saml.enabled", name = "security.saml2.enabled",
havingValue = "true", havingValue = "true",
matchIfMissing = false) matchIfMissing = false)
public AuthenticationProvider samlAuthenticationProvider() { public AuthenticationProvider samlAuthenticationProvider() {
OpenSaml4AuthenticationProvider authenticationProvider = OpenSaml4AuthenticationProvider authenticationProvider =
new OpenSaml4AuthenticationProvider(); new OpenSaml4AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter(convertResponseToAuthentication); authenticationProvider.setResponseAuthenticationConverter(
new CustomSaml2ResponseAuthenticationConverter(userService));
return authenticationProvider; return authenticationProvider;
} }
// @Bean // Client Registration Repository for OAUTH2 OIDC Login
// public AuthenticationProvider daoAuthenticationProvider() { @Bean
// DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); @ConditionalOnProperty(
// provider.setUserDetailsService(userDetailsService); // UserDetailsService value = "security.oauth2.enabled",
// provider.setPasswordEncoder(passwordEncoder()); // PasswordEncoder havingValue = "true",
// return provider; matchIfMissing = false)
// } public ClientRegistrationRepository clientRegistrationRepository() {
List<ClientRegistration> registrations = new ArrayList<>();
githubClientRegistration().ifPresent(registrations::add);
oidcClientRegistration().ifPresent(registrations::add);
googleClientRegistration().ifPresent(registrations::add);
keycloakClientRegistration().ifPresent(registrations::add);
if (registrations.isEmpty()) {
log.error("At least one OAuth2 provider must be configured");
System.exit(1);
}
return new InMemoryClientRegistrationRepository(registrations);
}
private Optional<ClientRegistration> googleClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
GoogleProvider google = client.getGoogle();
return google != null && google.isSettingsValid()
? Optional.of(
ClientRegistration.withRegistrationId(google.getName())
.clientId(google.getClientId())
.clientSecret(google.getClientSecret())
.scope(google.getScopes())
.authorizationUri(google.getAuthorizationuri())
.tokenUri(google.getTokenuri())
.userInfoUri(google.getUserinfouri())
.userNameAttributeName(google.getUseAsUsername())
.clientName(google.getClientName())
.redirectUri("{baseUrl}/login/oauth2/code/" + google.getName())
.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.getName())
.clientId(keycloak.getClientId())
.clientSecret(keycloak.getClientSecret())
.scope(keycloak.getScopes())
.userNameAttributeName(keycloak.getUseAsUsername())
.clientName(keycloak.getClientName())
.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.getName())
.clientId(github.getClientId())
.clientSecret(github.getClientSecret())
.scope(github.getScopes())
.authorizationUri(github.getAuthorizationuri())
.tokenUri(github.getTokenuri())
.userInfoUri(github.getUserinfouri())
.userNameAttributeName(github.getUseAsUsername())
.clientName(github.getClientName())
.redirectUri("{baseUrl}/login/oauth2/code/" + github.getName())
.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());
}
@Bean @Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception { @ConditionalOnProperty(
AuthenticationManagerBuilder authenticationManagerBuilder = name = "security.saml2.enabled",
http.getSharedObject(AuthenticationManagerBuilder.class); havingValue = "true",
matchIfMissing = false)
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
// authenticationManagerBuilder = SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
// authenticationManagerBuilder.authenticationProvider(
// daoAuthenticationProvider()); // Benutzername/Passwort
if (applicationProperties.getSecurity().getSaml() != null Resource privateKeyResource = samlConf.getPrivateKey();
&& applicationProperties.getSecurity().getSaml().getEnabled()) {
authenticationManagerBuilder.authenticationProvider( Resource certificateResource = samlConf.getSpCert();
samlAuthenticationProvider()); // SAML
Saml2X509Credential signingCredential =
new Saml2X509Credential(
CertificateUtils.readPrivateKey(privateKeyResource),
CertificateUtils.readCertificate(certificateResource),
Saml2X509CredentialType.SIGNING);
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
RelyingPartyRegistration rp =
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
.signingX509Credentials((c) -> c.add(signingCredential))
.assertingPartyDetails(
(details) ->
details.entityId(samlConf.getIdpIssuer())
.singleSignOnServiceLocation(
samlConf.getIdpSingleLoginUrl())
.verificationX509Credentials(
(c) -> c.add(verificationCredential))
.wantAuthnRequestsSigned(true))
.build();
return new InMemoryRelyingPartyRegistrationRepository(rp);
} }
return authenticationManagerBuilder.build();
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
/*
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
This is required for the internal; 'hasRole()' function to give out the correct role.
*/
@Bean
@ConditionalOnProperty(
value = "security.oauth2.enabled",
havingValue = "true",
matchIfMissing = false)
GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(
authority -> {
// Add existing OAUTH2 Authorities
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
// Add Authorities from database for existing user, if user is present.
if (authority instanceof OAuth2UserAuthority oauth2Auth) {
String useAsUsername =
applicationProperties
.getSecurity()
.getOauth2()
.getUseAsUsername();
Optional<User> userOpt =
userService.findByUsernameIgnoreCase(
(String) oauth2Auth.getAttributes().get(useAsUsername));
if (userOpt.isPresent()) {
User user = userOpt.get();
if (user != null) {
mappedAuthorities.add(
new SimpleGrantedAuthority(
userService.findRole(user).getAuthority()));
}
}
}
});
return mappedAuthorities;
};
} }
@Bean @Bean

View File

@@ -22,6 +22,7 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApiKeyAuthenticationToken; import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
@@ -111,7 +112,9 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter() response.getWriter()
.write( .write(
"Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternatively you can disable authentication if this is unexpected"); "Authentication required. Please provide a X-API-KEY in request header.\n"
+ "This is found in Settings -> Account Settings -> API Key\n"
+ "Alternatively you can disable authentication if this is unexpected");
return; return;
} }
} }
@@ -124,6 +127,8 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
username = ((UserDetails) principal).getUsername(); username = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) { } else if (principal instanceof OAuth2User) {
username = ((OAuth2User) principal).getName(); username = ((OAuth2User) principal).getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
} else if (principal instanceof String) { } else if (principal instanceof String) {
username = (String) principal; username = (String) principal;
} }

View File

@@ -20,6 +20,7 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface; import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface; import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.AuthenticationType; import stirling.software.SPDF.model.AuthenticationType;
@@ -338,6 +339,10 @@ public class UserService implements UserServiceInterface {
} else if (principal instanceof OAuth2User) { } else if (principal instanceof OAuth2User) {
OAuth2User oAuth2User = (OAuth2User) principal; OAuth2User oAuth2User = (OAuth2User) principal;
usernameP = oAuth2User.getName(); usernameP = oAuth2User.getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
CustomSaml2AuthenticatedPrincipal saml2User =
(CustomSaml2AuthenticatedPrincipal) principal;
usernameP = saml2User.getName();
} else if (principal instanceof String) { } else if (principal instanceof String) {
usernameP = (String) principal; usernameP = (String) principal;
} }

View File

@@ -51,8 +51,7 @@ public class CustomOAuth2AuthenticationFailureHandler
} }
log.error("OAuth2 Authentication error: " + errorCode); log.error("OAuth2 Authentication error: " + errorCode);
log.error("OAuth2AuthenticationException", exception); log.error("OAuth2AuthenticationException", exception);
getRedirectStrategy() getRedirectStrategy().sendRedirect(request, response, "/login?erroroauth=" + errorCode);
.sendRedirect(request, response, "/logout?erroroauth=" + errorCode);
return; return;
} }
log.error("Unhandled authentication exception", exception); log.error("Unhandled authentication exception", exception);

View File

@@ -75,6 +75,11 @@ public class CustomOAuth2AuthenticationSuccessHandler
throw new LockedException( throw new LockedException(
"Your account has been locked due to too many failed login attempts."); "Your account has been locked due to too many failed login attempts.");
} }
if (userService.isUserDisabled(username)) {
getRedirectStrategy()
.sendRedirect(request, response, "/logout?userIsDisabled=true");
return;
}
if (userService.usernameExistsIgnoreCase(username) if (userService.usernameExistsIgnoreCase(username)
&& userService.hasPassword(username) && userService.hasPassword(username)
&& !userService.isAuthenticationTypeByUsername( && !userService.isAuthenticationTypeByUsername(

View File

@@ -1,122 +0,0 @@
package stirling.software.SPDF.config.security.oauth2;
import java.io.IOException;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.Provider;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.UrlUtils;
@Slf4j
public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private final ApplicationProperties applicationProperties;
public CustomOAuth2LogoutSuccessHandler(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@Override
public void onLogoutSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
String param = "logout=true";
String registrationId = null;
String issuer = null;
String clientId = null;
if (authentication == null) {
if (request.getParameter("userIsDisabled") != null) {
response.sendRedirect(
request.getContextPath() + "/login?erroroauth=userIsDisabled");
} else {
super.onLogoutSuccess(request, response, authentication);
}
return;
}
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
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 (UnsupportedProviderException e) {
log.error(e.getMessage());
}
} else {
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
issuer = oauth.getIssuer();
clientId = oauth.getClientId();
}
String errorMessage = "";
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
param = "erroroauth=oauth2AuthenticationErrorWeb";
} else if ((errorMessage = request.getParameter("error")) != null) {
param = "error=" + sanitizeInput(errorMessage);
} else if ((errorMessage = request.getParameter("erroroauth")) != null) {
param = "erroroauth=" + sanitizeInput(errorMessage);
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
param = "error=oauth2AutoCreateDisabled";
} else if (request.getParameter("oauth2_admin_blocked_user") != null) {
param = "erroroauth=oauth2_admin_blocked_user";
} else if (request.getParameter("userIsDisabled") != null) {
param = "erroroauth=userIsDisabled";
} else if (request.getParameter("badcredentials") != null) {
param = "error=badcredentials";
}
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
switch (registrationId.toLowerCase()) {
case "keycloak":
// Add Keycloak specific logout URL if needed
String logoutUrl =
issuer
+ "/protocol/openid-connect/logout"
+ "?client_id="
+ clientId
+ "&post_logout_redirect_uri="
+ response.encodeRedirectURL(redirect_url);
log.info("Redirecting to Keycloak logout URL: " + logoutUrl);
response.sendRedirect(logoutUrl);
break;
case "github":
// Add GitHub specific logout URL if needed
String githubLogoutUrl = "https://github.com/logout";
log.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
response.sendRedirect(githubLogoutUrl);
break;
case "google":
// 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);
log.info("Google does not have a specific logout URL");
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
// response.sendRedirect(googleLogoutUrl);
// break;
default:
String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
log.info("Redirecting to default logout URL: " + defaultRedirectUrl);
response.sendRedirect(defaultRedirectUrl);
break;
}
}
private String sanitizeInput(String input) {
return input.replaceAll("[^a-zA-Z0-9 ]", "");
}
}

View File

@@ -1,68 +0,0 @@
package stirling.software.SPDF.config.security.saml;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.opensaml.saml.saml2.core.Assertion;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import lombok.extern.slf4j.Slf4j;
@Component
@Slf4j
public class ConvertResponseToAuthentication
implements Converter<ResponseToken, Saml2Authentication> {
private final Saml2AuthorityAttributeLookup saml2AuthorityAttributeLookup;
public ConvertResponseToAuthentication(
Saml2AuthorityAttributeLookup saml2AuthorityAttributeLookup) {
this.saml2AuthorityAttributeLookup = saml2AuthorityAttributeLookup;
}
@Override
public Saml2Authentication convert(ResponseToken responseToken) {
final Assertion assertion =
CollectionUtils.firstElement(responseToken.getResponse().getAssertions());
final Map<String, List<Object>> attributes =
SamlAssertionUtils.getAssertionAttributes(assertion);
final String registrationId =
responseToken.getToken().getRelyingPartyRegistration().getRegistrationId();
final ScimSaml2AuthenticatedPrincipal principal =
new ScimSaml2AuthenticatedPrincipal(
assertion,
attributes,
saml2AuthorityAttributeLookup.getIdentityMappings(registrationId));
final Collection<? extends GrantedAuthority> assertionAuthorities =
getAssertionAuthorities(
attributes,
saml2AuthorityAttributeLookup.getAuthorityAttribute(registrationId));
return new Saml2Authentication(
principal, responseToken.getToken().getSaml2Response(), assertionAuthorities);
}
private static Collection<? extends GrantedAuthority> getAssertionAuthorities(
final Map<String, List<Object>> attributes, final String authoritiesAttributeName) {
if (attributes == null || attributes.isEmpty()) {
return Collections.emptySet();
}
final List<Object> groups = new ArrayList<>(attributes.get(authoritiesAttributeName));
return groups.stream()
.filter(String.class::isInstance)
.map(String.class::cast)
.map(String::toLowerCase)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
}
}

View File

@@ -1,51 +0,0 @@
package stirling.software.SPDF.config.security.saml;
import java.io.IOException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CustomSAMLAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
if (exception instanceof BadCredentialsException) {
log.error("BadCredentialsException", exception);
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
return;
}
if (exception instanceof DisabledException) {
log.error("User is deactivated: ", exception);
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
return;
}
if (exception instanceof LockedException) {
log.error("Account locked: ", exception);
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
return;
}
if (exception instanceof Saml2AuthenticationException) {
log.error("SAML2 Authentication error: ", exception);
getRedirectStrategy()
.sendRedirect(request, response, "/logout?error=saml2AuthenticationError");
return;
}
log.error("Unhandled authentication exception", exception);
super.onAuthenticationFailure(request, response, exception);
}
}

View File

@@ -1,108 +0,0 @@
package stirling.software.SPDF.config.security.saml;
import java.io.IOException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.SavedRequest;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.LoginAttemptService;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.utils.RequestUriUtils;
@Slf4j
public class CustomSAMLAuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler {
private LoginAttemptService loginAttemptService;
private UserService userService;
private ApplicationProperties applicationProperties;
public CustomSAMLAuthenticationSuccessHandler(
LoginAttemptService loginAttemptService,
UserService userService,
ApplicationProperties applicationProperties) {
this.loginAttemptService = loginAttemptService;
this.userService = userService;
this.applicationProperties = applicationProperties;
}
@Override
public void onAuthenticationSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
Object principal = authentication.getPrincipal();
String username = "";
if (principal instanceof OAuth2User) {
OAuth2User oauthUser = (OAuth2User) principal;
username = oauthUser.getName();
} else if (principal instanceof UserDetails) {
UserDetails oauthUser = (UserDetails) principal;
username = oauthUser.getUsername();
} else if (principal instanceof ScimSaml2AuthenticatedPrincipal) {
ScimSaml2AuthenticatedPrincipal samlPrincipal =
(ScimSaml2AuthenticatedPrincipal) principal;
username = samlPrincipal.getName();
}
// Get the saved request
HttpSession session = request.getSession(false);
String contextPath = request.getContextPath();
SavedRequest savedRequest =
(session != null)
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
: null;
if (savedRequest != null
&& !RequestUriUtils.isStaticResource(contextPath, savedRequest.getRedirectUrl())) {
// Redirect to the original destination
super.onAuthenticationSuccess(request, response, authentication);
} else {
OAUTH2 oAuth = applicationProperties.getSecurity().getOauth2();
if (loginAttemptService.isBlocked(username)) {
if (session != null) {
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
}
throw new LockedException(
"Your account has been locked due to too many failed login attempts.");
}
if (userService.usernameExistsIgnoreCase(username)
&& userService.hasPassword(username)
&& !userService.isAuthenticationTypeByUsername(
username, AuthenticationType.OAUTH2)
&& oAuth.getAutoCreateUser()) {
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
return;
}
try {
if (oAuth.getBlockRegistration()
&& !userService.usernameExistsIgnoreCase(username)) {
response.sendRedirect(contextPath + "/logout?oauth2_admin_blocked_user=true");
return;
}
if (principal instanceof OAuth2User) {
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
}
response.sendRedirect(contextPath + "/");
return;
} catch (IllegalArgumentException e) {
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
return;
}
}
}
}

View File

@@ -1,38 +0,0 @@
package stirling.software.SPDF.config.security.saml;
import java.io.IOException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SAMLLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
@Override
public void onLogoutSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
String redirectUrl = determineTargetUrl(request, response, authentication);
if (response.isCommitted()) {
log.debug("Response has already been committed. Unable to redirect to " + redirectUrl);
return;
}
getRedirectStrategy().sendRedirect(request, response, redirectUrl);
}
protected String determineTargetUrl(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) {
// Default to the root URL
return "/";
}
}

View File

@@ -1,7 +0,0 @@
package stirling.software.SPDF.config.security.saml;
public interface Saml2AuthorityAttributeLookup {
String getAuthorityAttribute(String registrationId);
SimpleScimMappings getIdentityMappings(String registrationId);
}

View File

@@ -1,17 +0,0 @@
package stirling.software.SPDF.config.security.saml;
import org.springframework.stereotype.Component;
@Component
public class Saml2AuthorityAttributeLookupImpl implements Saml2AuthorityAttributeLookup {
@Override
public String getAuthorityAttribute(String registrationId) {
return "authorityAttributeName";
}
@Override
public SimpleScimMappings getIdentityMappings(String registrationId) {
return new SimpleScimMappings();
}
}

View File

@@ -1,63 +0,0 @@
package stirling.software.SPDF.config.security.saml;
import java.time.Instant;
import java.util.*;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.schema.*;
import org.opensaml.saml.saml2.core.Assertion;
public class SamlAssertionUtils {
public static Map<String, List<Object>> getAssertionAttributes(Assertion assertion) {
Map<String, List<Object>> attributeMap = new LinkedHashMap<>();
assertion
.getAttributeStatements()
.forEach(
attributeStatement -> {
attributeStatement
.getAttributes()
.forEach(
attribute -> {
List<Object> attributeValues = new ArrayList<>();
attribute
.getAttributeValues()
.forEach(
xmlObject -> {
Object attributeValue =
getXmlObjectValue(
xmlObject);
if (attributeValue != null) {
attributeValues.add(
attributeValue);
}
});
attributeMap.put(
attribute.getName(), attributeValues);
});
});
return attributeMap;
}
public static Object getXmlObjectValue(XMLObject xmlObject) {
if (xmlObject instanceof XSAny) {
return ((XSAny) xmlObject).getTextContent();
} else if (xmlObject instanceof XSString) {
return ((XSString) xmlObject).getValue();
} else if (xmlObject instanceof XSInteger) {
return ((XSInteger) xmlObject).getValue();
} else if (xmlObject instanceof XSURI) {
return ((XSURI) xmlObject).getURI();
} else if (xmlObject instanceof XSBoolean) {
return ((XSBoolean) xmlObject).getValue().getValue();
} else if (xmlObject instanceof XSDateTime) {
Instant dateTime = ((XSDateTime) xmlObject).getValue();
return (dateTime != null) ? Instant.ofEpochMilli(dateTime.toEpochMilli()) : null;
}
return null;
}
}

View File

@@ -1,42 +0,0 @@
package stirling.software.SPDF.config.security.saml;
import java.security.cert.CertificateException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
@Configuration
@Slf4j
public class SamlConfig {
@Autowired ApplicationProperties applicationProperties;
@Bean
@ConditionalOnProperty(
value = "security.saml.enabled",
havingValue = "true",
matchIfMissing = false)
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository()
throws CertificateException {
RelyingPartyRegistration registration =
RelyingPartyRegistrations.fromMetadataLocation(
applicationProperties
.getSecurity()
.getSaml()
.getIdpMetadataLocation())
.entityId(applicationProperties.getSecurity().getSaml().getEntityId())
.registrationId(
applicationProperties.getSecurity().getSaml().getRegistrationId())
.build();
return new InMemoryRelyingPartyRegistrationRepository(registration);
}
}

View File

@@ -1,89 +0,0 @@
package stirling.software.SPDF.config.security.saml;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.opensaml.saml.saml2.core.Assertion;
import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.util.Assert;
import com.unboundid.scim2.common.types.Email;
import com.unboundid.scim2.common.types.Name;
import com.unboundid.scim2.common.types.UserResource;
public class ScimSaml2AuthenticatedPrincipal implements AuthenticatedPrincipal, Serializable {
private static final long serialVersionUID = 1L;
private final transient UserResource userResource;
public ScimSaml2AuthenticatedPrincipal(
final Assertion assertion,
final Map<String, List<Object>> attributes,
final SimpleScimMappings attributeMappings) {
Assert.notNull(assertion, "assertion cannot be null");
Assert.notNull(assertion.getSubject(), "assertion subject cannot be null");
Assert.notNull(
assertion.getSubject().getNameID(), "assertion subject NameID cannot be null");
Assert.notNull(attributes, "attributes cannot be null");
Assert.notNull(attributeMappings, "attributeMappings cannot be null");
final Name name =
new Name()
.setFamilyName(
getAttribute(
attributes,
attributeMappings,
SimpleScimMappings::getFamilyName))
.setGivenName(
getAttribute(
attributes,
attributeMappings,
SimpleScimMappings::getGivenName));
final List<Email> emails = new ArrayList<>(1);
emails.add(
new Email()
.setValue(
getAttribute(
attributes,
attributeMappings,
SimpleScimMappings::getEmail))
.setPrimary(true));
userResource =
new UserResource()
.setUserName(assertion.getSubject().getNameID().getValue())
.setName(name)
.setEmails(emails);
}
private static String getAttribute(
final Map<String, List<Object>> attributes,
final SimpleScimMappings simpleScimMappings,
final Function<SimpleScimMappings, String> attributeMapper) {
final String key = attributeMapper.apply(simpleScimMappings);
final List<Object> values = attributes.getOrDefault(key, Collections.emptyList());
return values.stream()
.filter(String.class::isInstance)
.map(String.class::cast)
.findFirst()
.orElse(null);
}
@Override
public String getName() {
return this.userResource.getUserName();
}
public UserResource getUserResource() {
return this.userResource;
}
}

View File

@@ -1,10 +0,0 @@
package stirling.software.SPDF.config.security.saml;
import lombok.Data;
@Data
public class SimpleScimMappings {
String givenName;
String familyName;
String email;
}

View File

@@ -0,0 +1,48 @@
package stirling.software.SPDF.config.security.saml2;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import org.springframework.core.io.Resource;
import org.springframework.util.FileCopyUtils;
public class CertificateUtils {
public static X509Certificate readCertificate(Resource certificateResource) throws Exception {
String certificateString =
new String(
FileCopyUtils.copyToByteArray(certificateResource.getInputStream()),
StandardCharsets.UTF_8);
String certContent =
certificateString
.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", "")
.replaceAll("\\R", "")
.replaceAll("\\s+", "");
CertificateFactory cf = CertificateFactory.getInstance("X.509");
byte[] decodedCert = Base64.getDecoder().decode(certContent);
return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(decodedCert));
}
public static RSAPrivateKey readPrivateKey(Resource privateKeyResource) throws Exception {
String privateKeyString =
new String(
FileCopyUtils.copyToByteArray(privateKeyResource.getInputStream()),
StandardCharsets.UTF_8);
String privateKeyContent =
privateKeyString
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\R", "")
.replaceAll("\\s+", "");
KeyFactory kf = KeyFactory.getInstance("RSA");
byte[] decodedKey = Base64.getDecoder().decode(privateKeyContent);
return (RSAPrivateKey) kf.generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
}
}

View File

@@ -0,0 +1,45 @@
package stirling.software.SPDF.config.security.saml2;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
public class CustomSaml2AuthenticatedPrincipal
implements Saml2AuthenticatedPrincipal, Serializable {
private final String name;
private final Map<String, List<Object>> attributes;
private final String nameId;
private final List<String> sessionIndexes;
public CustomSaml2AuthenticatedPrincipal(
String name,
Map<String, List<Object>> attributes,
String nameId,
List<String> sessionIndexes) {
this.name = name;
this.attributes = attributes;
this.nameId = nameId;
this.sessionIndexes = sessionIndexes;
}
@Override
public String getName() {
return this.name;
}
@Override
public Map<String, List<Object>> getAttributes() {
return this.attributes;
}
public String getNameId() {
return this.nameId;
}
public List<String> getSessionIndexes() {
return this.sessionIndexes;
}
}

View File

@@ -0,0 +1,38 @@
package stirling.software.SPDF.config.security.saml2;
import java.io.IOException;
import org.springframework.security.authentication.ProviderNotFoundException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
if (exception instanceof Saml2AuthenticationException) {
Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error();
getRedirectStrategy()
.sendRedirect(request, response, "/login?erroroauth=" + error.getErrorCode());
} else if (exception instanceof ProviderNotFoundException) {
getRedirectStrategy()
.sendRedirect(
request,
response,
"/login?erroroauth=not_authentication_provider_found");
}
log.error("AuthenticationException: " + exception);
}
}

View File

@@ -0,0 +1,91 @@
package stirling.software.SPDF.config.security.saml2;
import java.io.IOException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.SavedRequest;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.AllArgsConstructor;
import stirling.software.SPDF.config.security.LoginAttemptService;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.utils.RequestUriUtils;
@AllArgsConstructor
public class CustomSaml2AuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler {
private LoginAttemptService loginAttemptService;
private ApplicationProperties applicationProperties;
private UserService userService;
@Override
public void onAuthenticationSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
Object principal = authentication.getPrincipal();
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
String username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
// Get the saved request
HttpSession session = request.getSession(false);
String contextPath = request.getContextPath();
SavedRequest savedRequest =
(session != null)
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
: null;
if (savedRequest != null
&& !RequestUriUtils.isStaticResource(
contextPath, savedRequest.getRedirectUrl())) {
// Redirect to the original destination
super.onAuthenticationSuccess(request, response, authentication);
} else {
SAML2 saml2 = applicationProperties.getSecurity().getSaml2();
if (loginAttemptService.isBlocked(username)) {
if (session != null) {
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
}
throw new LockedException(
"Your account has been locked due to too many failed login attempts.");
}
if (userService.usernameExistsIgnoreCase(username)
&& userService.hasPassword(username)
&& !userService.isAuthenticationTypeByUsername(
username, AuthenticationType.OAUTH2)
&& saml2.getAutoCreateUser()) {
response.sendRedirect(
contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
return;
}
try {
if (saml2.getBlockRegistration()
&& !userService.usernameExistsIgnoreCase(username)) {
response.sendRedirect(
contextPath + "/login?erroroauth=oauth2_admin_blocked_user");
return;
}
userService.processOAuth2PostLogin(username, saml2.getAutoCreateUser());
response.sendRedirect(contextPath + "/");
return;
} catch (IllegalArgumentException e) {
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
return;
}
}
} else {
super.onAuthenticationSuccess(request, response, authentication);
}
}
}

View File

@@ -0,0 +1,86 @@
package stirling.software.SPDF.config.security.saml2;
import java.util.*;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.schema.XSBoolean;
import org.opensaml.core.xml.schema.XSString;
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AttributeStatement;
import org.opensaml.saml.saml2.core.AuthnStatement;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.User;
@Component
@Slf4j
public class CustomSaml2ResponseAuthenticationConverter
implements Converter<ResponseToken, Saml2Authentication> {
private UserService userService;
public CustomSaml2ResponseAuthenticationConverter(UserService userService) {
this.userService = userService;
}
@Override
public Saml2Authentication convert(ResponseToken responseToken) {
// Extract the assertion from the response
Assertion assertion = responseToken.getResponse().getAssertions().get(0);
// Extract the NameID
String nameId = assertion.getSubject().getNameID().getValue();
Optional<User> userOpt = userService.findByUsernameIgnoreCase(nameId);
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
if (userOpt.isPresent()) {
User user = userOpt.get();
if (user != null) {
simpleGrantedAuthority =
new SimpleGrantedAuthority(userService.findRole(user).getAuthority());
}
}
// Extract the SessionIndexes
List<String> sessionIndexes = new ArrayList<>();
for (AuthnStatement authnStatement : assertion.getAuthnStatements()) {
sessionIndexes.add(authnStatement.getSessionIndex());
}
// Extract the Attributes
Map<String, List<Object>> attributes = extractAttributes(assertion);
// Create the custom principal
CustomSaml2AuthenticatedPrincipal principal =
new CustomSaml2AuthenticatedPrincipal(nameId, attributes, nameId, sessionIndexes);
// Create the Saml2Authentication
return new Saml2Authentication(
principal,
responseToken.getToken().getSaml2Response(),
Collections.singletonList(simpleGrantedAuthority));
}
private Map<String, List<Object>> extractAttributes(Assertion assertion) {
Map<String, List<Object>> attributes = new HashMap<>();
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
for (Attribute attribute : attributeStatement.getAttributes()) {
String attributeName = attribute.getName();
List<Object> values = new ArrayList<>();
for (XMLObject xmlObject : attribute.getAttributeValues()) {
log.info("BOOL: " + ((XSBoolean) xmlObject).getValue());
values.add(((XSString) xmlObject).getValue());
}
attributes.put(attributeName, values);
}
}
return attributes;
}
}

View File

@@ -16,6 +16,7 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.model.SessionEntity; import stirling.software.SPDF.model.SessionEntity;
@Component @Component
@@ -50,6 +51,8 @@ public class SessionPersistentRegistry implements SessionRegistry {
principalName = ((UserDetails) principal).getUsername(); principalName = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) { } else if (principal instanceof OAuth2User) {
principalName = ((OAuth2User) principal).getName(); principalName = ((OAuth2User) principal).getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
principalName = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
} else if (principal instanceof String) { } else if (principal instanceof String) {
principalName = (String) principal; principalName = (String) principal;
} }
@@ -79,6 +82,8 @@ public class SessionPersistentRegistry implements SessionRegistry {
principalName = ((UserDetails) principal).getUsername(); principalName = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) { } else if (principal instanceof OAuth2User) {
principalName = ((OAuth2User) principal).getName(); principalName = ((OAuth2User) principal).getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
principalName = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
} else if (principal instanceof String) { } else if (principal instanceof String) {
principalName = (String) principal; principalName = (String) principal;
} }

View File

@@ -32,6 +32,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.UserService; import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.AuthenticationType; import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
@@ -336,6 +337,8 @@ public class UserController {
userNameP = ((UserDetails) principal).getUsername(); userNameP = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) { } else if (principal instanceof OAuth2User) {
userNameP = ((OAuth2User) principal).getName(); userNameP = ((OAuth2User) principal).getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
userNameP = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
} else if (principal instanceof String) { } else if (principal instanceof String) {
userNameP = (String) principal; userNameP = (String) principal;
} }

View File

@@ -21,10 +21,13 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.*; import stirling.software.SPDF.model.*;
import stirling.software.SPDF.model.ApplicationProperties.Security;
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.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.provider.GithubProvider; import stirling.software.SPDF.model.provider.GithubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider; import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider; import stirling.software.SPDF.model.provider.KeycloakProvider;
@@ -51,38 +54,54 @@ public class AccountWebController {
Map<String, String> providerList = new HashMap<>(); Map<String, String> providerList = new HashMap<>();
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2(); Security securityProps = applicationProperties.getSecurity();
OAUTH2 oauth = securityProps.getOauth2();
if (oauth != null) { if (oauth != null) {
if (oauth.getEnabled()) {
if (oauth.isSettingsValid()) { if (oauth.isSettingsValid()) {
providerList.put("oidc", oauth.getProvider()); providerList.put("/oauth2/authorization/oidc", oauth.getProvider());
} }
Client client = oauth.getClient(); Client client = oauth.getClient();
if (client != null) { if (client != null) {
GoogleProvider google = client.getGoogle(); GoogleProvider google = client.getGoogle();
if (google.isSettingsValid()) { if (google.isSettingsValid()) {
providerList.put(google.getName(), google.getClientName()); providerList.put(
"/oauth2/authorization/" + google.getName(),
google.getClientName());
} }
GithubProvider github = client.getGithub(); GithubProvider github = client.getGithub();
if (github.isSettingsValid()) { if (github.isSettingsValid()) {
providerList.put(github.getName(), github.getClientName()); providerList.put(
"/oauth2/authorization/" + github.getName(),
github.getClientName());
} }
KeycloakProvider keycloak = client.getKeycloak(); KeycloakProvider keycloak = client.getKeycloak();
if (keycloak.isSettingsValid()) { if (keycloak.isSettingsValid()) {
providerList.put(keycloak.getName(), keycloak.getClientName()); providerList.put(
"/oauth2/authorization/" + keycloak.getName(),
keycloak.getClientName());
} }
} }
} }
}
SAML2 saml2 = securityProps.getSaml2();
if (saml2 != null) {
if (saml2.getEnabled()) {
providerList.put("/saml2/authenticate/" + saml2.getRegistrationId(), "SAML 2");
}
}
// Remove any null keys/values from the providerList // Remove any null keys/values from the providerList
providerList providerList
.entrySet() .entrySet()
.removeIf(entry -> entry.getKey() == null || entry.getValue() == null); .removeIf(entry -> entry.getKey() == null || entry.getValue() == null);
model.addAttribute("providerlist", providerList); model.addAttribute("providerlist", providerList);
model.addAttribute("loginMethod", applicationProperties.getSecurity().getLoginMethod()); model.addAttribute("loginMethod", securityProps.getLoginMethod());
model.addAttribute( model.addAttribute("altLogin", securityProps.isAltLogin());
"oAuth2Enabled", applicationProperties.getSecurity().getOauth2().getEnabled());
model.addAttribute("currentPage", "login"); model.addAttribute("currentPage", "login");
@@ -349,6 +368,17 @@ public class AccountWebController {
// Add oAuth2 Login attributes to the model // Add oAuth2 Login attributes to the model
model.addAttribute("oAuth2Login", true); model.addAttribute("oAuth2Login", true);
} }
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
// Cast the principal object to OAuth2User
CustomSaml2AuthenticatedPrincipal userDetails =
(CustomSaml2AuthenticatedPrincipal) principal;
// Retrieve username and other attributes
username = userDetails.getName();
// Add oAuth2 Login attributes to the model
model.addAttribute("oAuth2Login", true);
}
if (username != null) { if (username != null) {
// Fetch user details from the database // Fetch user details from the database
Optional<User> user = Optional<User> user =

View File

@@ -1,13 +1,17 @@
package stirling.software.SPDF.model; package stirling.software.SPDF.model;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; 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;
@@ -18,6 +22,8 @@ import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import lombok.Data; import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import stirling.software.SPDF.config.YamlPropertySourceFactory; import stirling.software.SPDF.config.YamlPropertySourceFactory;
import stirling.software.SPDF.model.provider.GithubProvider; import stirling.software.SPDF.model.provider.GithubProvider;
@@ -41,7 +47,6 @@ public class ApplicationProperties {
private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated(); private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated();
private EnterpriseEdition enterpriseEdition = new EnterpriseEdition(); private EnterpriseEdition enterpriseEdition = new EnterpriseEdition();
private AutoPipeline autoPipeline = new AutoPipeline(); private AutoPipeline autoPipeline = new AutoPipeline();
private static final Logger logger = LoggerFactory.getLogger(ApplicationProperties.class);
@Data @Data
public static class AutoPipeline { public static class AutoPipeline {
@@ -63,41 +68,108 @@ public class ApplicationProperties {
private Boolean csrfDisabled; private Boolean csrfDisabled;
private InitialLogin initialLogin = new InitialLogin(); private InitialLogin initialLogin = new InitialLogin();
private OAUTH2 oauth2 = new OAUTH2(); private OAUTH2 oauth2 = new OAUTH2();
private SAML saml = new SAML(); private SAML2 saml2 = new SAML2();
private int loginAttemptCount; private int loginAttemptCount;
private long loginResetTimeMinutes; private long loginResetTimeMinutes;
private String loginMethod = "all"; private String loginMethod = "all";
public Boolean isAltLogin() {
return saml2.getEnabled() || oauth2.getEnabled();
}
public enum LoginMethods {
ALL("all"),
NORMAL("normal"),
OAUTH2("oauth2"),
SAML2("saml2");
private String method;
LoginMethods(String method) {
this.method = method;
}
@Override
public String toString() {
return method;
}
}
public boolean isUserPass() {
return (loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())
|| loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString()));
}
public boolean isOauth2Activ() {
return (oauth2 != null
&& oauth2.getEnabled()
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
}
public boolean isSaml2Activ() {
return (saml2 != null
&& saml2.getEnabled()
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
}
@Data @Data
public static class InitialLogin { public static class InitialLogin {
private String username; private String username;
@ToString.Exclude private String password; @ToString.Exclude private String password;
} }
@Data @Getter
public static class SAML { @Setter
public static class SAML2 {
private Boolean enabled = false; private Boolean enabled = false;
private String entityId; private Boolean autoCreateUser = false;
private String registrationId; private Boolean blockRegistration = false;
private String spBaseUrl; private String registrationId = "stirling";
private String idpMetadataLocation; private String idpMetadataUri;
private KeyStore keystore; private String idpSingleLogoutUrl;
private String idpSingleLoginUrl;
private String idpIssuer;
private String idpCert;
private String privateKey;
private String spCert;
@Data public InputStream getIdpMetadataUri() throws IOException {
public static class KeyStore { if (idpMetadataUri.startsWith("classpath:")) {
private String keystoreLocation; return new ClassPathResource(idpMetadataUri.substring("classpath".length()))
private String keystorePassword; .getInputStream();
private String keyAlias;
private String keyPassword;
private String realmCertificateAlias;
public Resource getKeystoreResource() {
if (keystoreLocation.startsWith("classpath:")) {
return new ClassPathResource(
keystoreLocation.substring("classpath:".length()));
} else {
return new FileSystemResource(keystoreLocation);
} }
try {
URI uri = new URI(idpMetadataUri);
URL url = uri.toURL();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
return connection.getInputStream();
} catch (URISyntaxException e) {
throw new IOException("Invalid URI format: " + idpMetadataUri, e);
}
}
public Resource getSpCert() {
if (spCert.startsWith("classpath:")) {
return new ClassPathResource(spCert.substring("classpath:".length()));
} else {
return new FileSystemResource(spCert);
}
}
public Resource getidpCert() {
if (idpCert.startsWith("classpath:")) {
return new ClassPathResource(idpCert.substring("classpath:".length()));
} else {
return new FileSystemResource(idpCert);
}
}
public Resource getPrivateKey() {
if (privateKey.startsWith("classpath:")) {
return new ClassPathResource(privateKey.substring("classpath:".length()));
} else {
return new FileSystemResource(privateKey);
} }
} }
} }

View File

@@ -19,7 +19,6 @@ public class Provider implements ProviderInterface {
return true; return true;
} }
return false; return false;
// throw new IllegalArgumentException(getName() + ": " + name + " is required!");
} }
protected boolean isValid(Collection<String> value, String name) { protected boolean isValid(Collection<String> value, String name) {
@@ -27,66 +26,55 @@ public class Provider implements ProviderInterface {
return true; return true;
} }
return false; return false;
// throw new IllegalArgumentException(getName() + ": " + name + " is required!");
} }
@Override @Override
public Collection<String> getScopes() { public Collection<String> getScopes() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getScope'"); throw new UnsupportedOperationException("Unimplemented method 'getScope'");
} }
@Override @Override
public void setScopes(String scopes) { public void setScopes(String scopes) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'setScope'"); throw new UnsupportedOperationException("Unimplemented method 'setScope'");
} }
@Override @Override
public String getUseAsUsername() { public String getUseAsUsername() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getUseAsUsername'"); throw new UnsupportedOperationException("Unimplemented method 'getUseAsUsername'");
} }
@Override @Override
public void setUseAsUsername(String useAsUsername) { public void setUseAsUsername(String useAsUsername) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'setUseAsUsername'"); throw new UnsupportedOperationException("Unimplemented method 'setUseAsUsername'");
} }
@Override @Override
public String getIssuer() { public String getIssuer() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getIssuer'"); throw new UnsupportedOperationException("Unimplemented method 'getIssuer'");
} }
@Override @Override
public void setIssuer(String issuer) { public void setIssuer(String issuer) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'setIssuer'"); throw new UnsupportedOperationException("Unimplemented method 'setIssuer'");
} }
@Override @Override
public String getClientSecret() { public String getClientSecret() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getClientSecret'"); throw new UnsupportedOperationException("Unimplemented method 'getClientSecret'");
} }
@Override @Override
public void setClientSecret(String clientSecret) { public void setClientSecret(String clientSecret) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'setClientSecret'"); throw new UnsupportedOperationException("Unimplemented method 'setClientSecret'");
} }
@Override @Override
public String getClientId() { public String getClientId() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getClientId'"); throw new UnsupportedOperationException("Unimplemented method 'getClientId'");
} }
@Override @Override
public void setClientId(String clientId) { public void setClientId(String clientId) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'setClientId'"); throw new UnsupportedOperationException("Unimplemented method 'setClientId'");
} }
} }

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=تم تعطيل المستخدم، تم حظر تسجيل
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=حجب تلقائي autoRedact.title=حجب تلقائي

View File

@@ -3,8 +3,8 @@
########### ###########
# the direction that the language is written (ltr = left to right, rtl = right to left) # the direction that the language is written (ltr = left to right, rtl = right to left)
language.direction=ltr language.direction=ltr
addPageNumbers.fontSize=Font Size addPageNumbers.fontSize=Размер на шрифт
addPageNumbers.fontName=Font Name addPageNumbers.fontName=Име на шрифт
pdfPrompt=Изберете PDF(и) pdfPrompt=Изберете PDF(и)
multiPdfPrompt=Изберете PDF (2+) multiPdfPrompt=Изберете PDF (2+)
multiPdfDropPrompt=Изберете (или плъзнете и пуснете) всички PDF файлове, от които се нуждаете multiPdfDropPrompt=Изберете (или плъзнете и пуснете) всички PDF файлове, от които се нуждаете
@@ -56,12 +56,12 @@ userNotFoundMessage=Потребителят не е намерен
incorrectPasswordMessage=Текущата парола е неправилна. incorrectPasswordMessage=Текущата парола е неправилна.
usernameExistsMessage=Новият потребител вече съществува. usernameExistsMessage=Новият потребител вече съществува.
invalidUsernameMessage=Невалидно потребителско име, потребителското име може да съдържа само букви, цифри и следните специални знаци @._+- или трябва да е валиден имейл адрес. invalidUsernameMessage=Невалидно потребителско име, потребителското име може да съдържа само букви, цифри и следните специални знаци @._+- или трябва да е валиден имейл адрес.
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end. invalidPasswordMessage=Паролата не трябва да е празна и не трябва да има интервали в началото или в края.
confirmPasswordErrorMessage=New Password and Confirm New Password must match. confirmPasswordErrorMessage=Нова парола и Потвърждаване на новата парола трябва да съвпадат.
deleteCurrentUserMessage=Не може да се изтрие вписания в момента потребител. deleteCurrentUserMessage=Не може да се изтрие вписания в момента потребител.
deleteUsernameExistsMessage=Потребителското име не съществува и не може да бъде изтрито. deleteUsernameExistsMessage=Потребителското име не съществува и не може да бъде изтрито.
downgradeCurrentUserMessage=Не може да се понижи ролята на текущия потребител downgradeCurrentUserMessage=Не може да се понижи ролята на текущия потребител
disabledCurrentUserMessage=The current user cannot be disabled disabledCurrentUserMessage=Текущият потребител не може да бъде деактивиран
downgradeCurrentUserLongMessage=Не може да се понижи ролята на текущия потребител. Следователно текущият потребител няма да бъде показан. downgradeCurrentUserLongMessage=Не може да се понижи ролята на текущия потребител. Следователно текущият потребител няма да бъде показан.
userAlreadyExistsOAuthMessage=Потребителят вече съществува като OAuth2 потребител. userAlreadyExistsOAuthMessage=Потребителят вече съществува като OAuth2 потребител.
userAlreadyExistsWebMessage=Потребителят вече съществува като уеб-потребител. userAlreadyExistsWebMessage=Потребителят вече съществува като уеб-потребител.
@@ -75,16 +75,16 @@ visitGithub=Посетете Github Repository
donate=Направете дарение donate=Направете дарение
color=Цвят color=Цвят
sponsor=Спонсор sponsor=Спонсор
info=Info info=Информация
pro=Pro pro=Pro
page=Page page=Страница
pages=Pages pages=Страници
legal.privacy=Privacy Policy legal.privacy=Политика за поверителност
legal.terms=Terms and Conditions legal.terms=Правила и условия
legal.accessibility=Accessibility legal.accessibility=Достъпност
legal.cookie=Cookie Policy legal.cookie=Политика за бисквитки
legal.impressum=Impressum legal.impressum=Отпечатък
############### ###############
# Pipeline # # Pipeline #
@@ -96,7 +96,7 @@ pipeline.defaultOption=Персонализиран
pipeline.submitButton=Подайте pipeline.submitButton=Подайте
pipeline.help=Pipeline Помощ pipeline.help=Pipeline Помощ
pipeline.scanHelp=Помощ за сканиране на папки pipeline.scanHelp=Помощ за сканиране на папки
pipeline.deletePrompt=Are you sure you want to delete pipeline pipeline.deletePrompt=Сигурни ли сте, че искате да изтриете pipeline
###################### ######################
# Pipeline Options # # Pipeline Options #
@@ -114,21 +114,21 @@ pipelineOptions.validateButton=Валидирай
######################## ########################
# ENTERPRISE EDITION # # ENTERPRISE EDITION #
######################## ########################
enterpriseEdition.button=Upgrade to Pro enterpriseEdition.button=Направете надстройка до Pro версията
enterpriseEdition.warning=This feature is only available to Pro users. enterpriseEdition.warning=Тази функция е достъпна само за потребители на Pro версията.
enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features. enterpriseEdition.yamlAdvert=Stirling PDF Pro поддържа YAML конфигурационни файлове и други SSO функции.
enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro enterpriseEdition.ssoAdvert=Търсите повече функции за управление на потребителите? Погледнете за Stirling PDF Pro
################# #################
# Analytics # # Analytics #
################# #################
analytics.title=Do you want make Stirling PDF better? analytics.title=Искате ли да подобрите Stirling PDF?
analytics.paragraph1=Stirling PDF has opt in analytics to help us improve the product. We do not track any personal information or file contents. analytics.paragraph1=Stirling PDF включва анализи, за да ни помогне да подобрим продукта. Ние не проследяваме лична информация или съдържание на файлове.
analytics.paragraph2=Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better. analytics.paragraph2=Моля, обмислете възможността за анализ, за ​​да помогнете на Stirling-PDF да расте и да ни позволи да разберем по-добре нашите потребители.
analytics.enable=Enable analytics analytics.enable=Активиране на анализа
analytics.disable=Disable analytics analytics.disable=Деактивиране на анализа
analytics.settings=You can change the settings for analytics in the config/settings.yml file analytics.settings=Можете да промените настройките за анализ във config/settings.yml файла
############# #############
# NAVBAR # # NAVBAR #
@@ -145,7 +145,7 @@ navbar.sections.convertFrom=Преобразуване от PDF
navbar.sections.security=Подписване и сигурност navbar.sections.security=Подписване и сигурност
navbar.sections.advance=Разширено navbar.sections.advance=Разширено
navbar.sections.edit=Преглед и редактиране navbar.sections.edit=Преглед и редактиране
navbar.sections.popular=Popular navbar.sections.popular=Популярни
############# #############
# SETTINGS # # SETTINGS #
@@ -202,9 +202,9 @@ adminUserSettings.header=Настройки за администраторск
adminUserSettings.admin=Администратор adminUserSettings.admin=Администратор
adminUserSettings.user=Потребител adminUserSettings.user=Потребител
adminUserSettings.addUser=Добавяне на нов потребител adminUserSettings.addUser=Добавяне на нов потребител
adminUserSettings.deleteUser=Delete User adminUserSettings.deleteUser=Изтриване на потребител
adminUserSettings.confirmDeleteUser=Should the user be deleted? adminUserSettings.confirmDeleteUser=Трябва ли потребителят да бъде изтрит?
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled? adminUserSettings.confirmChangeUserStatus=Трябва ли потребителят да бъде деактивиран/активиран?
adminUserSettings.usernameInfo=Потребителското име може да съдържа само букви, цифри и следните специални символи @._+- или трябва да е валиден имейл адрес. adminUserSettings.usernameInfo=Потребителското име може да съдържа само букви, цифри и следните специални символи @._+- или трябва да е валиден имейл адрес.
adminUserSettings.roles=Роли adminUserSettings.roles=Роли
adminUserSettings.role=Роля adminUserSettings.role=Роля
@@ -218,32 +218,32 @@ adminUserSettings.forceChange=Принудете потребителя да п
adminUserSettings.submit=Съхранете потребителя adminUserSettings.submit=Съхранете потребителя
adminUserSettings.changeUserRole=Промяна на ролята на потребителя adminUserSettings.changeUserRole=Промяна на ролята на потребителя
adminUserSettings.authenticated=Удостоверен adminUserSettings.authenticated=Удостоверен
adminUserSettings.editOwnProfil=Edit own profile adminUserSettings.editOwnProfil=Редактиране на собствен профил
adminUserSettings.enabledUser=enabled user adminUserSettings.enabledUser=активиран потребител
adminUserSettings.disabledUser=disabled user adminUserSettings.disabledUser=деактивиран потребител
adminUserSettings.activeUsers=Active Users: adminUserSettings.activeUsers=Активни потребители:
adminUserSettings.disabledUsers=Disabled Users: adminUserSettings.disabledUsers=Деактивирани потребители:
adminUserSettings.totalUsers=Total Users: adminUserSettings.totalUsers=Общо потребители:
adminUserSettings.lastRequest=Last Request adminUserSettings.lastRequest=Последна заявка
database.title=Database Import/Export database.title=Импорт/Експорт на база данни
database.header=Database Import/Export database.header=Импорт/Експорт на база данни
database.fileName=File Name database.fileName=Име на файл
database.creationDate=Creation Date database.creationDate=Дата на създаване
database.fileSize=File Size database.fileSize=Размер на файла
database.deleteBackupFile=Delete Backup File database.deleteBackupFile=Изтриване на архивен файл
database.importBackupFile=Import Backup File database.importBackupFile=Импортиране на архивен файл
database.downloadBackupFile=Download Backup File database.downloadBackupFile=Изтеглете архивен файл
database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. database.info_1=Когато импортирате данни, е от решаващо значение да осигурите правилната структура. Ако не сте сигурни в това, което правите, потърсете съвет и подкрепа от професионалист. Грешка в структурата може да причини неизправност на приложението, включително пълна невъзможност за стартиране на приложението.
database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. database.info_2=Името на файла няма значение при качване. След това ще бъде преименуван, за да следва формата backup_user_yyyyMMddHHmm.sql, осигурявайки последователна конвенция за именуване.
database.submit=Import Backup database.submit=Импортиране на резервно копие
database.importIntoDatabaseSuccessed=Import into database successed database.importIntoDatabaseSuccessed=Импортирането в базата данни бе успешно
database.fileNotFound=File not Found database.fileNotFound=Файлът не е намерен
database.fileNullOrEmpty=File must not be null or empty database.fileNullOrEmpty=Файлът не трябва да е нулев или празен
database.failedImportFile=Failed Import File database.failedImportFile=Неуспешно импортиране на файл
session.expired=Your session has expired. Please refresh the page and try again. session.expired=Вашата сесия е изтекла. Моля, опреснете страницата и опитайте отново.
############# #############
# HOME-PAGE # # HOME-PAGE #
@@ -390,9 +390,9 @@ home.certSign.title=Подпишете със сертификат
home.certSign.desc=Подписва PDF със сертификат/ключ (PEM/P12) home.certSign.desc=Подписва PDF със сертификат/ключ (PEM/P12)
certSign.tags=удостоверяване,PEM,P12,официален,шифроване certSign.tags=удостоверяване,PEM,P12,официален,шифроване
home.removeCertSign.title=Remove Certificate Sign home.removeCertSign.title=Премахване на знака за сертификат
home.removeCertSign.desc=Remove certificate signature from PDF home.removeCertSign.desc=Премахване на подпис на сертификат от PDF
removeCertSign.tags=authenticate,PEM,P12,official,decrypt removeCertSign.tags=удостоверяване,PEM,P12,официален,декриптиране
home.pageLayout.title=Оформление с няколко страници home.pageLayout.title=Оформление с няколко страници
home.pageLayout.desc=Слейте няколко страници от PDF документ в една страница home.pageLayout.desc=Слейте няколко страници от PDF документ в една страница
@@ -498,33 +498,33 @@ home.BookToPDF.title=Книга към PDF
home.BookToPDF.desc=Преобразува формати на книги/комикси в PDF с помощта на calibre home.BookToPDF.desc=Преобразува формати на книги/комикси в PDF с помощта на calibre
BookToPDF.tags=Книга,комикс,calibre,конвертиране,манга,Amazon,Kindle BookToPDF.tags=Книга,комикс,calibre,конвертиране,манга,Amazon,Kindle
home.removeImagePdf.title=Remove image home.removeImagePdf.title=Премахване на изображение
home.removeImagePdf.desc=Remove image from PDF to reduce file size home.removeImagePdf.desc=Премахнете изображението от PDF, за да намалите размера на файла
removeImagePdf.tags=Remove Image,Page operations,Back end,server side removeImagePdf.tags=Премахване на изображение, операции на страници, админ страна, страна на сървъра
home.splitPdfByChapters.title=Split PDF by Chapters home.splitPdfByChapters.title=Разделете PDF по глави
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure. home.splitPdfByChapters.desc=Разделете PDF на множество файлове въз основа на неговата структура на глави.
splitPdfByChapters.tags=split,chapters,bookmarks,organize splitPdfByChapters.tags=разделяне, глави, отметки, организиране
#replace-invert-color #replace-invert-color
replace-color.title=Replace-Invert-Color replace-color.title=Замени-инвертиране-на-цвят
replace-color.header=Replace-Invert Color PDF replace-color.header=Замяна-инвертиране на цвят PDF
home.replaceColorPdf.title=Replace and Invert Color home.replaceColorPdf.title=Замяна и обръщане на цвят
home.replaceColorPdf.desc=Replace color for text and background in PDF and invert full color of pdf to reduce file size home.replaceColorPdf.desc=Заменете цвета на текста и фона в PDF и обърнете пълния цвят на PDF, за да намалите размера на файла
replaceColorPdf.tags=Replace Color,Page operations,Back end,server side replaceColorPdf.tags=Замяна на цвят, операции на страници, заден край, страна на сървъра
replace-color.selectText.1=Replace or Invert color Options replace-color.selectText.1=Опции за замяна или инвертиране на цвят
replace-color.selectText.2=Default(Default high contrast colors) replace-color.selectText.2=По подразбиране (цветове с висок контраст по подразбиране)
replace-color.selectText.3=Custom(Customized colors) replace-color.selectText.3=По избор (персонализирани цветове)
replace-color.selectText.4=Full-Invert(Invert all colors) replace-color.selectText.4=Пълно инвертиране (Инвертиране на всички цветове)
replace-color.selectText.5=High contrast color options replace-color.selectText.5=Цветови опции с висок контраст
replace-color.selectText.6=white text on black background replace-color.selectText.6=Бял текст на черен фон
replace-color.selectText.7=Black text on white background replace-color.selectText.7=Черен текст на бял фон
replace-color.selectText.8=Yellow text on black background replace-color.selectText.8=Жълт текст на черен фон
replace-color.selectText.9=Green text on black background replace-color.selectText.9=Зелен текст на черен фон
replace-color.selectText.10=Choose text Color replace-color.selectText.10=Изберете цвят на текста
replace-color.selectText.11=Choose background Color replace-color.selectText.11=Изберете цвят на фона
replace-color.submit=Replace replace-color.submit=Замени
@@ -543,18 +543,17 @@ login.locked=Вашият акаунт е заключен.
login.signinTitle=Моля впишете се login.signinTitle=Моля впишете се
login.ssoSignIn=Влизане чрез еднократно влизане login.ssoSignIn=Влизане чрез еднократно влизане
login.oauth2AutoCreateDisabled=OAUTH2 Автоматично създаване на потребител е деактивирано login.oauth2AutoCreateDisabled=OAUTH2 Автоматично създаване на потребител е деактивирано
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator. login.oauth2AdminBlockedUser=Регистрацията или влизането на нерегистрирани потребители в момента е блокирано. Моля, свържете се с администратора.
login.oauth2RequestNotFound=Заявката за оторизация не е намерена login.oauth2RequestNotFound=Заявката за оторизация не е намерена
login.oauth2InvalidUserInfoResponse=Невалидна информация за потребителя login.oauth2InvalidUserInfoResponse=Невалидна информация за потребителя
login.oauth2invalidRequest=Невалидна заявка login.oauth2invalidRequest=Невалидна заявка
login.oauth2AccessDenied=Отказан достъп login.oauth2AccessDenied=Отказан достъп
login.oauth2InvalidTokenResponse=Невалиден отговор на токена login.oauth2InvalidTokenResponse=Невалиден отговор на токена
login.oauth2InvalidIdToken=Невалиден токен за идентификатор login.oauth2InvalidIdToken=Невалиден токен за идентификатор
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator. login.userIsDisabled=Потребителят е деактивиран, влизането в момента е блокирано с това потребителско име. Моля, свържете се с администратора.
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=Вече сте влезли в
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=устройства. Моля, излезте от устройствата и опитайте отново.
login.toManySessions=You have too many active sessions login.toManySessions=Имате твърде много активни сесии
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Автоматично редактиране autoRedact.title=Автоматично редактиране
@@ -729,7 +728,7 @@ pageLayout.submit=Подайте
scalePages.title=Коригиране на мащаба на страницата scalePages.title=Коригиране на мащаба на страницата
scalePages.header=Коригиране на мащаба на страницата scalePages.header=Коригиране на мащаба на страницата
scalePages.pageSize=Размер на страница от документа. scalePages.pageSize=Размер на страница от документа.
scalePages.keepPageSize=Original Size scalePages.keepPageSize=Оригинален размер
scalePages.scaleFactor=Ниво на мащабиране (изрязване) на страница. scalePages.scaleFactor=Ниво на мащабиране (изрязване) на страница.
scalePages.submit=Подайте scalePages.submit=Подайте
@@ -753,10 +752,10 @@ certSign.submit=Подпишете PDF
#removeCertSign #removeCertSign
removeCertSign.title=Remove Certificate Signature removeCertSign.title=Премахване на подписа на сертификата
removeCertSign.header=Remove the digital certificate from the PDF removeCertSign.header=Премахнете цифровия сертификат от PDF
removeCertSign.selectPDF=Select a PDF file: removeCertSign.selectPDF=Изберете PDF файл:
removeCertSign.submit=Remove Signature removeCertSign.submit=Премахване на подпис
#removeBlanks #removeBlanks
@@ -778,8 +777,8 @@ removeAnnotations.submit=Премахване
#compare #compare
compare.title=Сравнявай compare.title=Сравнявай
compare.header=Сравнявай PDF-и compare.header=Сравнявай PDF-и
compare.highlightColor.1=Highlight Color 1: compare.highlightColor.1=Цвят на маркирането 1:
compare.highlightColor.2=Highlight Color 2: compare.highlightColor.2=Цвят на маркирането 2:
compare.document.1=Документ 1 compare.document.1=Документ 1
compare.document.2=Документ 2 compare.document.2=Документ 2
compare.submit=Сравнявай compare.submit=Сравнявай
@@ -831,7 +830,7 @@ ScannerImageSplit.selectText.7=Минимална контурна площ:
ScannerImageSplit.selectText.8=Задава минималния праг на контурната площ за изображение ScannerImageSplit.selectText.8=Задава минималния праг на контурната площ за изображение
ScannerImageSplit.selectText.9=Размер на рамката: ScannerImageSplit.selectText.9=Размер на рамката:
ScannerImageSplit.selectText.10=Задава размера на добавената и премахната граница, за да предотврати бели граници към изхода (по подразбиране: 1). ScannerImageSplit.selectText.10=Задава размера на добавената и премахната граница, за да предотврати бели граници към изхода (по подразбиране: 1).
ScannerImageSplit.info=Python is not installed. It is required to run. ScannerImageSplit.info=Python не е инсталиран. Изисква се да се изпълнява.
#OCR #OCR
@@ -858,7 +857,7 @@ ocr.submit=Обработка на PDF чрез OCR
extractImages.title=Извличане на изображения extractImages.title=Извличане на изображения
extractImages.header=Извличане на изображения extractImages.header=Извличане на изображения
extractImages.selectText=Изберете формат на изображението, в който да преобразувате извлечените изображения extractImages.selectText=Изберете формат на изображението, в който да преобразувате извлечените изображения
extractImages.allowDuplicates=Save duplicate images extractImages.allowDuplicates=Запазване на дублирани изображения
extractImages.submit=Извличане extractImages.submit=Извличане
@@ -896,7 +895,7 @@ merge.title=Обединяване
merge.header=Обединяване на множество PDF файлове (2+) merge.header=Обединяване на множество PDF файлове (2+)
merge.sortByName=Сортиране по име merge.sortByName=Сортиране по име
merge.sortByDate=Сортиране по дата merge.sortByDate=Сортиране по дата
merge.removeCertSign=Remove digital signature in the merged file? merge.removeCertSign=Премахване на цифровия подпис в обединения файл?
merge.submit=Обединяване merge.submit=Обединяване
@@ -914,7 +913,7 @@ pdfOrganiser.mode.6=Четно-нечетно разделяне
pdfOrganiser.mode.7=Премахни първо pdfOrganiser.mode.7=Премахни първо
pdfOrganiser.mode.8=Премахване на последния pdfOrganiser.mode.8=Премахване на последния
pdfOrganiser.mode.9=Премахване на първия и последния pdfOrganiser.mode.9=Премахване на първия и последния
pdfOrganiser.mode.10=Odd-Even Merge pdfOrganiser.mode.10=Обединяване на четно и нечетно
pdfOrganiser.placeholder=(напр. 1,3,2 или 4-8,2,10-12 или 2n-1) pdfOrganiser.placeholder=(напр. 1,3,2 или 4-8,2,10-12 или 2n-1)
@@ -983,7 +982,7 @@ pdfToImage.color=Цвят
pdfToImage.grey=Скала на сивото pdfToImage.grey=Скала на сивото
pdfToImage.blackwhite=Черно и бяло (може да загубите данни!) pdfToImage.blackwhite=Черно и бяло (може да загубите данни!)
pdfToImage.submit=Преобразуване pdfToImage.submit=Преобразуване
pdfToImage.info=Python is not installed. Required for WebP conversion. pdfToImage.info=Python не е инсталиран. Изисква се за конвертиране на WebP.
#addPassword #addPassword
@@ -1020,7 +1019,7 @@ watermark.selectText.6=дължинаSpacer (Разстояние между в
watermark.selectText.7=Непрозрачност (0% - 100%): watermark.selectText.7=Непрозрачност (0% - 100%):
watermark.selectText.8=Тип воден знак: watermark.selectText.8=Тип воден знак:
watermark.selectText.9=Изображение за воден знак: watermark.selectText.9=Изображение за воден знак:
watermark.selectText.10=Convert PDF to PDF-Image watermark.selectText.10=Конвертирайте PDF в PDF-изображение
watermark.submit=Добавяне на воден знак watermark.submit=Добавяне на воден знак
watermark.type.1=Текст watermark.type.1=Текст
watermark.type.2=Изображение watermark.type.2=Изображение
@@ -1077,7 +1076,7 @@ pdfToPDFA.credit=Тази услуга използва ghostscript за PDF/A
pdfToPDFA.submit=Преобразуване pdfToPDFA.submit=Преобразуване
pdfToPDFA.tip=В момента не работи за няколко входа наведнъж pdfToPDFA.tip=В момента не работи за няколко входа наведнъж
pdfToPDFA.outputFormat=Изходен формат pdfToPDFA.outputFormat=Изходен формат
pdfToPDFA.pdfWithDigitalSignature=The PDF contains a digital signature. This will be removed in the next step. pdfToPDFA.pdfWithDigitalSignature=PDF файлът съдържа цифров подпис. Това ще бъде премахнато в следващата стъпка.
#PDFToWord #PDFToWord
@@ -1118,10 +1117,10 @@ PDFToXML.credit=Тази услуга използва LibreOffice за прео
PDFToXML.submit=Преобразуване PDFToXML.submit=Преобразуване
#PDFToCSV #PDFToCSV
PDFToCSV.title=PDF ??? CSV PDFToCSV.title=PDF към CSV
PDFToCSV.header=PDF ??? CSV PDFToCSV.header=PDF към CSV
PDFToCSV.prompt=Изберете страница за извличане на таблица PDFToCSV.prompt=Изберете страница за извличане на таблица
PDFToCSV.submit=???? PDFToCSV.submit=Преобразуване
#split-by-size-or-count #split-by-size-or-count
split-by-size-or-count.title=Разделяне на PDF по размер или брой split-by-size-or-count.title=Разделяне на PDF по размер или брой
@@ -1179,15 +1178,15 @@ licenses.version=Версия
licenses.license=Лиценз licenses.license=Лиценз
#survey #survey
survey.nav=Survey survey.nav=Анкета
survey.title=Stirling-PDF Survey survey.title=Stirling-PDF Анкета
survey.description=Stirling-PDF has no tracking so we want to hear from our users to improve Stirling-PDF! survey.description=Stirling-PDF няма проследяване, така че искаме да чуем мнението на нашите потребители за подобряване на Stirling-PDF!
survey.changes=Stirling-PDF has changed since the last survey! To find out more please check our blog post here: survey.changes=Stirling-PDF се промени от последното проучване! За да научите повече, моля, проверете публикацията в нашия блог тук:
survey.changes2=With these changes we are getting paid business support and funding survey.changes2=С тези промени получаваме платена бизнес подкрепа и финансиране
survey.please=Please consider taking our survey! survey.please=Моля, помислете дали да не участвате в нашата анкета!
survey.disabled=(Survey popup will be disabled in following updates but available at foot of page) survey.disabled=(Изскачащият прозорец с анкетата ще бъде деактивиран при следващите актуализации, но ще бъде наличен в долната част на страницата)
survey.button=Take Survey survey.button=Участвайте в анкетата
survey.dontShowAgain=Don't show again survey.dontShowAgain=Не показвай повече
#error #error
@@ -1205,21 +1204,21 @@ error.discordSubmit=Discord - Изпратете запитване за под
#remove-image #remove-image
removeImage.title=Remove image removeImage.title=Премахване на изображението
removeImage.header=Remove image removeImage.header=Премахване на изображението
removeImage.removeImage=Remove image removeImage.removeImage=Премахване на изображението
removeImage.submit=Remove image removeImage.submit=Премахване на изображението
splitByChapters.title=Split PDF by Chapters splitByChapters.title=Разделете PDF по глави
splitByChapters.header=Split PDF by Chapters splitByChapters.header=Разделете PDF по глави
splitByChapters.bookmarkLevel=Bookmark Level splitByChapters.bookmarkLevel=Ниво на отметка
splitByChapters.includeMetadata=Include Metadata splitByChapters.includeMetadata=Включете метаданни
splitByChapters.allowDuplicates=Allow Duplicates splitByChapters.allowDuplicates=Разрешаване на дубликати
splitByChapters.desc.1=This tool splits a PDF file into multiple PDFs based on its chapter structure. splitByChapters.desc.1=Този инструмент разделя PDF файл на множество PDF файлове въз основа на неговата структура на глави.
splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for splitting (0 for top-level, 1 for second-level, etc.). splitByChapters.desc.2=Ниво на отметка: Изберете нивото на отметките, които да използвате за разделяне (0 за най-високо ниво, 1 за второ ниво и т.н.).
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF. splitByChapters.desc.3=Включване на метаданни: Ако е отметнато, метаданните на оригиналния PDF ще бъдат включени във всеки разделен PDF.
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs. splitByChapters.desc.4=Разрешаване на дубликати: Ако е отметнато, позволява множество отметки на една и съща страница за създаване на отделни PDF файлове.
splitByChapters.submit=Split PDF splitByChapters.submit=Разделяне на PDF

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Auto Redact autoRedact.title=Auto Redact

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Auto Redact autoRedact.title=Auto Redact

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=Bruger er deaktiveret, login er i øjeblikket blokeret med
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Auto Rediger autoRedact.title=Auto Rediger

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=Benutzer ist deaktiviert, die Anmeldung ist mit diesem Benu
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Automatisch zensieren/schwärzen autoRedact.title=Automatisch zensieren/schwärzen

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Αυτόματο Μαύρισμα Κειμένου autoRedact.title=Αυτόματο Μαύρισμα Κειμένου

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Auto Redact autoRedact.title=Auto Redact

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Auto Redact autoRedact.title=Auto Redact

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=El usuario está desactivado, actualmente el acceso está b
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Auto Redactar autoRedact.title=Auto Redactar

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Auto Idatzi autoRedact.title=Auto Idatzi

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Caviarder automatiquement autoRedact.title=Caviarder automatiquement

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Auto Redact autoRedact.title=Auto Redact

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=स्वत: गोपनीयकरण autoRedact.title=स्वत: गोपनीयकरण

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Automatsko uređivanje autoRedact.title=Automatsko uređivanje

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Érzékeny tartalom eltávolítása autoRedact.title=Érzékeny tartalom eltávolítása

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Redaksional Otomatis autoRedact.title=Redaksional Otomatis

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=L'utente è disattivato, l'accesso è attualmente bloccato
login.alreadyLoggedIn=Hai già effettuato l'accesso a login.alreadyLoggedIn=Hai già effettuato l'accesso a
login.alreadyLoggedIn2=dispositivi. Esci dai dispositivi e riprova. login.alreadyLoggedIn2=dispositivi. Esci dai dispositivi e riprova.
login.toManySessions=Hai troppe sessioni attive login.toManySessions=Hai troppe sessioni attive
login.toManySessions2=Si prega di uscire dai dispositivi e riprovare. In alternativa, è possibile effettuare l'upgrade a Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Redazione automatica autoRedact.title=Redazione automatica

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=ユーザーは非アクティブ化されており、現
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=自動塗りつぶし autoRedact.title=自動塗りつぶし

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=자동 검열 autoRedact.title=자동 검열

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Automatisch censureren autoRedact.title=Automatisch censureren

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Automatisk Sensurering autoRedact.title=Automatisk Sensurering

View File

@@ -3,8 +3,8 @@
########### ###########
# the direction that the language is written (ltr = left to right, rtl = right to left) # the direction that the language is written (ltr = left to right, rtl = right to left)
language.direction=ltr language.direction=ltr
addPageNumbers.fontSize=Font Size addPageNumbers.fontSize=Rozmiar Czcionki
addPageNumbers.fontName=Font Name addPageNumbers.fontName=Nazwa Czcionki
pdfPrompt=Wybierz PDF pdfPrompt=Wybierz PDF
multiPdfPrompt=Wybierz PDF (2+) multiPdfPrompt=Wybierz PDF (2+)
multiPdfDropPrompt=Wybierz (lub przeciągnij i puść) wszystkie dokumenty PDF multiPdfDropPrompt=Wybierz (lub przeciągnij i puść) wszystkie dokumenty PDF
@@ -56,12 +56,12 @@ userNotFoundMessage=Brak użytkownika.
incorrectPasswordMessage=Nieprawidłowe hasło. incorrectPasswordMessage=Nieprawidłowe hasło.
usernameExistsMessage=Taki uzytkownik już istnieje. usernameExistsMessage=Taki uzytkownik już istnieje.
invalidUsernameMessage=Niewłaściwa nazwa użytkownika - musi zawierać litery, cyfry i @._+- LUB być adresem email. invalidUsernameMessage=Niewłaściwa nazwa użytkownika - musi zawierać litery, cyfry i @._+- LUB być adresem email.
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end. invalidPasswordMessage=Hasło nie może być puste i nie może zawierać spacji na początku ani na końcu.
confirmPasswordErrorMessage=Wpisz poprawnie hasło w OBA pola. confirmPasswordErrorMessage=Wpisz poprawnie hasło w OBA pola.
deleteCurrentUserMessage=Nie można usunąć zalogowanego użytkownika deleteCurrentUserMessage=Nie można usunąć zalogowanego użytkownika
deleteUsernameExistsMessage=Nie można usunąć zalogowanego użytkownika deleteUsernameExistsMessage=Nie można usunąć zalogowanego użytkownika
downgradeCurrentUserMessage=Nie można obniżyć roli bieżącego użytkownika downgradeCurrentUserMessage=Nie można obniżyć roli bieżącego użytkownika
disabledCurrentUserMessage=The current user cannot be disabled disabledCurrentUserMessage=Nie można wyłączyć bieżącego użytkownika
downgradeCurrentUserLongMessage=Nie można obniżyć roli bieżącego użytkownika. W związku z tym bieżący użytkownik nie zostanie wyświetlony. downgradeCurrentUserLongMessage=Nie można obniżyć roli bieżącego użytkownika. W związku z tym bieżący użytkownik nie zostanie wyświetlony.
userAlreadyExistsOAuthMessage=Takie konto użytkownika istnieje - stworzone za pomocą OAuth2. userAlreadyExistsOAuthMessage=Takie konto użytkownika istnieje - stworzone za pomocą OAuth2.
userAlreadyExistsWebMessage=Takie konto użytkownika istnieje - stworzone za pomocą przeglądarki. userAlreadyExistsWebMessage=Takie konto użytkownika istnieje - stworzone za pomocą przeglądarki.
@@ -77,14 +77,14 @@ color=kolor
sponsor=sponsor sponsor=sponsor
info=informacje info=informacje
pro=Pro pro=Pro
page=Page page=Strona
pages=Pages pages=Strony
legal.privacy=Privacy Policy legal.privacy=Polityka Prywatności
legal.terms=Terms and Conditions legal.terms=Zasady i Postanowienia
legal.accessibility=Accessibility legal.accessibility=Dostępność
legal.cookie=Cookie Policy legal.cookie=Polityka plików cookie
legal.impressum=Impressum legal.impressum=Impresja
############### ###############
# Pipeline # # Pipeline #
@@ -114,21 +114,21 @@ pipelineOptions.validateButton=Waliduj
######################## ########################
# ENTERPRISE EDITION # # ENTERPRISE EDITION #
######################## ########################
enterpriseEdition.button=Upgrade to Pro enterpriseEdition.button=Uaktualnij do wersji Pro
enterpriseEdition.warning=This feature is only available to Pro users. enterpriseEdition.warning=Ta funkcja jest dostępna tylko dla użytkowników Pro.
enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features. enterpriseEdition.yamlAdvert=Stirling PDF Pro obsługuje pliki konfiguracyjne YAML i inne funkcje SSO.
enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro enterpriseEdition.ssoAdvert=Szukasz więcej funkcji zarządzania użytkownikami? Sprawdź Stirling PDF Pro
################# #################
# Analytics # # Analytics #
################# #################
analytics.title=Do you want make Stirling PDF better? analytics.title=Czy chcesz ulepszyć Stirling PDF?
analytics.paragraph1=Stirling PDF has opt in analytics to help us improve the product. We do not track any personal information or file contents. analytics.paragraph1=Stirling PDF ma opcję analizy, która pomaga nam udoskonalać produkt. Nie śledzimy żadnych danych osobowych ani zawartości plików.
analytics.paragraph2=Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better. analytics.paragraph2=Rozważ włączenie funkcji analitycznych, które pomogą w rozwoju Stirling-PDF i pozwolą nam lepiej zrozumieć naszych użytkowników.
analytics.enable=Enable analytics analytics.enable=Włącz analitykę
analytics.disable=Disable analytics analytics.disable=Wyłącz analitykę
analytics.settings=You can change the settings for analytics in the config/settings.yml file analytics.settings=Możesz zmienić ustawienia analityki w pliku config/settings.yml
############# #############
# NAVBAR # # NAVBAR #
@@ -138,14 +138,14 @@ navbar.darkmode=Tryb nocny
navbar.language=Języki navbar.language=Języki
navbar.settings=Ustawienia navbar.settings=Ustawienia
navbar.allTools=Narzędzia navbar.allTools=Narzędzia
navbar.multiTool=Multi Tools navbar.multiTool=Narzędzie Wielofunkcyjne
navbar.sections.organize=Organizuj navbar.sections.organize=Organizuj
navbar.sections.convertTo=Przetwórz na PDF navbar.sections.convertTo=Przetwórz na PDF
navbar.sections.convertFrom=Przetwórz z PDF navbar.sections.convertFrom=Przetwórz z PDF
navbar.sections.security=Podpis i bezpieczeństwo navbar.sections.security=Podpis i bezpieczeństwo
navbar.sections.advance=Zaawansowane navbar.sections.advance=Zaawansowane
navbar.sections.edit=Podgląd i edycja navbar.sections.edit=Podgląd i edycja
navbar.sections.popular=Popular navbar.sections.popular=Popularne
############# #############
# SETTINGS # # SETTINGS #
@@ -218,13 +218,13 @@ adminUserSettings.forceChange=Wymuś zmianę hasło po zalogowaniu
adminUserSettings.submit=Zapisz użytkownika adminUserSettings.submit=Zapisz użytkownika
adminUserSettings.changeUserRole=Zmień rolę użytkownika adminUserSettings.changeUserRole=Zmień rolę użytkownika
adminUserSettings.authenticated=Zalogowany adminUserSettings.authenticated=Zalogowany
adminUserSettings.editOwnProfil=Edit own profile adminUserSettings.editOwnProfil=Edytuj własny profil
adminUserSettings.enabledUser=enabled user adminUserSettings.enabledUser=włączony użytkownik
adminUserSettings.disabledUser=disabled user adminUserSettings.disabledUser=wyłączony użytkownik
adminUserSettings.activeUsers=Active Users: adminUserSettings.activeUsers=Aktywni Użytkownicy:
adminUserSettings.disabledUsers=Disabled Users: adminUserSettings.disabledUsers=Wyłączeni Użytkownicy:
adminUserSettings.totalUsers=Total Users: adminUserSettings.totalUsers=Łączna Liczba Użytkowników:
adminUserSettings.lastRequest=Last Request adminUserSettings.lastRequest=Ostatnie Zgłoszenie
database.title=Import/Eksport bazy danych database.title=Import/Eksport bazy danych
@@ -243,7 +243,7 @@ database.fileNotFound=Plik nie znaleziony
database.fileNullOrEmpty=Plik nie może być pusty database.fileNullOrEmpty=Plik nie może być pusty
database.failedImportFile=Nie udało się zaimportować pliku database.failedImportFile=Nie udało się zaimportować pliku
session.expired=Your session has expired. Please refresh the page and try again. session.expired=Twoja sesja wygasła. Odśwież stronę i spróbuj ponownie.
############# #############
# HOME-PAGE # # HOME-PAGE #
@@ -256,207 +256,207 @@ home.viewPdf.title=Podejrzyj PDF
home.viewPdf.desc=Wyświetl, adnotuj, dodaj tekst lub obrazy home.viewPdf.desc=Wyświetl, adnotuj, dodaj tekst lub obrazy
viewPdf.tags=wyświetl,czytaj,adnotuj,tekst,obraz viewPdf.tags=wyświetl,czytaj,adnotuj,tekst,obraz
home.multiTool.title=Multi narzędzie PDF home.multiTool.title=Wielofunkcyjne Narzędzie PDF
home.multiTool.desc=Łącz, dziel, obracaj, zmieniaj kolejność i usuwaj strony home.multiTool.desc=Łącz, dziel, obracaj, zmieniaj kolejność i usuwaj strony
multiTool.tags=Multi Tool,Multi operation,UI,click drag,front end,client side multiTool.tags=Wielofunkcyjne narzędzie, obsługa wielu operacji, interfejs użytkownika, przeciąganie kliknięć, front-end, strona klienta
home.merge.title=Połącz home.merge.title=Połącz
home.merge.desc=Łatwe łączenie wielu dokumentów PDF w jeden. home.merge.desc=Łatwe łączenie wielu dokumentów PDF w jeden.
merge.tags=merge,Page operations,Back end,server side merge.tags=scalanie, operacje na stronach, back-end, po stronie serwera
home.split.title=Podziel home.split.title=Podziel
home.split.desc=Podziel dokument PDF na wiele dokumentów home.split.desc=Podziel dokument PDF na wiele dokumentów
split.tags=Page operations,divide,Multi Page,cut,server side split.tags=Operacje na stronach, dzielenie, wiele stron, cięcie, po stronie serwera
home.rotate.title=Obróć home.rotate.title=Obróć
home.rotate.desc=Łatwo obracaj dokumenty PDF. home.rotate.desc=Łatwo obracaj dokumenty PDF.
rotate.tags=server side rotate.tags=strona serwera
home.imageToPdf.title=Obraz na PDF home.imageToPdf.title=Obraz na PDF
home.imageToPdf.desc=Konwertuj obraz (PNG, JPEG, GIF) do dokumentu PDF. home.imageToPdf.desc=Konwertuj obraz (PNG, JPEG, GIF) do dokumentu PDF.
imageToPdf.tags=conversion,img,jpg,picture,photo imageToPdf.tags=konwersja,img,jpg,obraz,zdjęcie
home.pdfToImage.title=PDF na Obraz home.pdfToImage.title=PDF na Obraz
home.pdfToImage.desc=Konwertuj plik PDF na obraz (PNG, JPEG, GIF). home.pdfToImage.desc=Konwertuj plik PDF na obraz (PNG, JPEG, GIF).
pdfToImage.tags=conversion,img,jpg,picture,photo pdfToImage.tags=konwersja,img,jpg,obraz,zdjęcie
home.pdfOrganiser.title=Uporządkuj home.pdfOrganiser.title=Uporządkuj
home.pdfOrganiser.desc=Usuń/Zmień kolejność stron w dowolnej kolejności home.pdfOrganiser.desc=Usuń/Zmień kolejność stron w dowolnej kolejności
pdfOrganiser.tags=duplex,even,odd,sort,move pdfOrganiser.tags=duplex,parzyste,nieparzyste,sortuj,przenieś
home.addImage.title=Dodaj obraz home.addImage.title=Dodaj obraz
home.addImage.desc=Dodaje obraz w wybranym miejscu w dokumencie PDF home.addImage.desc=Dodaje obraz w wybranym miejscu w dokumencie PDF
addImage.tags=img,jpg,picture,photo addImage.tags=img,jpg,obraz,zdjęcie
home.watermark.title=Dodaj znak wodny home.watermark.title=Dodaj znak wodny
home.watermark.desc=Dodaj niestandardowy znak wodny do dokumentu PDF. home.watermark.desc=Dodaj niestandardowy znak wodny do dokumentu PDF.
watermark.tags=Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo watermark.tags=Tekst,powtarzanie,etykieta,własne,prawa autorskie,znak wodny,img,jpg,obraz,zdjęcie
home.permissions.title=Zmień uprawnienia home.permissions.title=Zmień uprawnienia
home.permissions.desc=Zmień uprawnienia dokumentu PDF home.permissions.desc=Zmień uprawnienia dokumentu PDF
permissions.tags=read,write,edit,print permissions.tags=odczyt,zapis,edycja,drukowanie
home.removePages.title=Usuń home.removePages.title=Usuń
home.removePages.desc=Usuń niechciane strony z dokumentu PDF. home.removePages.desc=Usuń niechciane strony z dokumentu PDF.
removePages.tags=Remove pages,delete pages removePages.tags=Usuń strony,usuwaj strony
home.addPassword.title=Dodaj hasło home.addPassword.title=Dodaj hasło
home.addPassword.desc=Zaszyfruj dokument PDF za pomocą hasła. home.addPassword.desc=Zaszyfruj dokument PDF za pomocą hasła.
addPassword.tags=secure,security addPassword.tags=bezpieczeństwo,ochrona
home.removePassword.title=Usuń hasło home.removePassword.title=Usuń hasło
home.removePassword.desc=Usuń ochronę hasłem z dokumentu PDF. home.removePassword.desc=Usuń ochronę hasłem z dokumentu PDF.
removePassword.tags=secure,Decrypt,security,unpassword,delete password removePassword.tags=zabezpieczenie,odszyfrowanie,bezpieczeństwo,odhasłowanie,usunięcie hasła
home.compressPdfs.title=Kompresuj home.compressPdfs.title=Kompresuj
home.compressPdfs.desc=Kompresuj dokumenty PDF, aby zmniejszyć ich rozmiar. home.compressPdfs.desc=Kompresuj dokumenty PDF, aby zmniejszyć ich rozmiar.
compressPdfs.tags=squish,small,tiny compressPdfs.tags=zgniatać,mały,malutki
home.changeMetadata.title=Zmień metadane home.changeMetadata.title=Zmień metadane
home.changeMetadata.desc=Zmień/Usuń/Dodaj metadane w dokumencie PDF home.changeMetadata.desc=Zmień/Usuń/Dodaj metadane w dokumencie PDF
changeMetadata.tags=Title,author,date,creation,time,publisher,producer,stats changeMetadata.tags=Tytuł,autor,data,utworzenie,czas,wydawca,producent,statystyki
home.fileToPDF.title=Konwertuj plik do PDF home.fileToPDF.title=Konwertuj plik do PDF
home.fileToPDF.desc=Konwertuj dowolny plik do dokumentu PDF (DOCX, PNG, XLS, PPT, TXT i więcej) home.fileToPDF.desc=Konwertuj dowolny plik do dokumentu PDF (DOCX, PNG, XLS, PPT, TXT i więcej)
fileToPDF.tags=transformation,format,document,picture,slide,text,conversion,office,docs,word,excel,powerpoint fileToPDF.tags=transformacja,format,dokument,obraz,slajd,tekst,konwersja,office,dokumenty,word,excel,powerpoint
home.ocr.title=OCR / Zamiana na tekst home.ocr.title=OCR / Zamiana na tekst
home.ocr.desc=OCR skanuje i wykrywa tekst z obrazów w dokumencie PDF i zamienia go na tekst. home.ocr.desc=OCR skanuje i wykrywa tekst z obrazów w dokumencie PDF i zamienia go na tekst.
ocr.tags=recognition,text,image,scan,read,identify,detection,editable ocr.tags=rozpoznawanie, tekst, obraz, skanowanie, odczyt, identyfikacja, wykrywanie, edytowalność
home.extractImages.title=Wyodrębnij obrazy home.extractImages.title=Wyodrębnij obrazy
home.extractImages.desc=Wyodrębnia wszystkie obrazy z dokumentu PDF i zapisuje je w wybranym formacie home.extractImages.desc=Wyodrębnia wszystkie obrazy z dokumentu PDF i zapisuje je w wybranym formacie
extractImages.tags=picture,photo,save,archive,zip,capture,grab extractImages.tags=obraz, zdjęcie, zapisz, archiwum, zip, przechwyć, złap
home.pdfToPDFA.title=PDF na PDF/A home.pdfToPDFA.title=PDF na PDF/A
home.pdfToPDFA.desc=Konwertuj dokument PDF na PDF/A w celu długoterminowego przechowywania home.pdfToPDFA.desc=Konwertuj dokument PDF na PDF/A w celu długoterminowego przechowywania
pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation pdfToPDFA.tags=archiwum, długoterminowe, standardowe, konwersja, przechowywanie, konserwacja
home.PDFToWord.title=PDF na Word home.PDFToWord.title=PDF na Word
home.PDFToWord.desc=Konwertuj dokument PDF na formaty Word (DOC, DOCX i ODT) home.PDFToWord.desc=Konwertuj dokument PDF na formaty Word (DOC, DOCX i ODT)
PDFToWord.tags=doc,docx,odt,word,transformation,format,conversion,office,microsoft,docfile PDFToWord.tags=doc,docx,odt,word, przekształcenie, transformacja, konwersja, office, microsoft, plik doc
home.PDFToPresentation.title=PDF na Prezentację home.PDFToPresentation.title=PDF na Prezentację
home.PDFToPresentation.desc=Konwertuj dokument PDF na formaty prezentacji (PPT, PPTX i ODP) home.PDFToPresentation.desc=Konwertuj dokument PDF na formaty prezentacji (PPT, PPTX i ODP)
PDFToPresentation.tags=slides,show,office,microsoft PDFToPresentation.tags=slajdy, pokaz, office, microsoft
home.PDFToText.title=PDF na Tekst/RTF home.PDFToText.title=PDF na Tekst/RTF
home.PDFToText.desc=Konwertuj dokument PDF na tekst lub format RTF home.PDFToText.desc=Konwertuj dokument PDF na tekst lub format RTF
PDFToText.tags=richformat,richtextformat,rich text format PDFToText.tags=format tekstu sformatowanego,rtf format
home.PDFToHTML.title=PDF na HTML home.PDFToHTML.title=PDF na HTML
home.PDFToHTML.desc=Konwertuj dokument PDF na format HTML home.PDFToHTML.desc=Konwertuj dokument PDF na format HTML
PDFToHTML.tags=web content,browser friendly PDFToHTML.tags=zawartość internetowa, przyjazne dla przeglądarek
home.PDFToXML.title=PDF na XML home.PDFToXML.title=PDF na XML
home.PDFToXML.desc=Konwertuj dokument PDF na format XML home.PDFToXML.desc=Konwertuj dokument PDF na format XML
PDFToXML.tags=data-extraction,structured-content,interop,transformation,convert PDFToXML.tags=ekstrakcja danych, zawartość strukturalna, współdziałanie, transformacja, konwertowanie
home.ScannerImageSplit.title=Wykryj/Podziel zeskanowane zdjęcia home.ScannerImageSplit.title=Wykryj/Podziel zeskanowane zdjęcia
home.ScannerImageSplit.desc=Podziel na wiele zdjęć z jednego zdjęcia/PDF home.ScannerImageSplit.desc=Podziel na wiele zdjęć z jednego zdjęcia/PDF
ScannerImageSplit.tags=separate,auto-detect,scans,multi-photo,organize ScannerImageSplit.tags=oddzielne, automatyczne wykrywanie, skanowanie, wiele zdjęć, porządkowanie
home.sign.title=Podpis home.sign.title=Podpis
home.sign.desc=Dodaje podpis do dokumentu PDF za pomocą rysunku, tekstu lub obrazu home.sign.desc=Dodaje podpis do dokumentu PDF za pomocą rysunku, tekstu lub obrazu
sign.tags=authorize,initials,drawn-signature,text-sign,image-signature sign.tags=autoryzacja, inicjały, podpis odręczny, podpis tekstowy, podpis graficzny
home.flatten.title=Spłaszcz home.flatten.title=Spłaszcz
home.flatten.desc=Usuń wszystkie interaktywne elementy i formularze z dokumentu PDF home.flatten.desc=Usuń wszystkie interaktywne elementy i formularze z dokumentu PDF
flatten.tags=static,deactivate,non-interactive,streamline flatten.tags=statyczny, dezaktywacja, nieinteraktywny, opływowy, streamline
home.repair.title=Napraw home.repair.title=Napraw
home.repair.desc=Spróbuj naprawić uszkodzony dokument PDF home.repair.desc=Spróbuj naprawić uszkodzony dokument PDF
repair.tags=fix,restore,correction,recover repair.tags=naprawianie, naprawa, przywracanie, poprawianie, odzyskiwanie
home.removeBlanks.title=Usuń puste strony home.removeBlanks.title=Usuń puste strony
home.removeBlanks.desc=Wykrywa i usuwa puste strony z dokumentu PDF home.removeBlanks.desc=Wykrywa i usuwa puste strony z dokumentu PDF
removeBlanks.tags=cleanup,streamline,non-content,organize removeBlanks.tags=czyszczenie, usprawnianie, brak treści, organizowanie
home.removeAnnotations.title=Usuń notatki/przypisy home.removeAnnotations.title=Usuń notatki/przypisy
home.removeAnnotations.desc=Usuwa wszystkie notatki i przypisy z dokumentu PDF home.removeAnnotations.desc=Usuwa wszystkie notatki i przypisy z dokumentu PDF
removeAnnotations.tags=comments,highlight,notes,markup,remove removeAnnotations.tags=komentarze, podświetlanie, notatki, znaczniki, usuwanie
home.compare.title=Porównaj home.compare.title=Porównaj
home.compare.desc=Porównuje i pokazuje różnice między dwoma dokumentami PDF home.compare.desc=Porównuje i pokazuje różnice między dwoma dokumentami PDF
compare.tags=differentiate,contrast,changes,analysis compare.tags=rozróżnienie, kontrast, zmiany, analiza
home.certSign.title=Podpisz certyfikatem home.certSign.title=Podpisz certyfikatem
home.certSign.desc=Podpisz dokument PDF za pomocą certyfikatu/klucza prywatnego (PEM/P12) home.certSign.desc=Podpisz dokument PDF za pomocą certyfikatu/klucza prywatnego (PEM/P12)
certSign.tags=authenticate,PEM,P12,official,encrypt certSign.tags=uwierzytelnianie, PEM, P12, oficjalny, szyfrowanie
home.removeCertSign.title=Usuń podpis certyfikatem home.removeCertSign.title=Usuń podpis certyfikatem
home.removeCertSign.desc=Usuń podpis certyfikatem z dokumentu PDF home.removeCertSign.desc=Usuń podpis certyfikatem z dokumentu PDF
removeCertSign.tags=authenticate,PEM,P12,official,decrypt removeCertSign.tags=uwierzytelnianie, PEM, P12, oficjalny, odszyfrowywanie
home.pageLayout.title=Układ wielu stron home.pageLayout.title=Układ wielu stron
home.pageLayout.desc=Scal wiele stron dokumentu PDF w jedną stronę home.pageLayout.desc=Scal wiele stron dokumentu PDF w jedną stronę
pageLayout.tags=merge,composite,single-view,organize pageLayout.tags=scalanie, kompozycja, pojedynczy widok, organizowanie, porządkowanie
home.scalePages.title=Dopasuj rozmiar stron home.scalePages.title=Dopasuj rozmiar stron
home.scalePages.desc=Dopasuj rozmiar stron wybranego dokumentu PDF home.scalePages.desc=Dopasuj rozmiar stron wybranego dokumentu PDF
scalePages.tags=resize,modify,dimension,adapt scalePages.tags=zmiana rozmiaru, modyfikacja, rozmiar, dostosowanie
home.pipeline.title=Automatyzacja home.pipeline.title=Automatyzacja
home.pipeline.desc=Wykonaj wiele akcji na dokumentach PDF, tworząc automatyzację home.pipeline.desc=Wykonaj wiele akcji na dokumentach PDF, tworząc automatyzację
pipeline.tags=automate,sequence,scripted,batch-process pipeline.tags=automatyzacja, sekwencja, skrypt, przetwarzanie wsadowe
home.add-page-numbers.title=Dodaj numery stron home.add-page-numbers.title=Dodaj numery stron
home.add-page-numbers.desc=Dodaj numery strony w dokumencie PDF w podanej lokalizacji home.add-page-numbers.desc=Dodaj numery strony w dokumencie PDF w podanej lokalizacji
add-page-numbers.tags=paginate,label,organize,index add-page-numbers.tags=stronicowanie, etykieta, organizowanie, indeks, index
home.auto-rename.title=Automatycznie zmień nazwę PDF home.auto-rename.title=Automatycznie zmień nazwę PDF
home.auto-rename.desc=Automatycznie zmień nazwę PDF bazując na nagłówku home.auto-rename.desc=Automatycznie zmień nazwę PDF bazując na nagłówku
auto-rename.tags=auto-detect,header-based,organize,relabel auto-rename.tags=automatyczne wykrywanie, oparte na nagłówkach, organizowanie, ponowne etykietowanie
home.adjust-contrast.title=Zmień kolor/nasycenie/jasność home.adjust-contrast.title=Zmień kolor/nasycenie/jasność
home.adjust-contrast.desc=Zmień kolor/nasycenie/jasność w dokumencie PDF home.adjust-contrast.desc=Zmień kolor/nasycenie/jasność w dokumencie PDF
adjust-contrast.tags=color-correction,tune,modify,enhance adjust-contrast.tags=Korekcja kolorów, dostrajanie, modyfikacja, ulepszanie
home.crop.title=Przytnij PDF home.crop.title=Przytnij PDF
home.crop.desc=Przytnij dokument PDF w celu zmniejszenia rozmiaru home.crop.desc=Przytnij dokument PDF w celu zmniejszenia rozmiaru
crop.tags=trim,shrink,edit,shape crop.tags=przycinanie, zmniejszanie, edycja, kształtowanie
home.autoSplitPDF.title=Automatycznie podziel strony home.autoSplitPDF.title=Automatycznie podziel strony
home.autoSplitPDF.desc=Automatycznie podziel dokument na strony home.autoSplitPDF.desc=Automatycznie podziel dokument na strony
autoSplitPDF.tags=QR-based,separate,scan-segment,organize autoSplitPDF.tags=Oparty na QR, rozdzielanie, skanowanie, organizowanie
home.sanitizePdf.title=Dezynfekcja home.sanitizePdf.title=Dezynfekcja
home.sanitizePdf.desc=Usuń skrypt i inne elementy z dokumentu PDF home.sanitizePdf.desc=Usuń skrypt i inne elementy z dokumentu PDF
sanitizePdf.tags=clean,secure,safe,remove-threats sanitizePdf.tags=czyszczenie, ochrona, bezpieczeństwo, usuwanie zagrożeń
home.URLToPDF.title=Strona WWW do PDFa home.URLToPDF.title=Strona WWW do PDFa
home.URLToPDF.desc=Zapisuje podany adres WWW do PDFa home.URLToPDF.desc=Zapisuje podany adres WWW do PDFa
URLToPDF.tags=web-capture,save-page,web-to-doc,archive URLToPDF.tags=przechwytywanie stron internetowych, zapisywanie strony, strona internetowa do dokumentu, archiwizacja
home.HTMLToPDF.title=HTML do PDF home.HTMLToPDF.title=HTML do PDF
home.HTMLToPDF.desc=Zapisuje podany plik HTML/ZIP do PDF home.HTMLToPDF.desc=Zapisuje podany plik HTML/ZIP do PDF
HTMLToPDF.tags=markup,web-content,transformation,convert HTMLToPDF.tags=znaczniki, treść internetowa, transformacja, konwertowanie
home.MarkdownToPDF.title=Markdown do PDF home.MarkdownToPDF.title=Markdown do PDF
home.MarkdownToPDF.desc=Zapisuje dokument Markdown do PDF home.MarkdownToPDF.desc=Zapisuje dokument Markdown do PDF
MarkdownToPDF.tags=markup,web-content,transformation,convert MarkdownToPDF.tags=znaczniki, treść internetowa, transformacja, konwertowanie
home.getPdfInfo.title=Pobierz informacje o pliku PDF home.getPdfInfo.title=Pobierz informacje o pliku PDF
home.getPdfInfo.desc=Pobiera wszelkie informacje o pliku PDF home.getPdfInfo.desc=Pobiera wszelkie informacje o pliku PDF
getPdfInfo.tags=infomation,data,stats,statistics getPdfInfo.tags=informacje, dane, statystyka, statystyki
home.extractPage.title=Wyciągnij stronę z PDF home.extractPage.title=Wyciągnij stronę z PDF
home.extractPage.desc=Wyciąga stronę z dokumentu PDF home.extractPage.desc=Wyciąga stronę z dokumentu PDF
extractPage.tags=extract extractPage.tags=wydobycie,separacja,wyciaganie
home.PdfToSinglePage.title=PDF do jednej strony home.PdfToSinglePage.title=PDF do jednej strony
home.PdfToSinglePage.desc=Łączy wszystkie strony PDFa w jedną wielką stronę PDF home.PdfToSinglePage.desc=Łączy wszystkie strony PDFa w jedną wielką stronę PDF
PdfToSinglePage.tags=single page PdfToSinglePage.tags=pojedyncza strona
home.showJS.title=Pokaż kod JavaScript home.showJS.title=Pokaż kod JavaScript
@@ -465,66 +465,66 @@ showJS.tags=JS
home.autoRedact.title=Zaciemnij home.autoRedact.title=Zaciemnij
home.autoRedact.desc=Zaciemnia dokument PDF bazując na podanej wartości home.autoRedact.desc=Zaciemnia dokument PDF bazując na podanej wartości
autoRedact.tags=Redact,Hide,black out,black,marker,hidden autoRedact.tags=Redagowanie, ukrywanie, zaciemnianie, zaczernianie, zaznaczanie, ukrywanie
home.tableExtraxt.title=PDF do CSV home.tableExtraxt.title=PDF do CSV
home.tableExtraxt.desc=Konwertuje tabele z PDF do pliku CSV home.tableExtraxt.desc=Konwertuje tabele z PDF do pliku CSV
tableExtraxt.tags=CSV,Table Extraction,extract,convert tableExtraxt.tags=CSV, ekstrakcja tabeli, ekstrakcja, konwersja, wydobywanie
home.autoSizeSplitPDF.title=Podziel (Rozmiar/Ilość stron) home.autoSizeSplitPDF.title=Podziel (Rozmiar/Ilość stron)
home.autoSizeSplitPDF.desc=Rozdziela dokument PDF na wiele dokumentów bazując na podanym rozmiarze, ilości stron bądź ilości dokumentów home.autoSizeSplitPDF.desc=Rozdziela dokument PDF na wiele dokumentów bazując na podanym rozmiarze, ilości stron bądź ilości dokumentów
autoSizeSplitPDF.tags=pdf,split,document,organization autoSizeSplitPDF.tags=pdf, dzielenie, dokument, organizacja
home.overlay-pdfs.title=Nałóż PDFa home.overlay-pdfs.title=Nałóż PDFa
home.overlay-pdfs.desc=Nakłada dokumenty PDF na siebie home.overlay-pdfs.desc=Nakłada dokumenty PDF na siebie
overlay-pdfs.tags=Overlay overlay-pdfs.tags=Nakładka
home.split-by-sections.title=Podziel PDF na sekcje home.split-by-sections.title=Podziel PDF na sekcje
home.split-by-sections.desc=Podziel strony PDF w mniejsze sekcje home.split-by-sections.desc=Podziel strony PDF w mniejsze sekcje
split-by-sections.tags=Section Split, Divide, Customize split-by-sections.tags=Podział sekcji, dzielenie, dostosowywanie
home.AddStampRequest.title=Dodaj pieczęć home.AddStampRequest.title=Dodaj pieczęć
home.AddStampRequest.desc=Dodaj pieczęć tekstową/obrazową w wyznaczonej lokalizacji dokumentu home.AddStampRequest.desc=Dodaj pieczęć tekstową/obrazową w wyznaczonej lokalizacji dokumentu
AddStampRequest.tags=Stamp, Add image, center image, Watermark, PDF, Embed, Customize AddStampRequest.tags=Stempel, dodawanie obrazu, wyśrodkowanie obrazu, znak wodny, PDF, osadzanie, dostosowywanie
home.PDFToBook.title=PDF do eBooka home.PDFToBook.title=PDF do eBooka
home.PDFToBook.desc=Zapisuje dokument PDF w formacie eBooka za pomocą Calibre home.PDFToBook.desc=Zapisuje dokument PDF w formacie eBooka za pomocą Calibre
PDFToBook.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle PDFToBook.tags=Książka,komiks,Calibre, konwertowanie, manga, amazon, kindle
home.BookToPDF.title=eBook do PDF home.BookToPDF.title=eBook do PDF
home.BookToPDF.desc=Zapisuje ebooka do PDF za pomocą Calibre home.BookToPDF.desc=Zapisuje ebooka do PDF za pomocą Calibre
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
home.removeImagePdf.title=Remove image home.removeImagePdf.title=Usuń obraz
home.removeImagePdf.desc=Remove image from PDF to reduce file size home.removeImagePdf.desc=Usuń obraz z pliku PDF, aby zmniejszyć rozmiar pliku
removeImagePdf.tags=Remove Image,Page operations,Back end,server side removeImagePdf.tags=Usuń obraz, operacje na stronie, back-end, strona serwera
home.splitPdfByChapters.title=Split PDF by Chapters home.splitPdfByChapters.title=Podziel PDF według rozdziałów
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure. home.splitPdfByChapters.desc=Podział pliku PDF na wiele plików na podstawie struktury rozdziałów.
splitPdfByChapters.tags=split,chapters,bookmarks,organize splitPdfByChapters.tags=podział, rozdziały, zakładki, porządkowanie, organizacja
#replace-invert-color #replace-invert-color
replace-color.title=Replace-Invert-Color replace-color.title=Zamień-Odwróć-Kolor
replace-color.header=Replace-Invert Color PDF replace-color.header=Zamień-Odwróć kolor PDF
home.replaceColorPdf.title=Replace and Invert Color home.replaceColorPdf.title=Zastąp i Odwróć Kolor
home.replaceColorPdf.desc=Replace color for text and background in PDF and invert full color of pdf to reduce file size home.replaceColorPdf.desc=Zastąp kolor tekstu i tła w pliku PDF i odwróć pełen kolor pliku PDF, aby zmniejszyć rozmiar pliku
replaceColorPdf.tags=Replace Color,Page operations,Back end,server side replaceColorPdf.tags=Zastąp kolor, operacje na stronach, back-end, strona serwera
replace-color.selectText.1=Replace or Invert color Options replace-color.selectText.1=Zastąp lub Odwróć opcje kolorów
replace-color.selectText.2=Default(Default high contrast colors) replace-color.selectText.2=Domyślnie (domyślne kolory o wysokim kontraście)
replace-color.selectText.3=Custom(Customized colors) replace-color.selectText.3=Niestandardowe (kolory niestandardowe)
replace-color.selectText.4=Full-Invert(Invert all colors) replace-color.selectText.4=Całkowita-Odwrotność (Odwrócenie wszystkich kolorów)
replace-color.selectText.5=High contrast color options replace-color.selectText.5=Wysoki kontrast opcji kolorystycznych
replace-color.selectText.6=white text on black background replace-color.selectText.6=biały tekst na czarnym tle
replace-color.selectText.7=Black text on white background replace-color.selectText.7=Czarny tekst na białym tle
replace-color.selectText.8=Yellow text on black background replace-color.selectText.8=Żółty tekst na czarnym tle
replace-color.selectText.9=Green text on black background replace-color.selectText.9=Zielony tekst na czarnym tle
replace-color.selectText.10=Choose text Color replace-color.selectText.10=Wybierz Kolor tekstu
replace-color.selectText.11=Choose background Color replace-color.selectText.11=Wybierz Kolor tła
replace-color.submit=Replace replace-color.submit=Zamień
@@ -543,18 +543,17 @@ login.locked=Konto jest zablokowane
login.signinTitle=Zaloguj się login.signinTitle=Zaloguj się
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.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator. login.oauth2AdminBlockedUser=Rejestracja lub logowanie niezarejestrowanych użytkowników jest obecnie zablokowane. Prosimy o kontakt z administratorem.
login.oauth2RequestNotFound=Błąd logowania OAuth2 login.oauth2RequestNotFound=Błąd logowania OAuth2
login.oauth2InvalidUserInfoResponse=Niewłaściwe dane logowania login.oauth2InvalidUserInfoResponse=Niewłaściwe dane logowania
login.oauth2invalidRequest=Nieprawidłowe żądanie login.oauth2invalidRequest=Nieprawidłowe żądanie
login.oauth2AccessDenied=Brak dostępu login.oauth2AccessDenied=Brak dostępu
login.oauth2InvalidTokenResponse=Nieprawidłowa odpowiedź na token login.oauth2InvalidTokenResponse=Nieprawidłowa odpowiedź na token
login.oauth2InvalidIdToken=Nieprawidłowa wartość tokenu login.oauth2InvalidIdToken=Nieprawidłowa wartość tokenu
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator. login.userIsDisabled=Użytkownik jest nieaktywny, logowanie przy użyciu tej nazwy użytkownika jest obecnie zablokowane. Prosimy o kontakt z administratorem.
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=Jesteś już zalogowany na
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=urządzeniach. Wyloguj się z tych urządzeń i spróbuj ponownie.
login.toManySessions=You have too many active sessions login.toManySessions=Masz zbyt wiele aktywnych sesji
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Automatyczne zaciemnienie autoRedact.title=Automatyczne zaciemnienie
@@ -778,8 +777,8 @@ removeAnnotations.submit=Usuń
#compare #compare
compare.title=Porównaj compare.title=Porównaj
compare.header=Porównaj PDF(y) compare.header=Porównaj PDF(y)
compare.highlightColor.1=Highlight Color 1: compare.highlightColor.1=Kolor Podświetlenia 1:
compare.highlightColor.2=Highlight Color 2: compare.highlightColor.2=Kolor Podświetlenia 2:
compare.document.1=Dokument 1 compare.document.1=Dokument 1
compare.document.2=Dokument 2 compare.document.2=Dokument 2
compare.submit=Porównaj compare.submit=Porównaj
@@ -831,7 +830,7 @@ ScannerImageSplit.selectText.7=Minimalny obszar konturu:
ScannerImageSplit.selectText.8=Ustawia próg minimalnego obszaru konturu dla zdjęcia ScannerImageSplit.selectText.8=Ustawia próg minimalnego obszaru konturu dla zdjęcia
ScannerImageSplit.selectText.9=Rozmiar obramowania: ScannerImageSplit.selectText.9=Rozmiar obramowania:
ScannerImageSplit.selectText.10=Ustawia rozmiar dodawanego i usuwanego obramowania, aby uniknąć białych obramowań na wyjściu (domyślnie: 1). ScannerImageSplit.selectText.10=Ustawia rozmiar dodawanego i usuwanego obramowania, aby uniknąć białych obramowań na wyjściu (domyślnie: 1).
ScannerImageSplit.info=Python is not installed. It is required to run. ScannerImageSplit.info=Python nie został zainstalowany. Jest on wymagany do uruchomienia.
#OCR #OCR
@@ -858,7 +857,7 @@ ocr.submit=Przetwarzaj PDF za pomocą OCR
extractImages.title=Wyodrębnij obrazy extractImages.title=Wyodrębnij obrazy
extractImages.header=Wyodrębnij obrazy extractImages.header=Wyodrębnij obrazy
extractImages.selectText=Wybierz format obrazu, na który chcesz przekonwertować wyodrębniony obraz. extractImages.selectText=Wybierz format obrazu, na który chcesz przekonwertować wyodrębniony obraz.
extractImages.allowDuplicates=Save duplicate images extractImages.allowDuplicates=Zapisz zduplikowane obrazy
extractImages.submit=Wyodrębnij extractImages.submit=Wyodrębnij
@@ -919,8 +918,8 @@ pdfOrganiser.placeholder=(przykład 1,3,2 lub 4-8,2,10-12 lub 2n-1)
#multiTool #multiTool
multiTool.title=Multi narzędzie PDF multiTool.title=Narzędzie Wielofunkcyjne PDF
multiTool.header=Multi narzędzie PDF multiTool.header=Narzędzie Wielofunkcyjne PDF
multiTool.uploadPrompts=Nazwa pliku multiTool.uploadPrompts=Nazwa pliku
#view pdf #view pdf
@@ -983,7 +982,7 @@ pdfToImage.color=Kolor
pdfToImage.grey=Odcień szarości pdfToImage.grey=Odcień szarości
pdfToImage.blackwhite=Czarno-biały (może spowodować utratę danych!) pdfToImage.blackwhite=Czarno-biały (może spowodować utratę danych!)
pdfToImage.submit=Konwertuj pdfToImage.submit=Konwertuj
pdfToImage.info=Python is not installed. Required for WebP conversion. pdfToImage.info=Python nie został zainstalowany. Jest wymagany do konwersji WebP.
#addPassword #addPassword
@@ -1020,7 +1019,7 @@ watermark.selectText.6=Odstęp w pionie (odstęp między każdym znakiem wodnym
watermark.selectText.7=Nieprzezroczystość (0% - 100%): watermark.selectText.7=Nieprzezroczystość (0% - 100%):
watermark.selectText.8=Typ znaku wodnego: watermark.selectText.8=Typ znaku wodnego:
watermark.selectText.9=Obraz znaku wodnego: watermark.selectText.9=Obraz znaku wodnego:
watermark.selectText.10=Convert PDF to PDF-Image watermark.selectText.10=Konwertuj PDF do PDF-Image
watermark.submit=Dodaj znak wodny watermark.submit=Dodaj znak wodny
watermark.type.1=Tekst watermark.type.1=Tekst
watermark.type.2=Obraz watermark.type.2=Obraz
@@ -1120,7 +1119,7 @@ PDFToXML.submit=Konwertuj
#PDFToCSV #PDFToCSV
PDFToCSV.title=PDF na CSV PDFToCSV.title=PDF na CSV
PDFToCSV.header=PDF na CSV PDFToCSV.header=PDF na CSV
PDFToCSV.prompt=Choose page to extract table PDFToCSV.prompt=Wybierz stronę do wyodrębnienia tabeli
PDFToCSV.submit=Zatwierdź PDFToCSV.submit=Zatwierdź
#split-by-size-or-count #split-by-size-or-count
@@ -1182,8 +1181,8 @@ licenses.license=Licencja
survey.nav=Ankieta survey.nav=Ankieta
survey.title=Ankieta Stirling-PDF survey.title=Ankieta Stirling-PDF
survey.description=Stirling-PDF nie śledzi swoich użytkowników, więc chciałby poznać opinie swoich użytkowników! survey.description=Stirling-PDF nie śledzi swoich użytkowników, więc chciałby poznać opinie swoich użytkowników!
survey.changes=Stirling-PDF has changed since the last survey! To find out more please check our blog post here: survey.changes=Stirling-PDF zmieniło się od czasu ostatniej ankiety! Aby dowiedzieć się więcej, sprawdź nasz wpis na blogu tutaj:
survey.changes2=With these changes we are getting paid business support and funding survey.changes2=Dzięki tym zmianom otrzymujemy płatne wsparcie biznesowe i finansowanie
survey.please=Wypełnij proszę ankietę dla nas! survey.please=Wypełnij proszę ankietę dla nas!
survey.disabled=(Blokada wyskakującego okienka z ankieta zostanie dodane w następnych aktualizacjach, ale będzie dostępna na dole strony) survey.disabled=(Blokada wyskakującego okienka z ankieta zostanie dodane w następnych aktualizacjach, ale będzie dostępna na dole strony)
survey.button=Wypełnij ankietę survey.button=Wypełnij ankietę
@@ -1205,21 +1204,21 @@ error.discordSubmit=Discord - wyślij posta z prośbą o pomoc
#remove-image #remove-image
removeImage.title=Remove image removeImage.title=Usuń obraz
removeImage.header=Remove image removeImage.header=Usuń obraz
removeImage.removeImage=Remove image removeImage.removeImage=Usuń obraz
removeImage.submit=Remove image removeImage.submit=Usuń obraz
splitByChapters.title=Split PDF by Chapters splitByChapters.title=Podziel PDF według Rozdziałów
splitByChapters.header=Split PDF by Chapters splitByChapters.header=Podziel PDF według Rozdziałów
splitByChapters.bookmarkLevel=Bookmark Level splitByChapters.bookmarkLevel=Poziom Zakładek
splitByChapters.includeMetadata=Include Metadata splitByChapters.includeMetadata=Dołącz Metadane
splitByChapters.allowDuplicates=Allow Duplicates splitByChapters.allowDuplicates=Zezwalaj na Duplikaty
splitByChapters.desc.1=This tool splits a PDF file into multiple PDFs based on its chapter structure. splitByChapters.desc.1=Narzędzie to dzieli plik PDF na wiele plików PDF w oparciu o strukturę rozdziałów.
splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for splitting (0 for top-level, 1 for second-level, etc.). splitByChapters.desc.2=Poziom Zakładek: Wybierz poziom zakładek, który ma zostać użyty do podziału (0 dla najwyższego poziomu, 1 dla drugiego poziomu itd.).
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF. splitByChapters.desc.3=Dołącz Metadane: Jeśli opcja ta jest zaznaczona, metadane oryginalnego pliku PDF zostaną uwzględnione w każdym rozdzielonych plików PDF.
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs. splitByChapters.desc.4=Zezwól na Duplikaty: Jeśli ta opcja jest zaznaczona, pozwala na tworzenie oddzielnych plików PDF przez wiele zakładek na tej samej stronie.
splitByChapters.submit=Split PDF splitByChapters.submit=Podziel PDF

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=O usuário está desativado, o login está atualmente bloqu
login.alreadyLoggedIn=Você já está conectado login.alreadyLoggedIn=Você já está conectado
login.alreadyLoggedIn2=aparelhos. Por favor saia dos aparelhos e tente novamente. login.alreadyLoggedIn2=aparelhos. Por favor saia dos aparelhos e tente novamente.
login.toManySessions=Você tem muitas sessões ativas login.toManySessions=Você tem muitas sessões ativas
login.toManySessions2=Por favor saida dos aparelhos e tente novamente. Alternativamente você pode adquirir Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Redação Automática de Dados autoRedact.title=Redação Automática de Dados

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Edição Automática autoRedact.title=Edição Automática

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=Utilizatorul este dezactivat, conectarea este în prezent b
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Redactare Automată autoRedact.title=Redactare Automată

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Автоматическое редактирование autoRedact.title=Автоматическое редактирование

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Automatické redigovanie autoRedact.title=Automatické redigovanie

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Auto Cenzura autoRedact.title=Auto Cenzura

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=Användaren är inaktiverad, inloggning är för närvarand
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Auto-redigera autoRedact.title=Auto-redigera

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=ซ่อนข้อมูลอัตโนมัติ autoRedact.title=ซ่อนข้อมูลอัตโนมัติ

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=Kullanıcı devre dışı bırakıldı, şu anda bu kullan
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Otomatik Karartma autoRedact.title=Otomatik Karartma

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Автоматичне редагування autoRedact.title=Автоматичне редагування

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=Tự động biên tập autoRedact.title=Tự động biên tập

View File

@@ -554,7 +554,6 @@ login.userIsDisabled=用户被禁用,登录已被阻止。请联系管理员
login.alreadyLoggedIn=You are already logged in to login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again. login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact #auto-redact
autoRedact.title=自动删除 autoRedact.title=自动删除

View File

@@ -21,7 +21,7 @@ save=儲存
saveToBrowser=儲存到瀏覽器 saveToBrowser=儲存到瀏覽器
close=關閉 close=關閉
filesSelected=已選擇的檔案 filesSelected=已選擇的檔案
noFavourites=未新增收藏 noFavourites=還沒有功能被收藏
downloadComplete=下載完成 downloadComplete=下載完成
bored=等待時覺得無聊? bored=等待時覺得無聊?
alphabet=字母表 alphabet=字母表
@@ -90,7 +90,7 @@ legal.impressum=版本說明
# Pipeline # # Pipeline #
############### ###############
pipeline.header=管道功能選單(測試版) pipeline.header=管道功能選單(測試版)
pipeline.uploadButton=上傳自訂 pipeline.uploadButton=上傳自訂設定
pipeline.configureButton=設定 pipeline.configureButton=設定
pipeline.defaultOption=自訂 pipeline.defaultOption=自訂
pipeline.submitButton=送出 pipeline.submitButton=送出
@@ -256,9 +256,9 @@ home.viewPdf.title=檢視 PDF
home.viewPdf.desc=檢視、註釋、新增文字或圖片 home.viewPdf.desc=檢視、註釋、新增文字或圖片
viewPdf.tags=檢視,閱讀,註釋,文字,圖片 viewPdf.tags=檢視,閱讀,註釋,文字,圖片
home.multiTool.title=PDF 工具 home.multiTool.title=PDF 複合工具
home.multiTool.desc=合併、旋轉、重新排列和移除頁面 home.multiTool.desc=合併、旋轉、重新排列和移除頁面
multiTool.tags=工具,多操作,UI,點選拖,前端,客戶端,互動,互動,移動 multiTool.tags=複合工具,多功能,UI,點選拖,前端,客戶端,互動,互動,移動
home.merge.title=合併 home.merge.title=合併
home.merge.desc=輕鬆將多個 PDF 合併為一個。 home.merge.desc=輕鬆將多個 PDF 合併為一個。
@@ -554,7 +554,6 @@ login.userIsDisabled=使用者已停用,目前此使用者無法登入。請
login.alreadyLoggedIn=您已經登入了 login.alreadyLoggedIn=您已經登入了
login.alreadyLoggedIn2=個裝置。請登出其他裝置後再試一次。 login.alreadyLoggedIn2=個裝置。請登出其他裝置後再試一次。
login.toManySessions=您有太多使用中的工作階段 login.toManySessions=您有太多使用中的工作階段
login.toManySessions2=請登出其他裝置後再試一次。或者,您可以升級至 Stirling PDF 專業版。
#auto-redact #auto-redact
autoRedact.title=自動塗黑 autoRedact.title=自動塗黑
@@ -840,8 +839,8 @@ ocr.header=清理掃描 / OCR光學字元識別
ocr.selectText.1=選擇要在 PDF 中偵測的語言(列出的是目前可以偵測的語言): ocr.selectText.1=選擇要在 PDF 中偵測的語言(列出的是目前可以偵測的語言):
ocr.selectText.2=產生包含 OCR 文字的文字文件,並與 OCR 的 PDF 一起 ocr.selectText.2=產生包含 OCR 文字的文字文件,並與 OCR 的 PDF 一起
ocr.selectText.3=修正掃描的頁面傾斜角度,將它們旋轉回原位 ocr.selectText.3=修正掃描的頁面傾斜角度,將它們旋轉回原位
ocr.selectText.4=清理頁面,使 OCR 不太可能在背景噪音中找到文字。(無輸出變化) ocr.selectText.4=清理頁面以降低 OCR 在背景雜訊中識別文字的機率。(無輸出變化)
ocr.selectText.5=清理頁面,使 OCR 不太可能在背景噪音中找到文字,保持清理的輸出。 ocr.selectText.5=清理頁面以降低 OCR 在背景雜訊中識別文字的機率,保持乾淨的輸出。
ocr.selectText.6=忽略具有互動文字的頁面,只對影像頁面進行 OCR ocr.selectText.6=忽略具有互動文字的頁面,只對影像頁面進行 OCR
ocr.selectText.7=強制 OCR將對每一頁進行 OCR移除所有原始文字元素 ocr.selectText.7=強制 OCR將對每一頁進行 OCR移除所有原始文字元素
ocr.selectText.8=正常(如果 PDF 包含文字將出錯) ocr.selectText.8=正常(如果 PDF 包含文字將出錯)
@@ -858,7 +857,7 @@ ocr.submit=使用 OCR 處理 PDF
extractImages.title=提取圖片 extractImages.title=提取圖片
extractImages.header=提取圖片 extractImages.header=提取圖片
extractImages.selectText=選擇要轉換提取影像的影像格式 extractImages.selectText=選擇要轉換提取影像的影像格式
extractImages.allowDuplicates=Save duplicate images extractImages.allowDuplicates=儲存重複的圖片
extractImages.submit=提取 extractImages.submit=提取
@@ -876,10 +875,10 @@ compress.title=壓縮
compress.header=壓縮 PDF compress.header=壓縮 PDF
compress.credit=此服務使用 Ghostscript 進行 PDF 壓縮/最佳化。 compress.credit=此服務使用 Ghostscript 進行 PDF 壓縮/最佳化。
compress.selectText.1=手動模式 - 從 1 到 4 compress.selectText.1=手動模式 - 從 1 到 4
compress.selectText.2=最佳化級 compress.selectText.2=最佳化級:
compress.selectText.3=4對於文字影像非常糟糕 compress.selectText.3=4對於含有文字影像來說結果很糟
compress.selectText.4=自動模式 - 自動調整品質使 PDF 達到確定大小 compress.selectText.4=自動模式 - 自動調整品質使 PDF 達到指定的檔案大小
compress.selectText.5=預期的 PDF 大小(例如 25MB, 10.8MB, 25KB compress.selectText.5=指定的 PDF 檔案大小(例如 25MB, 10.8MB, 25KB
compress.submit=壓縮 compress.submit=壓縮
@@ -919,8 +918,8 @@ 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=檔名 multiTool.uploadPrompts=檔名
#view pdf #view pdf

View File

@@ -47,6 +47,18 @@ 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'
saml2:
enabled: false
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
registrationId: stirling
idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata
idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml
idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml
idpIssuer: http://www.okta.com/externalKey
idpCert: classpath:octa.crt
privateKey: classpath:saml-private-key.key
spCert: classpath:saml-public-cert.crt
# Enterprise edition settings unused for now please ignore! # Enterprise edition settings unused for now please ignore!
enterpriseEdition: enterpriseEdition:

View File

@@ -21,6 +21,13 @@
"moduleLicense": "The Apache Software License, Version 2.0", "moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0" "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0"
}, },
{
"moduleName": "com.coveo:saml-client",
"moduleUrl": "https://github.com/coveo/saml-client",
"moduleVersion": "5.0.0",
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{ {
"moduleName": "com.fasterxml.jackson.core:jackson-annotations", "moduleName": "com.fasterxml.jackson.core:jackson-annotations",
"moduleUrl": "https://github.com/FasterXML/jackson", "moduleUrl": "https://github.com/FasterXML/jackson",
@@ -748,8 +755,8 @@
}, },
{ {
"moduleName": "org.commonmark:commonmark-ext-gfm-tables", "moduleName": "org.commonmark:commonmark-ext-gfm-tables",
"moduleVersion": "0.23.0", "moduleVersion": "0.24.0",
"moduleLicense": "BSD 2-Clause License", "moduleLicense": "BSD-2-Clause",
"moduleLicenseUrl": "https://opensource.org/licenses/BSD-2-Clause" "moduleLicenseUrl": "https://opensource.org/licenses/BSD-2-Clause"
}, },
{ {
@@ -1398,7 +1405,7 @@
{ {
"moduleName": "org.springframework:spring-webmvc", "moduleName": "org.springframework:spring-webmvc",
"moduleUrl": "https://github.com/spring-projects/spring-framework", "moduleUrl": "https://github.com/spring-projects/spring-framework",
"moduleVersion": "6.1.13", "moduleVersion": "6.1.14",
"moduleLicense": "Apache License, Version 2.0", "moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
}, },

View File

@@ -1,7 +1,9 @@
select#font-select, select#font-select,
select#font-select option { select#font-select option {
height: 60px; /* Adjust as needed */ height: 60px;
font-size: 30px; /* Adjust as needed */ /* Adjust as needed */
font-size: 30px;
/* Adjust as needed */
} }
.drawing-pad-container { .drawing-pad-container {
@@ -13,10 +15,12 @@ select#font-select option {
width: 100%; width: 100%;
height: 300px; height: 300px;
} }
#box-drag-container { #box-drag-container {
position: relative; position: relative;
margin: 20px 0; margin: 20px 0;
} }
.draggable-buttons-box { .draggable-buttons-box {
position: absolute; position: absolute;
top: 0; top: 0;
@@ -24,16 +28,37 @@ select#font-select option {
width: 100%; width: 100%;
display: flex; display: flex;
gap: 5px; gap: 5px;
z-index: 5;
} }
.draggable-buttons-box > button {
z-index: 10; .draggable-buttons-box>button {
z-index: 4;
background-color: rgba(13, 110, 253, 0.1); background-color: rgba(13, 110, 253, 0.1);
} }
.draggable-canvas { .draggable-canvas {
border: 1px solid red; border: 2px solid #3498db;
position: absolute; position: absolute;
touch-action: none; touch-action: none;
user-select: none; user-select: none;
top: 0px; top: 0px;
left: 0; left: 0;
z-index: 100;
cursor: grab;
transition: transform 0.1s ease-out;
background-color: rgba(52, 152, 219, 0.1);
/* Light blue background */
}
.draggable-canvas:active {
cursor: grabbing;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
/* Shadow on active drag */
}
.draggable-canvas:hover {
border: 2px solid #2980b9;
/* Darker border on hover */
background-color: rgba(52, 152, 219, 0.2);
/* Darken background on hover */
} }

View File

@@ -0,0 +1,36 @@
const draggableElement = document.querySelector('.draggable-canvas');
// Variables to store the current position of the draggable element
let offsetX, offsetY, isDragging = false;
draggableElement.addEventListener('mousedown', (e) => {
// Get the offset when the mouse is clicked inside the element
offsetX = e.clientX - draggableElement.getBoundingClientRect().left;
offsetY = e.clientY - draggableElement.getBoundingClientRect().top;
// Set isDragging to true
isDragging = true;
// Add event listeners for mouse movement and release
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
function onMouseMove(e) {
if (isDragging) {
// Calculate the new position of the element
const left = e.clientX - offsetX;
const top = e.clientY - offsetY;
// Move the element by setting its style
draggableElement.style.left = `${left}px`;
draggableElement.style.top = `${top}px`;
}
}
function onMouseUp() {
// Stop dragging and remove event listeners
isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}

View File

@@ -283,7 +283,7 @@
</script> </script>
<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="editUserModal" tabindex="-1" role="dialog" aria-labelledby="editUserModalLabel" aria-hidden="true"> <div th:if="${altLogin}" class="modal fade" id="editUserModal" tabindex="-1" role="dialog" aria-labelledby="editUserModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document"> <div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">

View File

@@ -114,7 +114,7 @@
<img class="my-4" th:src="@{'/favicon.svg'}" alt="favicon" width="144" height="144"> <img class="my-4" th:src="@{'/favicon.svg'}" alt="favicon" width="144" height="144">
<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} and (${loginMethod} == 'all' or ${loginMethod} == 'oauth2')"> <div th:if="${altLogin} and (${loginMethod} == 'all' or ${loginMethod} == 'oauth2')">
<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> <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>
@@ -168,7 +168,7 @@
</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 th:if="${altLogin}" 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-dialog modal-dialog-centered" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@@ -181,7 +181,7 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="mb-3" th:each="provider : ${providerlist}"> <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> <a th:href="@{|${provider.key}|}" th:text="${provider.value}" class="w-100 btn btn-lg btn-primary">Login Provider</a>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">

View File

@@ -1,8 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
<head> xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{sign.title}, header=#{sign.header})}"></th:block> <th:block th:insert="~{fragments/common :: head(title=#{sign.title}, header=#{sign.header})}"></th:block>
<link rel="stylesheet" th:href="@{'/css/sign.css'}"> <link rel="stylesheet" th:href="@{'/css/sign.css'}">
<th:block th:each="font : ${fonts}"> <th:block th:each="font : ${fonts}">
<style th:inline="text"> <style th:inline="text">
@font-face { @font-face {
@@ -11,18 +14,17 @@
} }
#font-select option[value="[[${font.name}]]"] { #font-select option[value="[[${font.name}]]"] {
font-family: "[[${font.name}]]", cursive; font-family: "[[${font.name}]]",
} cursive;
#font-select option[value='/*[[${font.name}]]*/'] {
font-family: '/*[[${font.name}]]*/', cursive;
} }
</style> </style>
</th:block> </th:block>
<script th:src="@{'/js/thirdParty/signature_pad.umd.min.js'}"></script> <script th:src="@{'/js/thirdParty/signature_pad.umd.min.js'}"></script>
<script th:src="@{'/js/thirdParty/interact.min.js'}"></script> <script th:src="@{'/js/thirdParty/interact.min.js'}"></script>
</head> </head>
<body> <body>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
@@ -36,7 +38,9 @@
</div> </div>
<!-- pdf selector --> <!-- pdf selector -->
<div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multipleInputsForSingleRequest=false, disableMultipleFiles=true, accept='application/pdf')}"></div> <div
th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multipleInputsForSingleRequest=false, disableMultipleFiles=true, accept='application/pdf')}">
</div>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script> <script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script> <script>
let originalFileName = ''; let originalFileName = '';
@@ -45,30 +49,31 @@
if (file) { if (file) {
originalFileName = file.name.replace(/\.[^/.]+$/, ""); originalFileName = file.name.replace(/\.[^/.]+$/, "");
const pdfData = await file.arrayBuffer(); const pdfData = await file.arrayBuffer();
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs' pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise; const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
await DraggableUtils.renderPage(pdfDoc, 0); await DraggableUtils.renderPage(pdfDoc, 0);
document.querySelectorAll(".show-on-file-selected").forEach(el => { document.querySelectorAll(".show-on-file-selected").forEach(el => {
el.style.cssText = ''; el.style.cssText = '';
}) });
} }
}); });
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(".show-on-file-selected").forEach(el => { document.querySelectorAll(".show-on-file-selected").forEach(el => {
el.style.cssText = "display:none !important"; el.style.cssText = "display:none !important";
}) });
}); });
</script> </script>
<div class="tab-group show-on-file-selected"> <div class="tab-group show-on-file-selected">
<div class="tab-container" th:title="#{sign.upload}"> <div class="tab-container" th:title="#{sign.upload}">
<div th:replace="~{fragments/common :: fileSelector(name='image-upload', multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}"></div> <div
th:replace="~{fragments/common :: fileSelector(name='image-upload', multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}">
</div>
<script> <script>
const imageUpload = document.querySelector('input[name=image-upload]'); const imageUpload = document.querySelector('input[name=image-upload]');
imageUpload.addEventListener('change', e => { imageUpload.addEventListener('change', e => {
if(!e.target.files) { if (!e.target.files) return;
return;
}
for (const imageFile of e.target.files) { for (const imageFile of e.target.files) {
var reader = new FileReader(); var reader = new FileReader();
reader.readAsDataURL(imageFile); reader.readAsDataURL(imageFile);
@@ -79,11 +84,14 @@
}); });
</script> </script>
</div> </div>
<div class="tab-container drawing-pad-container" th:title="#{sign.draw}"> <div class="tab-container drawing-pad-container" th:title="#{sign.draw}">
<canvas id="drawing-pad-canvas"></canvas> <canvas id="drawing-pad-canvas"></canvas>
<br> <br>
<button id="clear-signature" class="btn btn-outline-danger mt-2" onclick="signaturePad.clear()" th:text="#{sign.clear}"></button> <button id="clear-signature" class="btn btn-outline-danger mt-2" onclick="signaturePad.clear()"
<button id="save-signature" class="btn btn-outline-success mt-2" onclick="addDraggableFromPad()" th:text="#{sign.add}"></button> th:text="#{sign.clear}"></button>
<button id="save-signature" class="btn btn-outline-success mt-2" onclick="addDraggableFromPad()"
th:text="#{sign.add}"></button>
<script> <script>
const signaturePadCanvas = document.getElementById('drawing-pad-canvas'); const signaturePadCanvas = document.getElementById('drawing-pad-canvas');
const signaturePad = new SignaturePad(signaturePadCanvas, { const signaturePad = new SignaturePad(signaturePadCanvas, {
@@ -91,20 +99,20 @@
maxWidth: 2, maxWidth: 2,
penColor: 'black', penColor: 'black',
}); });
function addDraggableFromPad() { function addDraggableFromPad() {
if (signaturePad.isEmpty()) return; if (signaturePad.isEmpty()) return;
const startTime = Date.now(); const startTime = Date.now();
const croppedDataUrl = getCroppedCanvasDataUrl(signaturePadCanvas) const croppedDataUrl = getCroppedCanvasDataUrl(signaturePadCanvas);
console.log(Date.now() - startTime); console.log(Date.now() - startTime);
DraggableUtils.createDraggableCanvasFromUrl(croppedDataUrl); DraggableUtils.createDraggableCanvasFromUrl(croppedDataUrl);
} }
function getCroppedCanvasDataUrl(canvas) {
// code is from: https://github.com/szimek/signature_pad/issues/49#issuecomment-1104035775
let originalCtx = canvas.getContext('2d');
function getCroppedCanvasDataUrl(canvas) {
let originalCtx = canvas.getContext('2d');
let originalWidth = canvas.width; let originalWidth = canvas.width;
let originalHeight = canvas.height; let originalHeight = canvas.height;
let imageData = originalCtx.getImageData(0,0, originalWidth, originalHeight); let imageData = originalCtx.getImageData(0, 0, originalWidth, originalHeight);
let minX = originalWidth + 1, maxX = -1, minY = originalHeight + 1, maxY = -1, x = 0, y = 0, currentPixelColorValueIndex; let minX = originalWidth + 1, maxX = -1, minY = originalHeight + 1, maxY = -1, x = 0, y = 0, currentPixelColorValueIndex;
@@ -135,10 +143,8 @@
return croppedCanvas.toDataURL(); return croppedCanvas.toDataURL();
} }
function resizeCanvas() { function resizeCanvas() {
// When zoomed out to less than 100%, for some very strange reason,
// some browsers report devicePixelRatio as less than 1
// and only part of the canvas is cleared then.
var ratio = Math.max(window.devicePixelRatio || 1, 1); var ratio = Math.max(window.devicePixelRatio || 1, 1);
var additionalFactor = 10; var additionalFactor = 10;
@@ -146,31 +152,30 @@
signaturePadCanvas.height = signaturePadCanvas.offsetHeight * ratio * additionalFactor; signaturePadCanvas.height = signaturePadCanvas.offsetHeight * ratio * additionalFactor;
signaturePadCanvas.getContext("2d").scale(ratio * additionalFactor, ratio * additionalFactor); signaturePadCanvas.getContext("2d").scale(ratio * additionalFactor, ratio * additionalFactor);
// This library does not listen for canvas changes, so after the canvas is automatically
// cleared by the browser, SignaturePad#isEmpty might still return false, even though the
// canvas looks empty, because the internal data of this library wasn't cleared. To make sure
// that the state of this library is consistent with visual state of the canvas, you
// have to clear it manually.
signaturePad.clear(); signaturePad.clear();
} }
new IntersectionObserver((entries, observer) => { new IntersectionObserver((entries, observer) => {
if (entries.some(entry => entry.intersectionRatio > 0)) { if (entries.some(entry => entry.intersectionRatio > 0)) {
resizeCanvas(); resizeCanvas();
} }
}).observe(signaturePadCanvas); }).observe(signaturePadCanvas);
new ResizeObserver(resizeCanvas).observe(signaturePadCanvas); new ResizeObserver(resizeCanvas).observe(signaturePadCanvas);
</script> </script>
</div> </div>
<div class="tab-container" th:title="#{sign.text}"> <div class="tab-container" th:title="#{sign.text}">
<label class="form-check-label" for="sigText" th:text="#{text}"></label> <label class="form-check-label" for="sigText" th:text="#{text}"></label>
<textarea class="form-control" id="sigText" name="sigText" rows="3"></textarea> <textarea class="form-control" id="sigText" name="sigText" rows="3"></textarea>
<label th:text="#{font}"></label> <label th:text="#{font}"></label>
<select class="form-control" name="font" id="font-select"> <select class="form-control" name="font" id="font-select">
<option th:each="font : ${fonts}" th:value="${font.name}" th:text="${font.name}" th:class="${font.name.toLowerCase()+'-font'}"> <option th:each="font : ${fonts}" th:value="${font.name}" th:text="${font.name}"
</option> th:class="${font.name.toLowerCase()+'-font'}"></option>
</select> </select>
<div class="margin-auto-parent"> <div class="margin-auto-parent">
<button id="save-text-signature" class="btn btn-outline-success mt-2 margin-center" onclick="addDraggableFromText()" th:text="#{sign.add}"></button> <button id="save-text-signature" class="btn btn-outline-success mt-2 margin-center"
onclick="addDraggableFromText()" th:text="#{sign.add}"></button>
</div> </div>
<script> <script>
function addDraggableFromText() { function addDraggableFromText() {
@@ -187,7 +192,7 @@
let paragraphs = sigText.split(/\r?\n/); let paragraphs = sigText.split(/\r?\n/);
canvas.width = textWidth; canvas.width = textWidth;
canvas.height = paragraphs.length * textHeight*1.35; //for tails canvas.height = paragraphs.length * textHeight * 1.35; // for tails
ctx.font = `${fontSize}px ${font}`; ctx.font = `${fontSize}px ${font}`;
ctx.textBaseline = 'top'; ctx.textBaseline = 'top';
@@ -203,27 +208,6 @@
DraggableUtils.createDraggableCanvasFromUrl(dataURL); DraggableUtils.createDraggableCanvasFromUrl(dataURL);
} }
</script> </script>
<script>
const sigTextInput = document.getElementById('sigText');
const fontSelect = document.getElementById('font-select');
const updateOptionTexts = () => {
Array.from(fontSelect.options).forEach(option => {
const fontName = option.value.replace(/-regular$/i, '');
option.text = sigTextInput.value || fontName;
});
}
sigTextInput.addEventListener('input', updateOptionTexts);
fontSelect.addEventListener('change', (e) => {
e.target.style.fontFamily = e.target.value;
updateOptionTexts();
});
// Manually trigger the change event
fontSelect.dispatchEvent(new Event('change'));
</script>
</div> </div>
</div> </div>
@@ -233,20 +217,31 @@
<script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script> <script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script>
<script th:src="@{'/js/draggable-utils.js'}"></script> <script th:src="@{'/js/draggable-utils.js'}"></script>
<div class="draggable-buttons-box ignore-rtl"> <div class="draggable-buttons-box ignore-rtl">
<button class="btn btn-outline-secondary" onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())"> <button class="btn btn-outline-secondary"
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16"> onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash"
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z"/> viewBox="0 0 16 16">
<path
d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z" />
<path
d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z" />
</svg> </svg>
</button> </button>
<button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()" style="margin-left:auto"> <button class="btn btn-outline-secondary"
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-left" viewBox="0 0 16 16"> onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()"
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/> style="margin-left:auto">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-chevron-left" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z" />
</svg> </svg>
</button> </button>
<button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()"> <button class="btn btn-outline-secondary"
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-right" viewBox="0 0 16 16"> onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()">
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-chevron-right" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z" />
</svg> </svg>
</button> </button>
</div> </div>
@@ -254,11 +249,12 @@
<!-- download button --> <!-- download button -->
<div class="margin-auto-parent"> <div class="margin-auto-parent">
<button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center" th:text="#{downloadPdf}"></button> <button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center"
th:text="#{downloadPdf}"></button>
</div> </div>
<script> <script>
document.getElementById("download-pdf").addEventListener('click', async() => { document.getElementById("download-pdf").addEventListener('click', async () => {
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument(); const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
const modifiedPdfBytes = await modifiedPdf.save(); const modifiedPdfBytes = await modifiedPdf.save();
const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' }); const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' });
@@ -272,7 +268,12 @@
</div> </div>
</div> </div>
</div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block> <th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body>
<!-- Link the draggable.js file -->
<script src="/path/to/your/draggable.js"></script>
</body>
</html> </html>