main -> branch

This commit is contained in:
Ludy87 2025-03-26 16:34:29 +01:00
parent 1b6ce2a3e7
commit b6c6a3445c
No known key found for this signature in database
GPG Key ID: 92696155E0220F94
83 changed files with 3313 additions and 874 deletions

View File

@ -74,6 +74,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
with:
sarif_file: results.sarif

View File

@ -25,20 +25,16 @@ LABEL org.opencontainers.image.keywords="PDF, manipulation, merge, split, conver
# Set Environment Variables
ENV DOCKER_ENABLE_SECURITY=false \
VERSION_TAG=$VERSION_TAG \
JAVA_TOOL_OPTIONS="-XX:+UnlockExperimentalVMOptions \
-XX:MaxRAMPercentage=75 \
-XX:InitiatingHeapOccupancyPercent=20 \
-XX:+G1PeriodicGCInvokesConcurrent \
-XX:G1PeriodicGCInterval=10000 \
-XX:+UseStringDeduplication \
-XX:G1PeriodicGCSystemLoadThreshold=70" \
JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \
JAVA_CUSTOM_OPTS="" \
HOME=/home/stirlingpdfuser \
PUID=1000 \
PGID=1000 \
UMASK=022 \
PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \
UNO_PATH=/usr/lib/libreoffice/program \
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \
PATH=$PATH:/opt/venv/bin
# JDK for app
@ -77,9 +73,8 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
py3-pillow@testing \
py3-pdf2image@testing && \
python3 -m venv /opt/venv && \
export PATH="/opt/venv/bin:$PATH" && \
pip install --upgrade pip && \
pip install --no-cache-dir --upgrade unoserver weasyprint && \
/opt/venv/bin/pip install --upgrade pip && \
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \
ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \

View File

@ -32,13 +32,8 @@ ARG VERSION_TAG
# Set Environment Variables
ENV DOCKER_ENABLE_SECURITY=false \
VERSION_TAG=$VERSION_TAG \
JAVA_TOOL_OPTIONS="-XX:+UnlockExperimentalVMOptions \
-XX:MaxRAMPercentage=75 \
-XX:InitiatingHeapOccupancyPercent=20 \
-XX:+G1PeriodicGCInvokesConcurrent \
-XX:G1PeriodicGCInterval=10000 \
-XX:+UseStringDeduplication \
-XX:G1PeriodicGCSystemLoadThreshold=70" \
JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \
JAVA_CUSTOM_OPTS="" \
HOME=/home/stirlingpdfuser \
PUID=1000 \
PGID=1000 \
@ -47,7 +42,8 @@ ENV DOCKER_ENABLE_SECURITY=false \
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \
UNO_PATH=/usr/lib/libreoffice/program \
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \
PATH=$PATH:/opt/venv/bin
# JDK for app
@ -87,9 +83,8 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
py3-pillow@testing \
py3-pdf2image@testing && \
python3 -m venv /opt/venv && \
export PATH="/opt/venv/bin:$PATH" && \
pip install --upgrade pip && \
pip install --no-cache-dir --upgrade unoserver weasyprint && \
/opt/venv/bin/pip install --upgrade pip && \
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \
ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \

View File

@ -7,13 +7,8 @@ ARG VERSION_TAG
ENV DOCKER_ENABLE_SECURITY=false \
HOME=/home/stirlingpdfuser \
VERSION_TAG=$VERSION_TAG \
JAVA_TOOL_OPTIONS="-XX:+UnlockExperimentalVMOptions \
-XX:MaxRAMPercentage=75 \
-XX:InitiatingHeapOccupancyPercent=20 \
-XX:+G1PeriodicGCInvokesConcurrent \
-XX:G1PeriodicGCInterval=10000 \
-XX:+UseStringDeduplication \
-XX:G1PeriodicGCSystemLoadThreshold=70" \
JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \
JAVA_CUSTOM_OPTS="" \
PUID=1000 \
PGID=1000 \
UMASK=022

View File

