Compare commits
14 Commits
saml2-keyc
...
testSign
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94dbb035cc | ||
|
|
e046172374 | ||
|
|
edd0ec9d23 | ||
|
|
899f3d267b | ||
|
|
88c0a9e26b | ||
|
|
dc6cec9daf | ||
|
|
a64dd2e282 | ||
|
|
c9b7d848b4 | ||
|
|
89a9ba6ebc | ||
|
|
22249ef9bf | ||
|
|
619a863b99 | ||
|
|
e098b2999c | ||
|
|
1149f2a30d | ||
|
|
eff1843061 |
12
README.md
12
README.md
@@ -172,18 +172,18 @@ Stirling PDF currently supports 38!
|
|||||||
|
|
||||||
| Language | Progress |
|
| Language | Progress |
|
||||||
| ------------------------------------------- | -------------------------------------- |
|
| ------------------------------------------- | -------------------------------------- |
|
||||||
| Arabic (العربية) (ar_AR) |  |
|
| Arabic (العربية) (ar_AR) |  |
|
||||||
| Basque (Euskara) (eu_ES) |  |
|
| Basque (Euskara) (eu_ES) |  |
|
||||||
| Bulgarian (Български) (bg_BG) |  |
|
| Bulgarian (Български) (bg_BG) |  |
|
||||||
| Catalan (Català) (ca_CA) |  |
|
| Catalan (Català) (ca_CA) |  |
|
||||||
| Croatian (Hrvatski) (hr_HR) |  |
|
| Croatian (Hrvatski) (hr_HR) |  |
|
||||||
| Czech (Česky) (cs_CZ) |  |
|
| Czech (Česky) (cs_CZ) |  |
|
||||||
| Danish (Dansk) (da_DK) |  |
|
| Danish (Dansk) (da_DK) |  |
|
||||||
| Dutch (Nederlands) (nl_NL) |  |
|
| Dutch (Nederlands) (nl_NL) |  |
|
||||||
| English (English) (en_GB) |  |
|
| English (English) (en_GB) |  |
|
||||||
| English (US) (en_US) |  |
|
| English (US) (en_US) |  |
|
||||||
| French (Français) (fr_FR) |  |
|
| French (Français) (fr_FR) |  |
|
||||||
| German (Deutsch) (de_DE) |  |
|
| German (Deutsch) (de_DE) |  |
|
||||||
| Greek (Ελληνικά) (el_GR) |  |
|
| Greek (Ελληνικά) (el_GR) |  |
|
||||||
| Hindi (हिंदी) (hi_IN) |  |
|
| Hindi (हिंदी) (hi_IN) |  |
|
||||||
| Hungarian (Magyar) (hu_HU) |  |
|
| Hungarian (Magyar) (hu_HU) |  |
|
||||||
@@ -193,7 +193,7 @@ Stirling PDF currently supports 38!
|
|||||||
| Japanese (日本語) (ja_JP) |  |
|
| Japanese (日本語) (ja_JP) |  |
|
||||||
| Korean (한국어) (ko_KR) |  |
|
| Korean (한국어) (ko_KR) |  |
|
||||||
| Norwegian (Norsk) (no_NB) |  |
|
| Norwegian (Norsk) (no_NB) |  |
|
||||||
| Polish (Polski) (pl_PL) |  |
|
| Polish (Polski) (pl_PL) |  |
|
||||||
| Portuguese (Português) (pt_PT) |  |
|
| Portuguese (Português) (pt_PT) |  |
|
||||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||||
| Romanian (Română) (ro_RO) |  |
|
| Romanian (Română) (ro_RO) |  |
|
||||||
@@ -207,7 +207,7 @@ Stirling PDF currently supports 38!
|
|||||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||||
| Turkish (Türkçe) (tr_TR) |  |
|
| Turkish (Türkçe) (tr_TR) |  |
|
||||||
| Ukrainian (Українська) (uk_UA) |  |
|
| Ukrainian (Українська) (uk_UA) |  |
|
||||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||||
|
|
||||||
## Contributing (creating issues, translations, fixing bugs, etc.)
|
## Contributing (creating issues, translations, fixing bugs, etc.)
|
||||||
|
|
||||||
|
|||||||
20
build.gradle
20
build.gradle
@@ -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"
|
||||||
|
|||||||
20
scripts/remove_translation_keys.sh
Normal file
20
scripts/remove_translation_keys.sh
Normal 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
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ]", "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 ]", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 "/";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package stirling.software.SPDF.config.security.saml;
|
|
||||||
|
|
||||||
public interface Saml2AuthorityAttributeLookup {
|
|
||||||
String getAuthorityAttribute(String registrationId);
|
|
||||||
|
|
||||||
SimpleScimMappings getIdentityMappings(String registrationId);
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package stirling.software.SPDF.config.security.saml;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class SimpleScimMappings {
|
|
||||||
String givenName;
|
|
||||||
String familyName;
|
|
||||||
String email;
|
|
||||||
}
|
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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=حجب تلقائي
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=Αυτόματο Μαύρισμα Κειμένου
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=स्वत: गोपनीयकरण
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=自動塗りつぶし
|
||||||
|
|||||||
@@ -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=자동 검열
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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ă
|
||||||
|
|||||||
@@ -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=Автоматическое редактирование
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=ซ่อนข้อมูลอัตโนมัติ
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=Автоматичне редагування
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=自动删除
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 */
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/main/resources/static/js/draggable.js
Normal file
36
src/main/resources/static/js/draggable.js
Normal 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);
|
||||||
|
}
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user