Compare commits

..
560 changed files with 11737 additions and 75116 deletions
+2 -4
View File
@@ -306,10 +306,8 @@ jobs:
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/config:/configs:rw
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/logs:/logs:rw
environment:
DISABLE_ADDITIONAL_FEATURES: "false"
SECURITY_ENABLELOGIN: "true"
SECURITY_INITIALLOGIN_USERNAME: "${{ secrets.TEST_LOGIN_USERNAME }}"
SECURITY_INITIALLOGIN_PASSWORD: "${{ secrets.TEST_LOGIN_PASSWORD }}"
DISABLE_ADDITIONAL_FEATURES: "true"
SECURITY_ENABLELOGIN: "false"
SYSTEM_DEFAULTLOCALE: en-GB
UI_APPNAME: "Stirling-PDF V2 PR#${{ needs.check-pr.outputs.pr_number }}"
UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Frontend/Backend Split Architecture"
+8 -9
View File
@@ -92,6 +92,8 @@ jobs:
toolchain: stable
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Set up JDK 21
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
@@ -178,7 +180,7 @@ jobs:
# DigiCert KeyLocker Setup (Cloud HSM)
- name: Setup DigiCert KeyLocker
id: digicert-setup
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' }}
uses: digicert/ssm-code-signing@v1.1.0
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
@@ -188,7 +190,7 @@ jobs:
SM_HOST: ${{ secrets.SM_HOST }}
- name: Setup DigiCert KeyLocker Certificate
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' }}
shell: pwsh
run: |
Write-Host "Setting up DigiCert KeyLocker environment..."
@@ -223,7 +225,7 @@ jobs:
# Traditional PFX Certificate Import (fallback if KeyLocker not configured)
- name: Import Windows Code Signing Certificate
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY == '' && github.ref == 'refs/heads/main' }}
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY == '' }}
env:
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
@@ -310,10 +312,7 @@ jobs:
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.APPIMAGETOOL_SIGN_PASSPHRASE }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY }}
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL }}
# Only enable Windows signing in Tauri when on main
SIGN: ${{ github.ref == 'refs/heads/main' && (env.SM_API_KEY == '' && env.WINDOWS_CERTIFICATE != '') && '1' || '0' }}
SIGN: ${{ (env.SM_API_KEY == '' && env.WINDOWS_CERTIFICATE != '') && '1' || '0' }}
CI: true
with:
projectPath: ./frontend
@@ -322,7 +321,7 @@ jobs:
# Sign with DigiCert KeyLocker (post-build)
- name: Sign Windows binaries with DigiCert KeyLocker
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' }}
shell: pwsh
run: |
Write-Host "=== DigiCert KeyLocker Signing ==="
@@ -513,7 +512,7 @@ jobs:
fi
- name: Verify Windows Code Signature
if: matrix.platform == 'windows-latest' && github.ref == 'refs/heads/main'
if: matrix.platform == 'windows-latest'
shell: pwsh
run: |
Write-Host "Verifying Windows code signatures..."
-15
View File
@@ -33,7 +33,6 @@ SwaggerDoc.json
# Gradle
.gradle
.gradle-home
.lock
# External tool builders
@@ -214,17 +213,3 @@ node_modules/
test_batch.json
*.backup.*.json
frontend/public/locales/*/translation.backup*.json
# Development/build artifacts
.gradle-cache/
scripts/pdf-collection/
**/tmp/
*.backup
# Type3 development data
docs/type3/signatures/
# Type3 sample PDFs (development only)
**/type3/samples/
+37 -37
View File
@@ -115,46 +115,46 @@ Stirling-PDF currently supports 40 languages!
| Language | Progress |
| -------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![87%](https://geps.dev/progress/87) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![86%](https://geps.dev/progress/86) |
| Basque (Euskara) (eu_ES) | ![86%](https://geps.dev/progress/86) |
| Bulgarian (Български) (bg_BG) | ![86%](https://geps.dev/progress/86) |
| Catalan (Català) (ca_CA) | ![85%](https://geps.dev/progress/85) |
| Croatian (Hrvatski) (hr_HR) | ![86%](https://geps.dev/progress/86) |
| Czech (Česky) (cs_CZ) | ![84%](https://geps.dev/progress/84) |
| Danish (Dansk) (da_DK) | ![85%](https://geps.dev/progress/85) |
| Dutch (Nederlands) (nl_NL) | ![85%](https://geps.dev/progress/85) |
| Arabic (العربية) (ar_AR) | ![94%](https://geps.dev/progress/94) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![93%](https://geps.dev/progress/93) |
| Basque (Euskara) (eu_ES) | ![93%](https://geps.dev/progress/93) |
| Bulgarian (Български) (bg_BG) | ![94%](https://geps.dev/progress/94) |
| Catalan (Català) (ca_CA) | ![93%](https://geps.dev/progress/93) |
| Croatian (Hrvatski) (hr_HR) | ![93%](https://geps.dev/progress/93) |
| Czech (Česky) (cs_CZ) | ![91%](https://geps.dev/progress/91) |
| Danish (Dansk) (da_DK) | ![92%](https://geps.dev/progress/92) |
| Dutch (Nederlands) (nl_NL) | ![93%](https://geps.dev/progress/93) |
| 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) | ![85%](https://geps.dev/progress/85) |
| German (Deutsch) (de_DE) | ![86%](https://geps.dev/progress/86) |
| Greek (Ελληνικά) (el_GR) | ![86%](https://geps.dev/progress/86) |
| Hindi (हिंदी) (hi_IN) | ![86%](https://geps.dev/progress/86) |
| Hungarian (Magyar) (hu_HU) | ![86%](https://geps.dev/progress/86) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![85%](https://geps.dev/progress/85) |
| Irish (Gaeilge) (ga_IE) | ![86%](https://geps.dev/progress/86) |
| Italian (Italiano) (it_IT) | ![85%](https://geps.dev/progress/85) |
| Japanese (日本語) (ja_JP) | ![86%](https://geps.dev/progress/86) |
| Korean (한국어) (ko_KR) | ![86%](https://geps.dev/progress/86) |
| Norwegian (Norsk) (no_NB) | ![86%](https://geps.dev/progress/86) |
| Persian (فارسی) (fa_IR) | ![86%](https://geps.dev/progress/86) |
| Polish (Polski) (pl_PL) | ![86%](https://geps.dev/progress/86) |
| Portuguese (Português) (pt_PT) | ![86%](https://geps.dev/progress/86) |
| Portuguese Brazilian (Português) (pt_BR) | ![86%](https://geps.dev/progress/86) |
| Romanian (Română) (ro_RO) | ![85%](https://geps.dev/progress/85) |
| Russian (Русский) (ru_RU) | ![86%](https://geps.dev/progress/86) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![86%](https://geps.dev/progress/86) |
| Simplified Chinese (简体中文) (zh_CN) | ![87%](https://geps.dev/progress/87) |
| Slovakian (Slovensky) (sk_SK) | ![86%](https://geps.dev/progress/86) |
| Slovenian (Slovenščina) (sl_SI) | ![86%](https://geps.dev/progress/86) |
| Spanish (Español) (es_ES) | ![86%](https://geps.dev/progress/86) |
| Swedish (Svenska) (sv_SE) | ![86%](https://geps.dev/progress/86) |
| Thai (ไทย) (th_TH) | ![86%](https://geps.dev/progress/86) |
| French (Français) (fr_FR) | ![93%](https://geps.dev/progress/93) |
| German (Deutsch) (de_DE) | ![93%](https://geps.dev/progress/93) |
| Greek (Ελληνικά) (el_GR) | ![93%](https://geps.dev/progress/93) |
| Hindi (हिंदी) (hi_IN) | ![94%](https://geps.dev/progress/94) |
| Hungarian (Magyar) (hu_HU) | ![94%](https://geps.dev/progress/94) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![93%](https://geps.dev/progress/93) |
| Irish (Gaeilge) (ga_IE) | ![94%](https://geps.dev/progress/94) |
| Italian (Italiano) (it_IT) | ![93%](https://geps.dev/progress/93) |
| Japanese (日本語) (ja_JP) | ![94%](https://geps.dev/progress/94) |
| Korean (한국어) (ko_KR) | ![94%](https://geps.dev/progress/94) |
| Norwegian (Norsk) (no_NB) | ![93%](https://geps.dev/progress/93) |
| Persian (فارسی) (fa_IR) | ![94%](https://geps.dev/progress/94) |
| Polish (Polski) (pl_PL) | ![93%](https://geps.dev/progress/93) |
| Portuguese (Português) (pt_PT) | ![93%](https://geps.dev/progress/93) |
| Portuguese Brazilian (Português) (pt_BR) | ![93%](https://geps.dev/progress/93) |
| Romanian (Română) (ro_RO) | ![93%](https://geps.dev/progress/93) |
| Russian (Русский) (ru_RU) | ![94%](https://geps.dev/progress/94) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![93%](https://geps.dev/progress/93) |
| Simplified Chinese (简体中文) (zh_CN) | ![94%](https://geps.dev/progress/94) |
| Slovakian (Slovensky) (sk_SK) | ![93%](https://geps.dev/progress/93) |
| Slovenian (Slovenščina) (sl_SI) | ![94%](https://geps.dev/progress/94) |
| Spanish (Español) (es_ES) | ![94%](https://geps.dev/progress/94) |
| Swedish (Svenska) (sv_SE) | ![93%](https://geps.dev/progress/93) |
| Thai (ไทย) (th_TH) | ![93%](https://geps.dev/progress/93) |
| Tibetan (བོད་ཡིག་) (bo_CN) | ![65%](https://geps.dev/progress/65) |
| Traditional Chinese (繁體中文) (zh_TW) | ![87%](https://geps.dev/progress/87) |
| Turkish (Türkçe) (tr_TR) | ![86%](https://geps.dev/progress/86) |
| Ukrainian (Українська) (uk_UA) | ![86%](https://geps.dev/progress/86) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![86%](https://geps.dev/progress/86) |
| Traditional Chinese (繁體中文) (zh_TW) | ![94%](https://geps.dev/progress/94) |
| Turkish (Türkçe) (tr_TR) | ![94%](https://geps.dev/progress/94) |
| Ukrainian (Українська) (uk_UA) | ![94%](https://geps.dev/progress/94) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![93%](https://geps.dev/progress/93) |
| Malayalam (മലയാളം) (ml_IN) | ![73%](https://geps.dev/progress/73) |
## Stirling PDF Enterprise
-2
View File
@@ -37,8 +37,6 @@ dependencies {
api 'com.drewnoakes:metadata-extractor:2.19.0' // Image metadata extractor
api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8'
api "org.apache.pdfbox:pdfbox:$pdfboxVersion"
api "org.apache.pdfbox:xmpbox:$pdfboxVersion"
api "org.apache.pdfbox:preflight:$pdfboxVersion"
api 'com.github.junrar:junrar:7.5.5' // RAR archive support for CBR files
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
api 'org.snakeyaml:snakeyaml-engine:2.10'
@@ -41,7 +41,7 @@ public class CustomPDFDocumentFactory {
// Memory thresholds and limits
public static final long SMALL_FILE_THRESHOLD = 10 * 1024 * 1024; // 10 MB
private static final long SMALL_FILE_THRESHOLD = 10 * 1024 * 1024; // 10 MB
// Files smaller than this threshold are loaded entirely in memory for better performance.
// These files use IOUtils.createMemoryOnlyStreamCache() which keeps all document data in RAM.
// No temp files are created for document data, reducing I/O operations but consuming more
@@ -9,7 +9,6 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@@ -38,9 +37,6 @@ public class JobExecutorService {
private final ExecutorService executor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
private final long effectiveTimeoutMs;
@Autowired(required = false)
private JobOwnershipService jobOwnershipService;
public JobExecutorService(
TaskManager taskManager,
FileStorage fileStorage,
@@ -102,17 +98,11 @@ public class JobExecutorService {
long customTimeoutMs,
boolean queueable,
int resourceWeight) {
// Generate base UUID
String baseJobId = UUID.randomUUID().toString();
String jobId = UUID.randomUUID().toString();
// Scope job to authenticated user if security is enabled
String scopedJobKey = getScopedJobKey(baseJobId);
log.debug("Generated jobId: {} (base: {})", scopedJobKey, baseJobId);
// Store the scoped job ID in the request for potential use by other components
// Store the job ID in the request for potential use by other components
if (request != null) {
request.setAttribute("jobId", scopedJobKey);
request.setAttribute("jobId", jobId);
// Also track this job ID in the user's session for authorization purposes
// This ensures users can only cancel their own jobs
@@ -126,13 +116,11 @@ public class JobExecutorService {
request.getSession().setAttribute("userJobIds", userJobIds);
}
userJobIds.add(scopedJobKey);
log.debug("Added scoped job ID {} to user session", scopedJobKey);
userJobIds.add(jobId);
log.debug("Added job ID {} to user session", jobId);
}
}
String jobId = scopedJobKey;
// Determine which timeout to use
long timeoutToUse = customTimeoutMs > 0 ? customTimeoutMs : effectiveTimeoutMs;
@@ -161,31 +149,17 @@ public class JobExecutorService {
taskManager.createTask(jobId);
// Create a specialized wrapper that updates the TaskManager
final String capturedJobIdForQueue = jobId;
Supplier<Object> wrappedWork =
() -> {
try {
// Set jobId in ThreadLocal context for the queued job
stirling.software.common.util.JobContext.setJobId(
capturedJobIdForQueue);
log.debug(
"Set jobId {} in JobContext for queued job execution",
capturedJobIdForQueue);
Object result = work.get();
processJobResult(capturedJobIdForQueue, result);
processJobResult(jobId, result);
return result;
} catch (Exception e) {
log.error(
"Error executing queued job {}: {}",
capturedJobIdForQueue,
e.getMessage(),
e);
taskManager.setError(capturedJobIdForQueue, e.getMessage());
"Error executing queued job {}: {}", jobId, e.getMessage(), e);
taskManager.setError(jobId, e.getMessage());
throw e;
} finally {
// Clean up ThreadLocal to avoid memory leaks
stirling.software.common.util.JobContext.clear();
}
};
@@ -197,36 +171,21 @@ public class JobExecutorService {
return ResponseEntity.ok().body(new JobResponse<>(true, jobId, null));
} else if (async) {
taskManager.createTask(jobId);
// Capture the jobId for the async thread
final String capturedJobId = jobId;
executor.execute(
() -> {
try {
log.debug(
"Running async job {} with timeout {} ms",
capturedJobId,
timeoutToUse);
// Set jobId in ThreadLocal context for the async thread
stirling.software.common.util.JobContext.setJobId(capturedJobId);
log.debug(
"Set jobId {} in JobContext for async execution",
capturedJobId);
"Running async job {} with timeout {} ms", jobId, timeoutToUse);
// Execute with timeout
Object result = executeWithTimeout(() -> work.get(), timeoutToUse);
processJobResult(capturedJobId, result);
processJobResult(jobId, result);
} catch (TimeoutException te) {
log.error("Job {} timed out after {} ms", jobId, timeoutToUse);
taskManager.setError(jobId, "Job timed out");
} catch (Exception e) {
log.error("Error executing job {}: {}", jobId, e.getMessage(), e);
taskManager.setError(jobId, e.getMessage());
} finally {
// Clean up ThreadLocal to avoid memory leaks
stirling.software.common.util.JobContext.clear();
}
});
@@ -235,10 +194,6 @@ public class JobExecutorService {
try {
log.debug("Running sync job with timeout {} ms", timeoutToUse);
// Make jobId available to downstream components on the worker thread
stirling.software.common.util.JobContext.setJobId(jobId);
log.debug("Set jobId {} in JobContext for sync execution", jobId);
// Execute with timeout
Object result = executeWithTimeout(() -> work.get(), timeoutToUse);
@@ -258,8 +213,6 @@ public class JobExecutorService {
// Construct a JSON error response
return ResponseEntity.internalServerError()
.body(Map.of("error", "Job failed: " + e.getMessage()));
} finally {
stirling.software.common.util.JobContext.clear();
}
}
}
@@ -513,23 +466,8 @@ public class JobExecutorService {
throws TimeoutException, Exception {
// Use the same executor as other async jobs for consistency
// This ensures all operations run on the same thread pool
String currentJobId = stirling.software.common.util.JobContext.getJobId();
java.util.concurrent.CompletableFuture<T> future =
java.util.concurrent.CompletableFuture.supplyAsync(
() -> {
if (currentJobId != null) {
stirling.software.common.util.JobContext.setJobId(currentJobId);
}
try {
return supplier.get();
} finally {
if (currentJobId != null) {
stirling.software.common.util.JobContext.clear();
}
}
},
executor);
java.util.concurrent.CompletableFuture.supplyAsync(supplier, executor);
try {
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
@@ -545,18 +483,4 @@ public class JobExecutorService {
throw new Exception("Execution was interrupted", e);
}
}
/**
* Get a scoped job key that includes user ownership when security is enabled.
*
* @param baseJobId the base job identifier
* @return scoped job key, or just baseJobId if no ownership service available
*/
private String getScopedJobKey(String baseJobId) {
if (jobOwnershipService != null) {
return jobOwnershipService.createScopedJobKey(baseJobId);
}
// Security disabled, return unsecured job key
return baseJobId;
}
}
@@ -1,42 +0,0 @@
package stirling.software.common.service;
import java.util.Optional;
/**
* Service interface for managing job ownership and access control. Implementations can provide
* user-scoped job isolation when security is enabled, or no-op behavior when security is disabled.
*/
public interface JobOwnershipService {
/**
* Get the current authenticated user's identifier.
*
* @return Optional containing user identifier, or empty if not authenticated
*/
Optional<String> getCurrentUserId();
/**
* Create a scoped job key that includes user ownership when security is enabled.
*
* @param jobId the base job identifier
* @return scoped job key in format "userId:jobId", or just jobId if no user authenticated
*/
String createScopedJobKey(String jobId);
/**
* Validate that the current user has access to the given job.
*
* @param scopedJobKey the scoped job key to validate
* @return true if current user owns the job or no authentication is required
* @throws SecurityException if current user does not own the job
*/
boolean validateJobAccess(String scopedJobKey);
/**
* Extract the base job ID from a scoped job key.
*
* @param scopedJobKey the scoped job key
* @return the base job ID without user prefix
*/
String extractJobId(String scopedJobKey);
}
@@ -1,18 +0,0 @@
package stirling.software.common.util;
/** Thread-local context for passing job ID across async boundaries */
public class JobContext {
private static final ThreadLocal<String> CURRENT_JOB_ID = new ThreadLocal<>();
public static void setJobId(String jobId) {
CURRENT_JOB_ID.set(jobId);
}
public static String getJobId() {
return CURRENT_JOB_ID.get();
}
public static void clear() {
CURRENT_JOB_ID.remove();
}
}
@@ -96,7 +96,6 @@ public class ProcessExecutor {
.getProcessExecutor()
.getSessionLimit()
.getOcrMyPdfSessionLimit();
case CFF_CONVERTER -> 1;
};
long timeoutMinutes =
@@ -151,7 +150,6 @@ public class ProcessExecutor {
.getProcessExecutor()
.getTimeoutMinutes()
.getOcrMyPdfTimeoutMinutes();
case CFF_CONVERTER -> 5L;
};
return new ProcessExecutor(semaphoreLimit, liveUpdates, timeoutMinutes);
});
@@ -304,8 +302,7 @@ public class ProcessExecutor {
TESSERACT,
QPDF,
GHOSTSCRIPT,
OCR_MY_PDF,
CFF_CONVERTER
OCR_MY_PDF
}
@Setter
@@ -78,23 +78,6 @@ class JobExecutorServiceTest {
verify(request).setAttribute(eq("jobId"), anyString());
}
@Test
void shouldExposeJobIdInJobContextDuringSyncExecution() throws Exception {
// Given
Supplier<Object> work = stirling.software.common.util.JobContext::getJobId;
// When
ResponseEntity<?> response = jobExecutorService.runJobGeneric(false, work);
// Then
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
var requestJobIdCaptor = ArgumentCaptor.forClass(String.class);
verify(request).setAttribute(eq("jobId"), requestJobIdCaptor.capture());
assertEquals(requestJobIdCaptor.getValue(), response.getBody());
}
@Test
void shouldRunAsyncJobSuccessfully() throws Exception {
// Given
@@ -336,8 +336,6 @@ public class EndpointConfiguration {
addEndpointToGroup("Convert", "eml-to-pdf");
addEndpointToGroup("Convert", "cbz-to-pdf");
addEndpointToGroup("Convert", "pdf-to-cbz");
addEndpointToGroup("Convert", "pdf-to-json");
addEndpointToGroup("Convert", "json-to-pdf");
// Adding endpoints to "Security" group
addEndpointToGroup("Security", "add-password");
@@ -473,8 +471,6 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "compress-pdf");
addEndpointToGroup("Java", "cbz-to-pdf");
addEndpointToGroup("Java", "pdf-to-cbz");
addEndpointToGroup("Java", "pdf-to-json");
addEndpointToGroup("Java", "json-to-pdf");
addEndpointToGroup("rar", "pdf-to-cbr");
// Javascript
@@ -25,7 +25,6 @@ public class ExternalAppDepConfig {
private final String weasyprintPath;
private final String unoconvPath;
private final Map<String, List<String>> commandToGroupMapping;
private volatile boolean dependenciesChecked = false;
public ExternalAppDepConfig(
EndpointConfiguration endpointConfiguration, RuntimePathConfig runtimePathConfig) {
@@ -112,10 +111,6 @@ public class ExternalAppDepConfig {
}
}
public boolean isDependenciesChecked() {
return dependenciesChecked;
}
@PostConstruct
public void checkDependencies() {
// Check core dependencies
@@ -167,7 +162,5 @@ public class ExternalAppDepConfig {
}
}
endpointConfiguration.logDisabledEndpointsSummary();
dependenciesChecked = true;
log.info("Dependency checks completed");
}
}
@@ -42,7 +42,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
// Tauri v1 uses tauri://localhost, v2 uses http(s)://tauri.localhost
logger.info("Tauri mode detected - enabling CORS for Tauri protocols (v1 and v2)");
registry.addMapping("/**")
.allowedOriginPatterns(
.allowedOrigins(
"tauri://localhost",
"http://tauri.localhost",
"https://tauri.localhost")
@@ -35,7 +35,6 @@ public class ConfigController {
private final EndpointConfiguration endpointConfiguration;
private final ServerCertificateServiceInterface serverCertificateService;
private final UserServiceInterface userService;
private final stirling.software.SPDF.config.ExternalAppDepConfig externalAppDepConfig;
public ConfigController(
ApplicationProperties applicationProperties,
@@ -44,14 +43,12 @@ public class ConfigController {
@org.springframework.beans.factory.annotation.Autowired(required = false)
ServerCertificateServiceInterface serverCertificateService,
@org.springframework.beans.factory.annotation.Autowired(required = false)
UserServiceInterface userService,
stirling.software.SPDF.config.ExternalAppDepConfig externalAppDepConfig) {
UserServiceInterface userService) {
this.applicationProperties = applicationProperties;
this.applicationContext = applicationContext;
this.endpointConfiguration = endpointConfiguration;
this.serverCertificateService = serverCertificateService;
this.userService = userService;
this.externalAppDepConfig = externalAppDepConfig;
}
@GetMapping("/app-config")
@@ -59,9 +56,6 @@ public class ConfigController {
Map<String, Object> configData = new HashMap<>();
try {
// Add dependency check status
configData.put("dependenciesReady", externalAppDepConfig.isDependenciesChecked());
// Get AppConfig bean
AppConfig appConfig = applicationContext.getBean(AppConfig.class);
@@ -196,9 +196,9 @@ public class StampController {
resourceDir =
switch (alphabet) {
case "arabic" -> "static/fonts/NotoSansArabic-Regular.ttf";
case "japanese" -> "static/fonts/NotoSansJP-Regular.ttf";
case "korean" -> "static/fonts/NotoSansKR-Regular.ttf";
case "chinese" -> "static/fonts/NotoSansSC-Regular.ttf";
case "japanese" -> "static/fonts/Meiryo.ttf";
case "korean" -> "static/fonts/malgun.ttf";
case "chinese" -> "static/fonts/SimSun.ttf";
case "thai" -> "static/fonts/NotoSansThai-Regular.ttf";
case "roman" -> "static/fonts/NotoSans-Regular.ttf";
default -> "static/fonts/NotoSans-Regular.ttf";
@@ -171,9 +171,9 @@ public class WatermarkController {
resourceDir =
switch (alphabet) {
case "arabic" -> "static/fonts/NotoSansArabic-Regular.ttf";
case "japanese" -> "static/fonts/NotoSansJP-Regular.ttf";
case "korean" -> "static/fonts/NotoSansKR-Regular.ttf";
case "chinese" -> "static/fonts/NotoSansSC-Regular.ttf";
case "japanese" -> "static/fonts/Meiryo.ttf";
case "korean" -> "static/fonts/malgun.ttf";
case "chinese" -> "static/fonts/SimSun.ttf";
case "thai" -> "static/fonts/NotoSansThai-Regular.ttf";
default -> "static/fonts/NotoSans-Regular.ttf";
};
@@ -5,7 +5,6 @@ import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
@@ -25,7 +24,6 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.job.JobResult;
import stirling.software.common.model.job.ResultFile;
import stirling.software.common.service.FileStorage;
import stirling.software.common.service.JobOwnershipService;
import stirling.software.common.service.JobQueue;
import stirling.software.common.service.TaskManager;
import stirling.software.common.util.RegexPatternUtils;
@@ -43,9 +41,6 @@ public class JobController {
private final JobQueue jobQueue;
private final HttpServletRequest request;
@Autowired(required = false)
private JobOwnershipService jobOwnershipService;
/**
* Get the status of a job
*
@@ -55,13 +50,6 @@ public class JobController {
@GetMapping("/job/{jobId}")
@Operation(summary = "Get job status")
public ResponseEntity<?> getJobStatus(@PathVariable("jobId") String jobId) {
// Validate job ownership
if (!validateJobAccess(jobId)) {
log.warn("Unauthorized attempt to access job status: {}", jobId);
return ResponseEntity.status(403)
.body(Map.of("message", "You are not authorized to access this job"));
}
JobResult result = taskManager.getJobResult(jobId);
if (result == null) {
return ResponseEntity.notFound().build();
@@ -91,13 +79,6 @@ public class JobController {
@GetMapping("/job/{jobId}/result")
@Operation(summary = "Get job result")
public ResponseEntity<?> getJobResult(@PathVariable("jobId") String jobId) {
// Validate job ownership
if (!validateJobAccess(jobId)) {
log.warn("Unauthorized attempt to access job result: {}", jobId);
return ResponseEntity.status(403)
.body(Map.of("message", "You are not authorized to access this job"));
}
JobResult result = taskManager.getJobResult(jobId);
if (result == null) {
return ResponseEntity.notFound().build();
@@ -163,8 +144,13 @@ public class JobController {
public ResponseEntity<?> cancelJob(@PathVariable("jobId") String jobId) {
log.debug("Request to cancel job: {}", jobId);
// Validate job ownership
if (!validateJobAccess(jobId)) {
// Verify that this job belongs to the current user
// We can use the current request's session to validate ownership
Object sessionJobIds = request.getSession().getAttribute("userJobIds");
if (sessionJobIds == null
|| !(sessionJobIds instanceof java.util.Set)
|| !((java.util.Set<?>) sessionJobIds).contains(jobId)) {
// Either no jobs in session or jobId doesn't match user's jobs
log.warn("Unauthorized attempt to cancel job: {}", jobId);
return ResponseEntity.status(403)
.body(Map.of("message", "You are not authorized to cancel this job"));
@@ -224,13 +210,6 @@ public class JobController {
@GetMapping("/job/{jobId}/result/files")
@Operation(summary = "Get job result files")
public ResponseEntity<?> getJobFiles(@PathVariable("jobId") String jobId) {
// Validate job ownership
if (!validateJobAccess(jobId)) {
log.warn("Unauthorized attempt to access job files: {}", jobId);
return ResponseEntity.status(403)
.body(Map.of("message", "You are not authorized to access this job"));
}
JobResult result = taskManager.getJobResult(jobId);
if (result == null) {
return ResponseEntity.notFound().build();
@@ -351,26 +330,4 @@ public class JobController {
return "attachment; filename=\"" + fileName + "\"";
}
}
/**
* Validate that the current user has access to the given job.
*
* @param jobId the job identifier to validate
* @return true if user has access, false otherwise
*/
private boolean validateJobAccess(String jobId) {
// If JobOwnershipService is available (security enabled), use it
if (jobOwnershipService != null) {
try {
return jobOwnershipService.validateJobAccess(jobId);
} catch (SecurityException e) {
log.warn("Job ownership validation failed for jobId {}: {}", jobId, e.getMessage());
return false;
}
}
// Security disabled - allow all access (backwards compatibility)
// When security is not enabled, any user can access any job by jobId
return true;
}
}
@@ -1,6 +1,5 @@
multipart.enabled=true
logging.level.org.springframework=WARN
logging.level.org.springframework.security=WARN
logging.level.org.hibernate=WARN
logging.level.org.eclipse.jetty=WARN
#logging.level.org.springframework.security.oauth2=DEBUG
@@ -8,9 +7,6 @@ logging.level.org.eclipse.jetty=WARN
#logging.level.org.opensaml=DEBUG
#logging.level.stirling.software.proprietary.security=DEBUG
logging.level.com.zaxxer.hikari=WARN
logging.level.stirling.software.SPDF.service.PdfJsonConversionService=INFO
logging.level.stirling.software.common.service.JobExecutorService=INFO
logging.level.stirling.software.common.service.TaskManager=INFO
spring.jpa.open-in-view=false
server.forward-headers-strategy=NATIVE
server.error.path=/error
@@ -11,7 +11,7 @@
#############################################################################################################
security:
enableLogin: true # set to 'true' to enable login
enableLogin: false # set to 'true' to enable login
csrfDisabled: false # set to 'true' to disable CSRF protection (not recommended for production)
loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
@@ -174,23 +174,6 @@ system:
databaseBackup:
cron: '0 0 0 * * ?' # Cron expression for automatic database backups "0 0 0 * * ?" daily at midnight
stirling:
pdf:
fallback-font: classpath:/static/fonts/NotoSans-Regular.ttf # Override to point at a custom fallback font
json:
font-normalization:
enabled: false # IMPORTANT: Disable to preserve ToUnicode CMaps for correct font rendering. Ghostscript strips Unicode mappings from CID fonts.
cff-converter:
enabled: true # Wrap CFF/Type1C fonts as OpenType-CFF for browser compatibility
method: python # Converter method: 'python' (fontTools, recommended - wraps as OTF), 'fontforge' (legacy - converts to TTF, may hang on CID fonts)
python-command: /opt/venv/bin/python3 # Python interpreter path
python-script: /scripts/convert_cff_to_ttf.py # Path to font wrapping script
fontforge-command: fontforge # Override if FontForge is installed under a different name/path
type3:
library:
enabled: true # Match common Type3 fonts against the built-in library of converted programs
index: classpath:/type3/library/index.json # Override to point at a custom index.json (supports http:, file:, classpath:)
ui:
appNameNavbar: '' # name displayed on the navigation bar
logoStyle: classic # Options: 'classic' (default - classic S icon) or 'modern' (minimalist logo)
Binary file not shown.
Binary file not shown.
@@ -1,592 +0,0 @@
[
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "1867"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "1888"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "2029"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "2069"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "2089"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "2116"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSansMono",
"encoding": "2174"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans-Oblique",
"encoding": "2192"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "2209"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "Cmsy10",
"encoding": "2228"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "STIXSizeThreeSym-Regular",
"encoding": "2233"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSansDisplay",
"encoding": "2239"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "4403"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "4438"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "4519"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "4685"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "4733"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "4782"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "4813"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "4834"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSansMono",
"encoding": "4878"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "4906"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "4929"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "4971"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5001"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5030"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5052"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5083"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5116"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5143"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5175"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5207"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5243"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "Cmr10",
"encoding": "5263"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "Cmex10",
"encoding": "5270"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "Cmsy10",
"encoding": "5275"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "Cmmi10",
"encoding": "5280"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5295"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans-Oblique",
"encoding": "5313"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSansDisplay",
"encoding": "5319"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5334"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5370"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5399"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5427"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5459"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5486"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5513"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5554"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5601"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5647"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5694"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5732"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5771"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5803"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5861"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5904"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5924"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "5951"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "6084"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "6445"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "7195"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "7409"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "7474"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "7708"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "7747"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "7885"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "9029"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "9617"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "10460"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "11445"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans-Bold",
"encoding": "11486"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "11497"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "11543"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "12280"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "12301"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "12350"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "12372"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "12395"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "12416"
},
{
"source": "01_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "13324"
},
{
"source": "02_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "3214"
},
{
"source": "02_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "3251"
},
{
"source": "02_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "7190"
},
{
"source": "02_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "9937"
},
{
"source": "02_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "10792"
},
{
"source": "02_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "10852"
},
{
"source": "02_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "14712"
},
{
"source": "02_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "18396"
},
{
"source": "02_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "18719"
},
{
"source": "02_Matplotlib.pdf",
"fontName": "DejaVuSans-Bold",
"encoding": "18741"
},
{
"source": "02_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "18778"
},
{
"source": "02_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "18804"
},
{
"source": "02_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "20974"
},
{
"source": "02_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "20993"
},
{
"source": "02_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "21093"
},
{
"source": "02_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "21117"
},
{
"source": "02_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "21141"
},
{
"source": "02_Matplotlib.pdf",
"fontName": "DejaVuSans",
"encoding": "21174"
},
{
"source": "03_handout-beginner.pdf",
"fontName": "BMQQDV+DejaVuSans",
"encoding": "17"
},
{
"source": "03_handout-beginner.pdf",
"fontName": "EVICAO+DejaVuSans-Bold",
"encoding": "133"
},
{
"source": "03_handout-beginner.pdf",
"fontName": "BMQQDV+DejaVuSans",
"encoding": "152"
},
{
"source": "04_handout-intermediate.pdf",
"fontName": "BMQQDV+DejaVuSans",
"encoding": "13"
},
{
"source": "04_handout-intermediate.pdf",
"fontName": "BMQQDV+DejaVuSans",
"encoding": "85"
},
{
"source": "04_handout-intermediate.pdf",
"fontName": "BMQQDV+DejaVuSans",
"encoding": "104"
},
{
"source": "04_handout-intermediate.pdf",
"fontName": "BMQQDV+DejaVuSans",
"encoding": "121"
},
{
"source": "04_handout-intermediate.pdf",
"fontName": "BMQQDV+DejaVuSans",
"encoding": "135"
},
{
"source": "04_handout-intermediate.pdf",
"fontName": "BMQQDV+DejaVuSans",
"encoding": "159"
},
{
"source": "04_handout-intermediate.pdf",
"fontName": "BMQQDV+DejaVuSans",
"encoding": "179"
},
{
"source": "04_handout-intermediate.pdf",
"fontName": "BMQQDV+DejaVuSans",
"encoding": "198"
},
{
"source": "04_handout-intermediate.pdf",
"fontName": "NVMZUP+SourceCodePro-Regular",
"encoding": "208"
},
{
"source": "04_handout-intermediate.pdf",
"fontName": "BMQQDV+DejaVuSans",
"encoding": "231"
},
{
"source": "04_handout-intermediate.pdf",
"fontName": "NVMZUP+SourceCodePro-Regular",
"encoding": "241"
},
{
"source": "07_matplotlib.pdf",
"fontName": "SauceCodePowerline-Bold",
"encoding": "22"
},
{
"source": "07_matplotlib.pdf",
"fontName": "SauceCodePowerline-Regular",
"encoding": "47"
},
{
"source": "07_matplotlib.pdf",
"fontName": "SauceCodePowerline-Regular",
"encoding": "65"
},
{
"source": "07_matplotlib.pdf",
"fontName": "SauceCodePowerline-Bold",
"encoding": "110"
},
{
"source": "08_matplotlib.pdf",
"fontName": "F36",
"encoding": "12"
},
{
"source": "08_matplotlib.pdf",
"fontName": "F59",
"encoding": "42"
}
]

Some files were not shown because too many files have changed in this diff Show More