@ -116,46 +116,46 @@ Stirling-PDF currently supports 39 languages!
| Language | Progress |
| -------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![88%](https://geps.dev/progress/88) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![87%](https://geps.dev/progress/87) |
| Basque (Euskara) (eu_ES) | ![51%](https://geps.dev/progress/51) |
| Bulgarian (Български) (bg_BG) | ![98%](https://geps.dev/progress/98) |
| Catalan (Català) (ca_CA) | ![94%](https://geps.dev/progress/94) |
| Croatian (Hrvatski) (hr_HR) | ![85%](https://geps.dev/progress/85) |
| Czech (Česky) (cs_CZ) | ![96%](https://geps.dev/progress/96) |
| Danish (Dansk) (da_DK) | ![84%](https://geps.dev/progress/84) |
| Dutch (Nederlands) (nl_NL) | ![83%](https://geps.dev/progress/83) |
| Arabic (العربية) (ar_AR) | ![86%](https://geps.dev/progress/86) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![85%](https://geps.dev/progress/85) |
| Basque (Euskara) (eu_ES) | ![49%](https://geps.dev/progress/49) |
| Bulgarian (Български) (bg_BG) | ![95%](https://geps.dev/progress/95) |
| Catalan (Català) (ca_CA) | ![92%](https://geps.dev/progress/92) |
| Croatian (Hrvatski) (hr_HR) | ![83%](https://geps.dev/progress/83) |
| Czech (Česky) (cs_CZ) | ![94%](https://geps.dev/progress/94) |
| Danish (Dansk) (da_DK) | ![82%](https://geps.dev/progress/82) |
| Dutch (Nederlands) (nl_NL) | ![81%](https://geps.dev/progress/81) |
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| French (Français) (fr_FR) | ![97%](https://geps.dev/progress/97) |
| German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) |
| Greek (Ελληνικά) (el_GR) | ![96%](https://geps.dev/progress/96) |
| Hindi (हिंदी) (hi_IN) | ![96%](https://geps.dev/progress/96) |
| Hungarian (Magyar) (hu_HU) | ![94%](https://geps.dev/progress/94) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![85%](https://geps.dev/progress/85) |
| Irish (Gaeilge) (ga_IE) | ![96%](https://geps.dev/progress/96) |
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) |
| Japanese (日本語) (ja_JP) | ![93%](https://geps.dev/progress/93) |
| Korean (한국어) (ko_KR) | ![97%](https://geps.dev/progress/97) |
| Norwegian (Norsk) (no_NB) | ![77%](https://geps.dev/progress/77) |
| Persian (فارسی) (fa_IR) | ![92%](https://geps.dev/progress/92) |
| Polish (Polski) (pl_PL) | ![84%](https://geps.dev/progress/84) |
| Portuguese (Português) (pt_PT) | ![96%](https://geps.dev/progress/96) |
| Portuguese Brazilian (Português) (pt_BR) | ![98%](https://geps.dev/progress/98) |
| Romanian (Română) (ro_RO) | ![79%](https://geps.dev/progress/79) |
| Russian (Русский) (ru_RU) | ![96%](https://geps.dev/progress/96) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![63%](https://geps.dev/progress/63) |
| Simplified Chinese (简体中文) (zh_CN) | ![98%](https://geps.dev/progress/98) |
| Slovakian (Slovensky) (sk_SK) | ![72%](https://geps.dev/progress/72) |
| Slovenian (Slovenščina) (sl_SI) | ![95%](https://geps.dev/progress/95) |
| Spanish (Español) (es_ES) | ![98%](https://geps.dev/progress/98) |
| Swedish (Svenska) (sv_SE) | ![92%](https://geps.dev/progress/92) |
| Thai (ไทย) (th_TH) | ![84%](https://geps.dev/progress/84) |
| Tibetan (བོད་ཡིག་) (zh_BO) | ![93%](https://geps.dev/progress/93) |
| Traditional Chinese (繁體中文) (zh_TW) | ![99%](https://geps.dev/progress/99) |
| Turkish (Türkçe) (tr_TR) | ![81%](https://geps.dev/progress/81) |
| Ukrainian (Українська) (uk_UA) | ![99%](https://geps.dev/progress/99) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![78%](https://geps.dev/progress/78) |
| French (Français) (fr_FR) | ![94%](https://geps.dev/progress/94) |
| German (Deutsch) (de_DE) | ![97%](https://geps.dev/progress/97) |
| Greek (Ελληνικά) (el_GR) | ![94%](https://geps.dev/progress/94) |
| Hindi (हिंदी) (hi_IN) | ![94%](https://geps.dev/progress/94) |
| Hungarian (Magyar) (hu_HU) | ![91%](https://geps.dev/progress/91) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![83%](https://geps.dev/progress/83) |
| Irish (Gaeilge) (ga_IE) | ![94%](https://geps.dev/progress/94) |
| Italian (Italiano) (it_IT) | ![96%](https://geps.dev/progress/96) |
| Japanese (日本語) (ja_JP) | ![91%](https://geps.dev/progress/91) |
| Korean (한국어) (ko_KR) | ![95%](https://geps.dev/progress/95) |
| Norwegian (Norsk) (no_NB) | ![89%](https://geps.dev/progress/89) |
| Persian (فارسی) (fa_IR) | ![90%](https://geps.dev/progress/90) |
| Polish (Polski) (pl_PL) | ![82%](https://geps.dev/progress/82) |
| Portuguese (Português) (pt_PT) | ![93%](https://geps.dev/progress/93) |
| Portuguese Brazilian (Português) (pt_BR) | ![96%](https://geps.dev/progress/96) |
| Romanian (Română) (ro_RO) | ![77%](https://geps.dev/progress/77) |
| Russian (Русский) (ru_RU) | ![94%](https://geps.dev/progress/94) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![62%](https://geps.dev/progress/62) |
| Simplified Chinese (简体中文) (zh_CN) | ![95%](https://geps.dev/progress/95) |
| Slovakian (Slovensky) (sk_SK) | ![71%](https://geps.dev/progress/71) |
| Slovenian (Slovenščina) (sl_SI) | ![93%](https://geps.dev/progress/93) |
| Spanish (Español) (es_ES) | ![96%](https://geps.dev/progress/96) |
| Swedish (Svenska) (sv_SE) | ![89%](https://geps.dev/progress/89) |
| Thai (ไทย) (th_TH) | ![82%](https://geps.dev/progress/82) |
| Tibetan (བོད་ཡིག་) (zh_BO) | ![91%](https://geps.dev/progress/91) |
| Traditional Chinese (繁體中文) (zh_TW) | ![97%](https://geps.dev/progress/97) |
| Turkish (Türkçe) (tr_TR) | ![79%](https://geps.dev/progress/79) |
| Ukrainian (Українська) (uk_UA) | ![97%](https://geps.dev/progress/97) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![76%](https://geps.dev/progress/76) |
## Stirling PDF Enterprise

View File

@ -25,7 +25,7 @@ ext {
}
group = "stirling.software"
version = "0.44.3"
version = "0.45.0"
java {
// 17 is lowest but we support and recommend 21
@ -330,9 +330,13 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-thymeleaf:$springBootVersion"
implementation 'com.posthog.java:posthog:1.2.0'
implementation 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE"
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"

View File

@ -1,7 +1,7 @@
services:
stirling-pdf:
container_name: Stirling-PDF-Security-Fat-with-login
image: stirlingtools/stirling-pdf:latest-fat
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest-fat
deploy:
resources:
limits:

View File

@ -1,5 +1,8 @@
#!/bin/bash
export JAVA_TOOL_OPTIONS="${JAVA_BASE_OPTS} ${JAVA_CUSTOM_OPTS}"
echo "running with JAVA_TOOL_OPTIONS ${JAVA_BASE_OPTS} ${JAVA_CUSTOM_OPTS}"
# Update the user and group IDs as per environment variables
if [ ! -z "$PUID" ] && [ "$PUID" != "$(id -u stirlingpdfuser)" ]; then
usermod -o -u "$PUID" stirlingpdfuser || true

View File

@ -8,6 +8,8 @@ import org.springframework.core.annotation.Order;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.EnterpriseEdition;
import stirling.software.SPDF.model.ApplicationProperties.Premium;
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
@ -22,6 +24,7 @@ public class EEAppConfig {
ApplicationProperties applicationProperties, LicenseKeyChecker licenseKeyChecker) {
this.applicationProperties = applicationProperties;
this.licenseKeyChecker = licenseKeyChecker;
migrateEnterpriseSettingsToPremium(this.applicationProperties);
}
@Bean(name = "runningEE")
@ -31,6 +34,74 @@ public class EEAppConfig {
@Bean(name = "SSOAutoLogin")
public boolean ssoAutoLogin() {
return applicationProperties.getEnterpriseEdition().isSsoAutoLogin();
return applicationProperties.getPremium().getProFeatures().isSsoAutoLogin();
}
// TODO: Remove post migration
public void migrateEnterpriseSettingsToPremium(ApplicationProperties applicationProperties) {
EnterpriseEdition enterpriseEdition = applicationProperties.getEnterpriseEdition();
Premium premium = applicationProperties.getPremium();
// Only proceed if both objects exist
if (enterpriseEdition == null || premium == null) {
return;
}
// Copy the license key if it's set in enterprise but not in premium
if (premium.getKey() == null
|| premium.getKey().equals("00000000-0000-0000-0000-000000000000")) {
if (enterpriseEdition.getKey() != null
&& !enterpriseEdition.getKey().equals("00000000-0000-0000-0000-000000000000")) {
premium.setKey(enterpriseEdition.getKey());
}
}
// Copy enabled state if enterprise is enabled but premium is not
if (!premium.isEnabled() && enterpriseEdition.isEnabled()) {
premium.setEnabled(true);
}
// Copy SSO auto login setting
if (!premium.getProFeatures().isSsoAutoLogin() && enterpriseEdition.isSsoAutoLogin()) {
premium.getProFeatures().setSsoAutoLogin(true);
}
// Copy CustomMetadata settings
Premium.ProFeatures.CustomMetadata premiumMetadata =
premium.getProFeatures().getCustomMetadata();
EnterpriseEdition.CustomMetadata enterpriseMetadata = enterpriseEdition.getCustomMetadata();
if (enterpriseMetadata != null && premiumMetadata != null) {
// Copy autoUpdateMetadata setting
if (!premiumMetadata.isAutoUpdateMetadata()
&& enterpriseMetadata.isAutoUpdateMetadata()) {
premiumMetadata.setAutoUpdateMetadata(true);
}
// Copy author if not set in premium but set in enterprise
if ((premiumMetadata.getAuthor() == null
|| premiumMetadata.getAuthor().trim().isEmpty()
|| "username".equals(premiumMetadata.getAuthor()))
&& enterpriseMetadata.getAuthor() != null
&& !enterpriseMetadata.getAuthor().trim().isEmpty()) {
premiumMetadata.setAuthor(enterpriseMetadata.getAuthor());
}
// Copy creator if not set in premium but set in enterprise and different from default
if ((premiumMetadata.getCreator() == null
|| "Stirling-PDF".equals(premiumMetadata.getCreator()))
&& enterpriseMetadata.getCreator() != null
&& !"Stirling-PDF".equals(enterpriseMetadata.getCreator())) {
premiumMetadata.setCreator(enterpriseMetadata.getCreator());
}
// Copy producer if not set in premium but set in enterprise and different from default
if ((premiumMetadata.getProducer() == null
|| "Stirling-PDF".equals(premiumMetadata.getProducer()))
&& enterpriseMetadata.getProducer() != null
&& !"Stirling-PDF".equals(enterpriseMetadata.getProducer())) {
premiumMetadata.setProducer(enterpriseMetadata.getProducer());
}
}
}
}

View File

@ -4,12 +4,17 @@ import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Base64;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.signers.Ed25519Signer;
import org.bouncycastle.util.encoders.Hex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.posthog.java.shaded.org.json.JSONException;
import com.posthog.java.shaded.org.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
@ -20,11 +25,19 @@ import stirling.software.SPDF.utils.GeneralUtils;
@Service
@Slf4j
public class KeygenLicenseVerifier {
// todo: place in config files?
// License verification configuration
private static final String ACCOUNT_ID = "e5430f69-e834-4ae4-befd-b602aae5f372";
private static final String BASE_URL = "https://api.keygen.sh/v1/accounts";
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final String PUBLIC_KEY =
"9fbc0d78593dcfcf03c945146edd60083bf5fae77dbc08aaa3935f03ce94a58d";
private static final String CERT_PREFIX = "-----BEGIN LICENSE FILE-----";
private static final String CERT_SUFFIX = "-----END LICENSE FILE-----";
private static final String JWT_PREFIX = "key/";
private static final ObjectMapper objectMapper = new ObjectMapper();
private final ApplicationProperties applicationProperties;
@Autowired
@ -32,9 +45,367 @@ public class KeygenLicenseVerifier {
this.applicationProperties = applicationProperties;
}
public boolean verifyLicense(String licenseKey) {
public boolean verifyLicense(String licenseKeyOrCert) {
if (isCertificateLicense(licenseKeyOrCert)) {
log.info("Detected certificate-based license. Processing...");
return verifyCertificateLicense(licenseKeyOrCert);
} else if (isJWTLicense(licenseKeyOrCert)) {
log.info("Detected JWT-style license key. Processing...");
return verifyJWTLicense(licenseKeyOrCert);
} else {
log.info("Detected standard license key. Processing...");
return verifyStandardLicense(licenseKeyOrCert);
}
}
private boolean isCertificateLicense(String license) {
return license != null && license.trim().startsWith(CERT_PREFIX);
}
private boolean isJWTLicense(String license) {
return license != null && license.trim().startsWith(JWT_PREFIX);
}
private boolean verifyCertificateLicense(String licenseFile) {
try {
log.info("Checking license key");
log.info("Verifying certificate-based license");
String encodedPayload = licenseFile;
// Remove the header
encodedPayload = encodedPayload.replace(CERT_PREFIX, "");
// Remove the footer
encodedPayload = encodedPayload.replace(CERT_SUFFIX, "");
// Remove all newlines
encodedPayload = encodedPayload.replaceAll("\\r?\\n", "");
byte[] payloadBytes = Base64.getDecoder().decode(encodedPayload);
String payload = new String(payloadBytes);
log.info("Decoded certificate payload: {}", payload);
String encryptedData = "";
String encodedSignature = "";
String algorithm = "";
try {
JSONObject attrs = new JSONObject(payload);
encryptedData = (String) attrs.get("enc");
encodedSignature = (String) attrs.get("sig");
algorithm = (String) attrs.get("alg");
log.info("Certificate algorithm: {}", algorithm);
} catch (JSONException e) {
log.error("Failed to parse license file: {}", e.getMessage());
return false;
}
// Verify license file algorithm
if (!algorithm.equals("base64+ed25519")) {
log.error(
"Unsupported algorithm: {}. Only base64+ed25519 is supported.", algorithm);
return false;
}
// Verify signature
boolean isSignatureValid = verifyEd25519Signature(encryptedData, encodedSignature);
if (!isSignatureValid) {
log.error("License file signature is invalid");
return false;
}
log.info("License file signature is valid");
// Decode the base64 data
String decodedData;
try {
decodedData = new String(Base64.getDecoder().decode(encryptedData));
} catch (IllegalArgumentException e) {
log.error("Failed to decode license data: {}", e.getMessage());
return false;
}
// Process the certificate data
boolean isValid = processCertificateData(decodedData);
return isValid;
} catch (Exception e) {
log.error("Error verifying certificate license: {}", e.getMessage(), e);
return false;
}
}
private boolean verifyEd25519Signature(String encryptedData, String encodedSignature) {
try {
log.info("Signature to verify: {}", encodedSignature);
log.info("Public key being used: {}", PUBLIC_KEY);
byte[] signatureBytes = Base64.getDecoder().decode(encodedSignature);
// Create the signing data format - prefix with "license/"
String signingData = String.format("license/%s", encryptedData);
byte[] signingDataBytes = signingData.getBytes();
log.info("Signing data length: {} bytes", signingDataBytes.length);
byte[] publicKeyBytes = Hex.decode(PUBLIC_KEY);
Ed25519PublicKeyParameters verifierParams =
new Ed25519PublicKeyParameters(publicKeyBytes, 0);
Ed25519Signer verifier = new Ed25519Signer();
verifier.init(false, verifierParams);
verifier.update(signingDataBytes, 0, signingDataBytes.length);
// Verify the signature
boolean result = verifier.verifySignature(signatureBytes);
if (!result) {
log.error("Signature verification failed with standard public key");
}
return result;
} catch (Exception e) {
log.error("Error verifying Ed25519 signature: {}", e.getMessage(), e);
return false;
}
}
private boolean processCertificateData(String certData) {
try {
log.info("Processing certificate data: {}", certData);
JSONObject licenseData = new JSONObject(certData);
JSONObject metaObj = licenseData.optJSONObject("meta");
if (metaObj != null) {
String issuedStr = metaObj.optString("issued", null);
String expiryStr = metaObj.optString("expiry", null);
if (issuedStr != null && expiryStr != null) {
java.time.Instant issued = java.time.Instant.parse(issuedStr);
java.time.Instant expiry = java.time.Instant.parse(expiryStr);
java.time.Instant now = java.time.Instant.now();
if (issued.isAfter(now)) {
log.error(
"License file issued date is in the future. Please adjust system time or request a new license");
return false;
}
// Check if the license file has expired
if (expiry.isBefore(now)) {
log.error("License file has expired on {}", expiryStr);
return false;
}
log.info("License file valid until {}", expiryStr);
}
}
// Get the main license data
JSONObject dataObj = licenseData.optJSONObject("data");
if (dataObj == null) {
log.error("No data object found in certificate");
return false;
}
// Extract license or machine information
JSONObject attributesObj = dataObj.optJSONObject("attributes");
if (attributesObj != null) {
log.info("Found attributes in certificate data");
// Extract metadata
JSONObject metadataObj = attributesObj.optJSONObject("metadata");
if (metadataObj != null) {
int users = metadataObj.optInt("users", 0);
if (users > 0) {
applicationProperties.getPremium().setMaxUsers(users);
log.info("License allows for {} users", users);
}
}
// Check maxUsers directly in attributes if present from policy definition
// if (attributesObj.has("maxUsers")) {
// int maxUsers = attributesObj.optInt("maxUsers", 0);
// if (maxUsers > 0) {
// applicationProperties.getPremium().setMaxUsers(maxUsers);
// log.info("License directly specifies {} max users",
// maxUsers);
// }
// }
// Check license status if available
String status = attributesObj.optString("status", null);
if (status != null
&& !status.equals("ACTIVE")
&& !status.equals("EXPIRING")) { // Accept "EXPIRING" status as valid
log.error("License status is not active: {}", status);
return false;
}
}
return true;
} catch (Exception e) {
log.error("Error processing certificate data: {}", e.getMessage(), e);
return false;
}
}
private boolean verifyJWTLicense(String licenseKey) {
try {
log.info("Verifying ED25519_SIGN format license key");
// Remove the "key/" prefix
String licenseData = licenseKey.substring(JWT_PREFIX.length());
// Split into payload and signature
String[] parts = licenseData.split("\\.", 2);
if (parts.length != 2) {
log.error(
"Invalid ED25519_SIGN license format. Expected format: key/payload.signature");
return false;
}
String encodedPayload = parts[0];
String encodedSignature = parts[1];
// Verify signature
boolean isSignatureValid = verifyJWTSignature(encodedPayload, encodedSignature);
if (!isSignatureValid) {
log.error("ED25519_SIGN license signature is invalid");
return false;
}
log.info("ED25519_SIGN license signature is valid");
// Decode and process payload - first convert from URL-safe base64 if needed
String base64Payload = encodedPayload.replace('-', '+').replace('_', '/');
byte[] payloadBytes = Base64.getDecoder().decode(base64Payload);
String payload = new String(payloadBytes);
// Process the license payload
boolean isValid = processJWTLicensePayload(payload);
return isValid;
} catch (Exception e) {
log.error("Error verifying ED25519_SIGN license: {}", e.getMessage());
return false;
}
}
private boolean verifyJWTSignature(String encodedPayload, String encodedSignature) {
try {
// Decode base64 signature
byte[] signatureBytes =
Base64.getDecoder()
.decode(encodedSignature.replace('-', '+').replace('_', '/'));
// For ED25519_SIGN format, the signing data is "key/" + encodedPayload
String signingData = String.format("key/%s", encodedPayload);
byte[] dataBytes = signingData.getBytes();
byte[] publicKeyBytes = Hex.decode(PUBLIC_KEY);
Ed25519PublicKeyParameters verifierParams =
new Ed25519PublicKeyParameters(publicKeyBytes, 0);
Ed25519Signer verifier = new Ed25519Signer();
verifier.init(false, verifierParams);
verifier.update(dataBytes, 0, dataBytes.length);
// Verify the signature
return verifier.verifySignature(signatureBytes);
} catch (Exception e) {
log.error("Error verifying JWT signature: {}", e.getMessage());
return false;
}
}
private boolean processJWTLicensePayload(String payload) {
try {
log.info("Processing license payload: {}", payload);
JSONObject licenseData = new JSONObject(payload);
JSONObject licenseObj = licenseData.optJSONObject("license");
if (licenseObj == null) {
String id = licenseData.optString("id", null);
if (id != null) {
log.info("Found license ID: {}", id);
licenseObj = licenseData; // Use the root object as the license object
} else {
log.error("License data not found in payload");
return false;
}
}
String licenseId = licenseObj.optString("id", "unknown");
log.info("Processing license with ID: {}", licenseId);
// Check expiry date
String expiryStr = licenseObj.optString("expiry", null);
if (expiryStr != null && !expiryStr.equals("null")) {
java.time.Instant expiry = java.time.Instant.parse(expiryStr);
java.time.Instant now = java.time.Instant.now();
if (now.isAfter(expiry)) {
log.error("License has expired on {}", expiryStr);
return false;
}
log.info("License valid until {}", expiryStr);
} else {
log.info("License has no expiration date");
}
// Extract account, product, policy info
JSONObject accountObj = licenseData.optJSONObject("account");
if (accountObj != null) {
String accountId = accountObj.optString("id", "unknown");
log.info("License belongs to account: {}", accountId);
// Verify this matches your expected account ID
if (!ACCOUNT_ID.equals(accountId)) {
log.warn("License account ID does not match expected account ID");
// You might want to fail verification here depending on your requirements
}
}
// Extract policy information if available
JSONObject policyObj = licenseData.optJSONObject("policy");
if (policyObj != null) {
String policyId = policyObj.optString("id", "unknown");
log.info("License uses policy: {}", policyId);
// Extract max users from policy if available (customize based on your policy
// structure)
int users = policyObj.optInt("users", 0);
if (users > 0) {
applicationProperties.getPremium().setMaxUsers(users);
log.info("License allows for {} users", users);
} else {
// Try to get users from metadata if present
Object metadataObj = policyObj.opt("metadata");
if (metadataObj instanceof JSONObject) {
JSONObject metadata = (JSONObject) metadataObj;
users = metadata.optInt("users", 1);
applicationProperties.getPremium().setMaxUsers(users);
log.info("License allows for {} users (from metadata)", users);
} else {
// Default value
applicationProperties.getPremium().setMaxUsers(1);
log.info("Using default of 1 user for license");
}
}
}
return true;
} catch (Exception e) {
log.error("Error processing license payload: {}", e.getMessage(), e);
return false;
}
}
private boolean verifyStandardLicense(String licenseKey) {
try {
log.info("Checking standard license key");
String machineFingerprint = generateMachineFingerprint();
// First, try to validate the license
@ -44,7 +415,7 @@ public class KeygenLicenseVerifier {
String licenseId = validationResponse.path("data").path("id").asText();
if (!isValid) {
String code = validationResponse.path("meta").path("code").asText();
log.debug(code);
log.info(code);
if ("NO_MACHINE".equals(code)
|| "NO_MACHINES".equals(code)
|| "FINGERPRINT_SCOPE_MISMATCH".equals(code)) {
@ -69,7 +440,7 @@ public class KeygenLicenseVerifier {
return false;
} catch (Exception e) {
log.error("Error verifying license: {}", e.getMessage());
log.error("Error verifying standard license: {}", e.getMessage());
return false;
}
}
@ -96,7 +467,7 @@ public class KeygenLicenseVerifier {
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
log.debug("ValidateLicenseResponse body: {}", response.body());
log.info("ValidateLicenseResponse body: {}", response.body());
JsonNode jsonResponse = objectMapper.readTree(response.body());
if (response.statusCode() == 200) {
JsonNode metaNode = jsonResponse.path("meta");
@ -105,9 +476,9 @@ public class KeygenLicenseVerifier {
String detail = metaNode.path("detail").asText();
String code = metaNode.path("code").asText();
log.debug("License validity: " + isValid);
log.debug("Validation detail: " + detail);
log.debug("Validation code: " + code);
log.info("License validity: " + isValid);
log.info("Validation detail: " + detail);
log.info("Validation code: " + code);
int users =
jsonResponse
@ -116,7 +487,7 @@ public class KeygenLicenseVerifier {
.path("metadata")
.path("users")
.asInt(0);
applicationProperties.getEnterpriseEdition().setMaxUsers(users);
applicationProperties.getPremium().setMaxUsers(users);
log.info(applicationProperties.toString());
} else {
@ -148,13 +519,8 @@ public class KeygenLicenseVerifier {
.put("fingerprint", machineFingerprint)
.put(
"platform",
System.getProperty(
"os.name")) // Added
// platform
// parameter
.put(
"name",
hostname)) // Added name parameter
System.getProperty("os.name"))
.put("name", hostname))
.put(
"relationships",
new JSONObject()
@ -176,16 +542,12 @@ public class KeygenLicenseVerifier {
.uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/machines"))
.header("Content-Type", "application/vnd.api+json")
.header("Accept", "application/vnd.api+json")
.header(
"Authorization",
"License " + licenseKey) // Keep the license key authentication
.POST(
HttpRequest.BodyPublishers.ofString(
body.toString())) // Send the JSON body
.header("Authorization", "License " + licenseKey)
.POST(HttpRequest.BodyPublishers.ofString(body.toString()))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
log.debug("activateMachine Response body: " + response.body());
log.info("activateMachine Response body: " + response.body());
if (response.statusCode() == 201) {
log.info("Machine activated successfully");
return true;

View File

@ -1,6 +1,9 @@
package stirling.software.SPDF.EE;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
@ -15,11 +18,13 @@ import stirling.software.SPDF.utils.GeneralUtils;
@Slf4j
public class LicenseKeyChecker {
private static final String FILE_PREFIX = "file:";
private final KeygenLicenseVerifier licenseService;
private final ApplicationProperties applicationProperties;
private boolean enterpriseEnabledResult = false;
private boolean premiumEnabledResult = false;
@Autowired
public LicenseKeyChecker(
@ -35,27 +40,58 @@ public class LicenseKeyChecker {
}
private void checkLicense() {
if (!applicationProperties.getEnterpriseEdition().isEnabled()) {
enterpriseEnabledResult = false;
if (!applicationProperties.getPremium().isEnabled()) {
premiumEnabledResult = false;
} else {
enterpriseEnabledResult =
licenseService.verifyLicense(
applicationProperties.getEnterpriseEdition().getKey());
if (enterpriseEnabledResult) {
log.info("License key is valid.");
String licenseKey = getLicenseKeyContent(applicationProperties.getPremium().getKey());
if (licenseKey != null) {
premiumEnabledResult = licenseService.verifyLicense(licenseKey);
if (premiumEnabledResult) {
log.info("License key is valid.");
} else {
log.info("License key is invalid.");
}
} else {
log.info("License key is invalid.");
log.error("Failed to obtain license key content.");
premiumEnabledResult = false;
}
}
}
private String getLicenseKeyContent(String keyOrFilePath) {
if (keyOrFilePath == null || keyOrFilePath.trim().isEmpty()) {
log.error("License key is not specified");
return null;
}
// Check if it's a file reference
if (keyOrFilePath.startsWith(FILE_PREFIX)) {
String filePath = keyOrFilePath.substring(FILE_PREFIX.length());
try {
Path path = Paths.get(filePath);
if (!Files.exists(path)) {
log.error("License file does not exist: {}", filePath);
return null;
}
log.info("Reading license from file: {}", filePath);
return Files.readString(path);
} catch (IOException e) {
log.error("Failed to read license file: {}", e.getMessage());
return null;
}
}
// It's a direct license key
return keyOrFilePath;
}
public void updateLicenseKey(String newKey) throws IOException {
applicationProperties.getEnterpriseEdition().setKey(newKey);
applicationProperties.getPremium().setKey(newKey);
GeneralUtils.saveKeyToSettings("EnterpriseEdition.key", newKey);
checkLicense();
}
public boolean getEnterpriseEnabledResult() {
return enterpriseEnabledResult;
return premiumEnabledResult;
}
}

View File

@ -182,7 +182,7 @@ public class AppConfig {
@Bean(name = "analyticsEnabled")
@Scope("request")
public boolean analyticsEnabled() {
if (applicationProperties.getEnterpriseEdition().isEnabled()) return true;
if (applicationProperties.getPremium().isEnabled()) return true;
return applicationProperties.getSystem().isAnalyticsEnabled();
}

View File

@ -9,6 +9,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
@ -56,6 +57,8 @@ public class ConfigInitializer {
YamlHelper settingsTemplateFile = new YamlHelper(tempTemplatePath);
YamlHelper settingsFile = new YamlHelper(settingTempPath);
migrateEnterpriseEditionToPremium(settingsFile, settingsTemplateFile);
boolean changesMade =
settingsTemplateFile.updateValuesFromYaml(settingsFile, settingsTemplateFile);
if (changesMade) {
@ -76,4 +79,46 @@ public class ConfigInitializer {
log.info("Created custom_settings file: {}", customSettingsPath.toString());
}
}
// TODO: Remove post migration
private void migrateEnterpriseEditionToPremium(YamlHelper yaml, YamlHelper template) {
if (yaml.getValueByExactKeyPath("enterpriseEdition", "enabled") != null) {
template.updateValue(
List.of("premium", "enabled"),
yaml.getValueByExactKeyPath("enterpriseEdition", "enabled"));
}
if (yaml.getValueByExactKeyPath("enterpriseEdition", "key") != null) {
template.updateValue(
List.of("premium", "key"),
yaml.getValueByExactKeyPath("enterpriseEdition", "key"));
}
if (yaml.getValueByExactKeyPath("enterpriseEdition", "SSOAutoLogin") != null) {
template.updateValue(
List.of("premium", "proFeatures", "SSOAutoLogin"),
yaml.getValueByExactKeyPath("enterpriseEdition", "SSOAutoLogin"));
}
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "autoUpdateMetadata")
!= null) {
template.updateValue(
List.of("premium", "proFeatures", "CustomMetadata", "autoUpdateMetadata"),
yaml.getValueByExactKeyPath(
"enterpriseEdition", "CustomMetadata", "autoUpdateMetadata"));
}
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "author") != null) {
template.updateValue(
List.of("premium", "proFeatures", "CustomMetadata", "author"),
yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "author"));
}
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "creator") != null) {
template.updateValue(
List.of("premium", "proFeatures", "CustomMetadata", "creator"),
yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "creator"));
}
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "producer")
!= null) {
template.updateValue(
List.of("premium", "proFeatures", "CustomMetadata", "producer"),
yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "producer"));
}
}
}

View File

@ -8,6 +8,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
@ -22,10 +23,14 @@ public class EndpointConfiguration {
private final ApplicationProperties applicationProperties;
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
private final boolean runningEE;
@Autowired
public EndpointConfiguration(ApplicationProperties applicationProperties) {
public EndpointConfiguration(
ApplicationProperties applicationProperties,
@Qualifier("runningEE") boolean runningEE) {
this.applicationProperties = applicationProperties;
this.runningEE = runningEE;
init();
processEnvironmentConfigs();
}
@ -281,6 +286,13 @@ public class EndpointConfiguration {
}
}
}
if (!runningEE) {
disableGroup("enterprise");
}
if (!applicationProperties.getSystem().getEnableUrlToPDF()) {
disableEndpoint("url-to-pdf");
}
}
public Set<String> getEndpointsForGroup(String group) {

View File

@ -36,7 +36,6 @@ public class EndpointInspector implements ApplicationListener<ContextRefreshedEv
if (!endpointsDiscovered) {
discoverEndpoints();
endpointsDiscovered = true;
logger.info("Discovered {} valid GET endpoints", validGetEndpoints.size());
}
}

View File

@ -11,6 +11,8 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.SessionsInterface;
import lombok.extern.slf4j.Slf4j;
@Component
@Slf4j
public class EndpointInterceptor implements HandlerInterceptor {

View File

@ -0,0 +1,38 @@
package stirling.software.SPDF.config;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class EnterpriseEndpointFilter extends OncePerRequestFilter {
private final boolean runningEE;
public EnterpriseEndpointFilter(@Qualifier("runningEE") boolean runningEE) {
this.runningEE = runningEE;
}
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!runningEE && isPrometheusEndpointRequest(request)) {
response.setStatus(HttpStatus.NOT_FOUND.value());
return;
}
filterChain.doFilter(request, response);
}
private boolean isPrometheusEndpointRequest(HttpServletRequest request) {
return request.getRequestURI().contains("/actuator/");
}
}

View File

@ -125,8 +125,7 @@ public class MergeController {
public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest form)
throws IOException {
List<File> filesToDelete = new ArrayList<>(); // List of temporary files to delete
ByteArrayOutputStream docOutputstream =
new ByteArrayOutputStream(); // Stream for the merged document
File mergedTempFile = null;
PDDocument mergedDocument = null;
boolean removeCertSign = form.isRemoveCertSign();
@ -139,21 +138,24 @@ public class MergeController {
form.getSortType())); // Sort files based on the given sort type
PDFMergerUtility mergerUtility = new PDFMergerUtility();
long totalSize = 0;
for (MultipartFile multipartFile : files) {
totalSize += multipartFile.getSize();
File tempFile =
GeneralUtils.convertMultipartFileToFile(
multipartFile); // Convert MultipartFile to File
filesToDelete.add(tempFile); // Add temp file to the list for later deletion
mergerUtility.addSource(tempFile); // Add source file to the merger utility
}
mergerUtility.setDestinationStream(
docOutputstream); // Set the output stream for the merged document
mergerUtility.mergeDocuments(null); // Merge the documents
byte[] mergedPdfBytes = docOutputstream.toByteArray(); // Get merged document bytes
mergedTempFile = Files.createTempFile("merged-", ".pdf").toFile();
mergerUtility.setDestinationFileName(mergedTempFile.getAbsolutePath());
mergerUtility.mergeDocuments(
pdfDocumentFactory.getStreamCacheFunction(totalSize)); // Merge the documents
// Load the merged PDF document
mergedDocument = pdfDocumentFactory.load(mergedPdfBytes);
mergedDocument = pdfDocumentFactory.load(mergedTempFile);
// Remove signatures if removeCertSign is true
if (removeCertSign) {
@ -180,21 +182,23 @@ public class MergeController {
String mergedFileName =
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_merged_unsigned.pdf";
return WebResponseUtils.bytesToWebResponse(
baos.toByteArray(), mergedFileName); // Return the modified PDF
return WebResponseUtils.boasToWebResponse(
baos, mergedFileName); // Return the modified PDF
} catch (Exception ex) {
log.error("Error in merge pdf process", ex);
throw ex;
} finally {
if (mergedDocument != null) {
mergedDocument.close(); // Close the merged document
}
for (File file : filesToDelete) {
if (file != null) {
Files.deleteIfExists(file.toPath()); // Delete temporary files
}
}
docOutputstream.close();
if (mergedDocument != null) {
mergedDocument.close(); // Close the merged document
if (mergedTempFile != null) {
Files.deleteIfExists(mergedTempFile.toPath());
}
}
}

View File

@ -40,9 +40,6 @@ public class SplitPdfBySizeController {
@Autowired
public SplitPdfBySizeController(CustomPDFDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
log.info(
"SplitPdfBySizeController initialized with pdfDocumentFactory: {}",
pdfDocumentFactory.getClass().getSimpleName());
}
@PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data")
@ -57,53 +54,49 @@ public class SplitPdfBySizeController {
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request)
throws Exception {
log.info("Starting PDF split process with request: {}", request);
log.debug("Starting PDF split process with request: {}", request);
MultipartFile file = request.getFileInput();
log.info(
"File received: name={}, size={} bytes",
file.getOriginalFilename(),
file.getSize());
Path zipFile = Files.createTempFile("split_documents", ".zip");
log.info("Created temporary zip file: {}", zipFile);
log.debug("Created temporary zip file: {}", zipFile);
String filename =
Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", "");
log.info("Base filename for output: {}", filename);
log.debug("Base filename for output: {}", filename);
byte[] data = null;
try {
log.info("Reading input file bytes");
log.debug("Reading input file bytes");
byte[] pdfBytes = file.getBytes();
log.info("Successfully read {} bytes from input file", pdfBytes.length);
log.debug("Successfully read {} bytes from input file", pdfBytes.length);
log.info("Creating ZIP output stream");
log.debug("Creating ZIP output stream");
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
log.info("Loading PDF document");
log.debug("Loading PDF document");
try (PDDocument sourceDocument = pdfDocumentFactory.load(pdfBytes)) {
log.info(
log.debug(
"Successfully loaded PDF with {} pages",
sourceDocument.getNumberOfPages());
int type = request.getSplitType();
String value = request.getSplitValue();
log.info("Split type: {}, Split value: {}", type, value);
log.debug("Split type: {}, Split value: {}", type, value);
if (type == 0) {
log.info("Processing split by size");
log.debug("Processing split by size");
long maxBytes = GeneralUtils.convertSizeToBytes(value);
log.info("Max bytes per document: {}", maxBytes);
log.debug("Max bytes per document: {}", maxBytes);
handleSplitBySize(sourceDocument, maxBytes, zipOut, filename);
} else if (type == 1) {
log.info("Processing split by page count");
log.debug("Processing split by page count");
int pageCount = Integer.parseInt(value);
log.info("Pages per document: {}", pageCount);
log.debug("Pages per document: {}", pageCount);
handleSplitByPageCount(sourceDocument, pageCount, zipOut, filename);
} else if (type == 2) {
log.info("Processing split by document count");
log.debug("Processing split by document count");
int documentCount = Integer.parseInt(value);
log.info("Total number of documents: {}", documentCount);
log.debug("Total number of documents: {}", documentCount);
handleSplitByDocCount(sourceDocument, documentCount, zipOut, filename);
} else {
log.error("Invalid split type: {}", type);
@ -111,7 +104,7 @@ public class SplitPdfBySizeController {
"Invalid argument for split type: " + type);
}
log.info("PDF splitting completed successfully");
log.debug("PDF splitting completed successfully");
} catch (Exception e) {
log.error("Error loading or processing PDF document", e);
throw e;
@ -126,23 +119,23 @@ public class SplitPdfBySizeController {
throw e; // Re-throw to ensure proper error response
} finally {
try {
log.info("Reading ZIP file data");
log.debug("Reading ZIP file data");
data = Files.readAllBytes(zipFile);
log.info("Successfully read {} bytes from ZIP file", data.length);
log.debug("Successfully read {} bytes from ZIP file", data.length);
} catch (IOException e) {
log.error("Error reading ZIP file data", e);
}
try {
log.info("Deleting temporary ZIP file");
log.debug("Deleting temporary ZIP file");
boolean deleted = Files.deleteIfExists(zipFile);
log.info("Temporary ZIP file deleted: {}", deleted);
log.debug("Temporary ZIP file deleted: {}", deleted);
} catch (IOException e) {
log.error("Error deleting temporary ZIP file", e);
}
}
log.info("Returning response with {} bytes of data", data != null ? data.length : 0);
log.debug("Returning response with {} bytes of data", data != null ? data.length : 0);
return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
}
@ -150,7 +143,7 @@ public class SplitPdfBySizeController {
private void handleSplitBySize(
PDDocument sourceDocument, long maxBytes, ZipOutputStream zipOut, String baseFilename)
throws IOException {
log.info("Starting handleSplitBySize with maxBytes={}", maxBytes);
log.debug("Starting handleSplitBySize with maxBytes={}", maxBytes);
PDDocument currentDoc =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
@ -163,7 +156,7 @@ public class SplitPdfBySizeController {
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
PDPage page = sourceDocument.getPage(pageIndex);
log.info("Processing page {} of {}", pageIndex + 1, totalPages);
log.debug("Processing page {} of {}", pageIndex + 1, totalPages);
// Add the page to current document
PDPage newPage = new PDPage(page.getCOSObject());
@ -177,21 +170,21 @@ public class SplitPdfBySizeController {
|| (pageAdded >= 20); // Always check after 20 pages
if (shouldCheckSize) {
log.info("Performing size check after {} pages", pageAdded);
log.debug("Performing size check after {} pages", pageAdded);
ByteArrayOutputStream checkSizeStream = new ByteArrayOutputStream();
currentDoc.save(checkSizeStream);
long actualSize = checkSizeStream.size();
log.info("Current document size: {} bytes (max: {} bytes)", actualSize, maxBytes);
log.debug("Current document size: {} bytes (max: {} bytes)", actualSize, maxBytes);
if (actualSize > maxBytes) {
// We exceeded the limit - remove the last page and save
if (currentDoc.getNumberOfPages() > 1) {
currentDoc.removePage(currentDoc.getNumberOfPages() - 1);
pageIndex--; // Process this page again in the next document
log.info("Size limit exceeded - removed last page");
log.debug("Size limit exceeded - removed last page");
}
log.info(
log.debug(
"Saving document with {} pages as part {}",
currentDoc.getNumberOfPages(),
fileIndex);
@ -206,7 +199,7 @@ public class SplitPdfBySizeController {
int pagesToLookAhead = Math.min(5, totalPages - pageIndex - 1);
if (pagesToLookAhead > 0) {
log.info(
log.debug(
"Testing {} upcoming pages for potential addition",
pagesToLookAhead);
@ -231,12 +224,12 @@ public class SplitPdfBySizeController {
if (testSize <= maxBytes) {
extraPagesAdded++;
log.info(
log.debug(
"Test: Can add page {} (size would be {})",
testPageIndex + 1,
testSize);
} else {
log.info(
log.debug(
"Test: Cannot add page {} (size would be {})",
testPageIndex + 1,
testSize);
@ -248,7 +241,7 @@ public class SplitPdfBySizeController {
// Add the pages we verified would fit
if (extraPagesAdded > 0) {
log.info("Adding {} verified pages ahead", extraPagesAdded);
log.debug("Adding {} verified pages ahead", extraPagesAdded);
for (int i = 0; i < extraPagesAdded; i++) {
int extraPageIndex = pageIndex + 1 + i;
PDPage extraPage = sourceDocument.getPage(extraPageIndex);
@ -265,26 +258,26 @@ public class SplitPdfBySizeController {
// Save final document if it has any pages
if (currentDoc.getNumberOfPages() > 0) {
log.info(
log.debug(
"Saving final document with {} pages as part {}",
currentDoc.getNumberOfPages(),
fileIndex);
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
}
log.info("Completed handleSplitBySize with {} document parts created", fileIndex - 1);
log.debug("Completed handleSplitBySize with {} document parts created", fileIndex - 1);
}
private void handleSplitByPageCount(
PDDocument sourceDocument, int pageCount, ZipOutputStream zipOut, String baseFilename)
throws IOException {
log.info("Starting handleSplitByPageCount with pageCount={}", pageCount);
log.debug("Starting handleSplitByPageCount with pageCount={}", pageCount);
int currentPageCount = 0;
log.info("Creating initial output document");
log.debug("Creating initial output document");
PDDocument currentDoc = null;
try {
currentDoc = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
log.info("Successfully created initial output document");
log.debug("Successfully created initial output document");
} catch (Exception e) {
log.error("Error creating initial output document", e);
throw new IOException("Failed to create initial output document", e);
@ -293,49 +286,49 @@ public class SplitPdfBySizeController {
int fileIndex = 1;
int pageIndex = 0;
int totalPages = sourceDocument.getNumberOfPages();
log.info("Processing {} pages", totalPages);
log.debug("Processing {} pages", totalPages);
try {
for (PDPage page : sourceDocument.getPages()) {
pageIndex++;
log.info("Processing page {} of {}", pageIndex, totalPages);
log.debug("Processing page {} of {}", pageIndex, totalPages);
try {
log.info("Adding page {} to current document", pageIndex);
log.debug("Adding page {} to current document", pageIndex);
currentDoc.addPage(page);
log.info("Successfully added page {} to current document", pageIndex);
log.debug("Successfully added page {} to current document", pageIndex);
} catch (Exception e) {
log.error("Error adding page {} to current document", pageIndex, e);
throw new IOException("Failed to add page to document", e);
}
currentPageCount++;
log.info("Current page count: {}/{}", currentPageCount, pageCount);
log.debug("Current page count: {}/{}", currentPageCount, pageCount);
if (currentPageCount == pageCount) {
log.info(
log.debug(
"Reached target page count ({}), saving current document as part {}",
pageCount,
fileIndex);
try {
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
log.info("Successfully saved document part {}", fileIndex - 1);
log.debug("Successfully saved document part {}", fileIndex - 1);
} catch (Exception e) {
log.error("Error saving document part {}", fileIndex - 1, e);
throw e;
}
try {
log.info("Creating new document for next part");
log.debug("Creating new document for next part");
currentDoc = new PDDocument();
log.info("Successfully created new document");
log.debug("Successfully created new document");
} catch (Exception e) {
log.error("Error creating new document for next part", e);
throw new IOException("Failed to create new document", e);
}
currentPageCount = 0;
log.info("Reset current page count to 0");
log.debug("Reset current page count to 0");
}
}
} catch (Exception e) {
@ -346,34 +339,34 @@ public class SplitPdfBySizeController {
// Add the last document if it contains any pages
try {
if (currentDoc.getPages().getCount() != 0) {
log.info(
log.debug(
"Saving final document with {} pages as part {}",
currentDoc.getPages().getCount(),
fileIndex);
try {
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
log.info("Successfully saved final document part {}", fileIndex - 1);
log.debug("Successfully saved final document part {}", fileIndex - 1);
} catch (Exception e) {
log.error("Error saving final document part {}", fileIndex - 1, e);
throw e;
}
} else {
log.info("Final document has no pages, skipping");
log.debug("Final document has no pages, skipping");
}
} catch (Exception e) {
log.error("Error checking or saving final document", e);
throw new IOException("Failed to process final document", e);
} finally {
try {
log.info("Closing final document");
log.debug("Closing final document");
currentDoc.close();
log.info("Successfully closed final document");
log.debug("Successfully closed final document");
} catch (Exception e) {
log.error("Error closing final document", e);
}
}
log.info("Completed handleSplitByPageCount with {} document parts created", fileIndex - 1);
log.debug("Completed handleSplitByPageCount with {} document parts created", fileIndex - 1);
}
private void handleSplitByDocCount(
@ -382,40 +375,40 @@ public class SplitPdfBySizeController {
ZipOutputStream zipOut,
String baseFilename)
throws IOException {
log.info("Starting handleSplitByDocCount with documentCount={}", documentCount);
log.debug("Starting handleSplitByDocCount with documentCount={}", documentCount);
int totalPageCount = sourceDocument.getNumberOfPages();
log.info("Total pages in source document: {}", totalPageCount);
log.debug("Total pages in source document: {}", totalPageCount);
int pagesPerDocument = totalPageCount / documentCount;
int extraPages = totalPageCount % documentCount;
log.info("Pages per document: {}, Extra pages: {}", pagesPerDocument, extraPages);
log.debug("Pages per document: {}, Extra pages: {}", pagesPerDocument, extraPages);
int currentPageIndex = 0;
int fileIndex = 1;
for (int i = 0; i < documentCount; i++) {
log.info("Creating document {} of {}", i + 1, documentCount);
log.debug("Creating document {} of {}", i + 1, documentCount);
PDDocument currentDoc = null;
try {
currentDoc = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
log.info("Successfully created document {} of {}", i + 1, documentCount);
log.debug("Successfully created document {} of {}", i + 1, documentCount);
} catch (Exception e) {
log.error("Error creating document {} of {}", i + 1, documentCount, e);
throw new IOException("Failed to create document", e);
}
int pagesToAdd = pagesPerDocument + (i < extraPages ? 1 : 0);
log.info("Adding {} pages to document {}", pagesToAdd, i + 1);
log.debug("Adding {} pages to document {}", pagesToAdd, i + 1);
for (int j = 0; j < pagesToAdd; j++) {
try {
log.info(
log.debug(
"Adding page {} (index {}) to document {}",
j + 1,
currentPageIndex,
i + 1);
currentDoc.addPage(sourceDocument.getPage(currentPageIndex));
log.info("Successfully added page {} to document {}", j + 1, i + 1);
log.debug("Successfully added page {} to document {}", j + 1, i + 1);
currentPageIndex++;
} catch (Exception e) {
log.error("Error adding page {} to document {}", j + 1, i + 1, e);
@ -424,37 +417,37 @@ public class SplitPdfBySizeController {
}
try {
log.info("Saving document {} with {} pages", i + 1, pagesToAdd);
log.debug("Saving document {} with {} pages", i + 1, pagesToAdd);
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
log.info("Successfully saved document {}", i + 1);
log.debug("Successfully saved document {}", i + 1);
} catch (Exception e) {
log.error("Error saving document {}", i + 1, e);
throw e;
}
}
log.info("Completed handleSplitByDocCount with {} documents created", documentCount);
log.debug("Completed handleSplitByDocCount with {} documents created", documentCount);
}
private void saveDocumentToZip(
PDDocument document, ZipOutputStream zipOut, String baseFilename, int index)
throws IOException {
log.info("Starting saveDocumentToZip for document part {}", index);
log.debug("Starting saveDocumentToZip for document part {}", index);
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
try {
log.info("Saving document part {} to byte array", index);
log.debug("Saving document part {} to byte array", index);
document.save(outStream);
log.info("Successfully saved document part {} ({} bytes)", index, outStream.size());
log.debug("Successfully saved document part {} ({} bytes)", index, outStream.size());
} catch (Exception e) {
log.error("Error saving document part {} to byte array", index, e);
throw new IOException("Failed to save document to byte array", e);
}
try {
log.info("Closing document part {}", index);
log.debug("Closing document part {}", index);
document.close();
log.info("Successfully closed document part {}", index);
log.debug("Successfully closed document part {}", index);
} catch (Exception e) {
log.error("Error closing document part {}", index, e);
// Continue despite close error
@ -463,17 +456,17 @@ public class SplitPdfBySizeController {
try {
// Create a new zip entry
String entryName = baseFilename + "_" + index + ".pdf";
log.info("Creating ZIP entry: {}", entryName);
log.debug("Creating ZIP entry: {}", entryName);
ZipEntry zipEntry = new ZipEntry(entryName);
zipOut.putNextEntry(zipEntry);
byte[] bytes = outStream.toByteArray();
log.info("Writing {} bytes to ZIP entry", bytes.length);
log.debug("Writing {} bytes to ZIP entry", bytes.length);
zipOut.write(bytes);
log.info("Closing ZIP entry");
log.debug("Closing ZIP entry");
zipOut.closeEntry();
log.info("Successfully added document part {} to ZIP", index);
log.debug("Successfully added document part {} to ZIP", index);
} catch (Exception e) {
log.error("Error adding document part {} to ZIP", index, e);
throw new IOException("Failed to add document to ZIP file", e);

View File

@ -30,6 +30,7 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.config.security.UserUtils;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User;
@ -45,10 +46,15 @@ public class UserController {
private static final String LOGIN_MESSAGETYPE_CREDSUPDATED = "/login?messageType=credsUpdated";
private final UserService userService;
private final SessionPersistentRegistry sessionRegistry;
private final ApplicationProperties applicationProperties;
public UserController(UserService userService, SessionPersistentRegistry sessionRegistry) {
public UserController(
UserService userService,
SessionPersistentRegistry sessionRegistry,
ApplicationProperties applicationProperties) {
this.userService = userService;
this.sessionRegistry = sessionRegistry;
this.applicationProperties = applicationProperties;
}
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@ -192,39 +198,44 @@ public class UserController {
boolean forceChange)
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!userService.isUsernameValid(username)) {
return new RedirectView("/addUsers?messageType=invalidUsername", true);
return new RedirectView("/adminSettings?messageType=invalidUsername", true);
}
if (applicationProperties.getPremium().isEnabled()
&& applicationProperties.getPremium().getMaxUsers()
<= userService.getTotalUsersCount()) {
return new RedirectView("/adminSettings?messageType=maxUsersReached", true);
}
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (userOpt.isPresent()) {
User user = userOpt.get();
if (user.getUsername().equalsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=usernameExists", true);
return new RedirectView("/adminSettings?messageType=usernameExists", true);
}
}
if (userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=usernameExists", true);
return new RedirectView("/adminSettings?messageType=usernameExists", true);
}
try {
// Validate the role
Role roleEnum = Role.fromString(role);
if (roleEnum == Role.INTERNAL_API_USER) {
// If the role is INTERNAL_API_USER, reject the request
return new RedirectView("/addUsers?messageType=invalidRole", true);
return new RedirectView("/adminSettings?messageType=invalidRole", true);
}
} catch (IllegalArgumentException e) {
// If the role ID is not valid, redirect with an error message
return new RedirectView("/addUsers?messageType=invalidRole", true);
return new RedirectView("/adminSettings?messageType=invalidRole", true);
}
if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) {
userService.saveUser(username, AuthenticationType.SSO, role);
} else {
if (password.isBlank()) {
return new RedirectView("/addUsers?messageType=invalidPassword", true);
return new RedirectView("/adminSettings?messageType=invalidPassword", true);
}
userService.saveUser(username, password, role, forceChange);
}
return new RedirectView(
"/addUsers", // Redirect to account page after adding the user
"/adminSettings", // Redirect to account page after adding the user
true);
}
@ -237,32 +248,32 @@ public class UserController {
throws SQLException, UnsupportedProviderException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (!userOpt.isPresent()) {
return new RedirectView("/addUsers?messageType=userNotFound", true);
return new RedirectView("/adminSettings?messageType=userNotFound", true);
}
if (!userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=userNotFound", true);
return new RedirectView("/adminSettings?messageType=userNotFound", true);
}
// Get the currently authenticated username
String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username
if (currentUsername.equalsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=downgradeCurrentUser", true);
return new RedirectView("/adminSettings?messageType=downgradeCurrentUser", true);
}
try {
// Validate the role
Role roleEnum = Role.fromString(role);
if (roleEnum == Role.INTERNAL_API_USER) {
// If the role is INTERNAL_API_USER, reject the request
return new RedirectView("/addUsers?messageType=invalidRole", true);
return new RedirectView("/adminSettings?messageType=invalidRole", true);
}
} catch (IllegalArgumentException e) {
// If the role ID is not valid, redirect with an error message
return new RedirectView("/addUsers?messageType=invalidRole", true);
return new RedirectView("/adminSettings?messageType=invalidRole", true);
}
User user = userOpt.get();
userService.changeRole(user, role);
return new RedirectView(
"/addUsers", // Redirect to account page after adding the user
"/adminSettings", // Redirect to account page after adding the user
true);
}
@ -275,16 +286,16 @@ public class UserController {
throws SQLException, UnsupportedProviderException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (userOpt.isEmpty()) {
return new RedirectView("/addUsers?messageType=userNotFound", true);
return new RedirectView("/adminSettings?messageType=userNotFound", true);
}
if (!userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=userNotFound", true);
return new RedirectView("/adminSettings?messageType=userNotFound", true);
}
// Get the currently authenticated username
String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username
if (currentUsername.equalsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=disabledCurrentUser", true);
return new RedirectView("/adminSettings?messageType=disabledCurrentUser", true);
}
User user = userOpt.get();
userService.changeUserEnabled(user, enabled);
@ -304,7 +315,7 @@ public class UserController {
}
}
return new RedirectView(
"/addUsers", // Redirect to account page after adding the user
"/adminSettings", // Redirect to account page after adding the user
true);
}
@ -313,13 +324,13 @@ public class UserController {
public RedirectView deleteUser(
@PathVariable("username") String username, Authentication authentication) {
if (!userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=deleteUsernameExists", true);
return new RedirectView("/adminSettings?messageType=deleteUsernameExists", true);
}
// Get the currently authenticated username
String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username
if (currentUsername.equalsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=deleteCurrentUser", true);
return new RedirectView("/adminSettings?messageType=deleteCurrentUser", true);
}
// Invalidate all sessions before deleting the user
List<SessionInformation> sessionsInformations =
@ -329,7 +340,7 @@ public class UserController {
sessionRegistry.removeSessionInformation(sessionsInformation.getSessionId());
}
userService.deleteUser(username);
return new RedirectView("/addUsers", true);
return new RedirectView("/adminSettings", true);
}
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")

View File

@ -20,6 +20,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.RuntimePathConfig;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.utils.GeneralUtils;
@ -35,12 +36,16 @@ public class ConvertWebsiteToPDF {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final RuntimePathConfig runtimePathConfig;
private final ApplicationProperties applicationProperties;
@Autowired
public ConvertWebsiteToPDF(
CustomPDFDocumentFactory pdfDocumentFactory, RuntimePathConfig runtimePathConfig) {
CustomPDFDocumentFactory pdfDocumentFactory,
RuntimePathConfig runtimePathConfig,
ApplicationProperties applicationProperties) {
this.pdfDocumentFactory = pdfDocumentFactory;
this.runtimePathConfig = runtimePathConfig;
this.applicationProperties = applicationProperties;
}
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
@ -53,6 +58,9 @@ public class ConvertWebsiteToPDF {
throws IOException, InterruptedException {
String URL = request.getUrlInput();
if (!applicationProperties.getSystem().getEnableUrlToPDF()) {
throw new IllegalArgumentException("This endpoint has been disabled by the admin.");
}
// Validate the URL format
if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
throw new IllegalArgumentException("Invalid URL format provided.");

View File

@ -86,7 +86,7 @@ public class MetadataController {
allRequestParams = new java.util.HashMap<String, String>();
}
// Load the PDF file into a PDDocument
PDDocument document = pdfDocumentFactory.load(pdfFile);
PDDocument document = pdfDocumentFactory.load(pdfFile, true);
// Get the document information from the PDF
PDDocumentInformation info = document.getDocumentInformation();

View File

@ -51,11 +51,12 @@ public class SanitizeController {
MultipartFile inputFile = request.getFileInput();
boolean removeJavaScript = request.isRemoveJavaScript();
boolean removeEmbeddedFiles = request.isRemoveEmbeddedFiles();
boolean removeXMPMetadata = request.isRemoveXMPMetadata();
boolean removeMetadata = request.isRemoveMetadata();
boolean removeLinks = request.isRemoveLinks();
boolean removeFonts = request.isRemoveFonts();
PDDocument document = pdfDocumentFactory.load(inputFile);
PDDocument document = pdfDocumentFactory.load(inputFile, true);
if (removeJavaScript) {
sanitizeJavaScript(document);
}
@ -64,10 +65,14 @@ public class SanitizeController {
sanitizeEmbeddedFiles(document);
}
if (removeMetadata) {
sanitizeMetadata(document);
if (removeXMPMetadata) {
sanitizeXMPMetadata(document);
}
if (removeMetadata) {
sanitizeDocumentInfoMetadata(document);
}
if (removeLinks) {
sanitizeLinks(document);
}
@ -145,7 +150,7 @@ public class SanitizeController {
}
}
private void sanitizeMetadata(PDDocument document) {
private void sanitizeXMPMetadata(PDDocument document) {
if (document.getDocumentCatalog() != null) {
PDMetadata metadata = document.getDocumentCatalog().getMetadata();
if (metadata != null) {
@ -153,6 +158,16 @@ public class SanitizeController {
}
}
}
private void sanitizeDocumentInfoMetadata(PDDocument document) {
PDDocumentInformation docInfo = document.getDocumentInformation();
if (docInfo != null) {
PDDocumentInformation newInfo = new PDDocumentInformation();
document.setDocumentInformation(newInfo);
}
}
private void sanitizeLinks(PDDocument document) throws IOException {
for (PDPage page : document.getPages()) {

View File

@ -123,11 +123,11 @@ public class AccountWebController {
if (securityProps.isSaml2Active()
&& applicationProperties.getSystem().getEnableAlphaFunctionality()
&& applicationProperties.getEnterpriseEdition().isEnabled()) {
&& applicationProperties.getPremium().isEnabled()) {
String samlIdp = saml2.getProvider();
String saml2AuthenticationPath = "/saml2/authenticate/" + saml2.getRegistrationId();
if (applicationProperties.getEnterpriseEdition().isSsoAutoLogin()) {
if (applicationProperties.getPremium().getProFeatures().isSsoAutoLogin()) {
return "redirect:" + request.getRequestURL() + saml2AuthenticationPath;
} else {
providerList.put(saml2AuthenticationPath, samlIdp + " (SAML 2)");
@ -201,7 +201,13 @@ public class AccountWebController {
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/addUsers")
@GetMapping("/usage")
public String showUsage() {
return "usage";
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/adminSettings")
public String showAddUserForm(
HttpServletRequest request, Model model, Authentication authentication) {
List<User> allUsers = userRepository.findAll();
@ -337,7 +343,8 @@ public class AccountWebController {
model.addAttribute("maxSessions", maxSessions);
model.addAttribute("maxUserSessions", maxUserSessions);
model.addAttribute("sessionCount", sessionCount);
return "addUsers";
model.addAttribute("maxEnterpriseUsers", applicationProperties.getPremium().getMaxUsers());
return "adminSettings";
}
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")

View File

@ -22,6 +22,7 @@ import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.EndpointInspector;
import stirling.software.SPDF.config.StartupApplicationListener;
import stirling.software.SPDF.model.ApplicationProperties;
@ -32,15 +33,17 @@ import stirling.software.SPDF.model.ApplicationProperties;
public class MetricsController {
private final ApplicationProperties applicationProperties;
private final MeterRegistry meterRegistry;
private final EndpointInspector endpointInspector;
private boolean metricsEnabled;
public MetricsController(
ApplicationProperties applicationProperties, MeterRegistry meterRegistry) {
ApplicationProperties applicationProperties,
MeterRegistry meterRegistry,
EndpointInspector endpointInspector) {
this.applicationProperties = applicationProperties;
this.meterRegistry = meterRegistry;
this.endpointInspector = endpointInspector;
}
@PostConstruct
@ -208,25 +211,43 @@ public class MetricsController {
}
private double getRequestCount(String method, Optional<String> endpoint) {
log.info(
"Getting request count for method: {}, endpoint: {}",
method,
endpoint.orElse("all"));
double count =
meterRegistry.find("http.requests").tag("method", method).counters().stream()
.filter(
counter ->
!endpoint.isPresent()
|| endpoint.get()
.equals(counter.getId().getTag("uri")))
.mapToDouble(Counter::count)
.sum();
log.info("Request count: {}", count);
return count;
return meterRegistry.find("http.requests").tag("method", method).counters().stream()
.filter(
counter -> {
String uri = counter.getId().getTag("uri");
// Apply filtering logic - Skip if uri is null
if (uri == null) {
return false;
}
// For POST requests, only include if they start with /api/v1
if ("POST".equals(method) && !uri.contains("api/v1")) {
return false;
}
if (uri.contains(".txt")) {
return false;
}
// For GET requests, validate if we have a list of valid endpoints
final boolean validateGetEndpoints =
endpointInspector.getValidGetEndpoints().size() != 0;
if ("GET".equals(method)
&& validateGetEndpoints
&& !endpointInspector.isValidGetEndpoint(uri)) {
log.debug("Skipping invalid GET endpoint: {}", uri);
return false;
}
// Filter for specific endpoint if provided
return !endpoint.isPresent() || endpoint.get().equals(uri);
})
.mapToDouble(Counter::count)
.sum();
}
private List<EndpointCount> getEndpointCounts(String method) {
log.info("Getting endpoint counts for method: {}", method);
Map<String, Double> counts = new HashMap<>();
meterRegistry
.find("http.requests")
@ -235,28 +256,72 @@ public class MetricsController {
.forEach(
counter -> {
String uri = counter.getId().getTag("uri");
// Skip if uri is null
if (uri == null) {
return;
}
// For POST requests, only include if they start with /api/v1
if ("POST".equals(method) && !uri.contains("api/v1")) {
return;
}
if (uri.contains(".txt")) {
return;
}
// For GET requests, validate if we have a list of valid endpoints
final boolean validateGetEndpoints =
endpointInspector.getValidGetEndpoints().size() != 0;
if ("GET".equals(method)
&& validateGetEndpoints
&& !endpointInspector.isValidGetEndpoint(uri)) {
log.debug("Skipping invalid GET endpoint: {}", uri);
return;
}
counts.merge(uri, counter.count(), Double::sum);
});
List<EndpointCount> result =
counts.entrySet().stream()
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
.collect(Collectors.toList());
log.info("Found {} endpoints with counts", result.size());
return result;
return counts.entrySet().stream()
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
.collect(Collectors.toList());
}
private double getUniqueUserCount(String method, Optional<String> endpoint) {
log.info(
"Getting unique user count for method: {}, endpoint: {}",
method,
endpoint.orElse("all"));
Set<String> uniqueUsers = new HashSet<>();
meterRegistry.find("http.requests").tag("method", method).counters().stream()
.filter(
counter ->
!endpoint.isPresent()
|| endpoint.get().equals(counter.getId().getTag("uri")))
counter -> {
String uri = counter.getId().getTag("uri");
// Skip if uri is null
if (uri == null) {
return false;
}
// For POST requests, only include if they start with /api/v1
if ("POST".equals(method) && !uri.contains("api/v1")) {
return false;
}
if (uri.contains(".txt")) {
return false;
}
// For GET requests, validate if we have a list of valid endpoints
final boolean validateGetEndpoints =
endpointInspector.getValidGetEndpoints().size() != 0;
if ("GET".equals(method)
&& validateGetEndpoints
&& !endpointInspector.isValidGetEndpoint(uri)) {
log.debug("Skipping invalid GET endpoint: {}", uri);
return false;
}
return !endpoint.isPresent() || endpoint.get().equals(uri);
})
.forEach(
counter -> {
String session = counter.getId().getTag("session");
@ -264,12 +329,10 @@ public class MetricsController {
uniqueUsers.add(session);
}
});
log.info("Unique user count: {}", uniqueUsers.size());
return uniqueUsers.size();
}
private List<EndpointCount> getUniqueUserCounts(String method) {
log.info("Getting unique user counts for method: {}", method);
Map<String, Set<String>> uniqueUsers = new HashMap<>();
meterRegistry
.find("http.requests")
@ -283,13 +346,10 @@ public class MetricsController {
uniqueUsers.computeIfAbsent(uri, k -> new HashSet<>()).add(session);
}
});
List<EndpointCount> result =
uniqueUsers.entrySet().stream()
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue().size()))
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
.collect(Collectors.toList());
log.info("Found {} endpoints with unique user counts", result.size());
return result;
return uniqueUsers.entrySet().stream()
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue().size()))
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
.collect(Collectors.toList());
}
@GetMapping("/uptime")

View File

@ -81,6 +81,8 @@ public class ApplicationProperties {
private Endpoints endpoints = new Endpoints();
private Metrics metrics = new Metrics();
private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated();
private Premium premium = new Premium();
private EnterpriseEdition enterpriseEdition = new EnterpriseEdition();
private AutoPipeline autoPipeline = new AutoPipeline();
private ProcessExecutor processExecutor = new ProcessExecutor();
@ -287,6 +289,7 @@ public class ApplicationProperties {
private Boolean enableAnalytics;
private Datasource datasource;
private Boolean disableSanitize;
private Boolean enableUrlToPDF;
private CustomPaths customPaths = new CustomPaths();
public boolean isAnalyticsEnabled() {
@ -390,6 +393,7 @@ public class ApplicationProperties {
private String appVersion;
}
// TODO: Remove post migration
@Data
public static class EnterpriseEdition {
private boolean enabled;
@ -415,6 +419,50 @@ public class ApplicationProperties {
}
}
@Data
public static class Premium {
private boolean enabled;
@ToString.Exclude private String key;
private int maxUsers;
private ProFeatures proFeatures = new ProFeatures();
private EnterpriseFeatures enterpriseFeatures = new EnterpriseFeatures();
@Data
public static class ProFeatures {
private boolean ssoAutoLogin;
private CustomMetadata customMetadata = new CustomMetadata();
@Data
public static class CustomMetadata {
private boolean autoUpdateMetadata;
private String author;
private String creator;
private String producer;
public String getCreator() {
return creator == null || creator.trim().isEmpty() ? "Stirling-PDF" : creator;
}
public String getProducer() {
return producer == null || producer.trim().isEmpty()
? "Stirling-PDF"
: producer;
}
}
}
@Data
public static class EnterpriseFeatures {
private PersistentMetrics persistentMetrics = new PersistentMetrics();
@Data
public static class PersistentMetrics {
private boolean enabled;
private int retentionDays;
}
}
}
@Data
public static class ProcessExecutor {
private SessionLimit sessionLimit = new SessionLimit();

View File

@ -17,9 +17,12 @@ public class SanitizePdfRequest extends PDFFile {
@Schema(description = "Remove embedded files from the PDF", defaultValue = "false")
private boolean removeEmbeddedFiles;
@Schema(description = "Remove metadata from the PDF", defaultValue = "false")
private boolean removeMetadata;
@Schema(description = "Remove XMP metadata from the PDF", defaultValue = "false")
private boolean removeXMPMetadata;
@Schema(description = "Remove document info metadata from the PDF", defaultValue = "false")
private boolean removeMetadata;
@Schema(description = "Remove links from the PDF", defaultValue = "false")
private boolean removeLinks;

View File

@ -68,10 +68,18 @@ public class CustomPDFDocumentFactory {
}
/**
* Main entry point for loading a PDF document from a file. Automatically selects the most
* appropriate loading strategy.
*/
* Main entry point for loading a PDF document from a file. Automatically selects the most
* appropriate loading strategy.
*/
public PDDocument load(File file) throws IOException {
return load(file, false);
}
/**
* Main entry point for loading a PDF document from a file with read-only option.
* Automatically selects the most appropriate loading strategy.
*/
public PDDocument load(File file, boolean readOnly) throws IOException {
if (file == null) {
throw new IllegalArgumentException("File cannot be null");
}
@ -79,14 +87,26 @@ public class CustomPDFDocumentFactory {
long fileSize = file.length();
log.debug("Loading PDF from file, size: {}MB", fileSize / (1024 * 1024));
return loadAdaptively(file, fileSize);
PDDocument doc = loadAdaptively(file, fileSize);
if (!readOnly) {
postProcessDocument(doc);
}
return doc;
}
/**
* Main entry point for loading a PDF document from a Path. Automatically selects the most
* appropriate loading strategy.
*/
* Main entry point for loading a PDF document from a Path. Automatically selects the most
* appropriate loading strategy.
*/
public PDDocument load(Path path) throws IOException {
return load(path, false);
}
/**
* Main entry point for loading a PDF document from a Path with read-only option.
* Automatically selects the most appropriate loading strategy.
*/
public PDDocument load(Path path, boolean readOnly) throws IOException {
if (path == null) {
throw new IllegalArgumentException("File cannot be null");
}
@ -94,11 +114,20 @@ public class CustomPDFDocumentFactory {
long fileSize = Files.size(path);
log.debug("Loading PDF from file, size: {}MB", fileSize / (1024 * 1024));
return loadAdaptively(path.toFile(), fileSize);
PDDocument doc = loadAdaptively(path.toFile(), fileSize);
if (!readOnly) {
postProcessDocument(doc);
}
return doc;
}
/** Load a PDF from byte array with automatic optimization. */
public PDDocument load(byte[] input) throws IOException {
return load(input, false);
}
/** Load a PDF from byte array with automatic optimization and read-only option. */
public PDDocument load(byte[] input, boolean readOnly) throws IOException {
if (input == null) {
throw new IllegalArgumentException("Input bytes cannot be null");
}
@ -106,11 +135,20 @@ public class CustomPDFDocumentFactory {
long dataSize = input.length;
log.debug("Loading PDF from byte array, size: {}MB", dataSize / (1024 * 1024));
return loadAdaptively(input, dataSize);
PDDocument doc = loadAdaptively(input, dataSize);
if (!readOnly) {
postProcessDocument(doc);
}
return doc;
}
/** Load a PDF from InputStream with automatic optimization. */
public PDDocument load(InputStream input) throws IOException {
return load(input, false);
}
/** Load a PDF from InputStream with automatic optimization and read-only option. */
public PDDocument load(InputStream input, boolean readOnly) throws IOException {
if (input == null) {
throw new IllegalArgumentException("InputStream cannot be null");
}
@ -119,11 +157,20 @@ public class CustomPDFDocumentFactory {
Path tempFile = createTempFile("pdf-stream-");
Files.copy(input, tempFile, StandardCopyOption.REPLACE_EXISTING);
return loadAdaptively(tempFile.toFile(), Files.size(tempFile));
PDDocument doc = loadAdaptively(tempFile.toFile(), Files.size(tempFile));
if (!readOnly) {
postProcessDocument(doc);
}
return doc;
}
/** Load with password from InputStream */
public PDDocument load(InputStream input, String password) throws IOException {
return load(input, password, false);
}
/** Load with password from InputStream and read-only option */
public PDDocument load(InputStream input, String password, boolean readOnly) throws IOException {
if (input == null) {
throw new IllegalArgumentException("InputStream cannot be null");
}
@ -132,14 +179,59 @@ public class CustomPDFDocumentFactory {
Path tempFile = createTempFile("pdf-stream-");
Files.copy(input, tempFile, StandardCopyOption.REPLACE_EXISTING);
return loadAdaptivelyWithPassword(tempFile.toFile(), Files.size(tempFile), password);
PDDocument doc = loadAdaptivelyWithPassword(tempFile.toFile(), Files.size(tempFile), password);
if (!readOnly) {
postProcessDocument(doc);
}
return doc;
}
/** Load from a file path string */
public PDDocument load(String path) throws IOException {
return load(path, false);
}
/** Load from a file path string with read-only option */
public PDDocument load(String path, boolean readOnly) throws IOException {
return load(new File(path), readOnly);
}
/** Load from a PDFFile object */
public PDDocument load(PDFFile pdfFile) throws IOException {
return load(pdfFile, false);
}
/** Load from a PDFFile object with read-only option */
public PDDocument load(PDFFile pdfFile, boolean readOnly) throws IOException {
return load(pdfFile.getFileInput(), readOnly);
}
/** Load from a MultipartFile */
public PDDocument load(MultipartFile pdfFile) throws IOException {
return load(pdfFile, false);
}
/** Load from a MultipartFile with read-only option */
public PDDocument load(MultipartFile pdfFile, boolean readOnly) throws IOException {
return load(pdfFile.getInputStream(), readOnly);
}
/** Load with password from MultipartFile */
public PDDocument load(MultipartFile fileInput, String password) throws IOException {
return load(fileInput, password, false);
}
/** Load with password from MultipartFile with read-only option */
public PDDocument load(MultipartFile fileInput, String password, boolean readOnly) throws IOException {
return load(fileInput.getInputStream(), password, readOnly);
}
/**
* Determine the appropriate caching strategy based on file size and available memory. This
* common method is used by both password and non-password loading paths.
*/
private StreamCacheCreateFunction getStreamCacheFunction(long contentSize) {
public StreamCacheCreateFunction getStreamCacheFunction(long contentSize) {
long maxMemory = Runtime.getRuntime().maxMemory();
long freeMemory = Runtime.getRuntime().freeMemory();
long totalMemory = Runtime.getRuntime().totalMemory();
@ -197,8 +289,6 @@ public class CustomPDFDocumentFactory {
} else {
throw new IllegalArgumentException("Unsupported source type: " + source.getClass());
}
postProcessDocument(document);
return document;
}
@ -220,8 +310,6 @@ public class CustomPDFDocumentFactory {
} else {
throw new IllegalArgumentException("Unsupported source type: " + source.getClass());
}
postProcessDocument(document);
return document;
}
@ -384,23 +472,4 @@ public class CustomPDFDocumentFactory {
}
}
/** Load from a file path string */
public PDDocument load(String path) throws IOException {
return load(new File(path));
}
/** Load from a PDFFile object */
public PDDocument load(PDFFile pdfFile) throws IOException {
return load(pdfFile.getFileInput());
}
/** Load from a MultipartFile */
public PDDocument load(MultipartFile pdfFile) throws IOException {
return load(pdfFile.getInputStream());
}
/** Load with password from MultipartFile */
public PDDocument load(MultipartFile fileInput, String password) throws IOException {
return load(fileInput.getInputStream(), password);
}
}

View File

@ -64,10 +64,19 @@ public class PdfMetadataService {
String creator = stirlingPDFLabel;
if (applicationProperties.getEnterpriseEdition().getCustomMetadata().isAutoUpdateMetadata()
if (applicationProperties
.getPremium()
.getProFeatures()
.getCustomMetadata()
.isAutoUpdateMetadata()
&& runningEE) {
creator = applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator();
creator =
applicationProperties
.getPremium()
.getProFeatures()
.getCustomMetadata()
.getCreator();
pdf.getDocumentInformation().setProducer(stirlingPDFLabel);
}
@ -84,9 +93,18 @@ public class PdfMetadataService {
pdf.getDocumentInformation().setModificationDate(Calendar.getInstance());
String author = pdfMetadata.getAuthor();
if (applicationProperties.getEnterpriseEdition().getCustomMetadata().isAutoUpdateMetadata()
if (applicationProperties
.getPremium()
.getProFeatures()
.getCustomMetadata()
.isAutoUpdateMetadata()
&& runningEE) {
author = applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor();
author =
applicationProperties
.getPremium()
.getProFeatures()
.getCustomMetadata()
.getAuthor();
if (userService != null) {
author = author.replace("username", userService.getCurrentUsername());

View File

@ -334,27 +334,40 @@ public class PostHogService {
addIfNotEmpty(
properties,
"enterpriseEdition_enabled",
applicationProperties.getEnterpriseEdition().isEnabled());
if (applicationProperties.getEnterpriseEdition().isEnabled()) {
applicationProperties.getPremium().isEnabled());
if (applicationProperties.getPremium().isEnabled()) {
addIfNotEmpty(
properties,
"enterpriseEdition_customMetadata_autoUpdateMetadata",
applicationProperties
.getEnterpriseEdition()
.getPremium()
.getProFeatures()
.getCustomMetadata()
.isAutoUpdateMetadata());
addIfNotEmpty(
properties,
"enterpriseEdition_customMetadata_author",
applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor());
applicationProperties
.getPremium()
.getProFeatures()
.getCustomMetadata()
.getAuthor());
addIfNotEmpty(
properties,
"enterpriseEdition_customMetadata_creator",
applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator());
applicationProperties
.getPremium()
.getProFeatures()
.getCustomMetadata()
.getCreator());
addIfNotEmpty(
properties,
"enterpriseEdition_customMetadata_producer",
applicationProperties.getEnterpriseEdition().getCustomMetadata().getProducer());
applicationProperties
.getPremium()
.getProFeatures()
.getCustomMetadata()
.getProducer());
}
// Capture AutoPipeline properties
addIfNotEmpty(

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=المستخدمين النشطين:
adminUserSettings.disabledUsers=المستخدمين المعطلين:
adminUserSettings.totalUsers=إجمالي المستخدمين:
adminUserSettings.lastRequest=آخر طلب
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=استيراد/تصدير قاعدة البيانات
database.header=استيراد/تصدير قاعدة البيانات
@ -709,9 +733,10 @@ sanitizePDF.title=تنظيف PDF
sanitizePDF.header=تنظيف ملف PDF
sanitizePDF.selectText.1=إزالة إجراءات جافا سكريبت
sanitizePDF.selectText.2=إزالة الملفات المضمنة
sanitizePDF.selectText.3=إزالة البيانات الوصفية
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=إزالة الروابط
sanitizePDF.selectText.5=إزالة الخطوط
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=تنظيف PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Aktiv İstifadəçilər:
adminUserSettings.disabledUsers=Deaktiv İstifadəçilər:
adminUserSettings.totalUsers=Ümumi İstifadəçilər:
adminUserSettings.lastRequest=Son sorğu
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Verilənlər bazasını Daxil/Xaric Et
database.header=Verilənlər bazasını Daxil/Xaric Et
@ -709,9 +733,10 @@ sanitizePDF.title=PDF-i Təmizlə
sanitizePDF.header=PDF Faylını Təmizlə
sanitizePDF.selectText.1=JavaScript Fəaliyyətlərini Sil
sanitizePDF.selectText.2=Daxil Edilmiş Faylları Sil
sanitizePDF.selectText.3=Metadatanı Sil
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Linkləri Sil
sanitizePDF.selectText.5=Şriftləri Sil
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=PDF-i Təmizlə

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Активни потребители:
adminUserSettings.disabledUsers=Деактивирани потребители:
adminUserSettings.totalUsers=Общо потребители:
adminUserSettings.lastRequest=Последна заявка
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Импорт/Експорт на база данни
database.header=Импорт/Експорт на база данни
@ -709,9 +733,10 @@ sanitizePDF.title=Дезинфектирай PDF
sanitizePDF.header=Дезинфектира PDF файл
sanitizePDF.selectText.1=Премахва JavaScript действия
sanitizePDF.selectText.2=Премахва вградени файлове
sanitizePDF.selectText.3=Премахва метаданни
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Премахва линкове
sanitizePDF.selectText.5=Премахва шрифтове
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Дезинфектирай PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Usuaris Actius:
adminUserSettings.disabledUsers=Usuaris Deshabilitats:
adminUserSettings.totalUsers=Total d'Usuaris:
adminUserSettings.lastRequest=Darrera Sol·licitud
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Importació/Exportació de Base de Dades
database.header=Importació/Exportació de Base de Dades
@ -709,9 +733,10 @@ sanitizePDF.title=Neteja PDF
sanitizePDF.header=Neteja un fitxer PDF
sanitizePDF.selectText.1=Elimina accions JavaScript
sanitizePDF.selectText.2=Elimina fitxers incrustats
sanitizePDF.selectText.3=Elimina metadades
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Elimina enllaços
sanitizePDF.selectText.5=Elimina fonts
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Neteja PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Aktivní uživatelé:
adminUserSettings.disabledUsers=Deaktivovaní uživatelé:
adminUserSettings.totalUsers=Celkem uživatelů:
adminUserSettings.lastRequest=Poslední požadavek
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Import/Export databáze
database.header=Import/Export databáze
@ -709,9 +733,10 @@ sanitizePDF.title=Sanitizovat PDF
sanitizePDF.header=Sanitizovat PDF soubor
sanitizePDF.selectText.1=Odstranit JavaScript akce
sanitizePDF.selectText.2=Odstranit vložené soubory
sanitizePDF.selectText.3=Odstranit metadata
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Odstranit odkazy
sanitizePDF.selectText.5=Odstranit písma
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Sanitizovat PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Aktive Brugere:
adminUserSettings.disabledUsers=Deaktiverede Brugere:
adminUserSettings.totalUsers=Samlet Antal Brugere:
adminUserSettings.lastRequest=Seneste Anmodning
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Database Import/Eksport
database.header=Database Import/Eksport
@ -709,9 +733,10 @@ sanitizePDF.title=Rens PDF
sanitizePDF.header=Rens en PDF-fil
sanitizePDF.selectText.1=Fjern JavaScript-handlinger
sanitizePDF.selectText.2=Fjern indlejrede filer
sanitizePDF.selectText.3=Fjern metadata
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Fjern links
sanitizePDF.selectText.5=Fjern skrifttyper
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Rens PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Aktive Benutzer:
adminUserSettings.disabledUsers=Deaktivierte Benutzer:
adminUserSettings.totalUsers=Gesamtzahl der Benutzer:
adminUserSettings.lastRequest=Letzte Anfrage
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Datenbank Import/Export
database.header=Datenbank Import/Export
@ -709,9 +733,10 @@ sanitizePDF.title=PDF Bereinigen
sanitizePDF.header=PDF Bereinigen
sanitizePDF.selectText.1=Javascript-Aktionen entfernen
sanitizePDF.selectText.2=Eingebettete Dateien entfernen
sanitizePDF.selectText.3=Metadaten entfernen
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Links entfernen
sanitizePDF.selectText.5=Schriftarten entfernen
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Bereinigen

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Ενεργοί χρήστες:
adminUserSettings.disabledUsers=Απενεργοποιημένοι χρήστες:
adminUserSettings.totalUsers=Συνολικοί χρήστες:
adminUserSettings.lastRequest=Τελευταίο αίτημα
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Εισαγωγή/Εξαγωγή βάσης δεδομένων
database.header=Εισαγωγή/Εξαγωγή βάσης δεδομένων
@ -709,9 +733,10 @@ sanitizePDF.title=Εξυγίανση PDF
sanitizePDF.header=Εξυγίανση αρχείου PDF
sanitizePDF.selectText.1=Αφαίρεση ενεργειών JavaScript
sanitizePDF.selectText.2=Αφαίρεση ενσωματωμένων αρχείων
sanitizePDF.selectText.3=Αφαίρεση μεταδεδομένων
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Αφαίρεση συνδέσμων
sanitizePDF.selectText.5=Αφαίρεση γραμματοσειρών
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Εξυγίανση PDF

View File

@ -231,33 +231,7 @@ adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
adminUserSettings.userSessions=User sessions
adminUserSettings.totalSessions=Total Sessions:
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Database Import/Export
database.header=Database Import/Export

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Database Import/Export
database.header=Database Import/Export
@ -709,9 +733,10 @@ sanitizePDF.title=Sanitize PDF
sanitizePDF.header=Sanitize a PDF file
sanitizePDF.selectText.1=Remove JavaScript actions
sanitizePDF.selectText.2=Remove embedded files
sanitizePDF.selectText.3=Remove metadata
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Remove links
sanitizePDF.selectText.5=Remove fonts
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Sanitize PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Usuarios Activos:
adminUserSettings.disabledUsers=Usuarios deshabilitados:
adminUserSettings.totalUsers=Usuarios totales:
adminUserSettings.lastRequest=Última petición
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Base de Datos Importar/Exportar
database.header=Base de Datos Importar/Exportar
@ -709,9 +733,10 @@ sanitizePDF.title=Limpiar archivo PDF
sanitizePDF.header=Limpiar un archivo PDF
sanitizePDF.selectText.1=Eliminar código JavaScript
sanitizePDF.selectText.2=Eliminar archivos incrustados
sanitizePDF.selectText.3=Eliminar metadatos
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Eliminar enlaces
sanitizePDF.selectText.5=Eliminar fuentes
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Limpiar PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Database Import/Export
database.header=Database Import/Export
@ -709,9 +733,10 @@ sanitizePDF.title=PDF-a desinfektatu
sanitizePDF.header=PDF fitxategi bat desinfektatu
sanitizePDF.selectText.1=Ezabatu JavaScript akzioak
sanitizePDF.selectText.2=Ezabatu embedded fitxategiak
sanitizePDF.selectText.3=Ezabatu metadata
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Ezabatu esketak
sanitizePDF.selectText.5=Ezabatu iturri letrak
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Desinfektatu PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=کاربران فعال:
adminUserSettings.disabledUsers=کاربران غیرفعال:
adminUserSettings.totalUsers=کل کاربران:
adminUserSettings.lastRequest=آخرین درخواست
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=وارد کردن/صادر کردن پایگاه داده
database.header=وارد کردن/صادر کردن پایگاه داده
@ -709,9 +733,10 @@ sanitizePDF.title=پاکسازی PDF
sanitizePDF.header=پاکسازی یک فایل PDF
sanitizePDF.selectText.1=حذف عملیات جاوااسکریپت
sanitizePDF.selectText.2=حذف فایل‌های جاسازی شده
sanitizePDF.selectText.3=حذف متاداده‌ها
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=حذف لینک‌ها
sanitizePDF.selectText.5=حذف فونت‌ها
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=پاکسازی PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Utilisateurs actifs :
adminUserSettings.disabledUsers=Utilisateurs désactivés :
adminUserSettings.totalUsers=Utilisateurs au total :
adminUserSettings.lastRequest=Dernière requête
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Import/Export de la Base de Données
database.header=Import/Export de la Base de Données
@ -709,9 +733,10 @@ sanitizePDF.title=Assainir
sanitizePDF.header=Assainir
sanitizePDF.selectText.1=Supprimer les actions JavaScript
sanitizePDF.selectText.2=Supprimer les fichiers intégrés
sanitizePDF.selectText.3=Supprimer les métadonnées
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Supprimer les liens
sanitizePDF.selectText.5=Supprimer les polices
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Assainir

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Úsáideoirí Gníomhacha:
adminUserSettings.disabledUsers=Úsáideoirí faoi mhíchumas:
adminUserSettings.totalUsers=Úsáideoirí Iomlán:
adminUserSettings.lastRequest=Iarratas Deiridh
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Iompórtáil/Easpórtáil Bunachar Sonraí
database.header=Iompórtáil/Easpórtáil Bunachar Sonraí
@ -709,9 +733,10 @@ sanitizePDF.title=PDF sláintíocht
sanitizePDF.header=Glanadh comhad PDF
sanitizePDF.selectText.1=Bain gníomhartha JavaScript
sanitizePDF.selectText.2=Bain comhaid leabaithe
sanitizePDF.selectText.3=Bain meiteashonraí
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Bain naisc
sanitizePDF.selectText.5=Bain clónna
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=PDF sláintíocht

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=सक्रिय उपयोगकर्ता:
adminUserSettings.disabledUsers=अक्षम उपयोगकर्ता:
adminUserSettings.totalUsers=कुल उपयोगकर्ता:
adminUserSettings.lastRequest=अंतिम अनुरोध
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=डेटाबेस आयात/निर्यात
database.header=डेटाबेस आयात/निर्यात
@ -709,9 +733,10 @@ sanitizePDF.title=PDF सैनिटाइज़ करें
sanitizePDF.header=PDF फ़ाइल सैनिटाइज़ करें
sanitizePDF.selectText.1=जावास्क्रिप्ट क्रियाएं हटाएं
sanitizePDF.selectText.2=एम्बेडेड फ़ाइलें हटाएं
sanitizePDF.selectText.3=मेटाडेटा हटाएं
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=लिंक हटाएं
sanitizePDF.selectText.5=फ़ॉन्ट्स हटाएं
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=PDF सैनिटाइज़ करें

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Aktivni korisnici:
adminUserSettings.disabledUsers=Isključeni korisnici:
adminUserSettings.totalUsers=Ukupan broj korisnika:
adminUserSettings.lastRequest=Zadnji zahtjev
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Database Import/Export
database.header=Database Import/Export
@ -709,9 +733,10 @@ sanitizePDF.title=Sanirajte PDF
sanitizePDF.header=Sanirajte PDF datoteku
sanitizePDF.selectText.1=Ukloni JavaScript akcije
sanitizePDF.selectText.2=Ukloni ugrađene datoteke
sanitizePDF.selectText.3=Ukloni metapodatke
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Ukloni poveznice
sanitizePDF.selectText.5=Uklonite fontove
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Sanirajte PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Aktív felhasználók:
adminUserSettings.disabledUsers=Letiltott felhasználók:
adminUserSettings.totalUsers=Összes felhasználó:
adminUserSettings.lastRequest=Utolsó kérés
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Adatbázis importálás/exportálás
database.header=Adatbázis importálás/exportálás
@ -709,9 +733,10 @@ sanitizePDF.title=PDF tisztítása
sanitizePDF.header=PDF fájl tisztítása
sanitizePDF.selectText.1=JavaScript műveletek eltávolítása
sanitizePDF.selectText.2=Beágyazott fájlok eltávolítása
sanitizePDF.selectText.3=Metaadatok eltávolítása
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Hivatkozások eltávolítása
sanitizePDF.selectText.5=Betűtípusok eltávolítása
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=PDF tisztítása

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Pengguna Aktif:
adminUserSettings.disabledUsers=Pengguna Dinonaktifkan:
adminUserSettings.totalUsers=Total Pengguna:
adminUserSettings.lastRequest=Permintaan Terakhir
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Impor/Ekspor Database
database.header=Impor/Ekspor Database
@ -709,9 +733,10 @@ sanitizePDF.title=Bersihkan PDF
sanitizePDF.header=Membersihkan berkas PDF
sanitizePDF.selectText.1=Hapus tindakan JavaScript
sanitizePDF.selectText.2=Hapus berkas yang disematkan
sanitizePDF.selectText.3=Hapus metadata
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Hapus tautan
sanitizePDF.selectText.5=Hapus font
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Membersihkan PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Utenti attivi:
adminUserSettings.disabledUsers=Utenti disabili:
adminUserSettings.totalUsers=Utenti totali:
adminUserSettings.lastRequest=Ultima richiesta
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Importazione/Esportazione database
database.header=Importazione/esportazione database
@ -709,9 +733,10 @@ sanitizePDF.title=Pulire PDF
sanitizePDF.header=Pulisci un file PDF
sanitizePDF.selectText.1=Rimuovi le azioni JavaScript
sanitizePDF.selectText.2=Rimuovi i file incorporati
sanitizePDF.selectText.3=Rimuovi i metadati
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Rimuovi collegamenti
sanitizePDF.selectText.5=Rimuovi i font
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Pulisci PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=アクティブユーザー:
adminUserSettings.disabledUsers=無効なユーザー:
adminUserSettings.totalUsers=ユーザー合計:
adminUserSettings.lastRequest=最後のリクエスト
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=データベースのインポート/エクスポート
database.header=データベースのインポート/エクスポート
@ -709,9 +733,10 @@ sanitizePDF.title=PDFをサニタイズ
sanitizePDF.header=PDFファイルをサニタイズ
sanitizePDF.selectText.1=JavaScriptアクションを削除
sanitizePDF.selectText.2=埋め込みファイルを削除
sanitizePDF.selectText.3=メタデータを削除
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=リンクを削除
sanitizePDF.selectText.5=フォントを削除
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=PDFをサニタイズする

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=활성 사용자:
adminUserSettings.disabledUsers=비활성화된 사용자:
adminUserSettings.totalUsers=전체 사용자:
adminUserSettings.lastRequest=마지막 요청
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=데이터베이스 가져오기/내보내기
database.header=데이터베이스 가져오기/내보내기
@ -709,9 +733,10 @@ sanitizePDF.title=PDF 정리
sanitizePDF.header=PDF 파일 정리
sanitizePDF.selectText.1=JavaScript 작업 제거
sanitizePDF.selectText.2=임베디드 파일 제거
sanitizePDF.selectText.3=메타데이터 제거
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=링크 제거
sanitizePDF.selectText.5=글꼴 제거
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=PDF 정리

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Laatste aanvraag
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Database Importeer/Exporteer
database.header=Database Importeer/Exporteer
@ -709,9 +733,10 @@ sanitizePDF.title=PDF opschonen
sanitizePDF.header=Een PDF-bestand opschonen
sanitizePDF.selectText.1=Verwijder Javascript-acties
sanitizePDF.selectText.2=Verwijder ingebedde bestanden
sanitizePDF.selectText.3=Verwijder metadata
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Verwijder links
sanitizePDF.selectText.5=Verwijder lettertypen
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=PDF opschonen

View File

@ -3,8 +3,8 @@
###########
# the direction that the language is written (ltr = left to right, rtl = right to left)
language.direction=ltr
addPageNumbers.fontSize=Font Size
addPageNumbers.fontName=Font Name
addPageNumbers.fontSize=Skriftstørrelse
addPageNumbers.fontName=Skrifttype
pdfPrompt=Velg PDF(er)
multiPdfPrompt=Velg PDF-filer (2+)
multiPdfDropPrompt=Velg (eller dra og slipp) alle PDF-ene du trenger
@ -29,7 +29,7 @@ downloadPdf=Last ned PDF
text=Tekst
font=Skrifttype
selectFillter=-- Velg --
pageNum=Sidnummer
pageNum=Sidenummer
sizes.small=Liten
sizes.medium=Middels
sizes.large=Stor
@ -56,12 +56,12 @@ userNotFoundMessage=Bruker ikke funnet.
incorrectPasswordMessage=Nåværende passord er feil.
usernameExistsMessage=Det nye brukernavnet eksisterer allerede.
invalidUsernameMessage=Ugyldig brukernavn, brukernavnet kan bare inneholde bokstaver, tall og følgende spesialtegn @._+- eller må være en gyldig e-postadresse.
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
invalidPasswordMessage=Passordet kan ikke være tomt og må ikke ha mellomrom i begynnelsen eller slutten.
confirmPasswordErrorMessage=Nytt passord og Bekreft nytt passord må være like.
deleteCurrentUserMessage=Kan ikke slette den innloggede brukeren.
deleteUsernameExistsMessage=Brukernavnet eksisterer ikke og kan ikke slettes.
downgradeCurrentUserMessage=Kan ikke nedgradere den innloggede brukerens rolle.
disabledCurrentUserMessage=The current user cannot be disabled
disabledCurrentUserMessage=Den pålogga brukeren kan ikke deaktiveres.
downgradeCurrentUserLongMessage=Kan ikke nedgradere den innloggede brukerens rolle. Derfor vil ikke den innloggede brukeren bli vist.
userAlreadyExistsOAuthMessage=Brukeren eksisterer allerede som en OAuth2-bruker.
userAlreadyExistsWebMessage=Brukeren eksisterer allerede som en web-bruker.
@ -77,18 +77,18 @@ color=Farge
sponsor=Sponsor
info=Info
pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
page=Side
pages=Sider
loading=Laster...
addToDoc=Legg til i dokument
reset=Reset
apply=Apply
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
legal.accessibility=Accessibility
legal.cookie=Cookie Policy
legal.impressum=Impressum
legal.privacy=Personvernerklæring
legal.terms=Vilkår og betingelser
legal.accessibility=Tilgjengelighet
legal.cookie=Informasjonskapsler
legal.impressum=Juridisk informasjon
###############
# Pipeline #
@ -100,7 +100,7 @@ pipeline.defaultOption=Tilpasset
pipeline.submitButton=Send inn
pipeline.help=Pipeline hjelp
pipeline.scanHelp=Mappe skanning hjelp
pipeline.deletePrompt=Are you sure you want to delete pipeline
pipeline.deletePrompt=Er du sikker på at du vil slette denne pipelinen?
######################
# Pipeline Options #
@ -118,21 +118,21 @@ pipelineOptions.validateButton=Valider
########################
# ENTERPRISE EDITION #
########################
enterpriseEdition.button=Upgrade to Pro
enterpriseEdition.warning=This feature is only available to Pro users.
enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features.
enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro
enterpriseEdition.button=Oppgrader til Pro
enterpriseEdition.warning=Denne funksjonen er kun tilgjengelig for Pro-brukere.
enterpriseEdition.yamlAdvert=Stirling PDF Pro støtter YAML-konfigurasjons filer og andre SSO funksjoner.
enterpriseEdition.ssoAdvert=Søker du etter flere administrerings funksjoner? Sjekk ut Stirling PDF Pro
#################
# Analytics #
#################
analytics.title=Do you want make Stirling PDF better?
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.paragraph2=Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better.
analytics.enable=Enable analytics
analytics.disable=Disable analytics
analytics.settings=You can change the settings for analytics in the config/settings.yml file
analytics.title=Vill du gjøre Stirling PDF bedre?
analytics.paragraph1=Stirling PDF har valgfri analyse for å hjelpe oss med å forbedre produktet. Vi sporer ikke personlig informasjon eller filinnhold.
analytics.paragraph2=Vennligst vurder å aktivere analyse for å hjelpe Stirling-PDF å vokse og for å la oss forstå brukerne våre bedre.
analytics.enable=Aktiver analyse
analytics.disable=Deaktiver analyse
analytics.settings=Du kan endre innstillingene for analyse i config/settings.yml filen
#############
# NAVBAR #
@ -144,14 +144,14 @@ navbar.language=Språk
navbar.settings=Innstillinger
navbar.allTools=Verktøy
navbar.multiTool=Multi Verktøy
navbar.search=Search
navbar.search=Søk
navbar.sections.organize=Organisere
navbar.sections.convertTo=Konverter til PDF
navbar.sections.convertFrom=Konverter fra PDF
navbar.sections.security=Signer & Sikkerhet
navbar.sections.advance=Avansert
navbar.sections.edit=Vis & Rediger
navbar.sections.popular=Popular
navbar.sections.popular=Populært
#############
# SETTINGS #
@ -210,7 +210,7 @@ adminUserSettings.user=Bruker
adminUserSettings.addUser=Legg til Ny Bruker
adminUserSettings.deleteUser=Slett Bruker
adminUserSettings.confirmDeleteUser=Skal brukeren slettes?
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.confirmChangeUserStatus=Skal brukeren deaktiveres/aktiveres?
adminUserSettings.usernameInfo=Brukernavn kan bare inneholde bokstaver, tall og følgende spesialtegn @._+- eller må være en gyldig e-postadresse.
adminUserSettings.roles=Roller
adminUserSettings.role=Rolle
@ -224,14 +224,38 @@ adminUserSettings.forceChange=Tving bruker til å endre passord ved innlogging
adminUserSettings.submit=Lagre Bruker
adminUserSettings.changeUserRole=Endre Brukerens Rolle
adminUserSettings.authenticated=Autentisert
adminUserSettings.editOwnProfil=Edit own profile
adminUserSettings.enabledUser=enabled user
adminUserSettings.disabledUser=disabled user
adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
adminUserSettings.editOwnProfil=Rediger din profil
adminUserSettings.enabledUser=aktivert bruker
adminUserSettings.disabledUser=deaktivert bruker
adminUserSettings.activeUsers=Aktive brukere:
adminUserSettings.disabledUsers=Deaktiverte brukere:
adminUserSettings.totalUsers=Totalt antall brukere:
adminUserSettings.lastRequest=Siste spørring
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Database Import/Eksport
database.header=Database Import/Eksport
@ -240,20 +264,20 @@ database.creationDate=Opprettelsesdato
database.fileSize=Filstørrelse
database.deleteBackupFile=Slett sikkerhetskopifil
database.importBackupFile=Importer sikkerhetskopifil
database.createBackupFile=Create Backup File
database.createBackupFile=Lag sikkerhetskopifil
database.downloadBackupFile=Last ned sikkerhetskopifil
database.info_1=Når du importerer data, er det avgjørende å sikre riktig struktur. Hvis du er usikker på hva du gjør, bør du søke råd og støtte fra en profesjonell. En feil i strukturen kan føre til applikasjonsfeil, inkludert fullstendig manglende evne til å kjøre applikasjonen.
database.info_2=Filnavnet spiller ingen rolle ved opplasting. Det vil bli omdøpt etterpå for å følge formatet backup_user_yyyyMMddHHmm.sql, for å sikre en konsekvent navnekonvensjon.
database.submit=Importer sikkerhetskopi
database.importIntoDatabaseSuccessed=Import til database vellykket
database.backupCreated=Database backup successful
database.backupCreated=Sikkerhetskopiering opprettet
database.fileNotFound=Fil ikke funnet
database.fileNullOrEmpty=Fil må ikke være tom eller null
database.failedImportFile=Import av fil mislyktes
database.notSupported=This function is not available for your database connection.
database.notSupported=Denne funksjonen er ikke tilgjengelig for din databasetilkobling.
session.expired=Your session has expired. Please refresh the page and try again.
session.refreshPage=Refresh Page
session.expired=Økten din har utløpt. Vennligst oppdater siden og prøv igjen.
session.refreshPage=Oppdater Side
#############
# HOME-PAGE #
@ -266,14 +290,14 @@ home.viewPdf.title=View/Edit PDF
home.viewPdf.desc=Vis, annoter, legg til tekst eller bilder
viewPdf.tags=vis,les,annoter,tekst,bilde
home.setFavorites=Set Favourites
home.hideFavorites=Hide Favourites
home.showFavorites=Show Favourites
home.legacyHomepage=Old homepage
home.newHomePage=Try our new homepage!
home.alphabetical=Alphabetical
home.globalPopularity=Global Popularity
home.sortBy=Sort by:
home.setFavorites=Angi Favoritter
home.hideFavorites=Skjul Favoritter
home.showFavorites=Vis Favoritter
home.legacyHomepage=Gammel hjemmeside
home.newHomePage=Prøv vår nye hjemmeside!
home.alphabetical=Alfabetisk
home.globalPopularity=Global Popularitet
home.sortBy=Sorter etter:
home.multiTool.title=PDF Multi Verktøy
home.multiTool.desc=Slå sammen, roter, omorganiser og fjern sider
@ -489,9 +513,9 @@ home.autoRedact.title=Automatisk Sensurering
home.autoRedact.desc=Automatisk sensurering (sverter ut) tekst i en PDF basert på inntastet tekst
autoRedact.tags=Sensurere,Skjule,sverte ut,svart,markør,skjult
home.redact.title=Manual Redaction
home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s)
redact.tags=Redact,Hide,black out,black,marker,hidden,manual
home.redact.title=Manuell Sensurering
home.redact.desc=Sensurerer en PDF basert på valgt tekst, tegnede former og/eller valgte side(r)
redact.tags=Sensurere,Skjule,sverte ut,svart,markør,skjult,manuell
home.tableExtraxt.title=PDF til CSV
home.tableExtraxt.desc=Ekstraherer tabeller fra en PDF og konverterer dem til CSV
@ -516,37 +540,37 @@ home.AddStampRequest.desc=Legg til tekst eller bilde stempler på angitte steder
AddStampRequest.tags=stempel,legg til bilde,senter bilde,vannmerke,PDF,embed,tilpass
home.removeImagePdf.title=Remove image
home.removeImagePdf.desc=Remove image from PDF to reduce file size
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
home.removeImagePdf.title=Fjern bilde
home.removeImagePdf.desc=Fjern bilde fra PDF for å redusere filstørrelsen
removeImagePdf.tags=Fjern Bilde,Sideoperasjoner,Backend,serverside
home.splitPdfByChapters.title=Split PDF by Chapters
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
splitPdfByChapters.tags=split,chapters,bookmarks,organize
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
home.validateSignature.title=Valider PDF-signatur
home.validateSignature.desc=Verifiser digitale signaturer og sertifikater i PDF-dokumenter
validateSignature.tags=signatur,verifiser,valider,pdf,sertifikat,digital signatur,Valider signatur,Valider sertifikat
#replace-invert-color
replace-color.title=Replace-Invert-Color
replace-color.header=Replace-Invert Color PDF
home.replaceColorPdf.title=Replace and Invert Color
home.replaceColorPdf.desc=Replace color for text and background in PDF and invert full color of pdf to reduce file size
replaceColorPdf.tags=Replace Color,Page operations,Back end,server side
replace-color.selectText.1=Replace or Invert color Options
replace-color.selectText.2=Default(Default high contrast colors)
replace-color.selectText.3=Custom(Customized colors)
replace-color.selectText.4=Full-Invert(Invert all colors)
replace-color.selectText.5=High contrast color options
replace-color.selectText.6=white text on black background
replace-color.selectText.7=Black text on white background
replace-color.selectText.8=Yellow text on black background
replace-color.selectText.9=Green text on black background
replace-color.selectText.10=Choose text Color
replace-color.selectText.11=Choose background Color
replace-color.submit=Replace
replace-color.title=Erstatt-Inverter-Farge
replace-color.header=Erstatt-Inverter Farge PDF
home.replaceColorPdf.title=Erstatt og Inverter Farge
home.replaceColorPdf.desc=Erstatt farge for tekst og bakgrunn i PDF og inverter full farge av pdf for å redusere filstørrelsen
replaceColorPdf.tags=Erstatt Farge,Sideoperasjoner,Backend,serverside
replace-color.selectText.1=Erstatt eller Inverter farge alternativer
replace-color.selectText.2=Standard(Standard høy kontrast farger)
replace-color.selectText.3=Tilpasset(Tilpassede farger)
replace-color.selectText.4=Full-Invertering(Inverter alle farger)
replace-color.selectText.5=Høy kontrast fargealternativer
replace-color.selectText.6=hvit tekst på svart bakgrunn
replace-color.selectText.7=Svart tekst på hvit bakgrunn
replace-color.selectText.8=Gul tekst på svart bakgrunn
replace-color.selectText.9=Grønn tekst på svart bakgrunn
replace-color.selectText.10=Velg tekstfarge
replace-color.selectText.11=Velg bakgrunnsfarge
replace-color.submit=Erstatt
@ -565,18 +589,18 @@ login.locked=Kontoen din har blitt låst.
login.signinTitle=Vennligst logg inn
login.ssoSignIn=Logg inn via Enkel Pålogging
login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Opretting av bruker deaktivert
login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oAuth2AdminBlockedUser=Registrering eller pålogging for ikke-registrerte brukere er for øyeblikket blokkert. Vennligst kontakt administrator
login.oauth2RequestNotFound=Autentiseringsforespørsel ikke funnet
login.oauth2InvalidUserInfoResponse=Ugyldig brukerinforespons
login.oauth2invalidRequest=Ugyldig forespørsel
login.oauth2AccessDenied=Tilgang nektet
login.oauth2InvalidTokenResponse=Ugyldig tokenrespons
login.oauth2InvalidIdToken=Ugyldig Id Token
login.relyingPartyRegistrationNotFound=No relying party registration found
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.relyingPartyRegistrationNotFound=Ingen konfigurasjon funnet for Relying Party"
login.userIsDisabled=Bruker er deaktivert, innlogging er for øyeblikket blokkert med dette brukernavnet. Vennligst kontakt administrator
login.alreadyLoggedIn=Du er allerede innlogget på
login.alreadyLoggedIn2=enheter. Logg ut og forsøk igjen
login.toManySessions=Du har for mange aktive økter
#auto-redact
autoRedact.title=Automatisk Sensurering
@ -591,31 +615,31 @@ autoRedact.convertPDFToImageLabel=Konverter PDF til PDF-bilde (Brukes for å fje
autoRedact.submitButton=Send inn
#redact
redact.title=Manual Redaction
redact.header=Manual Redaction
redact.submit=Redact
redact.textBasedRedaction=Text based Redaction
redact.pageBasedRedaction=Page-based Redaction
redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
redact.pageRedactionNumbers.title=Pages
redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
redact.redactionColor.title=Redaction Color
redact.export=Export
redact.upload=Upload
redact.boxRedaction=Box draw redaction
redact.title=Manuell Sensurering
redact.header=Manuell Sensurering
redact.submit=Sensurer
redact.textBasedRedaction=Tekstbasert sensurering
redact.pageBasedRedaction=Sidebasert sensurering
redact.convertPDFToImageLabel=Konverter PDF til PDF-bilde (Brukes for å fjerne tekst bak boksen)
redact.pageRedactionNumbers.title=Sider
redact.pageRedactionNumbers.placeholder=(f.eks. 1,2,8 eller 4,7,12-16 eller 2n-1)
redact.redactionColor.title=Sensureringsfarge
redact.export=Eksporter
redact.upload=Last opp
redact.boxRedaction=Tegn sensureringsboks
redact.zoom=Zoom
redact.zoomIn=Zoom in
redact.zoomOut=Zoom out
redact.nextPage=Next Page
redact.previousPage=Previous Page
redact.toggleSidebar=Toggle Sidebar
redact.showThumbnails=Show Thumbnails
redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items)
redact.showAttatchments=Show Attachments
redact.showLayers=Show Layers (double-click to reset all layers to the default state)
redact.colourPicker=Colour Picker
redact.findCurrentOutlineItem=Find current outline item
redact.applyChanges=Apply Changes
redact.zoomIn=Zoom inn
redact.zoomOut=Zoom ut
redact.nextPage=Neste side
redact.previousPage=Forrige side
redact.toggleSidebar=Vis/skjul sidepanel
redact.showThumbnails=Vis miniatyrbilder
redact.showDocumentOutline=Vis dokumentstruktur (dobbeltklikk for å utvide/skjule alle elementer)
redact.showAttatchments=Vis vedlegg
redact.showLayers=Vis lag (dobbeltklikk for å tilbakestille alle lag til standardtilstand)
redact.colourPicker=Fargevelger
redact.findCurrentOutlineItem=Finn gjeldende punkt i strukturen
redact.applyChanges=Bruk endringer
#showJS
showJS.title=Vis Javascript
@ -709,9 +733,10 @@ sanitizePDF.title=Rensker PDF
sanitizePDF.header=Rensker en PDF fil
sanitizePDF.selectText.1=Fjern JavaScript-handlinger
sanitizePDF.selectText.2=Fjern innebygde filer
sanitizePDF.selectText.3=Fjern metadata
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Fjern lenker
sanitizePDF.selectText.5=Fjern skrifter
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Rensk PDF
@ -836,9 +861,9 @@ compare.highlightColor.2=Uthevingsfarge 2:
compare.document.1=Dokument 1
compare.document.2=Dokument 2
compare.submit=Sammenlign
compare.complex.message=One or both of the provided documents are large files, accuracy of comparison may be reduced
compare.large.file.message=One or Both of the provided documents are too large to process
compare.no.text.message=One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison.
compare.complex.message=Ett eller begge av de angitte dokumentene er store filer, nøyaktigheten av sammenligningen kan bli redusert
compare.large.file.message=Ett eller begge av de angitte dokumentene er for store til å behandle
compare.no.text.message=En eller begge av de valgte PDF-ene har ingen tekstinnhold. Vennligst velg PDF-er med tekst for sammenligning.
#sign
sign.title=Signer
@ -848,20 +873,20 @@ sign.draw=Tegn signatur
sign.text=Tekstinput
sign.clear=Slett
sign.add=Legg til
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
sign.addToAll=Add to all pages
sign.delete=Delete
sign.first=First page
sign.last=Last page
sign.next=Next page
sign.previous=Previous page
sign.maintainRatio=Toggle maintain aspect ratio
sign.undo=Undo
sign.redo=Redo
sign.saved=Lagrede signaturer
sign.save=Lagre signatur
sign.personalSigs=Personlige signaturer
sign.sharedSigs=Delte signaturer
sign.noSavedSigs=Ingen lagrede signaturer funnet
sign.addToAll=Legg til på alle sider
sign.delete=Slett
sign.first=Første side
sign.last=Siste side
sign.next=Neste side
sign.previous=Forrige side
sign.maintainRatio=Bytt behold sideforhold
sign.undo=Angre
sign.redo=Gjør om
#repair
repair.title=Reparer
@ -887,7 +912,7 @@ ScannerImageSplit.selectText.7=Minimumskonturområde:
ScannerImageSplit.selectText.8=Angir minimumskonturområde terskel for et bilde
ScannerImageSplit.selectText.9=Kantstørrelse:
ScannerImageSplit.selectText.10=Angir størrelsen på kanten som legges til og fjernes for å forhindre hvite kanter i utdataen (standard: 1).
ScannerImageSplit.info=Python is not installed. It is required to run.
ScannerImageSplit.info=Python er ikke installert. Det er påkrevd for å kjøre.
#OCR
@ -1333,43 +1358,43 @@ fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
fileChooser.extractPDF=Extracting...
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
releases.footer=Versjoner
releases.title=Versjonsnotater
releases.header=Versjonsnotater
releases.current.version=Gjeldende Versjon
releases.note=Versjonsnotater er kun tilgjengelige på engelsk
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures
validateSignature.selectPDF=Select signed PDF file
validateSignature.submit=Validate Signatures
validateSignature.results=Validation Results
validateSignature.title=Valider PDF-signaturer
validateSignature.header=Valider Digitale Signaturer
validateSignature.selectPDF=Velg signert PDF-fil
validateSignature.submit=Valider Signaturer
validateSignature.results=Valideringsresultater
validateSignature.status=Status
validateSignature.signer=Signer
validateSignature.date=Date
validateSignature.reason=Reason
validateSignature.location=Location
validateSignature.noSignatures=No digital signatures found in this document
validateSignature.status.valid=Valid
validateSignature.status.invalid=Invalid
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
validateSignature.cert.expired=Certificate has expired
validateSignature.cert.revoked=Certificate has been revoked
validateSignature.signature.info=Signature Information
validateSignature.signature=Signature
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
validateSignature.cert.info=Certificate Details
validateSignature.cert.issuer=Issuer
validateSignature.cert.subject=Subject
validateSignature.cert.serialNumber=Serial Number
validateSignature.cert.validFrom=Valid From
validateSignature.cert.validUntil=Valid Until
validateSignature.cert.algorithm=Algorithm
validateSignature.cert.keySize=Key Size
validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed
validateSignature.signer=Signatar
validateSignature.date=Dato
validateSignature.reason=Årsak
validateSignature.location=Sted
validateSignature.noSignatures=Ingen digitale signaturer funnet i dette dokumentet
validateSignature.status.valid=Gyldig
validateSignature.status.invalid=Ugyldig
validateSignature.chain.invalid=Validering av sertifikatkjede feilet - kan ikke verifisere signatarens identitet
validateSignature.trust.invalid=Sertifikatet er ikke i tillitslager - kilden kan ikke verifiseres
validateSignature.cert.expired=Sertifikatet har utløpt
validateSignature.cert.revoked=Sertifikatet har blitt tilbakekalt
validateSignature.signature.info=Signaturinformasjon
validateSignature.signature=Signatur
validateSignature.signature.mathValid=Signaturen er matematisk gyldig MEN:
validateSignature.selectCustomCert=Tilpasset Sertifikatfil X.509 (Valgfritt)
validateSignature.cert.info=Sertifikatdetaljer
validateSignature.cert.issuer=Utsteder
validateSignature.cert.subject=Emne
validateSignature.cert.serialNumber=Serienummer
validateSignature.cert.validFrom=Gyldig Fra
validateSignature.cert.validUntil=Gyldig Til
validateSignature.cert.algorithm=Algoritme
validateSignature.cert.keySize=Nøkkelstørrelse
validateSignature.cert.version=Versjon
validateSignature.cert.keyUsage=Nøkkelbruk
validateSignature.cert.selfSigned=Selv-signert
validateSignature.cert.bits=bits

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Aktywni Użytkownicy:
adminUserSettings.disabledUsers=Wyłączeni Użytkownicy:
adminUserSettings.totalUsers=Łączna Liczba Użytkowników:
adminUserSettings.lastRequest=Ostatnie Zgłoszenie
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Import/Eksport bazy danych
database.header=Import/Eksport bazy danych
@ -709,9 +733,10 @@ sanitizePDF.title=Dezynfekuj PDF
sanitizePDF.header=Dezynfekuj dokument PDF
sanitizePDF.selectText.1=Usuń elementy JavaScript
sanitizePDF.selectText.2=Usuń załączone pliki
sanitizePDF.selectText.3=Usuń metadane
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Usuń linki
sanitizePDF.selectText.5=Usuń czcionki
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Dezynfekuj PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Usuários Ativos:
adminUserSettings.disabledUsers=Usuários Desabilitados:
adminUserSettings.totalUsers=Total de Usuários:
adminUserSettings.lastRequest=Última solicitação
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Importar/Exportar banco de dados
database.header=Importar/Exportar banco de dados
@ -709,9 +733,10 @@ sanitizePDF.title=Higienizar
sanitizePDF.header=Higienizar
sanitizePDF.selectText.1=Remover scripts de JavaScript.
sanitizePDF.selectText.2=Remover arquivos embutidos.
sanitizePDF.selectText.3=Remover metadados.
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Remover links.
sanitizePDF.selectText.5=Remover fontes.
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Higienizar PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Utilizadores Ativos:
adminUserSettings.disabledUsers=Utilizadores Desativados:
adminUserSettings.totalUsers=Total de Utilizadores:
adminUserSettings.lastRequest=Último Pedido
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Importar/Exportar Base de Dados
database.header=Importar/Exportar Base de Dados
@ -709,9 +733,10 @@ sanitizePDF.title=Sanitizar PDF
sanitizePDF.header=Sanitizar um ficheiro PDF
sanitizePDF.selectText.1=Remover ações JavaScript
sanitizePDF.selectText.2=Remover ficheiros incorporados
sanitizePDF.selectText.3=Remover metadados
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Remover ligações
sanitizePDF.selectText.5=Remover tipos de letra
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Sanitizar PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Utilizatori Activi:
adminUserSettings.disabledUsers=Utilizatori Dezactivați:
adminUserSettings.totalUsers=Total Utilizatori:
adminUserSettings.lastRequest=Ultima Cerere
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Import/Export Bază de Date
database.header=Import/Export Bază de Date
@ -709,9 +733,10 @@ sanitizePDF.title=Igienizează PDF
sanitizePDF.header=Igienizează un fișier PDF
sanitizePDF.selectText.1=Elimină acțiunile JavaScript
sanitizePDF.selectText.2=Elimină fișierele încorporate
sanitizePDF.selectText.3=Elimină metadatele
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Elimină link-urile
sanitizePDF.selectText.5=Elimină fonturile
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Igienizează PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Активные пользователи:
adminUserSettings.disabledUsers=Отключенные пользователи:
adminUserSettings.totalUsers=Всего пользователей:
adminUserSettings.lastRequest=Последний запрос
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Импорт/экспорт базы данных
database.header=Импорт/экспорт базы данных
@ -709,9 +733,10 @@ sanitizePDF.title=Очистить PDF
sanitizePDF.header=Очистить PDF-файл
sanitizePDF.selectText.1=Удалить JavaScript-действия
sanitizePDF.selectText.2=Удалить встроенные файлы
sanitizePDF.selectText.3=Удалить метаданные
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Удалить ссылки
sanitizePDF.selectText.5=Удалить шрифты
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Очистить PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Database Import/Export
database.header=Database Import/Export
@ -709,9 +733,10 @@ sanitizePDF.title=Vyčistiť PDF
sanitizePDF.header=Vyčistiť PDF súbor
sanitizePDF.selectText.1=Odstrániť JavaScript akcie
sanitizePDF.selectText.2=Odstrániť vložené súbory
sanitizePDF.selectText.3=Odstrániť metadáta
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Odstrániť odkazy
sanitizePDF.selectText.5=Odstrániť fonty
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Vyčistiť PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Aktivni uporabniki:
adminUserSettings.disabledUsers=Onemogočeni uporabniki:
adminUserSettings.totalUsers=Skupno število uporabnikov:
adminUserSettings.lastRequest=Zadnja zahteva
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Uvoz/izvoz baze podatkov
database.header=Uvoz/izvoz baze podatkov
@ -709,9 +733,10 @@ sanitizePDF.title=Prečisti PDF
sanitizePDF.header=Prečisti datoteko PDF
sanitizePDF.selectText.1=Odstrani dejanja JavaScript
sanitizePDF.selectText.2=Odstrani vdelane datoteke
sanitizePDF.selectText.3=Odstrani metapodatke
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Odstrani povezave
sanitizePDF.selectText.5=Odstrani pisave
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Prečisti PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Database Import/Export
database.header=Database Import/Export
@ -709,9 +733,10 @@ sanitizePDF.title=Sanitizacija PDF-a
sanitizePDF.header=Sanitizacija PDF fajla
sanitizePDF.selectText.1=Ukloni JavaScript akcije
sanitizePDF.selectText.2=Ukloni ugrađene fajlove
sanitizePDF.selectText.3=Ukloni metapodatke
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Ukloni linkove
sanitizePDF.selectText.5=Ukloni fontove
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Sanitizuj PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Aktiva användare:
adminUserSettings.disabledUsers=Inaktiverade användare:
adminUserSettings.totalUsers=Totalt antal användare:
adminUserSettings.lastRequest=Senaste begäran
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Databasimport/export
database.header=Databasimport/export
@ -709,9 +733,10 @@ sanitizePDF.title=Sanera PDF
sanitizePDF.header=Sanera en PDF-fil
sanitizePDF.selectText.1=Ta bort JavaScript-åtgärder
sanitizePDF.selectText.2=Ta bort inbäddade filer
sanitizePDF.selectText.3=Ta bort metadata
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Ta bort länkar
sanitizePDF.selectText.5=Ta bort typsnitt
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Sanera PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=ผู้ใช้ที่มีการใช
adminUserSettings.disabledUsers=ผู้ใช้ที่ถูกระงับการใช้งาน:
adminUserSettings.totalUsers=ผู้ใช้รวมทั้งหมด:
adminUserSettings.lastRequest=การขอข้อมูลล่าสุด
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=การนำเข้า/ส่งออกฐานข้อมูล
database.header=การนำเข้า/ส่งออกฐานข้อมูล
@ -709,9 +733,10 @@ sanitizePDF.title=ทำความสะอาด PDF
sanitizePDF.header=ทำความสะอาดไฟล์ PDF
sanitizePDF.selectText.1=ลบการกระทำ JavaScript
sanitizePDF.selectText.2=ลบไฟล์ฝังตัว
sanitizePDF.selectText.3=ลบข้อมูลเมตา
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=ลบลิงก์
sanitizePDF.selectText.5=ลบฟอนต์
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=ทำความสะอาด PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Aktif Kullanıcılar:
adminUserSettings.disabledUsers=Devre Dışı Kullanıcılar:
adminUserSettings.totalUsers=Toplam Kullanıcılar:
adminUserSettings.lastRequest=Son İstek
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Veri Tabanını İçe/Dışa Aktar
database.header=Veri Tabanını İçe/Dışa Aktar
@ -709,9 +733,10 @@ sanitizePDF.title=PDF'i Temizle
sanitizePDF.header=PDF dosyasını temizle
sanitizePDF.selectText.1=JavaScript işlemlerini kaldır
sanitizePDF.selectText.2=Gömülü dosyaları kaldır
sanitizePDF.selectText.3=Üst veriyi kaldır
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Linkleri kaldır
sanitizePDF.selectText.5=Fontları kaldır
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=PDF'i Temizle

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Активні користувачі:
adminUserSettings.disabledUsers=Заблоковані користувачі:
adminUserSettings.totalUsers=Всього користувачів:
adminUserSettings.lastRequest=Останній запит
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Імпорт/експорт бази даних
database.header=Імпорт/експорт бази даних
@ -709,9 +733,10 @@ sanitizePDF.title=Дезінфекція PDF
sanitizePDF.header=Дезінфекція PDF файлу
sanitizePDF.selectText.1=Видалити JavaScript
sanitizePDF.selectText.2=Видалити вбудовані файли
sanitizePDF.selectText.3=Видалити метадані
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Видалити посилання
sanitizePDF.selectText.5=Видалити шрифти
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Дезінфекція

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=Nhập/Xuất cơ sở dữ liệu
database.header=Nhập/Xuất cơ sở dữ liệu
@ -709,9 +733,10 @@ sanitizePDF.title=Làm sạch PDF
sanitizePDF.header=Làm sạch tệp PDF
sanitizePDF.selectText.1=Xóa các hành động JavaScript
sanitizePDF.selectText.2=Xóa các tệp nhúng
sanitizePDF.selectText.3=Xóa metadata
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=Xóa liên kết
sanitizePDF.selectText.5=Xóa phông chữ
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=Làm sạch PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=འགུལ་བཞིན་པའི་སྤ
adminUserSettings.disabledUsers=སྤྱོད་མི་ཆོག་པའི་སྤྱོད་མཁན།
adminUserSettings.totalUsers=སྤྱོད་མཁན་ཁྱོན་བསྡོམས།
adminUserSettings.lastRequest=རེ་ཞུ་མཐའ་མ།
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=གཞི་གྲངས་མཛོད་ནང་འདྲེན་/ཕྱིར་འདྲེན།
database.header=གཞི་གྲངས་མཛོད་ནང་འདྲེན་/ཕྱིར་འདྲེན།
@ -709,9 +733,10 @@ sanitizePDF.title=PDF གཙང་སེལ།
sanitizePDF.header=PDF ཡིག་ཆ་གཙང་སེལ།
sanitizePDF.selectText.1=Javascript བྱ་འགུལ་སུབ་པ།
sanitizePDF.selectText.2=ནང་འཇུག་ཡིག་ཆ་སུབ་པ།
sanitizePDF.selectText.3=གནས་ཆ་སུབ་པ།
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=འབྲེལ་ཐག་སུབ་པ།
sanitizePDF.selectText.5=ཡིག་གཟུགས་སུབ་པ།
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=PDF གཙང་སེལ།

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=激活用户:
adminUserSettings.disabledUsers=禁用用户:
adminUserSettings.totalUsers=总用户:
adminUserSettings.lastRequest=最后登录
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=数据库 导入/导出
database.header=数据库 导入/导出
@ -709,9 +733,10 @@ sanitizePDF.title=清理 PDF
sanitizePDF.header=清理 PDF 文件
sanitizePDF.selectText.1=移除 JavaScript 操作
sanitizePDF.selectText.2=移除嵌入的文件
sanitizePDF.selectText.3=移除元数据
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=移除链接
sanitizePDF.selectText.5=移除字体
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=清理PDF

View File

@ -231,7 +231,31 @@ adminUserSettings.activeUsers=使用中的使用者:
adminUserSettings.disabledUsers=已停用的使用者:
adminUserSettings.totalUsers=使用者總數:
adminUserSettings.lastRequest=最後請求時間
adminUserSettings.usage=View Usage
endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics
endpointStatistics.top10=Top 10
endpointStatistics.top20=Top 20
endpointStatistics.all=All
endpointStatistics.refresh=Refresh
endpointStatistics.includeHomepage=Include Homepage ('/')
endpointStatistics.includeLoginPage=Include Login Page ('/login')
endpointStatistics.totalEndpoints=Total Endpoints
endpointStatistics.totalVisits=Total Visits
endpointStatistics.showing=Showing
endpointStatistics.selectedVisits=Selected Visits
endpointStatistics.endpoint=Endpoint
endpointStatistics.visits=Visits
endpointStatistics.percentage=Percentage
endpointStatistics.loading=Loading...
endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing.
endpointStatistics.home=Home
endpointStatistics.login=Login
endpointStatistics.top=Top
endpointStatistics.numberOfVisits=Number of Visits
endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total)
endpointStatistics.retry=Retry
database.title=資料庫匯入/匯出
database.header=資料庫匯入/匯出
@ -709,9 +733,10 @@ sanitizePDF.title=清理 PDF
sanitizePDF.header=清理 PDF 檔案
sanitizePDF.selectText.1=移除 JavaScript 操作
sanitizePDF.selectText.2=移除內嵌文件
sanitizePDF.selectText.3=移除中繼資料
sanitizePDF.selectText.3=Remove XMP metadata
sanitizePDF.selectText.4=移除連結
sanitizePDF.selectText.5=移除字型
sanitizePDF.selectText.6=Remove Document Info Metadata
sanitizePDF.submit=清理 PDF

View File

@ -61,15 +61,17 @@ security:
privateKey: classpath:saml-private-key.key # Your private key. Generated from your keypair
spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair
enterpriseEdition:
enabled: false # set to 'true' to enable enterprise edition
premium:
key: 00000000-0000-0000-0000-000000000000
SSOAutoLogin: false # Enable to auto login to first provided SSO
CustomMetadata:
autoUpdateMetadata: false # set to 'true' to automatically update metadata with below values
author: username # supports text such as 'John Doe' or types such as username to autopopulate with user's username
creator: Stirling-PDF # supports text such as 'Company-PDF'
producer: Stirling-PDF # supports text such as 'Company-PDF'
enabled: false # Enable license key checks for pro/enterprise features
proFeatures:
SSOAutoLogin: false
CustomMetadata:
autoUpdateMetadata: false
author: username
creator: Stirling-PDF
producer: Stirling-PDF
legal:
termsAndConditions: https://www.stirlingpdf.com/terms-and-conditions # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder
@ -87,6 +89,7 @@ system:
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template HTML files
tessdataDir: /usr/share/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
enableAnalytics: null # set to 'true' to enable analytics, set to 'false' to disable analytics; for enterprise users, this is set to true
enableUrlToPDF: false # Set to 'true' to enable URL to PDF, INTERNAL ONLY, known security issues, should not be used externally
disableSanitize: false # set to true to disable Sanitize HTML; (can lead to injections in HTML)
datasource:
enableCustomDatabase: false # Enterprise users ONLY, set this property to 'true' if you would like to use your own custom database configuration

View File

@ -571,6 +571,49 @@
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "io.micrometer:micrometer-registry-prometheus",
"moduleUrl": "https://github.com/micrometer-metrics/micrometer",
"moduleVersion": "1.14.5",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "io.prometheus:prometheus-metrics-config",
"moduleVersion": "1.3.6",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "io.prometheus:prometheus-metrics-core",
"moduleVersion": "1.3.6",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "io.prometheus:prometheus-metrics-exposition-formats",
"moduleVersion": "1.3.6",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "io.prometheus:prometheus-metrics-exposition-textformats",
"moduleVersion": "1.3.6",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "io.prometheus:prometheus-metrics-model",
"moduleVersion": "1.3.6",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "io.prometheus:prometheus-metrics-tracer-common",
"moduleVersion": "1.3.6",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "io.smallrye:jandex",
"moduleVersion": "3.2.0",

View File

@ -0,0 +1,83 @@
.active-user {
color: green;
text-shadow: 0 0 5px green;
}
.text-overflow {
max-width: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chart-container {
position: relative;
height: 400px;
width: 100%;
margin-bottom: 20px;
}
.stats-box {
background: var(--md-sys-color-outline-variant);
padding: .8rem;
margin: 10px 0;
border-radius: 2rem;
text-align: center;
}
.chart-controls {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.filter-controls {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 15px;
flex-wrap: wrap;
}
.filter-checkbox {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
}
.filter-checkbox input {
margin-right: 5px;
}
.loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
html[data-bs-theme="light"] .loading {
background-color: rgba(255, 255, 255, 0.7);
}
html[data-bs-theme="dark"] .loading {
background-color: rgba(15, 20, 26, 0.7);
}
/* Add some animation to the refresh button */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.refreshing .material-symbols-rounded {
animation: spin 1s linear infinite;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,363 @@
// We'll fetch data from the API instead of hardcoding it
let allEndpointData = [];
let filteredData = [];
// We'll store these as global variables that get updated when we fetch data
let sortedData = [];
let totalEndpoints = 0;
let totalVisits = 0;
// Chart instance
let myChart;
// Function to get chart colors based on current theme
function getChartColors() {
var style = window.getComputedStyle(document.body)
const colours = {
textColor: style.getPropertyValue('--md-sys-color-on-surface') ,
primaryColor: style.getPropertyValue('--md-sys-color-primary'),
backgroundColor: style.getPropertyValue('--md-sys-color-background'),
gridColor: style.getPropertyValue('--md-sys-color-on-surface'),
tooltipBgColor: style.getPropertyValue('--md-sys-color-inverse-on-surface'),
tooltipTextColor: style.getPropertyValue('--md-sys-color-inverse-surface')
}
return colours;
}
// Watch for theme changes and update chart if needed
function setupThemeChangeListener() {
// Start observing theme changes
document.addEventListener("modeChanged", (event) => {
setTimeout(function() {
if (myChart) {
const currentLimit = document.getElementById('currentlyShowing').textContent;
const limit = (currentLimit === endpointStatsTranslations.all)
? filteredData.length
: (currentLimit === endpointStatsTranslations.top20 ? 20 : 10);
updateChart(limit);
}
}, 100);
});
// Also watch for system preference changes
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', () => {
if (myChart) {
const currentLimit = document.getElementById('currentlyShowing').textContent;
const limit = (currentLimit === endpointStatsTranslations.all)
? filteredData.length
: (currentLimit === endpointStatsTranslations.top20 ? 20 : 10);
updateChart(limit);
}
});
}
// Function to filter data based on checkbox settings
function filterData() {
const includeHome = document.getElementById('hideHomeCheckbox').checked;
const includeLogin = document.getElementById('hideLoginCheckbox').checked;
filteredData = allEndpointData.filter(item => {
if (!includeHome && item.endpoint === '/') return false;
if (!includeLogin && item.endpoint === '/login') return false;
return true;
});
// Sort and calculate
sortedData = [...filteredData].sort((a, b) => b.count - a.count);
totalEndpoints = filteredData.length;
totalVisits = filteredData.reduce((sum, item) => sum + item.count, 0);
// Update stats
document.getElementById('totalEndpoints').textContent = totalEndpoints.toLocaleString();
document.getElementById('totalVisits').textContent = totalVisits.toLocaleString();
// Update the chart with current limit
const currentLimit = document.getElementById('currentlyShowing').textContent;
const limit = (currentLimit === endpointStatsTranslations.all)
? filteredData.length
: (currentLimit === endpointStatsTranslations.top20 ? 20 : 10);
updateChart(limit);
}
// Function to fetch data from the API
async function fetchEndpointData() {
try {
// Show loading state
const chartContainer = document.querySelector('.chart-container');
const loadingDiv = document.createElement('div');
loadingDiv.className = 'loading';
loadingDiv.innerHTML = `
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">${endpointStatsTranslations.loading}</span>
</div>`;
chartContainer.appendChild(loadingDiv);
// Also add animation to refresh button
const refreshBtn = document.getElementById('refreshBtn');
refreshBtn.classList.add('refreshing');
refreshBtn.disabled = true;
const response = await fetch('/api/v1/info/load/all');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
allEndpointData = data;
// Apply filters
filterData();
// Remove loading state
chartContainer.removeChild(loadingDiv);
refreshBtn.classList.remove('refreshing');
refreshBtn.disabled = false;
} catch (error) {
console.error('Error fetching endpoint data:', error);
// Show error message to user
showError(endpointStatsTranslations.failedToLoad);
// Reset refresh button
const refreshBtn = document.getElementById('refreshBtn');
refreshBtn.classList.remove('refreshing');
refreshBtn.disabled = false;
}
}
// Function to format endpoint names
function formatEndpointName(endpoint) {
if (endpoint === '/') return endpointStatsTranslations.home;
if (endpoint === '/login') return endpointStatsTranslations.login;
return endpoint.replace('/', '').replace(/-/g, ' ');
}
// Function to update the table
function updateTable(data) {
const tableBody = document.getElementById('endpointTableBody');
tableBody.innerHTML = '';
data.forEach((item, index) => {
const percentage = ((item.count / totalVisits) * 100).toFixed(2);
const row = document.createElement('tr');
// Format endpoint for better readability
let displayEndpoint = item.endpoint;
if (displayEndpoint.length > 40) {
displayEndpoint = displayEndpoint.substring(0, 37) + '...';
}
row.innerHTML = `
<td>${index + 1}</td>
<td title="${item.endpoint}">${displayEndpoint}</td>
<td>${item.count.toLocaleString()}</td>
<td>${percentage}%</td>
`;
tableBody.appendChild(row);
});
}
// Function to update the chart
function updateChart(dataLimit) {
const chartData = sortedData.slice(0, dataLimit);
// Calculate displayed statistics
const displayedVisits = chartData.reduce((sum, item) => sum + item.count, 0);
const displayedPercentage = totalVisits > 0
? ((displayedVisits / totalVisits) * 100).toFixed(2)
: '0';
document.getElementById('displayedVisits').textContent = displayedVisits.toLocaleString();
document.getElementById('displayedPercentage').textContent = displayedPercentage;
// If the limit equals the total filtered items, show "All"; otherwise "Top X"
document.getElementById('currentlyShowing').textContent =
(dataLimit === filteredData.length)
? endpointStatsTranslations.all
: endpointStatsTranslations.top + dataLimit;
// Update the table with new data
updateTable(chartData);
// Prepare labels and datasets
const labels = chartData.map(item => formatEndpointName(item.endpoint));
const data = chartData.map(item => item.count);
// Get theme-specific colors
const colors = getChartColors();
// Destroy previous chart if it exists
if (myChart) {
myChart.destroy();
}
// Create chart context
const ctx = document.getElementById('endpointChart').getContext('2d');
// Create new chart with theme-appropriate colors
myChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: endpointStatsTranslations.numberOfVisits,
data: data,
backgroundColor: colors.primaryColor.replace('rgb', 'rgba').replace(')', ', 0.6)'),
borderColor: colors.primaryColor,
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
indexAxis: dataLimit > 20 ? 'x' : 'y',
plugins: {
legend: {
display: true,
position: 'top',
labels: {
color: colors.primaryColor,
font: {
weight: 'bold'
}
}
},
tooltip: {
backgroundColor: colors.tooltipBgColor,
titleColor: colors.tooltipTextColor,
bodyColor: colors.tooltipTextColor,
borderColor: colors.tooltipBgColor,
borderWidth: 1,
padding: 12,
cornerRadius: 8,
titleFont: {
size: 14,
weight: 'bold'
},
bodyFont: {
size: 13
},
callbacks: {
label: (context) => {
const value = context.raw;
const percentage = totalVisits > 0
? ((value / totalVisits) * 100).toFixed(2)
: '0';
// Insert your i18n text in the final string:
// e.g. "Visits: 12 (34% of total)"
// If your translation includes placeholders, you'd parse them here:
return endpointStatsTranslations.visitsTooltip
.replace('{0}', value.toLocaleString())
.replace('{1}', percentage);
}
}
}
},
scales: {
x: {
border: {
color: colors.gridColor
},
ticks: {
color: colors.gridColor,
font: {
size: 12
},
callback: function(value, index, values) {
let label = this.getLabelForValue(value);
return label.length > 15 ? label.substr(0, 15) + '...' : label;
}
},
grid: {
color: `${colors.gridColor}`
}
},
y: {
border: {
color: colors.gridColor
},
min: 0,
ticks: {
color: colors.gridColor,
font: {
size: 12
},
precision: 0
},
grid: {
color: `${colors.gridColor}`
}
}
}
}
});
}
// Initialize with fetch and top 10
document.addEventListener('DOMContentLoaded', function() {
// Set up theme change listener
setupThemeChangeListener();
// Initial data fetch
fetchEndpointData();
// Set up button event listeners
document.getElementById('top10Btn').addEventListener('click', function() {
updateChart(10);
setActiveButton(this);
});
document.getElementById('top20Btn').addEventListener('click', function() {
updateChart(20);
setActiveButton(this);
});
document.getElementById('allBtn').addEventListener('click', function() {
updateChart(filteredData.length);
setActiveButton(this);
});
document.getElementById('refreshBtn').addEventListener('click', function() {
fetchEndpointData();
});
// Set up filter checkbox listeners
document.getElementById('hideHomeCheckbox').addEventListener('change', filterData);
document.getElementById('hideLoginCheckbox').addEventListener('change', filterData);
});
function setActiveButton(activeButton) {
// Remove active class from all buttons
document.querySelectorAll('.chart-controls button').forEach(button => {
button.classList.remove('active');
});
// Add active class to clicked button
activeButton.classList.add('active');
}
// Function to handle errors in a user-friendly way
function showError(message) {
const chartContainer = document.querySelector('.chart-container');
const errorDiv = document.createElement('div');
errorDiv.className = 'alert alert-danger';
errorDiv.innerHTML = `
<span class="material-symbols-rounded" style="vertical-align: bottom; margin-right: 5px;">error</span>
${message}
<button id="errorRetryBtn" class="btn btn-outline-danger btn-sm" style="margin-left: 10px;">
<span class="material-symbols-rounded" style="font-size: 1rem; vertical-align: bottom;">refresh</span>
${endpointStatsTranslations.retry}
</button>
`;
chartContainer.innerHTML = '';
chartContainer.appendChild(errorDiv);
// Add retry button functionality
document.getElementById('errorRetryBtn').addEventListener('click', fetchEndpointData);
}

View File

@ -4630,7 +4630,7 @@ if (DESCRIPTORS && !('size' in URLSearchParamsPrototype)) {
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
@ -4644,14 +4644,14 @@ if (DESCRIPTORS && !('size' in URLSearchParamsPrototype)) {
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/************************************************************************/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
@ -4664,12 +4664,12 @@ if (DESCRIPTORS && !('size' in URLSearchParamsPrototype)) {
/******/ }
/******/ };
/******/ })();
/******/
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/
/************************************************************************/
var __webpack_exports__ = globalThis.pdfjsLib = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
@ -6385,8 +6385,8 @@ function setLayerDimensions(div, viewport, mustFlip = false, mustRotate = true)
const useRound = util_FeatureTest.isCSSRoundSupported;
const w = `var(--scale-factor) * ${pageWidth}px`,
h = `var(--scale-factor) * ${pageHeight}px`;
const widthStr = useRound ? `round(up, ${w}, 1px)` : `calc(${w})`,
heightStr = useRound ? `round(up, ${h}, 1px)` : `calc(${h})`;
const widthStr = useRound ? `round(${w}, 1px)` : `calc(${w})`,
heightStr = useRound ? `round(${h}, 1px)` : `calc(${h})`;
if (!mustFlip || viewport.rotation % 180 === 0) {
style.width = widthStr;
style.height = heightStr;

View File

@ -349,7 +349,7 @@
</script>
<div class="mb-3 mt-4 text-center">
<a th:href="@{'/logout'}" role="button" class="btn btn-danger" th:text="#{account.signOut}">Sign Out</a>
<a th:if="${role == 'ROLE_ADMIN'}" class="btn btn-info" th:href="@{'/addUsers'}" role="button" th:text="#{account.adminSettings}" target="_blank">Admin Settings</a>
<a th:if="${role == 'ROLE_ADMIN'}" class="btn btn-info" th:href="@{'/adminSettings'}" role="button" th:text="#{account.adminSettings}" target="_blank">Admin Settings</a>
</div>
</div>
</div>

View File

@ -1,308 +0,0 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{adminUserSettings.title}, header=#{adminUserSettings.header})}"></th:block>
<style>
.active-user {
color: green;
text-shadow: 0 0 5px green;
}
.text-overflow {
max-width: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow:ellipsis;
}
</style>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-12 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon organize">manage_accounts</span>
<span class="tool-header-text" th:text="#{adminUserSettings.header}">Admin User Control Settings</span>
</div>
<!-- User Settings Title -->
<div style="background: var(--md-sys-color-outline-variant);padding: .8rem; margin: 10px 0; border-radius: 2rem; text-align: center;">
<a href="#" data-bs-toggle="modal" data-bs-target="#addUserModal" class="btn btn-outline-success" th:title="#{adminUserSettings.addUser}">
<span class="material-symbols-rounded">person_add</span>
<span th:text="#{adminUserSettings.addUser}">Add New User</span>
</a>
<a href="#" data-bs-toggle="modal" data-bs-target="#changeUserRoleModal" class="btn btn-outline-success" th:title="#{adminUserSettings.changeUserRole}">
<span class="material-symbols-rounded">edit</span>
<span th:text="#{adminUserSettings.changeUserRole}">Change User's Role</span>
</a>
<div class="my-4">
<strong th:text="#{adminUserSettings.totalUsers}">Total Users:</strong> <span th:text="${totalUsers}"></span>
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.activeUsers}">Active Users:</strong> <span th:text="${activeUsers}"></span>
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.disabledUsers}">Disabled Users:</strong> <span th:text="${disabledUsers}"></span>
<th:block th:if="${@runningEE}">
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalSessions}">Total Sessions:</strong> <span th:text="${sessionCount}"></span>
</th:block>
<th:block th:if="${!@runningEE}">
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalSessions}">Total Sessions:</strong> <span th:text="${sessionCount}"></span>/<span th:text="${maxSessions}"></span>
</th:block>
</div>
</div>
<div th:if="${addMessage}" class="p-3" style="background: var(--md-sys-color-outline-variant);border-radius: 2rem; text-align: center;">
<div class="alert alert-danger mb-auto">
<span th:text="#{${addMessage}}">Default message if not found</span>
</div>
</div>
<div th:if="${changeMessage}" class="p-3" style="background: var(--md-sys-color-outline-variant);border-radius: 2rem; text-align: center;">
<div class="alert alert-danger mb-auto">
<span th:text="#{${changeMessage}}">Default message if not found</span>
</div>
</div>
<div th:if="${deleteMessage}" class="alert alert-danger">
<span th:text="#{${deleteMessage}}">Default message if not found</span>
</div>
<div class="bg-card mt-3 mb-3 table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col" th:title="#{username}" th:text="#{username}">Username</th>
<th scope="col" th:title="#{adminUserSettings.roles}" th:text="#{adminUserSettings.roles}">Roles</th>
<th scope="col" th:title="#{adminUserSettings.authenticated}" class="text-overflow" th:text="#{adminUserSettings.authenticated}">Authenticated</th>
<th scope="col" th:title="#{adminUserSettings.lastRequest}" class="text-overflow" th:text="#{adminUserSettings.lastRequest}">Last Request</th>
<th scope="col" th:title="#{adminUserSettings.userSessions}" th:text="#{adminUserSettings.userSessions}">User Sessions</th>
<th scope="col" th:title="#{adminUserSettings.actions}" th:text="#{adminUserSettings.actions}" colspan="2">Actions</th>
<!-- <th scope="col"></th> -->
</tr>
</thead>
<tbody>
<tr th:each="user : ${users}">
<th scope="row" style="align-content: center;" th:text="${user.id}"></th>
<td style="align-content: center;" th:text="${user.username}" th:classappend="${userSessions[user.username] ? 'active-user' : ''}"></td>
<td style="align-content: center;" th:text="#{${user.roleName}}"></td>
<td style="align-content: center;" th:text="${user.authenticationType}"></td>
<td style="align-content: center;" th:text="${userLastRequest[user.username] != null ? #dates.format(userLastRequest[user.username], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}"></td>
<th:block th:if="${@runningEE}">
<td style="align-content: center;" th:text="${userActiveSessions[user.username] != null ? userActiveSessions[user.username] : 0}"></td>
</th:block>
<th:block th:if="${!@runningEE}">
<td style="align-content: center;" th:text="${userActiveSessions[user.username] != null ? userActiveSessions[user.username] : 0} + '/' + ${maxUserSessions}"></td>
</th:block>
<td style="align-content: center;">
<form th:if="${user.username != currentUsername}" th:action="@{'/api/v1/user/admin/deleteUser/' + ${user.username}}" method="post" onsubmit="return confirmDeleteUser()">
<button type="submit" th:title="#{adminUserSettings.deleteUser}" class="btn btn-info btn-sm"><span class="material-symbols-rounded">person_remove</span></button>
</form>
<a th:if="${user.username == currentUsername}" th:title="#{adminUserSettings.editOwnProfil}" th:href="@{'/account'}" class="btn btn-outline-success btn-sm"><span class="material-symbols-rounded">edit</span></a>
</td>
<td style="align-content: center;">
<form th:action="@{'/api/v1/user/admin/changeUserEnabled/' + ${user.username}}" method="post" onsubmit="return confirmChangeUserStatus()">
<input type="hidden" name="enabled" th:value="!${user.enabled}" />
<button type="submit" th:if="${user.enabled}" th:title="#{adminUserSettings.enabledUser}" class="btn btn-success btn-sm">
<span class="material-symbols-rounded">person</span>
</button>
<button type="submit" th:unless="${user.enabled}" th:title="#{adminUserSettings.disabledUser}" class="btn btn-danger btn-sm">
<span class="material-symbols-rounded">person_off</span>
</button>
</form>
</td>
</tr>
</tbody>
</table>
</div>
<p th:text="#{enterpriseEdition.ssoAdvert}"></p>
<script th:inline="javascript">
const delete_confirm_text = /*[[#{adminUserSettings.confirmDeleteUser}]]*/ 'Should the user be deleted?';
const change_confirm_text = /*[[#{adminUserSettings.confirmChangeUserStatus}]]*/ 'Should the user be disabled/enabled?';
function confirmDeleteUser() {
return confirm(delete_confirm_text);
}
function confirmChangeUserStatus() {
return confirm(change_confirm_text);
}
</script>
</div>
</div>
</div>
</div>
<!-- change User role Modal start -->
<div class="modal fade" id="changeUserRoleModal" tabindex="-1" aria-labelledby="changeUserRoleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 th:text="#{adminUserSettings.changeUserRole}">Change User's Role</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="modal-body">
<button class="btn btn-outline-info" data-toggle="tooltip" data-placement="auto" th:title="#{downgradeCurrentUserLongMessage}" th:text="#{help}">Help</button>
<form th:action="@{'/api/v1/user/admin/changeRole'}" method="post">
<div class="mb-3">
<label for="username" th:text="#{username}">Username</label>
<select name="username" class="form-control" required>
<option value="" disabled selected th:text="#{selectFillter}">-- Select --</option>
<option th:each="user : ${users}" th:if="${user.username != currentUsername}" th:value="${user.username}" th:text="${user.username}">Username</option>
</select>
</div>
<div class="mb-3">
<label for="role" th:text="#{adminUserSettings.role}">Role</label>
<select name="role" class="form-control" required>
<option value="" disabled selected th:text="#{selectFillter}">-- Select --</option>
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}" th:text="#{${roleDetail.value}}">Role</option>
</select>
</div>
<!-- Add other fields as required -->
<button type="submit" class="btn btn-primary" th:text="#{adminUserSettings.submit}">Save User</button>
</form>
</div>
<div class="modal-footer"></div>
</div>
</div>
</div>
<!-- change User role Modal end -->
<!-- Add User Modal start -->
<div class="modal fade" id="addUserModal" tabindex="-1" aria-labelledby="addUserModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addUserModalLabel" th:text="#{adminUserSettings.addUser}">Add New User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="modal-body">
<button class="btn btn-outline-info" data-toggle="tooltip" data-placement="auto" th:title="#{adminUserSettings.usernameInfo}" th:text="#{help}">Help</button>
<form id="formsaveuser" th:action="@{'/api/v1/user/admin/saveUser'}" method="post">
<div class="mb-3">
<label for="username" th:text="#{username}">Username</label>
<input type="text" class="form-control" name="username" id="username" th:title="#{adminUserSettings.usernameInfo}" required>
<span id="usernameError" style="display: none;" th:text="#{invalidUsernameMessage}">Invalid username!</span>
</div>
<div class="mb-3" id="passwordContainer">
<label for="password" th:text="#{password}">Password</label>
<input type="password" class="form-control" name="password" id="password" required>
</div>
<div class="mb-3">
<label for="role" th:text="#{adminUserSettings.role}">Role</label>
<select name="role" class="form-control" id="role" required>
<option value="" disabled selected th:text="#{selectFillter}">-- Select --</option>
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}" th:text="#{${roleDetail.value}}">Role</option>
</select>
</div>
<div class="mb-3">
<label for="authType">Authentication Type</label>
<select id="authType" name="authType" class="form-control" required>
<option value="web" selected>WEB</option>
<option value="sso">SSO</option>
</select>
</div>
<div class="form-check mb-3" id="checkboxContainer">
<input type="checkbox" class="form-check-input" id="forceChange" name="forceChange">
<label class="form-check-label" for="forceChange" th:text="#{adminUserSettings.forceChange}">Force user to change username/password on login</label>
</div>
<button type="submit" class="btn btn-primary" th:text="#{adminUserSettings.submit}">Save User</button>
</form>
</div>
<div class="modal-footer"></div>
</div>
</div>
</div>
<!-- Add User Modal end -->
<script th:inline="javascript">
jQuery.validator.addMethod("usernamePattern", function(value, element) {
// Regular expression for user name: Min. 3 characters, max. 50 characters
const regexUsername = /^[a-zA-Z0-9](?!.*[-@._+]{2,})([a-zA-Z0-9@._+-]{1,48})[a-zA-Z0-9]$/;
// Regular expression for email addresses: Max. 320 characters, with RFC-like validation
const regexEmail = /^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*(?:\.[A-Za-z]{2,})$/;
// Check if the field is optional or meets the requirements
return this.optional(element) || regexUsername.test(value) || regexEmail.test(value);
}, /*[[#{invalidUsernameMessage}]]*/ "Invalid username format");
$(document).ready(function() {
$('[data-toggle="tooltip"]').tooltip();
$('#formsaveuser').validate({
rules: {
username: {
required: true,
usernamePattern: true
},
password: {
required: true
},
role: {
required: true
},
authType: {
required: true
}
},
messages: {
username: {
usernamePattern: /*[[#{invalidUsernameMessage}]]*/ "Invalid username format"
},
},
errorPlacement: function(error, element) {
if (element.attr("name") === "username") {
$("#usernameError").text(error.text()).show();
} else if (element.attr("name") !== "role" && element.attr("name") !== "authType") {
error.insertAfter(element);
}
},
success: function(label, element) {
if ($(element).attr("name") === "username") {
$("#usernameError").hide();
}
}
});
$('#username').on('input', function() {
var usernameInput = $(this);
var isValid = usernameInput[0].checkValidity();
var errorSpan = $('#usernameError');
if (isValid) {
usernameInput.removeClass('invalid').addClass('valid');
errorSpan.hide();
} else {
usernameInput.removeClass('valid').addClass('invalid');
errorSpan.show();
}
});
$('#authType').on('change', function() {
var authType = $(this).val();
var passwordField = $('#password');
var passwordFieldContainer = $('#passwordContainer');
var checkboxContainer = $('#checkboxContainer');
if (authType === 'sso') {
passwordField.removeAttr('required');
passwordField.prop('disabled', true).val('');
passwordFieldContainer.slideUp('fast');
checkboxContainer.slideUp('fast');
} else {
passwordField.prop('disabled', false);
passwordField.attr('required', 'required');
passwordFieldContainer.slideDown('fast');
checkboxContainer.slideDown('fast');
}
});
});
</script>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>

View File

@ -0,0 +1,360 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block
th:insert="~{fragments/common :: head(title=#{adminUserSettings.title}, header=#{adminUserSettings.header})}">
</th:block>
<style>
.active-user {
color: green;
text-shadow: 0 0 5px green;
}
.text-overflow {
max-width: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-12 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon organize">manage_accounts</span>
<span class="tool-header-text" th:text="#{adminUserSettings.header}">Admin User Control Settings</span>
</div>
<!-- User Settings Title -->
<div
style="background: var(--md-sys-color-outline-variant);padding: .8rem; margin: 10px 0; border-radius: 2rem; text-align: center;">
<a href="#" th:data-bs-toggle="${@runningEE && totalUsers >= maxEnterpriseUsers} ? null : 'modal'"
th:data-bs-target="${@runningEE && totalUsers >= maxEnterpriseUsers} ? null : '#addUserModal'"
th:class="${@runningEE && totalUsers >= maxEnterpriseUsers} ? 'btn btn-danger' : 'btn btn-outline-success'"
th:title="${@runningEE && totalUsers >= maxEnterpriseUsers} ? #{adminUserSettings.maxUsersReached} : #{adminUserSettings.addUser}">
<span class="material-symbols-rounded">person_add</span>
<span th:text="#{adminUserSettings.addUser}">Add New User</span>
</a>
<a href="#" data-bs-toggle="modal" data-bs-target="#changeUserRoleModal" class="btn btn-outline-success"
th:title="#{adminUserSettings.changeUserRole}">
<span class="material-symbols-rounded">edit</span>
<span th:text="#{adminUserSettings.changeUserRole}">Change User's Role</span>
</a>
<a href="/usage" class="btn btn-outline-success" th:title="#{adminUserSettings.usage}">
<span class="material-symbols-rounded">analytics</span>
<span th:text="#{adminUserSettings.usage}">Usage Statistics</span>
</a>
<div class="my-4">
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalUsers}">Total Users:</strong> <span
th:text="${totalUsers}"></span><span th:if="${@runningEE}" th:text="'/'+${maxEnterpriseUsers}"></span>
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.activeUsers}">Active Users:</strong>
<span th:text="${activeUsers}"></span>
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.disabledUsers}">Disabled Users:</strong>
<span th:text="${disabledUsers}"></span>
<th:block th:if="${@runningEE}">
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalSessions}">Total
Sessions:</strong> <span th:text="${sessionCount}"></span>
</th:block>
<th:block th:if="${!@runningEE}">
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalSessions}">Total
Sessions:</strong> <span th:text="${sessionCount}"></span>/<span th:text="${maxSessions}"></span>
</th:block>
</div>
</div>
<div th:if="${addMessage}" class="p-3"
style="background: var(--md-sys-color-outline-variant);border-radius: 2rem; text-align: center;">
<div class="alert alert-danger mb-auto">
<span th:text="#{${addMessage}}">Default message if not found</span>
</div>
</div>
<div th:if="${changeMessage}" class="p-3"
style="background: var(--md-sys-color-outline-variant);border-radius: 2rem; text-align: center;">
<div class="alert alert-danger mb-auto">
<span th:text="#{${changeMessage}}">Default message if not found</span>
</div>
</div>
<div th:if="${deleteMessage}" class="alert alert-danger">
<span th:text="#{${deleteMessage}}">Default message if not found</span>
</div>
<div class="bg-card mt-3 mb-3 table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col" th:title="#{username}" th:text="#{username}">Username</th>
<th scope="col" th:title="#{adminUserSettings.roles}" th:text="#{adminUserSettings.roles}">Roles
</th>
<th scope="col" th:title="#{adminUserSettings.authenticated}" class="text-overflow"
th:text="#{adminUserSettings.authenticated}">Authenticated</th>
<th scope="col" th:title="#{adminUserSettings.lastRequest}" class="text-overflow"
th:text="#{adminUserSettings.lastRequest}">Last Request</th>
<th scope="col" th:title="#{adminUserSettings.userSessions}"
th:text="#{adminUserSettings.userSessions}">User Sessions</th>
<th scope="col" th:title="#{adminUserSettings.actions}" th:text="#{adminUserSettings.actions}"
colspan="2">Actions</th>
<!-- <th scope="col"></th> -->
</tr>
</thead>
<tbody>
<tr th:each="user : ${users}">
<th scope="row" style="align-content: center;" th:text="${user.id}"></th>
<td style="align-content: center;" th:text="${user.username}"
th:classappend="${userSessions[user.username] ? 'active-user' : ''}"></td>
<td style="align-content: center;" th:text="#{${user.roleName}}"></td>
<td style="align-content: center;" th:text="${user.authenticationType}"></td>
<td style="align-content: center;"
th:text="${userLastRequest[user.username] != null ? #dates.format(userLastRequest[user.username], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}">
</td>
<th:block th:if="${@runningEE}">
<td style="align-content: center;"
th:text="${userActiveSessions[user.username] != null ? userActiveSessions[user.username] : 0}">
</td>
</th:block>
<th:block th:if="${!@runningEE}">
<td style="align-content: center;"
th:text="${userActiveSessions[user.username] != null ? userActiveSessions[user.username] : 0} + '/' + ${maxUserSessions}">
</td>
</th:block>
<td style="align-content: center;">
<form th:if="${user.username != currentUsername}"
th:action="@{'/api/v1/user/admin/deleteUser/' + ${user.username}}" method="post"
onsubmit="return confirmDeleteUser()">
<button type="submit" th:title="#{adminUserSettings.deleteUser}"
class="btn btn-info btn-sm"><span
class="material-symbols-rounded">person_remove</span></button>
</form>
<a th:if="${user.username == currentUsername}" th:title="#{adminUserSettings.editOwnProfil}"
th:href="@{'/account'}" class="btn btn-outline-success btn-sm"><span
class="material-symbols-rounded">edit</span></a>
</td>
<td style="align-content: center;">
<form th:action="@{'/api/v1/user/admin/changeUserEnabled/' + ${user.username}}" method="post"
onsubmit="return confirmChangeUserStatus()">
<input type="hidden" name="enabled" th:value="!${user.enabled}" />
<button type="submit" th:if="${user.enabled}" th:title="#{adminUserSettings.enabledUser}"
class="btn btn-success btn-sm">
<span class="material-symbols-rounded">person</span>
</button>
<button type="submit" th:unless="${user.enabled}" th:title="#{adminUserSettings.disabledUser}"
class="btn btn-danger btn-sm">
<span class="material-symbols-rounded">person_off</span>
</button>
</form>
</td>
</tr>
</tbody>
</table>
</div>
<p th:if="${!@runningEE}" th:text="#{enterpriseEdition.ssoAdvert}"></p>
<script th:inline="javascript">
const delete_confirm_text = /*[[#{adminUserSettings.confirmDeleteUser}]]*/ 'Should the user be deleted?';
const change_confirm_text = /*[[#{adminUserSettings.confirmChangeUserStatus}]]*/ 'Should the user be disabled/enabled?';
function confirmDeleteUser() {
return confirm(delete_confirm_text);
}
function confirmChangeUserStatus() {
return confirm(change_confirm_text);
}
</script>
</div>
</div>
</div>
</div>
<!-- change User role Modal start -->
<div class="modal fade" id="changeUserRoleModal" tabindex="-1" aria-labelledby="changeUserRoleModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 th:text="#{adminUserSettings.changeUserRole}">Change User's Role</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="modal-body">
<button class="btn btn-outline-info" data-toggle="tooltip" data-placement="auto"
th:title="#{downgradeCurrentUserLongMessage}" th:text="#{help}">Help</button>
<form th:action="@{'/api/v1/user/admin/changeRole'}" method="post">
<div class="mb-3">
<label for="username" th:text="#{username}">Username</label>
<select name="username" class="form-control" required>
<option value="" disabled selected th:text="#{selectFillter}">-- Select --</option>
<option th:each="user : ${users}" th:if="${user.username != currentUsername}"
th:value="${user.username}" th:text="${user.username}">Username</option>
</select>
</div>
<div class="mb-3">
<label for="role" th:text="#{adminUserSettings.role}">Role</label>
<select name="role" class="form-control" required>
<option value="" disabled selected th:text="#{selectFillter}">-- Select --</option>
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}"
th:text="#{${roleDetail.value}}">Role</option>
</select>
</div>
<!-- Add other fields as required -->
<button type="submit" class="btn btn-primary" th:text="#{adminUserSettings.submit}">Save User</button>
</form>
</div>
<div class="modal-footer"></div>
</div>
</div>
</div>
<!-- change User role Modal end -->
<!-- Add User Modal start -->
<div class="modal fade" id="addUserModal" tabindex="-1" aria-labelledby="addUserModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addUserModalLabel" th:text="#{adminUserSettings.addUser}">Add New User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="modal-body">
<button class="btn btn-outline-info" data-toggle="tooltip" data-placement="auto"
th:title="#{adminUserSettings.usernameInfo}" th:text="#{help}">Help</button>
<form id="formsaveuser" th:action="@{'/api/v1/user/admin/saveUser'}" method="post">
<div class="mb-3">
<label for="username" th:text="#{username}">Username</label>
<input type="text" class="form-control" name="username" id="username"
th:title="#{adminUserSettings.usernameInfo}" required>
<span id="usernameError" style="display: none;" th:text="#{invalidUsernameMessage}">Invalid
username!</span>
</div>
<div class="mb-3" id="passwordContainer">
<label for="password" th:text="#{password}">Password</label>
<input type="password" class="form-control" name="password" id="password" required>
</div>
<div class="mb-3">
<label for="role" th:text="#{adminUserSettings.role}">Role</label>
<select name="role" class="form-control" id="role" required>
<option value="" disabled selected th:text="#{selectFillter}">-- Select --</option>
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}"
th:text="#{${roleDetail.value}}">Role</option>
</select>
</div>
<div class="mb-3">
<label for="authType">Authentication Type</label>
<select id="authType" name="authType" class="form-control" required>
<option value="web" selected>WEB</option>
<option value="sso">SSO</option>
</select>
</div>
<div class="form-check mb-3" id="checkboxContainer">
<input type="checkbox" class="form-check-input" id="forceChange" name="forceChange">
<label class="form-check-label" for="forceChange" th:text="#{adminUserSettings.forceChange}">Force user
to change username/password on login</label>
</div>
<button type="submit" class="btn btn-primary" th:text="#{adminUserSettings.submit}">Save User</button>
</form>
</div>
<div class="modal-footer"></div>
</div>
</div>
</div>
<!-- Add User Modal end -->
<script th:inline="javascript">
jQuery.validator.addMethod("usernamePattern", function (value, element) {
// Regular expression for user name: Min. 3 characters, max. 50 characters
const regexUsername = /^[a-zA-Z0-9](?!.*[-@._+]{2,})([a-zA-Z0-9@._+-]{1,48})[a-zA-Z0-9]$/;
// Regular expression for email addresses: Max. 320 characters, with RFC-like validation
const regexEmail = /^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*(?:\.[A-Za-z]{2,})$/;
// Check if the field is optional or meets the requirements
return this.optional(element) || regexUsername.test(value) || regexEmail.test(value);
}, /*[[#{invalidUsernameMessage}]]*/ "Invalid username format");
$(document).ready(function () {
$('[data-toggle="tooltip"]').tooltip();
$('#formsaveuser').validate({
rules: {
username: {
required: true,
usernamePattern: true
},
password: {
required: true
},
role: {
required: true
},
authType: {
required: true
}
},
messages: {
username: {
usernamePattern: /*[[#{invalidUsernameMessage}]]*/ "Invalid username format"
},
},
errorPlacement: function (error, element) {
if (element.attr("name") === "username") {
$("#usernameError").text(error.text()).show();
} else if (element.attr("name") !== "role" && element.attr("name") !== "authType") {
error.insertAfter(element);
}
},
success: function (label, element) {
if ($(element).attr("name") === "username") {
$("#usernameError").hide();
}
}
});
$('#username').on('input', function () {
var usernameInput = $(this);
var isValid = usernameInput[0].checkValidity();
var errorSpan = $('#usernameError');
if (isValid) {
usernameInput.removeClass('invalid').addClass('valid');
errorSpan.hide();
} else {
usernameInput.removeClass('valid').addClass('invalid');
errorSpan.show();
}
});
$('#authType').on('change', function () {
var authType = $(this).val();
var passwordField = $('#password');
var passwordFieldContainer = $('#passwordContainer');
var checkboxContainer = $('#checkboxContainer');
if (authType === 'sso') {
passwordField.removeAttr('required');
passwordField.prop('disabled', true).val('');
passwordFieldContainer.slideUp('fast');
checkboxContainer.slideUp('fast');
} else {
passwordField.prop('disabled', false);
passwordField.attr('required', 'required');
passwordFieldContainer.slideDown('fast');
checkboxContainer.slideDown('fast');
}
});
});
</script>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>

View File

@ -29,8 +29,12 @@
<label for="removeEmbeddedFiles" th:text="#{sanitizePDF.selectText.2}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="removeMetadata" name="removeMetadata" checked>
<label for="removeMetadata" th:text="#{sanitizePDF.selectText.3}"></label>
<input type="checkbox" id="removeXMPMetadata" name="removeXMPMetadata">
<label for="removeXMPMetadata" th:text="#{sanitizePDF.selectText.3}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="removeMetadata" name="removeMetadata">
<label for="removeMetadata" th:text="#{sanitizePDF.selectText.6}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="removeLinks" name="removeLinks">

View File

@ -0,0 +1,110 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{endpointStatistics.title}, header=#{endpointStatistics.header})}"></th:block>
<script th:src="@{'/js/thirdParty/chart.umd.min.js'}"></script>
<link rel="stylesheet" th:href="@{'/css/usage.css'}">
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-9 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon">analytics</span>
<span class="tool-header-text" th:text="#{endpointStatistics.header}">Endpoint Statistics</span>
</div>
<!-- Statistics Summary Box -->
<div class="stats-box">
<div class="chart-controls">
<button id="top10Btn" class="btn btn-outline-primary active">
<span class="material-symbols-rounded">bar_chart</span>
<span th:text="#{endpointStatistics.top10}">Top 10</span>
</button>
<button id="top20Btn" class="btn btn-outline-primary">
<span class="material-symbols-rounded">data_usage</span>
<span th:text="#{endpointStatistics.top20}">Top 20</span>
</button>
<button id="allBtn" class="btn btn-outline-primary">
<span class="material-symbols-rounded">insights</span>
<span th:text="#{endpointStatistics.all}">All</span>
</button>
<button id="refreshBtn" class="btn btn-outline-secondary">
<span class="material-symbols-rounded">refresh</span>
<span th:text="#{endpointStatistics.refresh}">Refresh</span>
</button>
</div>
<div class="filter-controls">
<label class="filter-checkbox">
<input type="checkbox" id="hideHomeCheckbox" checked>
<span th:text="#{endpointStatistics.includeHomepage}">Include Homepage ('/')</span>
</label>
<label class="filter-checkbox">
<input type="checkbox" id="hideLoginCheckbox" checked>
<span th:text="#{endpointStatistics.includeLoginPage}">Include Login Page ('/login')</span>
</label>
</div>
<div class="my-4" style="color: var(--md-sys-color-on-surface); font-weight: 500;">
<span style="margin: 0 10px;"><strong th:text="#{endpointStatistics.totalEndpoints} + ':'">Total Endpoints:</strong> <span id="totalEndpoints">0</span></span>
<span style="margin: 0 10px;"><strong th:text="#{endpointStatistics.totalVisits} + ':'">Total Visits:</strong> <span id="totalVisits">0</span></span>
<span style="margin: 0 10px;"><strong th:text="#{endpointStatistics.showing} + ':'">Showing:</strong> <span id="currentlyShowing">Top 10</span></span>
<span style="margin: 0 10px;"><strong th:text="#{endpointStatistics.selectedVisits} + ':'">Selected Visits:</strong> <span id="displayedVisits">0</span> (<span id="displayedPercentage">0</span>%)</span>
</div>
</div>
<!-- Chart Container -->
<div class="bg-card mt-3 mb-3">
<div class="chart-container">
<canvas id="endpointChart"></canvas>
</div>
</div>
<!-- Table for detailed data -->
<div class="bg-card mt-3 mb-3">
<table class="endpoint-table">
<thead>
<tr>
<th scope="col" style="width: 5%;">#</th>
<th scope="col" style="width: 55%;" th:text="#{endpointStatistics.endpoint}">Endpoint</th>
<th scope="col" style="width: 20%;" th:text="#{endpointStatistics.visits}">Visits</th>
<th scope="col" style="width: 20%;" th:text="#{endpointStatistics.percentage}">Percentage</th>
</tr>
</thead>
<tbody id="endpointTableBody">
<!-- Table rows will be dynamically generated -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script type="module" th:src="@{'/js/usage.js'}"></script>
<script th:inline="javascript">
const endpointStatsTranslations = {
all: /*[[#{endpointStatistics.all}]]*/ 'All',
top20: /*[[#{endpointStatistics.top20}]]*/ 'Top 20',
loading: /*[[#{endpointStatistics.loading}]]*/ 'Loading...',
failedToLoad: /*[[#{endpointStatistics.failedToLoad}]]*/ 'Failed to load endpoint data. Please try refreshing.',
home: /*[[#{endpointStatistics.home}]]*/ 'Home',
login: /*[[#{endpointStatistics.login}]]*/ 'Login',
top: /*[[#{endpointStatistics.top}]]*/ 'Top ',
numberOfVisits: /*[[#{endpointStatistics.numberOfVisits}]]*/ 'Number of Visits',
visitsTooltip: /*[[#{endpointStatistics.visitsTooltip}]]*/ 'Visits: {0} ({1}% of total)',
retry: /*[[#{endpointStatistics.retry}]]*/ 'Retry'
};
</script>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>

View File

@ -9,6 +9,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import stirling.software.SPDF.config.RuntimePathConfig;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
@ -18,12 +19,18 @@ public class ConvertWebsiteToPdfTest {
@Mock private RuntimePathConfig runtimePathConfig;
private ApplicationProperties applicationProperties;
private ConvertWebsiteToPDF convertWebsiteToPDF;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
convertWebsiteToPDF = new ConvertWebsiteToPDF(mockPdfDocumentFactory, runtimePathConfig);
applicationProperties = new ApplicationProperties();
applicationProperties.getSystem().setEnableUrlToPDF(true);
convertWebsiteToPDF =
new ConvertWebsiteToPDF(
mockPdfDocumentFactory, runtimePathConfig, applicationProperties);
}
@Test

View File

@ -60,7 +60,6 @@
/split-pdf-by-sections
/split-pdfs
/stamp
/url-to-pdf
/validate-signature
/view-pdf
/swagger-ui/index.html