Compare commits

...
Author SHA1 Message Date
Anthony Stirling d0a1adef8f csrf 2025-11-16 20:27:09 +00:00
Anthony Stirling 4d1af9ad3f arm test 2025-11-14 17:48:39 +00:00
Anthony Stirling a5e21bc35c demo reupload plus translations and pdf editor cleanups, test amd64 2025-11-14 17:22:06 +00:00
Anthony Stirling e8d8aa8f46 text edit fix 2025-11-14 16:27:30 +00:00
Anthony Stirling 9d21bf002e Merge branch 'demo' of git@github.com:Stirling-Tools/Stirling-PDF.git
into demo
2025-11-14 16:07:26 +00:00
Anthony Stirling cf560b7488 demo 2025-11-14 16:06:35 +00:00
Anthony Stirling 19d9f0fdc2 Upgrade JDK version from 17 to 21 2025-11-14 15:57:49 +00:00
Anthony Stirling b49e681f16 Update push-docker-demo.yml 2025-11-14 15:57:26 +00:00
Anthony Stirling 9970d5a7f2 push demo init 2025-11-14 15:56:46 +00:00
Anthony Stirling 7aae124236 Merge remote-tracking branch
'origin/codex/add-pdf-to-json-and-json-to-pdf-features' into demo
2025-11-14 15:50:48 +00:00
Anthony Stirling 2c1d93887a Merge remote-tracking branch 'origin/codex/add-pdf-to-json-and-json-to-pdf-features' into demo 2025-11-14 15:35:54 +00:00
Anthony Stirling bd383fb1e7 tabs 2025-11-14 15:33:22 +00:00
Anthony Stirling 458cb7fab4 fix pagebox 2025-11-14 11:24:45 +00:00
Anthony Stirling a97f5e5257 text editor cool stuff v2 2025-11-14 01:20:02 +00:00
Anthony Stirling 8247156f96 init cool stuff 2025-11-14 01:09:04 +00:00
Anthony Stirling a58c69016b paragraphs 2025-11-13 22:55:39 +00:00
Anthony Stirling c74f71af3a file loading removal 2025-11-13 15:54:31 +00:00
Anthony Stirling 5d40fcf49f remove file loading 2025-11-13 15:54:22 +00:00
Anthony Stirling b7d4995042 fix text rendering 2025-11-13 15:24:35 +00:00
Anthony Stirling c13b32fab3 Merge branch 'codex/add-pdf-to-json-and-json-to-pdf-features' into demo 2025-11-13 00:11:19 +00:00
Anthony Stirling 90fea2ba9d image handling cleanups 2025-11-13 00:10:09 +00:00
Anthony Stirling 338afbc8f3 blank images 2025-11-12 23:51:37 +00:00
Anthony Stirling f753cbb477 Merge branch 'settingsDisplay' into demo 2025-11-12 22:02:49 +00:00
Anthony Stirling ded2edcfc5 Merge remote-tracking branch 'origin/V2' into
codex/add-pdf-to-json-and-json-to-pdf-features
2025-11-12 21:13:42 +00:00
Anthony Stirling 2239e9cc2d move to propietary for settings 2025-11-12 17:18:35 +00:00
Anthony Stirling 0da1ae06d9 init 2025-11-12 15:05:27 +00:00
Anthony Stirling aad1b06324 rename tool 2025-11-12 10:16:36 +00:00
Anthony Stirling 246d7a7b16 early access sticker 2025-11-12 00:44:39 +00:00
Anthony Stirling eef02108c9 paragraph fixes 2025-11-12 00:33:28 +00:00
Anthony Stirling 5d8827e4aa warnings 2025-11-12 00:28:41 +00:00
Anthony Stirling 3bc8ae1cf2 paragraph new line 2025-11-11 22:06:30 +00:00
Anthony Stirling 7220db0ac2 scrolling 2025-11-11 18:08:50 +00:00
Anthony Stirling 4272edc89d remove logging and stop rerender 2025-11-11 17:21:50 +00:00
Anthony Stirling 84c0962b4c font detection 2025-11-11 15:37:29 +00:00
Anthony Stirling 2d8113b3f6 auto paragrpah mode 2025-11-11 12:09:40 +00:00
Anthony Stirling 3ed62c8dbf paragraphs? :o 2025-11-10 22:55:16 +00:00
Anthony Stirling 5fadb92f51 x button 2025-11-10 18:18:21 +00:00
Anthony Stirling 559ba3ccab tails fix 2025-11-10 18:11:57 +00:00
Anthony Stirling addaf797cb location click 2025-11-10 17:58:49 +00:00
Anthony Stirling 08e58349dc fix spaces 2025-11-10 17:49:26 +00:00
Anthony Stirling a1b69b4b8b docs update 2025-11-10 10:16:57 +00:00
Anthony Stirling a7f17947df size race condition fix 2025-11-10 08:57:52 +00:00
Anthony Stirling b4e33002b6 remove old 2025-11-10 00:55:21 +00:00
Anthony Stirling 9c1358157b Merge remote-tracking branch 'origin/V2' into
codex/add-pdf-to-json-and-json-to-pdf-features
2025-11-10 00:51:12 +00:00
Anthony Stirling e2ba4cf693 move fonts and auto bold 2025-11-10 00:47:26 +00:00
Anthony Stirling f645eaff18 fix issues with type0 2025-11-10 00:26:55 +00:00
Anthony Stirling 6db66f1f1a security 2025-11-09 22:39:39 +00:00
Anthony Stirling abbfde0756 missed class from previous merge 2025-11-09 18:34:24 +00:00
Anthony Stirling de05d38a47 jobID requiring userID 2025-11-09 18:34:04 +00:00
Anthony Stirling 9b27499763 fonts 2025-11-09 13:39:24 +00:00
Anthony Stirling e915e1aa7d tpye3 text edit init 2025-11-09 12:43:33 +00:00
Anthony Stirling a6bee1436f optimisations 2025-11-06 09:46:22 +00:00
Anthony Stirling 5a5bab8f63 render backend pdf 2025-11-06 00:16:23 +00:00
Anthony Stirling d4c702f96c json size clenaup 450 to 35mb 2025-11-05 23:35:08 +00:00
Anthony Stirling d4e95a6ed7 cleaner UI 2025-11-02 23:31:04 +00:00
Anthony Stirling bbcb23ca11 editor revamp, complete change 2025-11-02 21:00:03 +00:00
Anthony Stirling ec0ae36a82 fix font issue when missing 2025-11-01 16:16:26 +00:00
Anthony Stirling b5da272dbd fix login for testing 2025-11-01 15:17:01 +00:00
Anthony Stirling 732b60b246 enable login for quick tests 2025-11-01 13:07:54 +00:00
Anthony Stirling 3e5c7abfab warnings 2025-11-01 12:44:24 +00:00
Anthony Stirling d79cd76b56 fixes 2025-11-01 12:37:09 +00:00
Anthony Stirling efaec14e08 editor carriies over vector 2025-11-01 12:14:54 +00:00
Anthony Stirling 29e8270eea Merge remote-tracking branch 'origin/V2' into
codex/add-pdf-to-json-and-json-to-pdf-features
2025-11-01 11:20:36 +00:00
Anthony Stirling a00245c920 propietary move 2025-11-01 10:10:49 +00:00
Anthony Stirling 0d9321e6a1 garbage commit font remappings 2025-10-24 15:34:35 +01:00
Anthony Stirling c7c5613c13 clean fonts 2025-10-23 23:36:53 +01:00
Anthony Stirling af19a5af23 image stuff 2025-10-23 16:38:06 +01:00
Anthony Stirling 930c68c8c5 translations 2025-10-23 12:57:45 +01:00
Anthony Stirling 716fb3bbde testiong more 2025-10-23 12:50:30 +01:00
Anthony Stirling 4d9cf45009 test 2025-10-23 11:20:16 +01:00
Anthony Stirling 5780b3a119 Fix PDF↔JSON conversion compatibility with PDFBox 3 2025-10-22 16:07:57 +01:00
Anthony Stirling ece4641432 Add PDF JSON conversion endpoints 2025-10-22 15:59:21 +01:00
262 changed files with 42734 additions and 147 deletions
+4 -2
View File
@@ -306,8 +306,10 @@ 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: "true"
SECURITY_ENABLELOGIN: "false"
DISABLE_ADDITIONAL_FEATURES: "false"
SECURITY_ENABLELOGIN: "true"
SECURITY_INITIALLOGIN_USERNAME: "${{ secrets.TEST_LOGIN_USERNAME }}"
SECURITY_INITIALLOGIN_PASSWORD: "${{ secrets.TEST_LOGIN_PASSWORD }}"
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"
+81
View File
@@ -0,0 +1,81 @@
name: Push Docker Image - V2 Demo (Unified)
on:
workflow_dispatch:
push:
branches:
- demo
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
push:
runs-on: ubuntu-latest
permissions:
packages: write
id-token: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Get version number
id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- name: Generate tags
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
with:
images: |
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
tags: |
type=raw,value=v2-demo
- name: Build and push Unified Dockerfile (v2-demo)
id: build-push-demo
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./docker/Dockerfile.unified
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8
provenance: true
sbom: true
+14
View File
@@ -213,3 +213,17 @@ 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/
+2
View File
@@ -37,6 +37,8 @@ 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'
@@ -261,6 +261,8 @@ public class EndpointConfiguration {
addEndpointToGroup("Convert", "pdf-to-csv");
addEndpointToGroup("Convert", "pdf-to-markdown");
addEndpointToGroup("Convert", "eml-to-pdf");
addEndpointToGroup("Convert", "pdf-to-json");
addEndpointToGroup("Convert", "json-to-pdf");
// Adding endpoints to "Security" group
addEndpointToGroup("Security", "add-password");
@@ -294,12 +296,6 @@ public class EndpointConfiguration {
addEndpointToGroup("Other", "replace-and-invert-color-pdf");
addEndpointToGroup("Other", "multi-tool");
// Adding form-related endpoints to "Other" group
addEndpointToGroup("Other", "fields");
addEndpointToGroup("Other", "modify-fields");
addEndpointToGroup("Other", "delete-fields");
addEndpointToGroup("Other", "fill");
// Adding endpoints to "Advance" group
addEndpointToGroup("Advance", "adjust-contrast");
addEndpointToGroup("Advance", "compress-pdf");
@@ -394,6 +390,8 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "pdf-to-markdown");
addEndpointToGroup("Java", "add-attachments");
addEndpointToGroup("Java", "compress-pdf");
addEndpointToGroup("Java", "pdf-to-json");
addEndpointToGroup("Java", "json-to-pdf");
addEndpointToGroup("rar", "pdf-to-cbr");
// Javascript
@@ -41,7 +41,7 @@ public class CustomPDFDocumentFactory {
// Memory thresholds and limits
private static final long SMALL_FILE_THRESHOLD = 10 * 1024 * 1024; // 10 MB
public 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,6 +9,7 @@ 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;
@@ -37,6 +38,9 @@ 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,
@@ -98,11 +102,17 @@ public class JobExecutorService {
long customTimeoutMs,
boolean queueable,
int resourceWeight) {
String jobId = UUID.randomUUID().toString();
// Generate base UUID
String baseJobId = UUID.randomUUID().toString();
// Store the job ID in the request for potential use by other components
// 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
if (request != null) {
request.setAttribute("jobId", jobId);
request.setAttribute("jobId", scopedJobKey);
// Also track this job ID in the user's session for authorization purposes
// This ensures users can only cancel their own jobs
@@ -116,11 +126,13 @@ public class JobExecutorService {
request.getSession().setAttribute("userJobIds", userJobIds);
}
userJobIds.add(jobId);
log.debug("Added job ID {} to user session", jobId);
userJobIds.add(scopedJobKey);
log.debug("Added scoped job ID {} to user session", scopedJobKey);
}
}
String jobId = scopedJobKey;
// Determine which timeout to use
long timeoutToUse = customTimeoutMs > 0 ? customTimeoutMs : effectiveTimeoutMs;
@@ -149,17 +161,31 @@ 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(jobId, result);
processJobResult(capturedJobIdForQueue, result);
return result;
} catch (Exception e) {
log.error(
"Error executing queued job {}: {}", jobId, e.getMessage(), e);
taskManager.setError(jobId, e.getMessage());
"Error executing queued job {}: {}",
capturedJobIdForQueue,
e.getMessage(),
e);
taskManager.setError(capturedJobIdForQueue, e.getMessage());
throw e;
} finally {
// Clean up ThreadLocal to avoid memory leaks
stirling.software.common.util.JobContext.clear();
}
};
@@ -171,21 +197,36 @@ 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", jobId, timeoutToUse);
"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);
// Execute with timeout
Object result = executeWithTimeout(() -> work.get(), timeoutToUse);
processJobResult(jobId, result);
processJobResult(capturedJobId, 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();
}
});
@@ -194,6 +235,10 @@ 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);
@@ -213,6 +258,8 @@ 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();
}
}
}
@@ -466,8 +513,23 @@ 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(supplier, executor);
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);
try {
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
@@ -483,4 +545,18 @@ 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;
}
}
@@ -0,0 +1,42 @@
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);
}
@@ -0,0 +1,18 @@
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,6 +96,7 @@ public class ProcessExecutor {
.getProcessExecutor()
.getSessionLimit()
.getOcrMyPdfSessionLimit();
case CFF_CONVERTER -> 1;
};
long timeoutMinutes =
@@ -150,6 +151,7 @@ public class ProcessExecutor {
.getProcessExecutor()
.getTimeoutMinutes()
.getOcrMyPdfTimeoutMinutes();
case CFF_CONVERTER -> 5L;
};
return new ProcessExecutor(semaphoreLimit, liveUpdates, timeoutMinutes);
});
@@ -302,7 +304,8 @@ public class ProcessExecutor {
TESSERACT,
QPDF,
GHOSTSCRIPT,
OCR_MY_PDF
OCR_MY_PDF,
CFF_CONVERTER
}
@Setter
@@ -78,6 +78,23 @@ 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
@@ -5,6 +5,7 @@ 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;
@@ -24,6 +25,7 @@ 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;
@@ -41,6 +43,9 @@ public class JobController {
private final JobQueue jobQueue;
private final HttpServletRequest request;
@Autowired(required = false)
private JobOwnershipService jobOwnershipService;
/**
* Get the status of a job
*
@@ -50,6 +55,13 @@ 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();
@@ -79,6 +91,13 @@ 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();
@@ -144,13 +163,8 @@ public class JobController {
public ResponseEntity<?> cancelJob(@PathVariable("jobId") String jobId) {
log.debug("Request to cancel job: {}", 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
// Validate job ownership
if (!validateJobAccess(jobId)) {
log.warn("Unauthorized attempt to cancel job: {}", jobId);
return ResponseEntity.status(403)
.body(Map.of("message", "You are not authorized to cancel this job"));
@@ -210,6 +224,13 @@ 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();
@@ -330,4 +351,26 @@ 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,5 +1,6 @@
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
@@ -7,6 +8,9 @@ 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: false # set to 'true' to enable login
enableLogin: true # 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,6 +174,23 @@ 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)
@@ -0,0 +1,592 @@
[
{
"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