Compare commits

...
Author SHA1 Message Date
Ludy87 aa37513395 Update FileRunEventRepository.java 2026-09-01 11:48:00 +02:00
Ludy 1e5eb466b3 Merge branch 'main' into format_java 2026-09-01 11:30:04 +02:00
Ludy87 fb9cb74f9c saas: wrap long strings + Jackson import reorder
Reflow and wrap long string literals across multiple SaaS modules (logging messages, SQL/JPQL queries, email/content headers, and agreement text) and relocate tools.jackson imports for consistent grouping. These are formatting-only changes to improve line lengths and readability; no functional logic was altered.
2026-09-01 11:23:50 +02:00
Ludy87 dcdd517ffd Fix indentation of string concatenations
Normalize indentation/whitespace for multi-line string concatenations in app/core/.../FormFillController.java and app/proprietary/.../AccountLinkClientTest.java. Pure formatting changes only; no behavior or logic modified.
2026-09-01 11:18:33 +02:00
ConnorYoh d55d8acbfa fix(portal): keep the processor's cache across a trip to the editor (#7729)
# Description of Changes

## The problem

The portal's query client was created per mount:

```ts
const [queryClient] = useState(createPortalQueryClient);
```

The portal is a route (`/processor/*`, a lazy element), and the switch
to the editor is a client-side `navigate()`. So leaving the processor
unmounts `PortalApp`, the client goes with the component, and the cache
goes with the client. Coming back refetches everything, whether or not
anything changed: four requests for the Users page alone (roster,
grants, teams, auth config), and 21 `useQuery` sites across the portal.

The editor's client sits above the router in `AppProviders` and survives
the same trip. The round trip only ever cost in one direction.

## The fix

The module already kept the instance in a module-level slot so
`tryGetPortalQueryClient()` could find it. It just replaced it on every
mount instead of reusing it, so the change is to create it lazily and
hand out the same one:

```ts
export function getPortalQueryClient(): QueryClient {
  current ??= new QueryClient({ defaultOptions: { queries: baseQueryOptions } });
  return current;
}
```

Still a separate instance from the editor's. The two namespace their
keys apart (`["portal", ...]` against `["editor", ...]`) and invalidate
independently, which this does not change.

## What this does not do

`gcTime` is 5 minutes, from the shared `baseQueryOptions`. An entry with
no observer is still collected on that timer, so this warms a quick trip
to the editor and back, not a return after a long editing session.
Raising the portal's `gcTime` is a separate decision and is not made
here.

## Why it is safe

**Signing out.** A cache that outlives a mount must not outlive a
session, because the portal's holds the admin roster, emails and roles.
Logout goes through `window.location.assign`, a full page load, so the
whole JS context is discarded and no cache can survive it. Nothing in
the codebase calls `queryClient.clear()` on sign-out, and nothing needs
to. If logout ever becomes a client-side navigation, this needs an
explicit reset, and `resetPortalQueryClient()` is the hook for it.

**The one caller of the null check.** `resolveTeam` in
`saas/portal/usersBackend.ts` uses `tryGetPortalQueryClient()` and falls
back to a direct fetch when there is no client, which its comment
describes as the unit-test path; the cache path is preferred because it
honours both `staleTime` and invalidation. A longer-lived client means
the preferred path is taken more often, not less.

## Testing

Three tests in `queryClient.test.tsx`, and the first two fail if the
client goes back to being created per call:

| | |
|---|---|
| A remount is served from cache rather than refetching | the behaviour
this changes |
| Every caller gets the same instance | the mechanism |
| No client is reported until the portal first mounts | the contract
`resolveTeam` reads |

The three existing portal caching suites called the factory expecting a
fresh client per case. They now call `resetPortalQueryClient()` in a
`beforeEach`, which is what keeps `sharing.test.tsx`'s "a later screen
refetches nothing" case honest rather than passing on a leaked cache.

`task frontend:check` passes typecheck, lint and oxfmt, and 2402 of 2404
editor tests. The two failures, `workbenchSession.test.ts` and
`notificationActions.test.tsx`, are untouched here and fail the same way
on `main`.
2026-09-01 08:56:35 +00:00
stirlingbot[bot] af97e1b27b Update Backend 3rd Party Licenses (#7713)
Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
2026-08-31 12:45:42 +01:00
dependabot[bot] 01c908e95d build(deps-dev): bump openai from 2.53.0 to 3.3.1 in /engine (#7700)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-31 11:37:12 +01:00
dependabot[bot] 0920ea9493 build(deps): bump go-task/setup-task from 2.1.0 to 2.2.0 (#7532)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-31 10:17:05 +01:00
dependabot[bot] 96207a7304 build(deps): bump @tanstack/react-query from 5.101.4 to 5.102.0 in /frontend in the tanstack group across 1 directory (#7749)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-31 10:15:11 +01:00
dependabot[bot] d93049db9f build(deps): bump log from 0.4.33 to 0.4.34 in /frontend/editor/src-tauri (#7747)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-31 10:14:56 +01:00
dependabot[bot] 54e839ae65 build(deps-dev): bump reportlab from 5.0.0 to 5.0.1 in /engine (#7699)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-31 10:14:31 +01:00
dependabot[bot] 3aca7a26f6 build(deps-dev): bump python-dotenv from 1.2.2 to 1.2.3 in /engine (#7702)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-31 10:14:19 +01:00
dependabot[bot] 5fe7df3933 build(deps): bump jackson2Version from 2.22.1 to 2.22.2 (#7703)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-31 10:14:04 +01:00
dependabot[bot] b1e857fd01 build(deps): bump com.tngtech.archunit:archunit-junit5 from 1.4.2 to 1.5.0 (#7704)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-31 10:13:47 +01:00
dependabot[bot] b92f88361e build(deps): bump docker/setup-buildx-action from 4.2.0 to 4.3.0 (#7711)
Bumps
[docker/setup-buildx-action](https://github.com/docker/setup-buildx-action)
from 4.2.0 to 4.3.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/docker/setup-buildx-action/releases">docker/setup-buildx-action's
releases</a>.</em></p>
<blockquote>
<h2>v4.3.0</h2>
<ul>
<li>Bump <code>@​docker/actions-toolkit</code> from 0.92.0 to 0.95.0 in
<a
href="https://redirect.github.com/docker/setup-buildx-action/pull/595">docker/setup-buildx-action#595</a></li>
<li>Bump brace-expansion from 1.1.13 to 1.1.18 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/600">docker/setup-buildx-action#600</a></li>
<li>Bump js-yaml from 5.2.0 to 5.3.0 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/585">docker/setup-buildx-action#585</a></li>
<li>Bump postcss from 8.5.10 to 8.5.25 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/598">docker/setup-buildx-action#598</a></li>
<li>Bump undici from 6.27.0 to 6.28.0 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/601">docker/setup-buildx-action#601</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/docker/setup-buildx-action/compare/v4.2.0...v4.3.0">https://github.com/docker/setup-buildx-action/compare/v4.2.0...v4.3.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/37fe631027851001ddb9b187196cc803df7f5f0e"><code>37fe631</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/setup-buildx-action/issues/595">#595</a>
from docker/dependabot/npm_and_yarn/docker/actions-to...</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/b5c4f91922681cc7c58d15ab7838986951f09d19"><code>b5c4f91</code></a>
[dependabot skip] chore: update generated content</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/3e93b637c6430ba8fa896fad44d3aa6821899d63"><code>3e93b63</code></a>
build(deps): bump <code>@​docker/actions-toolkit</code> from 0.92.0 to
0.95.0</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/e527031b32c86649307d5d492506855f90470604"><code>e527031</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/setup-buildx-action/issues/600">#600</a>
from docker/dependabot/npm_and_yarn/brace-expansion-1...</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/c68814b33cb66f1f7538e546190d410ae557a640"><code>c68814b</code></a>
[dependabot skip] chore: update generated content</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/3f891b01bd5012a434f582800366972569aa1886"><code>3f891b0</code></a>
build(deps): bump brace-expansion from 1.1.13 to 1.1.18</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/787db26fcde8ddcabd49a81472318028f7113962"><code>787db26</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/setup-buildx-action/issues/585">#585</a>
from docker/dependabot/npm_and_yarn/js-yaml-5.2.1</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/f7793687c711790ca336bd4934f1b1bf5f778e17"><code>f779368</code></a>
[dependabot skip] chore: update generated content</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/7d5e60413489a33d28077e11d71c668580cfaf8d"><code>7d5e604</code></a>
build(deps): bump js-yaml from 5.2.0 to 5.3.0</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/292c2fb3837a12d3ac2d1e47bbc5c00712bad939"><code>292c2fb</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/setup-buildx-action/issues/590">#590</a>
from docker/dependabot/github_actions/actions/setup-n...</li>
<li>Additional commits viewable in <a
href="https://github.com/docker/setup-buildx-action/compare/bb05f3f5519dd87d3ba754cc423b652a5edd6d2c...37fe631027851001ddb9b187196cc803df7f5f0e">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-31 07:14:30 +00:00
dependabot[bot] f6124223e4 build(deps): bump github/codeql-action/upload-sarif from 4.37.7 to 4.37.8 (#7750)
Bumps
[github/codeql-action/upload-sarif](https://github.com/github/codeql-action)
from 4.37.7 to 4.37.8.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/github/codeql-action/releases">github/codeql-action/upload-sarif's
releases</a>.</em></p>
<blockquote>
<h2>v4.37.8</h2>
<p>No user facing changes.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/github/codeql-action/blob/main/CHANGELOG.md">github/codeql-action/upload-sarif's
changelog</a>.</em></p>
<blockquote>
<h1>CodeQL Action Changelog</h1>
<p>See the <a
href="https://github.com/github/codeql-action/releases">releases
page</a> for the relevant changes to the CodeQL CLI and language
packs.</p>
<h2>[UNRELEASED]</h2>
<p>No user facing changes.</p>
<h2>4.37.9 - 26 Aug 2026</h2>
<ul>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.4">2.26.4</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/4106">#4106</a></li>
</ul>
<h2>4.37.8 - 21 Aug 2026</h2>
<p>No user facing changes.</p>
<h2>4.37.7 - 13 Aug 2026</h2>
<ul>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.3">2.26.3</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/4085">#4085</a></li>
</ul>
<h2>4.37.6 - 04 Aug 2026</h2>
<ul>
<li>Changed the default filepath for the new remote file address format
that was introduced in CodeQL Action 4.37.0 / 3.37.0 to
<code>.github/codeql-config.yml</code> to align it with the suggested
path that is used elsewhere. <a
href="https://redirect.github.com/github/codeql-action/pull/4070">#4070</a></li>
</ul>
<h2>4.37.5 - 03 Aug 2026</h2>
<ul>
<li>Fixed a bug where a network error while streaming the download of
the CodeQL bundle could terminate the <code>init</code> Action instead
of falling back to downloading the bundle before extracting it. <a
href="https://redirect.github.com/github/codeql-action/pull/4061">#4061</a></li>
</ul>
<h2>4.37.4 - 29 Jul 2026</h2>
<ul>
<li>This version of the CodeQL Action adds support for the
<code>tools</code> input for the <code>codeql-action/init</code> step to
be specified using a <code>github-codeql-tools</code> <a
href="https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization">repository
property</a>. This feature will gradually be rolled out following the
release of this version. Once rolled out, this allows for the CodeQL CLI
version that is used in GitHub-managed workflows, such as Default Setup,
to be set to a custom value. For example, customers who run into issues
with rate limits when a new CodeQL CLI version is released can set the
value to <code>toolcache</code> to always use the CodeQL CLI version
that is available in the runner toolcache. For Advanced Setup workflows,
the value provided for <code>tools</code> in the workflow definition
always takes precedence unless the value of the repository property
starts with <code>!</code>. <a
href="https://redirect.github.com/github/codeql-action/pull/4037">#4037</a></li>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.2">2.26.2</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/4051">#4051</a></li>
</ul>
<h2>4.37.3 - 22 Jul 2026</h2>
<p>No user facing changes.</p>
<h2>4.37.2 - 21 Jul 2026</h2>
<ul>
<li>The new address format for the <code>config-file</code> input that
was introduced in CodeQL Action 4.37.0 is now enabled by default. In
addition to the format described there, the <code>remote=</code> prefix
can now be used to explicitly indicate that the input refers to a remote
file. All previous input formats continue to be accepted as well. <a
href="https://redirect.github.com/github/codeql-action/pull/4023">#4023</a></li>
<li>The CodeQL Action can now make use of <a
href="https://docs.github.com/en/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries">configured
private registries</a> in Default Setup to retrieve CodeQL configuration
files from remote repositories that require authentication. This will
allow customers to store their CodeQL configuration in a single
repository that can then be referenced by Default Setup workflows in
other repositories. We expect to roll this and other, related changes
out to everyone in July. <a
href="https://redirect.github.com/github/codeql-action/pull/4007">#4007</a></li>
</ul>
<h2>4.37.1 - 16 Jul 2026</h2>
<ul>
<li><em>Upcoming breaking change</em>: Add a deprecation warning for
customers using CodeQL version 2.20.6 and earlier. These versions of
CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise
Server 3.16, and will be unsupported by the next minor release of the
CodeQL Action. <a
href="https://redirect.github.com/github/codeql-action/pull/3956">#3956</a></li>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.1">2.26.1</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/4019">#4019</a></li>
</ul>
<h2>4.37.0 - 08 Jul 2026</h2>
<ul>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.0">2.26.0</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3995">#3995</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/github/codeql-action/commit/db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28"><code>db488dd</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/4102">#4102</a>
from github/update-v4.37.8-9ee088e13</li>
<li><a
href="https://github.com/github/codeql-action/commit/1845f5ba8b4057590f49ee8e246c95ef2ba4b53f"><code>1845f5b</code></a>
Update changelog for v4.37.8</li>
<li><a
href="https://github.com/github/codeql-action/commit/9ee088e13615f8d1eaef4766f9dde95d3356a8f6"><code>9ee088e</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/4080">#4080</a>
from github/henrymercer/studious-giggle</li>
<li><a
href="https://github.com/github/codeql-action/commit/1aef003397c876c0ab5bd118e1b1f34c175622e9"><code>1aef003</code></a>
Address review feedback on overlay disk flags</li>
<li><a
href="https://github.com/github/codeql-action/commit/508b83bc415e8df76ce8ea08c0cf42c2529ebc63"><code>508b83b</code></a>
Merge main into overlay minimum disk feature branch</li>
<li><a
href="https://github.com/github/codeql-action/commit/d97b3428e8eebbb1810cf454d6397886d136b4ba"><code>d97b342</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/4098">#4098</a>
from github/mbg/permission-error-as-configuration-error</li>
<li><a
href="https://github.com/github/codeql-action/commit/47fa6222231b12097f83215dd7a6b4a0915841fd"><code>47fa622</code></a>
Make <code>EACCES</code> a <code>ConfigurationError</code></li>
<li><a
href="https://github.com/github/codeql-action/commit/45693cc6882bb175b58a06818c91876e201037c7"><code>45693cc</code></a>
Refactor <code>ENOSPC</code> check into
<code>isDiskConfigurationError</code> function</li>
<li><a
href="https://github.com/github/codeql-action/commit/c2fd8f54d19fa46c94ed79cb92e6dd6606d61762"><code>c2fd8f5</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/4081">#4081</a>
from github/mario-campos/version-cache-to-disk</li>
<li><a
href="https://github.com/github/codeql-action/commit/c56f48e9bd458a387eb68a68534459e503e56b17"><code>c56f48e</code></a>
Log unexpected conditions during caching CLI output</li>
<li>Additional commits viewable in <a
href="https://github.com/github/codeql-action/compare/ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd...db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github/codeql-action/upload-sarif&package-manager=github_actions&previous-version=4.37.7&new-version=4.37.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-31 07:14:20 +00:00
dependabot[bot] 3235645203 build(deps): bump step-security/harden-runner from 2.20.0 to 2.21.0 (#7746)
Bumps
[step-security/harden-runner](https://github.com/step-security/harden-runner)
from 2.20.0 to 2.21.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/step-security/harden-runner/releases">step-security/harden-runner's
releases</a>.</em></p>
<blockquote>
<h2>v2.21.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Support for denied endpoints in block mode. This is included in the
enterprise tier. Customers can deny outbound calls, for example, to
public package registries.</li>
<li>Improved Support for AWS CodeBuild GitHub Actions Runners.</li>
<li>Bug fixes.</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/step-security/harden-runner/compare/v2.20.1...v2.21.0">https://github.com/step-security/harden-runner/compare/v2.20.1...v2.21.0</a></p>
<h2>v2.20.1</h2>
<h2>What's Changed</h2>
<ul>
<li>AWS CodeBuild-hosted runner support</li>
<li>Implicitly allow single-labeled (internal) domains in
block-mode</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/step-security/harden-runner/compare/v2.20.0...v2.20.1">https://github.com/step-security/harden-runner/compare/v2.20.0...v2.20.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/step-security/harden-runner/commit/05e31511f85b41b11d1cf0ef85d0992719546e2c"><code>05e3151</code></a>
Merge pull request <a
href="https://redirect.github.com/step-security/harden-runner/issues/684">#684</a>
from step-security/rc-42</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/0f37afa338f57c61ee3dfc274daca8834963d83e"><code>0f37afa</code></a>
fix: ignore denied-endpoints on non-enterprise tier</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/93b58ee491c5b6cf3a5324966fca2908f8d447f3"><code>93b58ee</code></a>
fix: resolve cache host read-first and never downgrade egress
policy</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/e7399dd3e93d6c159d314af54b4704bc48abf6bc"><code>e7399dd</code></a>
fix: align deny-list mode detection with agent and log when both
endpoint inp...</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/c16689f716a10cdfd9cfe22e63938b8c6c0657de"><code>c16689f</code></a>
test: add denied_endpoints to Configuration fixtures and cover deny-list
merge</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/40b99cf0c7161e4dcdc6c5508927188b65028df9"><code>40b99cf</code></a>
Merge pull request <a
href="https://redirect.github.com/step-security/harden-runner/issues/682">#682</a>
from rohan-stepsecurity/rp/feat/codebuild-self-v2</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/fedec027a205365a7d64001a81931e4c36a1af6e"><code>fedec02</code></a>
Merge branch 'rc-42' into rp/feat/codebuild-self-v2</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/5361fb178b926b2be6df52e11ee257823821567b"><code>5361fb1</code></a>
feat: add build artifacts</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/286474fffe0b8fe7c9db855f132d04a9b48ab564"><code>286474f</code></a>
feat: Support Bravo agent install on CodeBuild runners</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/051ec05283d064bd82f41279db4f70f0717bf778"><code>051ec05</code></a>
Merge pull request <a
href="https://redirect.github.com/step-security/harden-runner/issues/683">#683</a>
from h0x0er/jatin/deny-list</li>
<li>Additional commits viewable in <a
href="https://github.com/step-security/harden-runner/compare/v2.20.0...05e31511f85b41b11d1cf0ef85d0992719546e2c">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=step-security/harden-runner&package-manager=github_actions&previous-version=2.20.0&new-version=2.21.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-31 07:13:30 +00:00
dependabot[bot] 1f4cc2612d build(deps): bump the eclipse-temurin group across 3 directories with 1 update (#7740)
> [!WARNING]
> Cooldown could not be applied because no publication date was
available from the registry.
>

Bumps the eclipse-temurin group with 1 update in the /docker/backend
directory: eclipse-temurin.
Bumps the eclipse-temurin group with 1 update in the /docker/base
directory: eclipse-temurin.
Bumps the eclipse-temurin group with 1 update in the /docker/embedded
directory: eclipse-temurin.

Updates `eclipse-temurin` from `fbcf915` to `b4c93a5`

Updates `eclipse-temurin` from `fbcf915` to `b4c93a5`

Updates `eclipse-temurin` from `fbcf915` to `b4c93a5`


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-31 07:12:18 +00:00
dependabot[bot] e539eb1ab1 build(deps): bump the ubuntu group across 2 directories with 1 update (#7698)
> [!WARNING]
> Cooldown could not be applied because no publication date was
available from the registry.
>

Bumps the ubuntu group with 1 update in the /docker/base directory:
ubuntu.
Bumps the ubuntu group with 1 update in the /docker/unoserver directory:
ubuntu.

Updates `ubuntu` from `561618e` to `33ceb71`

Updates `ubuntu` from `561618e` to `33ceb71`

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-30 10:13:31 +00:00
stirlingbot[bot] 1bb6961414 Update Frontend 3rd Party Licenses (#7738)
Auto-generated by stirlingbot[bot]

This PR updates the frontend license report based on changes to
package.json dependencies.

Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-08-30 10:12:56 +00:00
Ludy 124817c774 Merge branch 'main' into format_java 2026-08-30 12:08:51 +02:00
Ludy 7b5a847b58 Merge branch 'main' into format_java 2026-08-25 15:41:22 +02:00
Ludy 2b3349b24a Merge branch 'main' into format_java 2026-08-23 13:22:03 +02:00
Ludy87 d48bcab412 Update FontEmbeddingService.java 2026-08-23 02:25:33 +02:00
Ludy87 de4d769ec5 Update spotless.gradle 2026-08-23 02:19:30 +02:00
Ludy dde9259ee4 Merge branch 'main' into format_java 2026-08-23 01:32:35 +02:00
Ludy87 12cac259f8 Update settings.json 2026-08-23 01:24:26 +02:00
Ludy87 4f69946a43 Format Java sources and guard symlink test
Updated the Java formatter to Google Java Format 1.35.0 and enabled long-string reflow/skip-javadoc formatting in Spotless, then reformatted a large set of Java sources across app/common, app/core, and app/proprietary for consistent wrapping and readability. Also hardened the folder identity test to skip symbolic-link checks when the runner does not support them, and refreshed the VS Code Java settings to match the newer formatter.
2026-08-23 01:21:26 +02:00
269 changed files with 1939 additions and 1478 deletions
+2 -2
View File
@@ -182,7 +182,7 @@ jobs:
fetch-depth: 0 # Fetch full history for commit hash detection
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Get version number
id: versionNumber
@@ -353,7 +353,7 @@ jobs:
- name: Install Task for Storybook
if: steps.sb-changes.outputs.storybook == 'true'
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Build and deploy Storybook
id: storybook
@@ -206,7 +206,7 @@ jobs:
distribution: "temurin"
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Run Gradle Command
run: |
if [ "${{ needs.check-comment.outputs.disable_security }}" == "true" ]; then
@@ -222,7 +222,7 @@ jobs:
STIRLING_PDF_DESKTOP_UI: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
+2 -2
View File
@@ -27,7 +27,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
with:
egress-policy: audit
@@ -45,7 +45,7 @@ jobs:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
engine/uv.lock
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Quality-check engine
id: engine-check
+1 -1
View File
@@ -52,7 +52,7 @@ jobs:
distribution: "temurin"
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Check Java formatting (Spotless)
# Runs once per matrix combination - pick the cheapest leg
# (core - no proprietary, no saas) so we don't wait for the
+1 -1
View File
@@ -95,7 +95,7 @@ jobs:
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Install Playwright (chromium only)
run: task e2e:install -- chromium
- name: Build frontend (needed for playwright's vite preview webServer)
+1 -1
View File
@@ -75,7 +75,7 @@ jobs:
cache-dependency-path: frontend/package-lock.json
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Verify generated models are up to date
id: models-check
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
distribution: "temurin"
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Check licenses for compatibility
run: task backend:licenses:check
env:
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
distribution: "temurin"
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Generate OpenAPI documentation
run: task backend:swagger
env:
+1 -1
View File
@@ -57,7 +57,7 @@ jobs:
# runtime token isn't exposed) since the docker driver can't use it.
- name: Set up Docker Buildx
if: inputs.docker-base-changed != 'true'
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
# Expose ACTIONS_RUNTIME_TOKEN / ACTIONS_RESULTS_URL for docker buildx type=gha cache backend.
- name: Expose GitHub runtime for Buildx cache
+1 -1
View File
@@ -45,7 +45,7 @@ jobs:
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Install Playwright (chromium only)
run: task e2e:install -- chromium
- name: Build frontend (production bundle for vite preview)
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Build frontend (production bundle for vite preview)
env:
VITE_BUILD_FOR_PREVIEW: "1"
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: a11y gate (changed stories)
run: task frontend:storybook:a11y:changed -- origin/${{ github.base_ref || 'main' }}
- name: Upload scan reports
@@ -97,7 +97,7 @@ jobs:
run: npm ci --ignore-scripts --audit=false --fund=false
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Generate frontend license report (Push only)
if: github.event_name == 'push'
@@ -367,7 +367,7 @@ jobs:
distribution: "temurin"
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Check licenses and generate report
id: license-check
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Quality-check frontend
id: frontend-check
run: task frontend:check:all
+3 -3
View File
@@ -69,7 +69,7 @@ jobs:
distribution: "temurin"
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Get version number
id: versionNumber
run: |
@@ -169,7 +169,7 @@ jobs:
cache-dependency-path: frontend/package-lock.json
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Build JAR
run: ./gradlew build ${{ matrix.variant.build_frontend && '-PbuildWithFrontend=true' || '' }} -x spotlessApply -x spotlessCheck -x test -x sonarqube
@@ -268,7 +268,7 @@ jobs:
distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }}
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
# Build the universal JRE before desktop:prepare so the jlink:runtime
# task short-circuits on its `test -d runtime/jre` status check.
+3 -3
View File
@@ -38,7 +38,7 @@ jobs:
cache-dependency-path: frontend/package-lock.json
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Install all Playwright browsers
run: task e2e:install
@@ -89,7 +89,7 @@ jobs:
cache-dependency-path: frontend/package-lock.json
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: a11y gate (every story, ${{ matrix.theme }})
run: task frontend:storybook:a11y:${{ matrix.theme }}
@@ -162,7 +162,7 @@ jobs:
engine/uv.lock
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Start the fat image with login and storage enabled
run: docker compose -f docker/embedded/compose/test_cicd.yml up -d --build
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
engine/uv.lock
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Run pre-commit checks
run: task pre-commit
+1 -1
View File
@@ -69,7 +69,7 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Set up QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
+2 -2
View File
@@ -85,10 +85,10 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Get version number
id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
+1 -1
View File
@@ -75,6 +75,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
uses: github/codeql-action/upload-sarif@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
with:
sarif_file: results.sarif
+1 -1
View File
@@ -63,7 +63,7 @@ jobs:
SWAGGERHUB_USER: "Frooodle"
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Get version number
id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
+1 -1
View File
@@ -65,7 +65,7 @@ jobs:
uv sync --project engine --locked --group tools
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Sync translation TOML files
run: |
+1 -1
View File
@@ -212,7 +212,7 @@ jobs:
distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }}
- name: Setup Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Build universal macOS JRE
if: matrix.platform == 'macos-15'
+3 -3
View File
@@ -127,7 +127,7 @@ jobs:
distribution: "temurin"
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
- name: Build application
run: task backend:build
env:
@@ -142,7 +142,7 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Set base image and platform for this build
id: build-params
@@ -229,7 +229,7 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Build docker/unoserver/Dockerfile
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
+1 -1
View File
@@ -42,7 +42,7 @@
"java.configuration.updateBuildConfiguration": "interactive",
"java.format.enabled": true,
"java.format.settings.profile": "GoogleStyle",
"java.format.settings.google.version": "1.28.0",
"java.format.settings.google.version": "1.35.0",
"java.format.settings.google.extra": "--aosp --skip-sorting-imports --skip-javadoc-formatting",
// (DE) Aktiviert Kommentare im Java-Format.
// (EN) Enables comments in Java formatting.
@@ -174,7 +174,8 @@ public class EndpointConfiguration {
&& disabledGroups.contains(group)
&& entry.getValue().contains(endpoint)) {
log.debug(
"isEndpointEnabled('{}') -> false (single tool group '{}' disabled, no alternatives)",
"isEndpointEnabled('{}') -> false (single tool group '{}' disabled, no"
+ " alternatives)",
original,
group);
return false;
@@ -333,7 +334,8 @@ public class EndpointConfiguration {
String.join(", ", functionallyDisabledEndpoints));
} else if (!disabledToolGroups.isEmpty()) {
log.info(
"No endpoints disabled despite missing tools - fallback implementations available");
"No endpoints disabled despite missing tools - fallback implementations"
+ " available");
}
}
@@ -85,7 +85,8 @@ public class AutoJobAspect {
return joinPoint.proceed(args);
} catch (Throwable ex) {
log.error(
"AutoJobAspect caught exception during job execution: {}",
"AutoJobAspect caught exception during job execution:"
+ " {}",
ex.getMessage(),
ex);
// Rethrow RuntimeException as-is to preserve exception type
@@ -165,8 +166,8 @@ public class AutoJobAspect {
} catch (Throwable ex) {
lastException = ex;
log.error(
"AutoJobAspect caught exception during job execution (attempt"
+ " {}/{}): {}",
"AutoJobAspect caught exception during job execution"
+ " (attempt {}/{}): {}",
currentAttempt,
maxRetries,
ex.getMessage(),
@@ -183,7 +184,8 @@ public class AutoJobAspect {
String jobId = jobIdRef.get();
if (jobId != null) {
log.debug(
"Recording retry attempt for job {} in TaskManager",
"Recording retry attempt for job {} in"
+ " TaskManager",
jobId);
// Retry info is tracked in TaskManager for REST API
// access
@@ -43,9 +43,9 @@ public class ClusterConfig {
} else if ("inprocess".equalsIgnoreCase(backplane)) {
// enabled+inprocess only coordinates the local JVM; cross-node lookups will 410.
log.warn(
"cluster.enabled=true with backplane=inprocess - only the local"
+ " JVM is coordinated. Cross-node lookups and the file proxy will fail."
+ " Use backplane=valkey for real multi-node deployments.");
"cluster.enabled=true with backplane=inprocess - only the local JVM is"
+ " coordinated. Cross-node lookups and the file proxy will fail. Use"
+ " backplane=valkey for real multi-node deployments.");
} else {
// Fail fast on typos like "valky" so Spring doesn't later report a cryptic
// "no ClusterBackplane bean" - the operator-facing error names the bad value.
@@ -230,12 +230,14 @@ public class RuntimePathConfig {
// Check if one path is a parent of the other
if (path1.startsWith(path2)) {
log.warn(
"Watched folder path '{}' is nested inside '{}' - this may cause duplicate processing",
"Watched folder path '{}' is nested inside '{}' - this may cause"
+ " duplicate processing",
path1,
path2);
} else if (path2.startsWith(path1)) {
log.warn(
"Watched folder path '{}' is nested inside '{}' - this may cause duplicate processing",
"Watched folder path '{}' is nested inside '{}' - this may cause"
+ " duplicate processing",
path2,
path1);
}
@@ -253,21 +255,24 @@ public class RuntimePathConfig {
// Check if watched folder is same as finished folder
if (watchedPath.equals(finishedPath)) {
log.error(
"CRITICAL: Watched folder '{}' is the same as finished folder '{}' - this will cause processing loops!",
"CRITICAL: Watched folder '{}' is the same as finished folder '{}' -"
+ " this will cause processing loops!",
watchedPath,
finishedPath);
}
// Check if watched folder contains finished folder
else if (finishedPath.startsWith(watchedPath)) {
log.warn(
"Finished folder '{}' is nested inside watched folder '{}' - this may cause issues",
"Finished folder '{}' is nested inside watched folder '{}' - this may"
+ " cause issues",
finishedPath,
watchedPath);
}
// Check if finished folder contains watched folder
else if (watchedPath.startsWith(finishedPath)) {
log.error(
"CRITICAL: Watched folder '{}' is nested inside finished folder '{}' - this will cause processing loops!",
"CRITICAL: Watched folder '{}' is nested inside finished folder '{}' -"
+ " this will cause processing loops!",
watchedPath,
finishedPath);
}
@@ -295,15 +300,17 @@ public class RuntimePathConfig {
// Warn if manual endpoint count doesn't match sessionLimit
if (configured.size() != sessionLimit) {
log.warn(
"Manual UNO endpoint count ({}) differs from libreOfficeSessionLimit ({}). "
+ "Concurrency will be limited by endpoint count, not sessionLimit.",
"Manual UNO endpoint count ({}) differs from libreOfficeSessionLimit"
+ " ({}). Concurrency will be limited by endpoint count, not"
+ " sessionLimit.",
configured.size(),
sessionLimit);
}
return configured;
}
log.warn(
"autoUnoServer disabled but no unoServerEndpoints configured; defaulting to 127.0.0.1:2003.");
"autoUnoServer disabled but no unoServerEndpoints configured; defaulting to"
+ " 127.0.0.1:2003.");
return Collections.singletonList(
new ApplicationProperties.ProcessExecutor.UnoServerEndpoint());
}
@@ -144,7 +144,8 @@ public class ApplicationProperties {
sizeInMB);
} else {
log.warn(
"SYSTEM_MAXFILESIZE value {} is out of valid range (1-999), ignoring",
"SYSTEM_MAXFILESIZE value {} is out of valid range (1-999),"
+ " ignoring",
sizeInMB);
}
} catch (NumberFormatException e) {
@@ -24,7 +24,8 @@ public class PDFFile {
@Schema(
description =
"File ID for server-side files (can be used instead of fileInput if job was previously done on file in async mode)")
"File ID for server-side files (can be used instead of fileInput if job was"
+ " previously done on file in async mode)")
private String fileId;
@AssertTrue(message = "Either fileInput or fileId must be provided")
@@ -209,7 +209,8 @@ public class ResourceMonitor {
return (double) m.invoke(osMXBean);
} catch (Exception e2) {
log.trace(
"Could not get CPU load through reflection, assuming moderate load (0.5)");
"Could not get CPU load through reflection, assuming moderate load"
+ " (0.5)");
return 0.5;
}
}
@@ -167,7 +167,8 @@ public class TempFileCleanupService {
|| unregisteredDeletedCount > 0
|| directoriesDeletedCount > 0) {
log.info(
"Scheduled cleanup complete. Deleted {} registered files, {} unregistered files, {} directories",
"Scheduled cleanup complete. Deleted {} registered files, {} unregistered"
+ " files, {} directories",
registeredDeletedCount,
unregisteredDeletedCount,
directoriesDeletedCount);
@@ -252,7 +253,8 @@ public class TempFileCleanupService {
dirDeletedCount.incrementAndGet();
if (log.isDebugEnabled()) {
log.debug(
"Deleted temp file during {} cleanup: {}",
"Deleted temp file during {} cleanup:"
+ " {}",
phase,
path);
}
@@ -41,7 +41,8 @@ public class AttachmentUtils {
viewerPrefs.setBoolean(COSName.getPDFName("DisplayDocTitle"), true);
log.info(
"Set PDF PageMode to UseAttachments to automatically show attachments pane");
"Set PDF PageMode to UseAttachments to automatically show attachments"
+ " pane");
}
} catch (Exception e) {
log.error("Failed to set catalog viewer preferences for attachments", e);
@@ -342,26 +342,26 @@ public class EmlProcessingUtils {
private String getFallbackStyles() {
return """
/* Minimal fallback - main CSS resource failed to load */
body {
font-family: var(--font-family, Helvetica, sans-serif);
font-size: var(--font-size, 12px);
line-height: var(--line-height, 1.4);
color: var(--text-color, #202124);
margin: 0;
padding: 20px;
word-wrap: break-word;
}
.email-container { max-width: 100%; }
.email-header { border-bottom: 1px solid #ccc; margin-bottom: 16px; padding-bottom: 12px; }
.email-header h1 { margin: 0 0 8px 0; font-size: 18px; }
.email-meta { font-size: 12px; color: #666; }
.email-body { line-height: 1.6; }
.attachment-section { margin-top: 20px; padding: 12px; background: #f5f5f5; border-radius: 4px; }
.attachment-item { padding: 6px 0; border-bottom: 1px solid #ddd; }
.no-content { padding: 20px; text-align: center; color: #888; font-style: italic; }
img { max-width: 100%; height: auto; }
""";
/* Minimal fallback - main CSS resource failed to load */
body {
font-family: var(--font-family, Helvetica, sans-serif);
font-size: var(--font-size, 12px);
line-height: var(--line-height, 1.4);
color: var(--text-color, #202124);
margin: 0;
padding: 20px;
word-wrap: break-word;
}
.email-container { max-width: 100%; }
.email-header { border-bottom: 1px solid #ccc; margin-bottom: 16px; padding-bottom: 12px; }
.email-header h1 { margin: 0 0 8px 0; font-size: 18px; }
.email-meta { font-size: 12px; color: #666; }
.email-body { line-height: 1.6; }
.attachment-section { margin-top: 20px; padding: 12px; background: #f5f5f5; border-radius: 4px; }
.attachment-item { padding: 6px 0; border-bottom: 1px solid #ddd; }
.no-content { padding: 20px; text-align: center; color: #888; font-style: italic; }
img { max-width: 100%; height: auto; }
""";
}
private void appendAttachmentsSection(
@@ -290,7 +290,8 @@ public class ExceptionUtils {
// Additional safety check: warn about very large images (> 1GB estimated)
if (estimatedBytes > 1024L * 1024 * 1024) {
log.warn(
"Page {} will create a very large image: {}x{} pixels (~{} MB) at {} DPI. This may cause memory issues.",
"Page {} will create a very large image: {}x{} pixels (~{} MB) at {} DPI. This"
+ " may cause memory issues.",
pageNumber,
widthInPixels,
heightInPixels,
@@ -394,7 +395,8 @@ public class ExceptionUtils {
message = getMessage(contextKey, defaultMsg, context);
} else {
message =
"PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.";
"PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF'"
+ " feature first to fix the file before proceeding with this operation.";
}
return new PdfCorruptedException(message, cause, ErrorCode.PDF_CORRUPTED.getCode());
@@ -1119,19 +1121,25 @@ public class ExceptionUtils {
PDF_CORRUPTED(
"E001",
"error.pdfCorrupted",
"PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation."),
"PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF'"
+ " feature first to fix the file before proceeding with this operation."),
PDF_MULTIPLE_CORRUPTED(
"E002",
"error.multiplePdfCorrupted",
"One or more PDF files appear to be corrupted or damaged. Please try using the 'Repair PDF' feature on each file first before attempting to merge them."),
"One or more PDF files appear to be corrupted or damaged. Please try using the"
+ " 'Repair PDF' feature on each file first before attempting to merge them."),
PDF_ENCRYPTION(
"E003",
"error.pdfEncryption",
"The PDF appears to have corrupted encryption data. This can happen when the PDF was created with incompatible encryption methods. Please try using the 'Repair PDF' feature first, or contact the document creator for a new copy."),
"The PDF appears to have corrupted encryption data. This can happen when the PDF"
+ " was created with incompatible encryption methods. Please try using the"
+ " 'Repair PDF' feature first, or contact the document creator for a new"
+ " copy."),
PDF_PASSWORD(
"E004",
"error.pdfPassword",
"The PDF Document is passworded and either the password was not provided or was incorrect"),
"The PDF Document is passworded and either the password was not provided or was"
+ " incorrect"),
PDF_NO_PAGES("E005", "error.pdfNoPages", "PDF file contains no pages"),
PDF_NOT_PDF("E006", "error.notPdfFile", "File must be in PDF format"),
@@ -1139,20 +1147,25 @@ public class ExceptionUtils {
CBR_INVALID_FORMAT(
"E010",
"error.cbrInvalidFormat",
"Invalid or corrupted CBR/RAR archive. The file may be corrupted, use an unsupported RAR format (RAR5+), encrypted, or may not be a valid RAR archive."),
"Invalid or corrupted CBR/RAR archive. The file may be corrupted, use an"
+ " unsupported RAR format (RAR5+), encrypted, or may not be a valid RAR"
+ " archive."),
CBR_NO_IMAGES(
"E012",
"error.cbrNoImages",
"No valid images found in the CBR file. The archive may be empty, or all images may be corrupted or in unsupported formats."),
"No valid images found in the CBR file. The archive may be empty, or all images may"
+ " be corrupted or in unsupported formats."),
CBR_NOT_CBR("E014", "error.notCbrFile", "File must be a CBR or RAR archive"),
CBZ_INVALID_FORMAT(
"E015",
"error.cbzInvalidFormat",
"Invalid or corrupted CBZ/ZIP archive. The file may be empty, corrupted, or may not be a valid ZIP archive."),
"Invalid or corrupted CBZ/ZIP archive. The file may be empty, corrupted, or may not"
+ " be a valid ZIP archive."),
CBZ_NO_IMAGES(
"E016",
"error.cbzNoImages",
"No valid images found in the CBZ file. The archive may be empty, or all images may be corrupted or in unsupported formats."),
"No valid images found in the CBZ file. The archive may be empty, or all images may"
+ " be corrupted or in unsupported formats."),
CBZ_NOT_CBZ("E018", "error.notCbzFile", "File must be a CBZ or ZIP archive"),
// EML errors
@@ -1205,7 +1218,8 @@ public class ExceptionUtils {
FFMPEG_REQUIRED(
"E063",
"error.ffmpegRequired",
"FFmpeg must be installed to convert PDFs to video. Install FFmpeg and ensure it is available on the system PATH."),
"FFmpeg must be installed to convert PDFs to video. Install FFmpeg and ensure it is"
+ " available on the system PATH."),
// Validation errors
INVALID_ARGUMENT("E070", "error.invalidArgument", "Invalid argument ''{0}'': {1}"),
@@ -1221,7 +1235,10 @@ public class ExceptionUtils {
OUT_OF_MEMORY_DPI(
"E081",
"error.outOfMemoryDpi",
"Out of memory or image-too-large error while rendering PDF page {0} at {1} DPI. This can occur when the resulting image exceeds Java's array/memory limits (e.g., NegativeArraySizeException). Please use a lower DPI value (recommended: 150 or less) or process the document in smaller chunks.");
"Out of memory or image-too-large error while rendering PDF page {0} at {1} DPI."
+ " This can occur when the resulting image exceeds Java's array/memory limits"
+ " (e.g., NegativeArraySizeException). Please use a lower DPI value"
+ " (recommended: 150 or less) or process the document in smaller chunks.");
private final String code;
private final String messageKey;
@@ -456,7 +456,8 @@ public class FormUtils {
|| !Float.isFinite(finalW)
|| !Float.isFinite(finalH)) {
log.warn(
"Widget coordinates are not finite for field '{}': page={}, x={}, y={}, w={}, h={}",
"Widget coordinates out of bounds for field '{}': page={}, x={}, y={}, w={},"
+ " h={}",
field.getFullyQualifiedName(),
pageIndex,
finalX,
@@ -392,9 +392,9 @@ public class PdfUtils {
&& e.getMessage().contains("Maximum size of image exceeded")) {
throw ExceptionUtils.createIllegalArgumentException(
"error.pageTooBigFor300Dpi",
"PDF page {0} is too large to render at 300 DPI. The resulting image"
+ " would exceed Java's maximum array size. Please use a lower DPI"
+ " value for PDF-to-image conversion.",
"PDF page {0} is too large to render at 300 DPI. The resulting"
+ " image would exceed Java's maximum array size. Please use a"
+ " lower DPI value for PDF-to-image conversion.",
pageIndex + 1);
}
throw e;
@@ -253,7 +253,8 @@ public class ProcessExecutor {
}
} catch (InterruptedIOException e) {
log.warn(
"Error reader thread was interrupted due to timeout.");
"Error reader thread was interrupted due to"
+ " timeout.");
} catch (IOException e) {
log.error("exception", e);
}
@@ -278,7 +279,8 @@ public class ProcessExecutor {
}
} catch (InterruptedIOException e) {
log.warn(
"Error reader thread was interrupted due to timeout.");
"Error reader thread was interrupted due to"
+ " timeout.");
} catch (IOException e) {
log.error("exception", e);
}
@@ -237,6 +237,7 @@ class ApplicationPropertiesLogicTest {
assertTrue(
oauth2.isValid(oneBlank, "scopes"),
"Dokumentiert aktuelles Verhalten: nicht-leere Liste gilt als gültig, auch wenn Element leer/blank ist");
"Dokumentiert aktuelles Verhalten: nicht-leere Liste gilt als gültig, auch wenn"
+ " Element leer/blank ist");
}
}
@@ -130,7 +130,8 @@ class PdfMarkdownConverterTest {
if (similarity < THRESHOLD) {
fail(
String.format(
"Markdown output differs from golden file '%s' by %.1f%% (threshold %.0f%%):%n%s",
"Markdown output differs from golden file '%s' by %.1f%% (threshold"
+ " %.0f%%):%n%s",
mdName,
(1.0 - similarity) * 100,
(1.0 - THRESHOLD) * 100,
@@ -60,10 +60,10 @@ class CustomHtmlSanitizerTest {
new String[] {"<p>", "<strong>", "<em>"}),
Arguments.of(
"<p>Text with <b>bold</b>, <i>italic</i>, <u>underline</u>,"
+ " <em>emphasis</em>, <strong>strong</strong>,"
+ " <strike>strikethrough</strike>, <s>strike</s>,"
+ " <sub>subscript</sub>, <sup>superscript</sup>, <tt>teletype</tt>,"
+ " <code>code</code>, <big>big</big>, <small>small</small>.</p>",
+ " <em>emphasis</em>, <strong>strong</strong>,"
+ " <strike>strikethrough</strike>, <s>strike</s>,"
+ " <sub>subscript</sub>, <sup>superscript</sup>, <tt>teletype</tt>,"
+ " <code>code</code>, <big>big</big>, <small>small</small>.</p>",
new String[] {
"<b>bold</b>",
"<i>italic</i>",
@@ -271,8 +271,8 @@ class CustomHtmlSanitizerTest {
// Arrange
String htmlWithObjects =
"<p>Safe content</p><object data=\"data.swf\""
+ " type=\"application/x-shockwave-flash\"></object><embed src=\"embed.swf\""
+ " type=\"application/x-shockwave-flash\">";
+ " type=\"application/x-shockwave-flash\"></object><embed src=\"embed.swf\""
+ " type=\"application/x-shockwave-flash\">";
// Act
String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithObjects);
@@ -309,11 +309,11 @@ class CustomHtmlSanitizerTest {
// Arrange
String complexHtml =
"<div class=\"container\"> <h1 style=\"color: blue;\">Welcome</h1> <p>This is a"
+ " <strong>test</strong> with <a href=\"https://example.com\">link</a>.</p> "
+ " <table> <tr><th>Name</th><th>Value</th></tr> <tr><td>Item"
+ " 1</td><td>100</td></tr> </table> <img src=\"image.jpg\" alt=\"Test"
+ " image\"> <script>alert('XSS');</script> <iframe"
+ " src=\"https://evil.com\"></iframe></div>";
+ " <strong>test</strong> with <a href=\"https://example.com\">link</a>.</p> "
+ " <table> <tr><th>Name</th><th>Value</th></tr> <tr><td>Item"
+ " 1</td><td>100</td></tr> </table> <img src=\"image.jpg\" alt=\"Test"
+ " image\"> <script>alert('XSS');</script> <iframe"
+ " src=\"https://evil.com\"></iframe></div>";
// Act
String sanitizedHtml = customHtmlSanitizer.sanitize(complexHtml);
@@ -120,10 +120,10 @@ class EmlToPdfTest {
void parseHtmlEmailWithStyling() throws IOException {
String htmlBody =
"<html><head><style>.header{color:blue;font-weight:bold;}"
+ ".content{margin:10px;}.footer{font-size:12px;}</style></head>"
+ "<body><div class=\"header\">Important Notice</div>"
+ "<div class=\"content\">This is <strong>HTML content</strong> with styling.</div>"
+ "<div class=\"footer\">Best regards</div></body></html>";
+ ".content{margin:10px;}.footer{font-size:12px;}</style></head><body><div"
+ " class=\"header\">Important Notice</div><div class=\"content\">This is"
+ " <strong>HTML content</strong> with styling.</div><div"
+ " class=\"footer\">Best regards</div></body></html>";
String emlContent =
createHtmlEmail(
@@ -286,11 +286,13 @@ class EmlToPdfTest {
@DisplayName("Should handle complex nested HTML structures")
void handleComplexNestedHtml() throws IOException {
String complexHtml =
"<html><head><title>Complex Email</title></head><body>"
+ "<div class=\"container\"><header><h1>Email Header</h1></header><main><section>"
+ "<p>Paragraph with <a href=\"https://example.com\">link</a></p><ul>"
+ "<li>List item 1</li><li>List item 2 with <em>emphasis</em></li></ul><table>"
+ "<tr><td>Cell 1</td><td>Cell 2</td></tr><tr><td>Cell 3</td><td>Cell 4</td></tr>"
"<html><head><title>Complex Email</title></head><body><div"
+ " class=\"container\"><header><h1>Email"
+ " Header</h1></header><main><section><p>Paragraph with <a"
+ " href=\"https://example.com\">link</a></p><ul><li>List item"
+ " 1</li><li>List item 2 with"
+ " <em>emphasis</em></li></ul><table><tr><td>Cell 1</td><td>Cell"
+ " 2</td></tr><tr><td>Cell 3</td><td>Cell 4</td></tr>"
+ "</table></section></main></div></body></html>";
String emlContent =
@@ -346,7 +348,8 @@ class EmlToPdfTest {
This line breaks header format
Content-Type: text/plain
Body content""";
Body content\
""";
byte[] emlBytes = malformedEml.getBytes(StandardCharsets.UTF_8);
EmlToPdfRequest request = createBasicRequest();
@@ -781,7 +784,13 @@ class EmlToPdfTest {
String from, String to, String subject, String body, String charset) {
return String.format(
Locale.ROOT,
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nContent-Type: text/plain; charset=%s\nContent-Transfer-Encoding: 8bit\n\n%s",
"From: %s\n"
+ "To: %s\n"
+ "Subject: %s\n"
+ "Date: %s\n"
+ "Content-Type: text/plain; charset=%s\n"
+ "Content-Transfer-Encoding: 8bit\n\n"
+ "%s",
from,
to,
subject,
@@ -793,7 +802,11 @@ class EmlToPdfTest {
private String createEmailWithCustomHeaders() {
return String.format(
Locale.ROOT,
"From: sender@example.com\nDate: %s\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\n\n%s",
"From: sender@example.com\n"
+ "Date: %s\n"
+ "Content-Type: text/plain; charset=UTF-8\n"
+ "Content-Transfer-Encoding: 8bit\n\n"
+ "%s",
getTimestamp(),
"This is an email body with some headers missing.");
}
@@ -801,7 +814,13 @@ class EmlToPdfTest {
private String createHtmlEmail(String from, String to, String subject, String htmlBody) {
return String.format(
Locale.ROOT,
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nContent-Type: text/html; charset=UTF-8\nContent-Transfer-Encoding: 8bit\n\n%s",
"From: %s\n"
+ "To: %s\n"
+ "Subject: %s\n"
+ "Date: %s\n"
+ "Content-Type: text/html; charset=UTF-8\n"
+ "Content-Transfer-Encoding: 8bit\n\n"
+ "%s",
from,
to,
subject,
@@ -823,26 +842,27 @@ class EmlToPdfTest {
return String.format(
Locale.ROOT,
"""
From: %s
To: %s
Subject: %s
Date: %s
Content-Type: multipart/mixed; boundary="%s"
From: %s
To: %s
Subject: %s
Date: %s
Content-Type: multipart/mixed; boundary="%s"
--%s
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
--%s
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
%s
%s
--%s
Content-Type: text/plain; charset=UTF-8
Content-Disposition: attachment; filename="%s"
Content-Transfer-Encoding: base64
--%s
Content-Type: text/plain; charset=UTF-8
Content-Disposition: attachment; filename="%s"
Content-Transfer-Encoding: base64
%s
%s
--%s--""",
--%s--\
""",
from,
to,
subject,
@@ -863,26 +883,27 @@ class EmlToPdfTest {
return String.format(
Locale.ROOT,
"""
From: %s
To: %s
Subject: %s
Date: %s
Content-Type: multipart/mixed; boundary="%s"
From: %s
To: %s
Subject: %s
Date: %s
Content-Type: multipart/mixed; boundary="%s"
--%s
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
--%s
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
%s
%s
--%s
Content-Type: message/rfc822; name="%s"
Content-Disposition: attachment; filename="%s"
Content-Transfer-Encoding: base64
--%s
Content-Type: message/rfc822; name="%s"
Content-Disposition: attachment; filename="%s"
Content-Transfer-Encoding: base64
%s
%s
--%s--""",
--%s--\
""",
"outer@example.com",
"outer_recipient@example.com",
"Fwd: Inner Email Subject",
@@ -902,26 +923,27 @@ class EmlToPdfTest {
return String.format(
Locale.ROOT,
"""
From: %s
To: %s
Subject: %s
Date: %s
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="%s"
From: %s
To: %s
Subject: %s
Date: %s
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="%s"
--%s
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 7bit
--%s
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 7bit
%s
%s
--%s
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: 7bit
--%s
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: 7bit
%s
%s
--%s--""",
--%s--\
""",
"sender@example.com",
"receiver@example.com",
"Multipart/Alternative Test",
@@ -937,7 +959,14 @@ class EmlToPdfTest {
private String createQuotedPrintableEmail() {
return String.format(
Locale.ROOT,
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: quoted-printable\n\n%s",
"From: %s\n"
+ "To: %s\n"
+ "Subject: %s\n"
+ "Date: %s\n"
+ "MIME-Version: 1.0\n"
+ "Content-Type: text/plain; charset=UTF-8\n"
+ "Content-Transfer-Encoding: quoted-printable\n\n"
+ "%s",
"sender@example.com",
"recipient@example.com",
"Quoted-Printable Test",
@@ -950,7 +979,14 @@ class EmlToPdfTest {
Base64.getEncoder().encodeToString(body.getBytes(StandardCharsets.UTF_8));
return String.format(
Locale.ROOT,
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: base64\n\n%s",
"From: %s\n"
+ "To: %s\n"
+ "Subject: %s\n"
+ "Date: %s\n"
+ "MIME-Version: 1.0\n"
+ "Content-Type: text/plain; charset=UTF-8\n"
+ "Content-Transfer-Encoding: base64\n\n"
+ "%s",
"sender@example.com",
"recipient@example.com",
"Base64 Test",
@@ -963,27 +999,28 @@ class EmlToPdfTest {
return String.format(
Locale.ROOT,
"""
From: %s
To: %s
Subject: %s
Date: %s
Content-Type: multipart/related; boundary="%s"
From: %s
To: %s
Subject: %s
Date: %s
Content-Type: multipart/related; boundary="%s"
--%s
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: 8bit
--%s
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: 8bit
%s
%s
--%s
Content-Type: image/png
Content-Transfer-Encoding: base64
Content-ID: <%s>
Content-Disposition: inline; filename="image.png"
--%s
Content-Type: image/png
Content-Transfer-Encoding: base64
Content-ID: <%s>
Content-Disposition: inline; filename="image.png"
%s
%s
--%s--""",
--%s--\
""",
"sender@example.com",
"receiver@example.com",
"Inline Image Test",
@@ -1008,39 +1045,40 @@ class EmlToPdfTest {
return String.format(
Locale.ROOT,
"""
From: %s
To: %s
Subject: %s
Date: %s
Content-Type: multipart/mixed; boundary="%s"
From: %s
To: %s
Subject: %s
Date: %s
Content-Type: multipart/mixed; boundary="%s"
--%s
Content-Type: multipart/related; boundary="related-%s"
--%s
Content-Type: multipart/related; boundary="related-%s"
--related-%s
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: 8bit
--related-%s
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: 8bit
%s
%s
--related-%s
Content-Type: image/png
Content-Transfer-Encoding: base64
Content-ID: <%s>
Content-Disposition: inline; filename="image.png"
--related-%s
Content-Type: image/png
Content-Transfer-Encoding: base64
Content-ID: <%s>
Content-Disposition: inline; filename="image.png"
%s
%s
--related-%s--
--related-%s--
--%s
Content-Type: text/plain; charset=UTF-8
Content-Disposition: attachment; filename="%s"
Content-Transfer-Encoding: base64
--%s
Content-Type: text/plain; charset=UTF-8
Content-Disposition: attachment; filename="%s"
Content-Transfer-Encoding: base64
%s
%s
--%s--""",
--%s--\
""",
"sender@example.com",
"receiver@example.com",
"Mixed Attachments Test",
@@ -31,21 +31,22 @@ class OfficeDocumentSanitizerTest {
private static final String INTERNAL_TARGET = "media/image1.png";
private static final String DOCX_RELS =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ "<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">"
+ "<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\""
+ " Target=\""
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><Relationships"
+ " xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"><Relationship"
+ " Id=\"rId1\""
+ " Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\""
+ " Target=\""
+ EXTERNAL_URL
+ "\" TargetMode=\"External\"/>"
+ "<Relationship Id=\"rId2\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\""
+ "\" TargetMode=\"External\"/><Relationship Id=\"rId2\""
+ " Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\""
+ " Target=\""
+ INTERNAL_TARGET
+ "\"/>"
+ "</Relationships>";
private static final String DOCX_DOCUMENT =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ "<w:document xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\">"
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><w:document"
+ " xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\">"
+ "<w:body><w:p/></w:body></w:document>";
private static final String ODF_CONTENT_EXTERNAL =
@@ -57,8 +58,8 @@ class OfficeDocumentSanitizerTest {
+ "<office:body><office:text>"
+ "<draw:frame><draw:image xlink:href=\""
+ EXTERNAL_URL
+ "\" xlink:type=\"simple\"/></draw:frame>"
+ "<draw:frame><draw:image xlink:href=\"Pictures/image1.png\" xlink:type=\"simple\"/></draw:frame>"
+ "\" xlink:type=\"simple\"/></draw:frame><draw:frame><draw:image"
+ " xlink:href=\"Pictures/image1.png\" xlink:type=\"simple\"/></draw:frame>"
+ "</office:text></office:body></office:document-content>";
private SsrfProtectionService ssrfProtectionService;
@@ -113,10 +114,11 @@ class OfficeDocumentSanitizerTest {
@Test
void sanitize_pptxExternalImageRelStripped() throws IOException {
String pptxRels =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ "<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">"
+ "<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\""
+ " Target=\""
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><Relationships"
+ " xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"><Relationship"
+ " Id=\"rId1\""
+ " Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\""
+ " Target=\""
+ EXTERNAL_URL
+ "\" TargetMode=\"External\"/>"
+ "</Relationships>";
@@ -135,10 +137,11 @@ class OfficeDocumentSanitizerTest {
@Test
void sanitize_xlsxExternalImageRelStripped() throws IOException {
String xlsxRels =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ "<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">"
+ "<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\""
+ " Target=\""
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><Relationships"
+ " xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"><Relationship"
+ " Id=\"rId1\""
+ " Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\""
+ " Target=\""
+ EXTERNAL_URL
+ "\" TargetMode=\"External\"/>"
+ "</Relationships>";
@@ -162,7 +165,7 @@ class OfficeDocumentSanitizerTest {
entries.put("content.xml", ODF_CONTENT_EXTERNAL.getBytes(StandardCharsets.UTF_8));
String manifestXml =
"<?xml version=\"1.0\"?><manifest:manifest"
+ " xmlns:manifest=\"urn:oasis:names:tc:opendocument:xmlns:manifest:1.0\"/>";
+ " xmlns:manifest=\"urn:oasis:names:tc:opendocument:xmlns:manifest:1.0\"/>";
entries.put("META-INF/manifest.xml", manifestXml.getBytes(StandardCharsets.UTF_8));
byte[] odt = zip(entries);
@@ -294,11 +297,11 @@ class OfficeDocumentSanitizerTest {
@Test
void sanitize_internalLinksKeptWhenNoExternalPresent() throws IOException {
String internalOnlyRels =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ "<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">"
+ "<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\""
+ " Target=\"media/image1.png\"/>"
+ "</Relationships>";
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><Relationships"
+ " xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"><Relationship"
+ " Id=\"rId1\""
+ " Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\""
+ " Target=\"media/image1.png\"/></Relationships>";
Map<String, byte[]> entries = new LinkedHashMap<>();
entries.put(
"word/_rels/document.xml.rels", internalOnlyRels.getBytes(StandardCharsets.UTF_8));
@@ -249,7 +249,8 @@ class ProcessExecutorGapTest {
@Test
@DisplayName(
"injects --host/--port after the executable, defaults omit host-location and protocol")
"injects --host/--port after the executable, defaults omit host-location and"
+ " protocol")
void injectsHostAndPortWithDefaults() throws Exception {
List<String> command = List.of("unoconvert", "in.docx", "out.pdf");
ApplicationProperties.ProcessExecutor.UnoServerEndpoint ep =
@@ -38,7 +38,8 @@ class SvgSanitizerTest {
@Test
void testSanitize_removesScriptElement() throws IOException {
String svg =
"<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert('xss')</script><circle r=\"10\"/></svg>";
"<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert('xss')</script><circle"
+ " r=\"10\"/></svg>";
byte[] result = sanitizer.sanitize(svg.getBytes(StandardCharsets.UTF_8));
String output = new String(result, StandardCharsets.UTF_8);
assertFalse(output.contains("script"));
@@ -48,7 +49,8 @@ class SvgSanitizerTest {
@Test
void testSanitize_removesEventHandler() throws IOException {
String svg =
"<svg xmlns=\"http://www.w3.org/2000/svg\"><circle r=\"10\" onclick=\"alert('xss')\"/></svg>";
"<svg xmlns=\"http://www.w3.org/2000/svg\"><circle r=\"10\""
+ " onclick=\"alert('xss')\"/></svg>";
byte[] result = sanitizer.sanitize(svg.getBytes(StandardCharsets.UTF_8));
String output = new String(result, StandardCharsets.UTF_8);
assertFalse(output.contains("onclick"));
@@ -57,7 +59,8 @@ class SvgSanitizerTest {
@Test
void testSanitize_removesJavascriptUrl() throws IOException {
String svg =
"<svg xmlns=\"http://www.w3.org/2000/svg\"><a href=\"javascript:alert('xss')\"><circle r=\"10\"/></a></svg>";
"<svg xmlns=\"http://www.w3.org/2000/svg\"><a"
+ " href=\"javascript:alert('xss')\"><circle r=\"10\"/></a></svg>";
byte[] result = sanitizer.sanitize(svg.getBytes(StandardCharsets.UTF_8));
String output = new String(result, StandardCharsets.UTF_8);
assertFalse(output.contains("javascript"));
@@ -86,7 +89,8 @@ class SvgSanitizerTest {
@Test
void testSanitize_removesForeignObject() throws IOException {
String svg =
"<svg xmlns=\"http://www.w3.org/2000/svg\"><foreignObject><body>evil</body></foreignObject><rect width=\"10\" height=\"10\"/></svg>";
"<svg xmlns=\"http://www.w3.org/2000/svg\"><foreignObject><body>evil</body></foreignObject><rect"
+ " width=\"10\" height=\"10\"/></svg>";
byte[] result = sanitizer.sanitize(svg.getBytes(StandardCharsets.UTF_8));
String output = new String(result, StandardCharsets.UTF_8);
assertFalse(output.toLowerCase().contains("foreignobject"));
@@ -113,8 +117,8 @@ class SvgSanitizerTest {
void testSanitize_removesRelativeLocalPath() throws IOException {
when(ssrfProtectionService.isUrlAllowed(anyString())).thenReturn(false);
String svg =
"<svg xmlns=\"http://www.w3.org/2000/svg\">"
+ "<image href=\"../../assets/image.png\" width=\"10\" height=\"10\"/></svg>";
"<svg xmlns=\"http://www.w3.org/2000/svg\"><image href=\"../../assets/image.png\""
+ " width=\"10\" height=\"10\"/></svg>";
byte[] result = sanitizer.sanitize(svg.getBytes(StandardCharsets.UTF_8));
String output = new String(result, StandardCharsets.UTF_8);
assertFalse(output.contains("assets/image.png"), "Relative local path must be stripped");
@@ -36,7 +36,8 @@ public class ReplaceAndInvertColorFactory {
if (replaceAndInvertOption == ReplaceAndInvert.COLOR_SPACE_CONVERSION
&& !endpointConfiguration.isGroupEnabled("Ghostscript")) {
throw new IllegalStateException(
"CMYK color space conversion requires Ghostscript, which is not available on this system");
"CMYK color space conversion requires Ghostscript, which is not available on"
+ " this system");
}
return switch (replaceAndInvertOption) {
@@ -74,7 +74,8 @@ public class GlobalErrorResponseCustomizer implements GlobalOpenApiCustomizer {
private ApiResponse create400Response() {
return new ApiResponse()
.description(
"Bad request - Invalid input parameters, unsupported format, or corrupted file")
"Bad request - Invalid input parameters, unsupported format, or corrupted"
+ " file")
.content(
new Content()
.addMediaType(
@@ -83,12 +84,14 @@ public class GlobalErrorResponseCustomizer implements GlobalOpenApiCustomizer {
.schema(
createErrorSchema(
400,
"Invalid input parameters or corrupted file",
"Invalid input parameters or"
+ " corrupted file",
"/api/v1/example/endpoint"))
.example(
createErrorExample(
400,
"Invalid input parameters or corrupted file",
"Invalid input parameters or"
+ " corrupted file",
"/api/v1/example/endpoint"))));
}
@@ -103,12 +106,14 @@ public class GlobalErrorResponseCustomizer implements GlobalOpenApiCustomizer {
.schema(
createErrorSchema(
413,
"File size exceeds maximum allowed limit",
"File size exceeds maximum allowed"
+ " limit",
"/api/v1/example/endpoint"))
.example(
createErrorExample(
413,
"File size exceeds maximum allowed limit",
"File size exceeds maximum allowed"
+ " limit",
"/api/v1/example/endpoint"))));
}
@@ -123,12 +128,14 @@ public class GlobalErrorResponseCustomizer implements GlobalOpenApiCustomizer {
.schema(
createErrorSchema(
422,
"File is valid but cannot be processed",
"File is valid but cannot be"
+ " processed",
"/api/v1/example/endpoint"))
.example(
createErrorExample(
422,
"File is valid but cannot be processed",
"File is valid but cannot be"
+ " processed",
"/api/v1/example/endpoint"))));
}
@@ -143,12 +150,14 @@ public class GlobalErrorResponseCustomizer implements GlobalOpenApiCustomizer {
.schema(
createErrorSchema(
500,
"Unexpected error during processing",
"Unexpected error during"
+ " processing",
"/api/v1/example/endpoint"))
.example(
createErrorExample(
500,
"Unexpected error during processing",
"Unexpected error during"
+ " processing",
"/api/v1/example/endpoint"))));
}
@@ -51,7 +51,8 @@ public class LocaleConfiguration implements WebMvcConfigurer {
defaultLocale = tempLocale;
} else {
System.err.println(
"Invalid SYSTEM_DEFAULTLOCALE environment variable value. Falling back to default en-US.");
"Invalid SYSTEM_DEFAULTLOCALE environment variable value. Falling back"
+ " to default en-US.");
}
}
}
@@ -46,7 +46,12 @@ public class SpringDocConfig {
openApi.getInfo()
.title("Stirling PDF - Processing API")
.description(
"APIs for converting, editing, securing, and analysing PDF documents. Use these endpoints to automate common PDF tasks (like split, merge, convert, OCR) and plug them into your own apps and backend jobs."));
"APIs for converting, editing, securing, and"
+ " analysing PDF documents. Use these"
+ " endpoints to automate common PDF tasks"
+ " (like split, merge, convert, OCR) and"
+ " plug them into your own apps and"
+ " backend jobs."));
})
.build();
}
@@ -79,7 +84,9 @@ public class SpringDocConfig {
openApi.getInfo()
.title("Stirling PDF - Management API")
.description(
"Endpoints for authentication, user management, invitations, audit logging, and system configuration."));
"Endpoints for authentication, user management,"
+ " invitations, audit logging, and system"
+ " configuration."));
})
.build();
}
@@ -102,7 +109,8 @@ public class SpringDocConfig {
openApi.getInfo()
.title("Stirling PDF - System API")
.description(
"System information, UI metadata, job status, and file management endpoints."));
"System information, UI metadata, job status,"
+ " and file management endpoints."));
})
.build();
}
@@ -45,7 +45,8 @@ public class TauriProcessMonitor {
startMonitoring();
} else {
logger.warn(
"TAURI_PARENT_PID environment variable not found. Tauri process monitoring disabled.");
"TAURI_PARENT_PID environment variable not found. Tauri process monitoring"
+ " disabled.");
}
}
@@ -74,7 +75,8 @@ public class TauriProcessMonitor {
try {
if (!isProcessAlive(parentProcessId)) {
logger.warn(
"Parent Tauri process (PID: {}) is no longer alive. Initiating graceful shutdown...",
"Parent Tauri process (PID: {}) is no longer alive. Initiating graceful"
+ " shutdown...",
parentProcessId);
initiateGracefulShutdown();
}
@@ -118,7 +120,8 @@ public class TauriProcessMonitor {
} else {
// Fallback to system exit
logger.warn(
"Unable to shutdown Spring context gracefully, using System.exit");
"Unable to shutdown Spring context gracefully, using"
+ " System.exit");
System.exit(0);
}
} catch (Exception e) {
@@ -29,7 +29,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
type = "string",
format = "binary",
description =
"CSV file containing extracted table data")),
"CSV file containing extracted table"
+ " data")),
@Content(
mediaType = "application/zip",
schema =
@@ -37,7 +38,9 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
type = "string",
format = "binary",
description =
"ZIP archive containing multiple CSV files when multiple tables are extracted"))
"ZIP archive containing multiple CSV files"
+ " when multiple tables are"
+ " extracted"))
}),
@ApiResponse(
responseCode = "400",
@@ -51,7 +51,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
@ApiResponse(
responseCode = "422",
description =
"Unprocessable entity - PDF is valid but cannot be analyzed for filtering",
"Unprocessable entity - PDF is valid but cannot be analyzed for"
+ " filtering",
content =
@Content(
mediaType = "application/json",
@@ -28,7 +28,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
@Schema(
type = "object",
description =
"JSON object containing the requested data or analysis results"))),
"JSON object containing the requested"
+ " data or analysis results"))),
@ApiResponse(
responseCode = "400",
description = "Invalid PDF file or request parameters",
@@ -21,7 +21,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
@ApiResponse(
responseCode = "200",
description =
"Files processed successfully. Returns single file or ZIP archive containing multiple files.",
"Files processed successfully. Returns single file or ZIP archive"
+ " containing multiple files.",
content = {
@Content(
mediaType = "application/pdf",
@@ -37,7 +38,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
type = "string",
format = "binary",
description =
"ZIP archive containing multiple output files")),
"ZIP archive containing multiple output"
+ " files")),
@Content(
mediaType = "image/png",
schema =
@@ -30,11 +30,13 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
type = "string",
format = "binary",
description =
"Microsoft PowerPoint presentation (PPTX)"))),
"Microsoft PowerPoint presentation"
+ " (PPTX)"))),
@ApiResponse(
responseCode = "400",
description =
"Bad request - Invalid input parameters, unsupported format, or corrupted PDF",
"Bad request - Invalid input parameters, unsupported format, or"
+ " corrupted PDF",
content =
@Content(
mediaType = "application/json",
@@ -49,7 +51,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
@ApiResponse(
responseCode = "422",
description =
"Unprocessable entity - PDF is valid but cannot be converted to PowerPoint format",
"Unprocessable entity - PDF is valid but cannot be converted to"
+ " PowerPoint format",
content =
@Content(
mediaType = "application/json",
@@ -41,7 +41,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
@ApiResponse(
responseCode = "400",
description =
"Bad request - Invalid input parameters, unsupported format, or corrupted PDF",
"Bad request - Invalid input parameters, unsupported format, or"
+ " corrupted PDF",
content =
@Content(
mediaType = "application/json",
@@ -56,7 +57,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
@ApiResponse(
responseCode = "422",
description =
"Unprocessable entity - PDF is valid but cannot be converted to Word format",
"Unprocessable entity - PDF is valid but cannot be converted to Word"
+ " format",
content =
@Content(
mediaType = "application/json",
@@ -39,18 +39,18 @@ public class AdditionalLanguageJsController {
// Generiere die `getDetailedLanguageCode`-Funktion
writer.println(
"""
function getDetailedLanguageCode() {
const userLanguages = navigator.languages ? navigator.languages : [navigator.language];
for (let lang of userLanguages) {
let matchedLang = supportedLanguages.find(supportedLang => supportedLang.startsWith(lang.replace('-', '_')));
if (matchedLang) {
return matchedLang;
}
}
// Fallback
return "en_US";
function getDetailedLanguageCode() {
const userLanguages = navigator.languages ? navigator.languages : [navigator.language];
for (let lang of userLanguages) {
let matchedLang = supportedLanguages.find(supportedLang => supportedLang.startsWith(lang.replace('-', '_')));
if (matchedLang) {
return matchedLang;
}
""");
}
// Fallback
return "en_US";
}
""");
writer.flush();
}
@@ -54,8 +54,9 @@ public class BookletImpositionController {
summary = "Create a booklet with proper page imposition",
description =
"This operation combines page reordering for booklet printing with multi-page"
+ " layout. It rearranges pages in the correct order for booklet printing and"
+ " places multiple pages on each sheet for proper folding and binding.")
+ " layout. It rearranges pages in the correct order for booklet printing"
+ " and places multiple pages on each sheet for proper folding and"
+ " binding.")
public ResponseEntity<Resource> createBookletImposition(
@ModelAttribute BookletImpositionRequest request) throws IOException {
@@ -73,7 +74,8 @@ public class BookletImpositionController {
// Validate pages per sheet for booklet - only 2-up landscape is proper booklet
if (pagesPerSheet != 2) {
throw new IllegalArgumentException(
"Booklet printing uses 2 pages per side (landscape). For 4-up, use the N-up feature.");
"Booklet printing uses 2 pages per side (landscape). For 4-up, use the N-up"
+ " feature.");
}
try (PDDocument sourceDocument = pdfDocumentFactory.load(file)) {
@@ -150,7 +150,8 @@ public class CropController {
|| request.getWidth() == null
|| request.getHeight() == null) {
throw new IllegalArgumentException(
"Crop coordinates (x, y, width, height) are required when auto-crop is not enabled");
"Crop coordinates (x, y, width, height) are required when auto-crop is not"
+ " enabled");
}
if (request.isRemoveDataOutsideCrop() && isGhostscriptEnabled()) {
@@ -90,13 +90,14 @@ public class EditTextController {
summary = "Edit text in a PDF via find and replace",
description =
"Applies an ordered list of find/replace operations to the text in a PDF and"
+ " returns the edited PDF. Useful for find-and-replace, bulk renames (e.g."
+ " updating a company name throughout a document), and copy editing where the AI"
+ " agent has identified specific replacements. Matching is performed against the"
+ " joined text of each page, so find strings can span multiple visual runs"
+ " (titles split per word, kerning-broken phrases). Cross-element matches are"
+ " written as a single replacement run anchored at the leftmost matched position;"
+ " centered or tracked text may shift left when its content changes.")
+ " returns the edited PDF. Useful for find-and-replace, bulk renames (e.g."
+ " updating a company name throughout a document), and copy editing where"
+ " the AI agent has identified specific replacements. Matching is"
+ " performed against the joined text of each page, so find strings can"
+ " span multiple visual runs (titles split per word, kerning-broken"
+ " phrases). Cross-element matches are written as a single replacement run"
+ " anchored at the leftmost matched position; centered or tracked text may"
+ " shift left when its content changes.")
public ResponseEntity<Resource> editText(@ModelAttribute EditTextRequest request)
throws Exception {
MultipartFile inputFile = request.getFileInput();
@@ -246,8 +246,8 @@ public class MergeController {
summary = "Merge multiple PDF files into one",
description =
"This endpoint merges multiple PDF files into a single PDF file. The merged"
+ " file will contain all pages from the input files in the order they were"
+ " provided.")
+ " file will contain all pages from the input files in the order they were"
+ " provided.")
public ResponseEntity<Resource> mergePdfs(
@ModelAttribute MergePdfsRequest request,
@RequestParam(value = "fileOrder", required = false) String fileOrder)
@@ -220,8 +220,9 @@ public class MultiPageLayoutController {
"error.invalidFormat",
"Invalid {0} format: {1}",
"margin/layout configuration",
"Invalid margin or layout configuration: resulting cell size is non-positive. "
+ "Please reduce outer margins or adjust rows/columns.");
"Invalid margin or layout configuration: resulting cell size is"
+ " non-positive. Please reduce outer margins or adjust"
+ " rows/columns.");
}
float innerWidth = cellWidth - 2 * innerMargin;
@@ -57,8 +57,8 @@ public class PosterPdfController {
summary = "Split large PDF pages into smaller printable chunks",
description =
"This endpoint splits large or oddly-sized PDF pages into smaller chunks"
+ " suitable for printing on standard paper sizes (e.g., A4, Letter). Divides each"
+ " page into a grid of smaller pages using Apache PDFBox.")
+ " suitable for printing on standard paper sizes (e.g., A4, Letter)."
+ " Divides each page into a grid of smaller pages using Apache PDFBox.")
public ResponseEntity<Resource> posterPdf(@ModelAttribute PosterPdfRequest request)
throws Exception {
@@ -214,7 +214,8 @@ public class PosterPdfController {
}
log.trace(
"Created output page for grid cell [{},{}] of page {}: cropX={}, cropY={}, translate=({}, {})",
"Created output page for grid cell [{},{}] of page {}:"
+ " cropX={}, cropY={}, translate=({}, {})",
row,
actualCol,
pageIndex,
@@ -241,8 +241,8 @@ public class RearrangePagesPDFController {
summary = "Rearrange pages in a PDF file",
description =
"This endpoint rearranges pages in a given PDF file based on the specified page"
+ " order or custom mode. Users can provide a page order as a comma-separated list"
+ " of page numbers or page ranges, or a custom mode.")
+ " order or custom mode. Users can provide a page order as a"
+ " comma-separated list of page numbers or page ranges, or a custom mode.")
public ResponseEntity<Resource> rearrangePages(@ModelAttribute RearrangePagesRequest request)
throws IOException {
MultipartFile pdfFile = request.getFileInput();
@@ -60,8 +60,8 @@ public class SplitPDFController {
summary = "Split a PDF file into separate documents",
description =
"This endpoint splits a given PDF file into separate documents based on the"
+ " specified page numbers or ranges. Users can specify pages using individual"
+ " numbers, ranges, or 'all' for every page.")
+ " specified page numbers or ranges. Users can specify pages using"
+ " individual numbers, ranges, or 'all' for every page.")
public ResponseEntity<Resource> splitPdf(@ModelAttribute SplitPagesRequest request)
throws IOException {
@@ -62,8 +62,8 @@ public class SplitPdfBySectionsController {
summary = "Split PDF pages into smaller sections",
description =
"Split each page of a PDF into smaller sections based on the user's choice"
+ " which page to split, and how to split ( halves, thirds, quarters, etc.), both"
+ " vertically and horizontally.")
+ " which page to split, and how to split ( halves, thirds, quarters,"
+ " etc.), both vertically and horizontally.")
public ResponseEntity<Resource> splitPdf(
@Valid @ModelAttribute SplitPdfBySectionsRequest request) throws Exception {
MultipartFile file = request.getFileInput();
@@ -60,9 +60,9 @@ public class SplitPdfBySizeController {
summary = "Auto split PDF pages into separate documents based on size or count",
description =
"split PDF into multiple paged documents based on size/count, ie if 20 pages"
+ " and split into 5, it does 5 documents each 4 pages\r\n if 10MB and each page"
+ " is 1MB and you enter 2MB then 5 docs each 2MB (rounded so that it accepts"
+ " 1.9MB but not 2.1MB)")
+ " and split into 5, it does 5 documents each 4 pages\r\n"
+ " if 10MB and each page is 1MB and you enter 2MB then 5 docs each 2MB"
+ " (rounded so that it accepts 1.9MB but not 2.1MB)")
public ResponseEntity<Resource> autoSplitPdf(
@ModelAttribute SplitPdfBySizeOrCountRequest request) throws Exception {
@@ -46,8 +46,8 @@ public class ToSinglePageController {
summary = "Convert a multi-page PDF into a single long page PDF",
description =
"This endpoint converts a multi-page PDF document into a single paged PDF"
+ " document. The width of the single page will be same as the input's width, but"
+ " the height will be the sum of all the pages' heights.")
+ " document. The width of the single page will be same as the input's"
+ " width, but the height will be the sum of all the pages' heights.")
public ResponseEntity<Resource> pdfToSinglePage(@ModelAttribute PDFFile request)
throws IOException {
@@ -56,9 +56,9 @@ public class ConvertEmlToPDF {
summary = "Convert EML/MSG to PDF",
description =
"This endpoint converts EML (email) and MSG (Outlook) files to PDF format with"
+ " extensive customization options. Features include font settings, image"
+ " constraints, display modes, attachment handling, and HTML debug output. or MSG"
+ " file, or HTML file.")
+ " extensive customization options. Features include font settings, image"
+ " constraints, display modes, attachment handling, and HTML debug output."
+ " or MSG file, or HTML file.")
public ResponseEntity<Resource> convertEmlToPdf(@ModelAttribute EmlToPdfRequest request) {
MultipartFile inputFile = request.getFileInput();
@@ -48,7 +48,8 @@ public class ConvertHtmlToPDF {
@Operation(
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
description =
"This endpoint takes an HTML or ZIP file input and converts it to a PDF format.")
"This endpoint takes an HTML or ZIP file input and converts it to a PDF"
+ " format.")
public ResponseEntity<Resource> HtmlToPdf(@ModelAttribute HTMLToPdfRequest request)
throws Exception {
MultipartFile fileInput = request.getFileInput();
@@ -95,8 +95,8 @@ public class ConvertImgPDFController {
summary = "Convert PDF to image(s)",
description =
"This endpoint converts a PDF file to image(s) with the specified image format,"
+ " color type, and DPI. Users can choose to get a single image or multiple"
+ " images.")
+ " color type, and DPI. Users can choose to get a single image or multiple"
+ " images.")
public ResponseEntity<?> convertToImage(@ModelAttribute ConvertToImageRequest request)
throws Exception {
MultipartFile file = request.getFileInput();
@@ -97,8 +97,8 @@ public class ConvertPDFToEpubController {
if (!endpointConfiguration.isGroupEnabled(CALIBRE_GROUP)) {
throw new IllegalStateException(
"Calibre support is disabled. Enable the Calibre group or install Calibre to use"
+ " this feature.");
"Calibre support is disabled. Enable the Calibre group or install Calibre to"
+ " use this feature.");
}
MultipartFile inputFile = request.getFileInput();
@@ -453,32 +453,32 @@ public class ConvertPDFToPDFA {
String pdfaDefContent =
String.format(
"""
%% This is a sample prefix file for creating a PDF/A document.
%% Feel free to modify entries marked with "Customize".
%% This is a sample prefix file for creating a PDF/A document.
%% Feel free to modify entries marked with "Customize".
%% Define entries in the document Info dictionary.
[/Title (%s)
/DOCINFO pdfmark
%% Define entries in the document Info dictionary.
[/Title (%s)
/DOCINFO pdfmark
%% Define an ICC profile.
[/_objdef {icc_PDFA} /type /stream /OBJ pdfmark
[{icc_PDFA} <<
/N 3
>> /PUT pdfmark
[{icc_PDFA} (%s) (r) file /PUT pdfmark
%% Define an ICC profile.
[/_objdef {icc_PDFA} /type /stream /OBJ pdfmark
[{icc_PDFA} <<
/N 3
>> /PUT pdfmark
[{icc_PDFA} (%s) (r) file /PUT pdfmark
%% Define the output intent dictionary.
[/_objdef {OutputIntent_PDFA} /type /dict /OBJ pdfmark
[{OutputIntent_PDFA} <<
/Type /OutputIntent
/S /GTS_PDFA1
/DestOutputProfile {icc_PDFA}
/OutputConditionIdentifier (sRGB IEC61966-2.1)
/Info (sRGB IEC61966-2.1)
/RegistryName (http://www.color.org)
>> /PUT pdfmark
[{Catalog} <</OutputIntents [ {OutputIntent_PDFA} ]>> /PUT pdfmark
""",
%% Define the output intent dictionary.
[/_objdef {OutputIntent_PDFA} /type /dict /OBJ pdfmark
[{OutputIntent_PDFA} <<
/Type /OutputIntent
/S /GTS_PDFA1
/DestOutputProfile {icc_PDFA}
/OutputConditionIdentifier (sRGB IEC61966-2.1)
/Info (sRGB IEC61966-2.1)
/RegistryName (http://www.color.org)
>> /PUT pdfmark
[{Catalog} <</OutputIntents [ {OutputIntent_PDFA} ]>> /PUT pdfmark
""",
title, rgbProfilePath);
Files.writeString(pdfaDefFile, pdfaDefContent);
@@ -598,8 +598,9 @@ public class ConvertPDFToPDFA {
summary = "Convert a PDF to a PDF/A or PDF/X",
description =
"This endpoint converts a PDF file to a PDF/A or PDF/X file using Ghostscript"
+ " (preferred) or PDFBox/LibreOffice (fallback). PDF/A is a format designed for"
+ " long-term archiving, while PDF/X is optimized for print production.")
+ " (preferred) or PDFBox/LibreOffice (fallback). PDF/A is a format"
+ " designed for long-term archiving, while PDF/X is optimized for print"
+ " production.")
public ResponseEntity<Resource> pdfToPdfA(@ModelAttribute PdfToPdfARequest request)
throws Exception {
MultipartFile inputFile = request.getFileInput();
@@ -661,7 +662,8 @@ public class ConvertPDFToPDFA {
if (!isGhostscriptAvailable()) {
log.error("Ghostscript is required for PDF/X conversion");
throw new IOException(
"Ghostscript is required for PDF/X conversion but is not available on the system");
"Ghostscript is required for PDF/X conversion but is not available on the"
+ " system");
}
log.info("Using Ghostscript for PDF/X conversion to {}", profile.getDisplayName());
@@ -743,7 +745,8 @@ public class ConvertPDFToPDFA {
if (fontNameStr.contains("+") || fontNameStr.contains("Subset")) {
descDict.removeItem(COSName.CHAR_SET);
log.debug(
"Removed potentially invalid CharSet from subsetted Type1 font: {}",
"Removed potentially invalid CharSet from subsetted Type1"
+ " font: {}",
fontNameStr);
} else if (!hasFontFile && fontEmbedded) {
// Font is embedded but we can't verify CharSet, remove it
@@ -761,7 +764,8 @@ public class ConvertPDFToPDFA {
if (!glyphSet.isEmpty()) {
descDict.setString(COSName.CHAR_SET, glyphSet);
log.debug(
"Added missing CharSet for Type1 font {} with {} glyphs",
"Added missing CharSet for Type1 font {} with {}"
+ " glyphs",
fontNameStr,
countGlyphs(glyphSet));
}
@@ -1935,7 +1939,8 @@ public class ConvertPDFToPDFA {
return WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename);
} catch (IOException | InterruptedException e) {
log.warn(
"Ghostscript conversion failed, falling back to PDFBox/LibreOffice method",
"Ghostscript conversion failed, falling back to PDFBox/LibreOffice"
+ " method",
e);
}
} else {
@@ -2536,7 +2541,8 @@ public class ConvertPDFToPDFA {
return converted;
} catch (IOException | InterruptedException e) {
log.warn(
"Ghostscript conversion failed, falling back to PDFBox/LibreOffice method",
"Ghostscript conversion failed, falling back to PDFBox/LibreOffice"
+ " method",
e);
}
} else {
@@ -62,7 +62,8 @@ public class ConvertPdfJsonController {
@Operation(
summary = "Convert PDF to Text Editor Format",
description =
"Extracts PDF text, fonts, and metadata into an editable JSON structure for the text editor tool.")
"Extracts PDF text, fonts, and metadata into an editable JSON structure for the"
+ " text editor tool.")
public ResponseEntity<Resource> convertPdfToJson(
@ModelAttribute PDFFile request,
@RequestParam(value = "lightweight", defaultValue = "false") boolean lightweight)
@@ -104,7 +105,8 @@ public class ConvertPdfJsonController {
@Operation(
summary = "Convert Text Editor Format to PDF",
description =
"Rebuilds a PDF from the editable JSON structure generated by the text editor tool.")
"Rebuilds a PDF from the editable JSON structure generated by the text editor"
+ " tool.")
public ResponseEntity<Resource> convertJsonToPdf(@ModelAttribute GeneralFile request)
throws Exception {
MultipartFile jsonFile = request.getFileInput();
@@ -137,9 +139,9 @@ public class ConvertPdfJsonController {
@Operation(
summary = "Extract PDF metadata for text editor lazy loading",
description =
"Extracts document metadata, fonts, and page dimensions for the text editor tool. Caches the document for"
+ " subsequent page requests. Returns a server-generated jobId scoped to the"
+ " authenticated user.")
"Extracts document metadata, fonts, and page dimensions for the text editor"
+ " tool. Caches the document for subsequent page requests. Returns a"
+ " server-generated jobId scoped to the authenticated user.")
public ResponseEntity<Resource> extractPdfMetadata(@ModelAttribute PDFFile request)
throws Exception {
MultipartFile inputFile = request.getFileInput();
@@ -181,9 +183,10 @@ public class ConvertPdfJsonController {
@Operation(
summary = "Apply incremental edits from text editor to a cached PDF",
description =
"Applies edits for the specified pages of a cached PDF and returns an updated PDF."
+ " Requires the PDF to have been previously cached via the text editor metadata endpoint."
+ " The jobId must be obtained from the metadata extraction endpoint.")
"Applies edits for the specified pages of a cached PDF and returns an updated"
+ " PDF. Requires the PDF to have been previously cached via the text"
+ " editor metadata endpoint. The jobId must be obtained from the metadata"
+ " extraction endpoint.")
public ResponseEntity<Resource> exportPartialPdf(
@PathVariable String jobId,
@RequestBody PdfJsonDocument document,
@@ -224,9 +227,9 @@ public class ConvertPdfJsonController {
@Operation(
summary = "Extract single page from cached PDF for text editor",
description =
"Retrieves a single page's content from a previously cached PDF document for the text editor tool."
+ " Requires prior call to /pdf/text-editor/metadata. The jobId must belong to the"
+ " authenticated user.")
"Retrieves a single page's content from a previously cached PDF document for"
+ " the text editor tool. Requires prior call to /pdf/text-editor/metadata."
+ " The jobId must belong to the authenticated user.")
public ResponseEntity<Resource> extractSinglePage(
@PathVariable String jobId, @PathVariable int pageNumber) throws Exception {
@@ -253,9 +256,9 @@ public class ConvertPdfJsonController {
@Operation(
summary = "Extract fonts used by a single cached page for text editor",
description =
"Retrieves the font payloads used by a single page from a previously cached PDF document."
+ " Requires prior call to /pdf/text-editor/metadata. The jobId must belong to the"
+ " authenticated user.")
"Retrieves the font payloads used by a single page from a previously cached PDF"
+ " document. Requires prior call to /pdf/text-editor/metadata. The jobId"
+ " must belong to the authenticated user.")
public ResponseEntity<Resource> extractPageFonts(
@PathVariable String jobId, @PathVariable int pageNumber) throws Exception {
@@ -285,9 +288,9 @@ public class ConvertPdfJsonController {
@Operation(
summary = "Clear cached PDF document for text editor",
description =
"Manually clears a cached PDF document used by the text editor to free up server resources."
+ " Called automatically after 30 minutes. The jobId must belong to the"
+ " authenticated user.")
"Manually clears a cached PDF document used by the text editor to free up"
+ " server resources. Called automatically after 30 minutes. The jobId must"
+ " belong to the authenticated user.")
public ResponseEntity<Void> clearCache(@PathVariable String jobId) {
validateJobAccess(jobId);
@@ -68,10 +68,11 @@ public class ConvertSvgToPDF {
summary = "Convert SVG to PDF",
description =
"This endpoint converts one or more SVG (Scalable Vector Graphics) files to PDF"
+ " format. Each SVG is converted to a separate PDF file. The conversion preserves"
+ " vector graphics for crisp output at any resolution - no rasterization occurs."
+ " SVG dimensions (width/height) determine the PDF page size; defaults to A4 if"
+ " not specified. SVG content is sanitized to prevent XSS attacks.")
+ " format. Each SVG is converted to a separate PDF file. The conversion"
+ " preserves vector graphics for crisp output at any resolution - no"
+ " rasterization occurs. SVG dimensions (width/height) determine the PDF"
+ " page size; defaults to A4 if not specified. SVG content is sanitized to"
+ " prevent XSS attacks.")
public ResponseEntity<Resource> convertSvgToPdf(@ModelAttribute SvgToPdfRequest request) {
MultipartFile[] inputFiles = request.getFileInput();
@@ -221,7 +221,8 @@ public class PdfVectorExportController {
if (result.getRc() != 0) {
log.error(
"Ghostscript PDF to {} conversion failed with rc={} and messages={}. Command: {}",
"Ghostscript PDF to {} conversion failed with rc={} and messages={}. Command:"
+ " {}",
outputFormat.toUpperCase(),
result.getRc(),
result.getMessages(),
@@ -261,7 +262,8 @@ public class PdfVectorExportController {
ExceptionUtils.detectGhostscriptCriticalError(result.getMessages());
if (criticalError != null) {
log.error(
"Ghostscript PostScript-to-PDF conversion detected critical error: {}. Command: {}",
"Ghostscript PostScript-to-PDF conversion detected critical error: {}. Command:"
+ " {}",
criticalError.getMessage(),
String.join(" ", command));
throw criticalError;
@@ -269,7 +271,8 @@ public class PdfVectorExportController {
if (result.getRc() != 0) {
log.error(
"Ghostscript PostScript-to-PDF conversion failed with rc={} and messages={}. Command: {}",
"Ghostscript PostScript-to-PDF conversion failed with rc={} and messages={}."
+ " Command: {}",
result.getRc(),
result.getMessages(),
String.join(" ", command));
@@ -295,7 +295,8 @@ public class FormFillController {
@Operation(
summary = "Extract form fields as XLSX",
description =
"Returns an Excel (XLSX) file containing all form field names and their current values")
"Returns an Excel (XLSX) file containing all form field names and their current"
+ " values")
public ResponseEntity<byte[]> extractXlsx(
@Parameter(
description = "The input PDF file",
@@ -427,8 +428,8 @@ public class FormFillController {
@Parameter(
description =
"Return a ZIP holding the updated PDF plus the field list it"
+ " produced, instead of the bare PDF. Saves re-uploading"
+ " the result just to read its fields back.")
+ " produced, instead of the bare PDF. Saves re-uploading"
+ " the result just to read its fields back.")
@RequestParam(value = "includeFields", defaultValue = "false")
boolean includeFields)
throws IOException {
@@ -79,9 +79,10 @@ public class AddCommentsController {
summary = "Add sticky-note comments to a PDF at specified positions or anchored text",
description =
"Attaches PDF Text (sticky-note) annotations to the document. Each CommentSpec"
+ " can either supply absolute coordinates or an `anchorText` hint; when provided,"
+ " the tool locates the first matching line on the target page and anchors the"
+ " icon there (falling back to the coordinates if no match).")
+ " can either supply absolute coordinates or an `anchorText` hint; when"
+ " provided, the tool locates the first matching line on the target page"
+ " and anchors the icon there (falling back to the coordinates if no"
+ " match).")
public ResponseEntity<Resource> addComments(@ModelAttribute AddCommentsRequest request)
throws IOException {
@@ -149,7 +149,8 @@ public class AttachmentController {
@Operation(
summary = "Extract attachments from PDF",
description =
"This endpoint extracts all embedded attachments from a PDF into a ZIP archive.")
"This endpoint extracts all embedded attachments from a PDF into a ZIP"
+ " archive.")
public ResponseEntity<Resource> extractAttachments(
@ModelAttribute ExtractAttachmentsRequest request) throws IOException {
try (PDDocument document = pdfDocumentFactory.load(request, true)) {
@@ -282,8 +282,8 @@ public class AutoSplitPdfController {
summary = "Auto split PDF pages into separate documents",
description =
"This endpoint accepts a PDF file, scans each page for a specific QR code, and"
+ " splits the document at the QR code boundaries. The output is a zip file"
+ " containing each separate PDF document.")
+ " splits the document at the QR code boundaries. The output is a zip file"
+ " containing each separate PDF document.")
public ResponseEntity<Resource> autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request)
throws IOException {
MultipartFile file = request.getFileInput();
@@ -94,7 +94,7 @@ public class BlankPageController {
summary = "Remove blank pages from a PDF file",
description =
"This endpoint removes blank pages from a given PDF file. Users can specify the"
+ " threshold and white percentage to tune the detection of blank pages.")
+ " threshold and white percentage to tune the detection of blank pages.")
public ResponseEntity<Resource> removeBlankPages(
@ModelAttribute RemoveBlankPagesRequest request)
throws IOException, InterruptedException {
@@ -68,8 +68,8 @@ public class ExtractImageScansController {
summary = "Extract image scans from an input file",
description =
"This endpoint extracts image scans from a given file based on certain"
+ " parameters. Users can specify angle threshold, tolerance, minimum area,"
+ " minimum contour area, and border size.")
+ " parameters. Users can specify angle threshold, tolerance, minimum area,"
+ " minimum contour area, and border size.")
public ResponseEntity<Resource> extractImageScans(
@ModelAttribute ExtractImageScansRequest request)
throws IOException, InterruptedException {
@@ -45,8 +45,8 @@ import stirling.software.common.service.MobileScannerService.FileMetadata;
@Tag(
name = "Mobile Scanner",
description =
"Endpoints for mobile-to-desktop file transfer via QR code scanning. "
+ "Files are temporarily stored and automatically cleaned up after 10 minutes.")
"Endpoints for mobile-to-desktop file transfer via QR code scanning. Files are"
+ " temporarily stored and automatically cleaned up after 10 minutes.")
@Hidden
@Slf4j
public class MobileScannerController {
@@ -271,7 +271,8 @@ public class MobileScannerController {
@Operation(
summary = "Download a specific file",
description =
"Download a file that was uploaded to a session. File is automatically deleted after download.")
"Download a file that was uploaded to a session. File is automatically deleted"
+ " after download.")
@ApiResponse(responseCode = "200", description = "File downloaded successfully")
@ApiResponse(responseCode = "403", description = "Mobile scanner feature not enabled")
@ApiResponse(responseCode = "404", description = "File or session not found")
@@ -104,9 +104,9 @@ public class OCRController {
summary = "Process a PDF file with OCR",
description =
"This endpoint processes a PDF file using OCR (Optical Character Recognition)."
+ " Users can specify languages, sidecar, deskew, clean, cleanFinal, ocrType,"
+ " ocrRenderType, and removeImagesAfter options. Uses OCRmyPDF if available,"
+ " falls back to Tesseract.")
+ " Users can specify languages, sidecar, deskew, clean, cleanFinal,"
+ " ocrType, ocrRenderType, and removeImagesAfter options. Uses OCRmyPDF if"
+ " available, falls back to Tesseract.")
public ResponseEntity<Resource> processPdfWithOCR(
@ModelAttribute ProcessPdfWithOcrRequest request)
throws IOException, InterruptedException {
@@ -442,7 +442,8 @@ public class OCRController {
// Verify the OCR'd PDF was created
if (!pageOutputPath.exists()) {
log.warn(
"Tesseract did not create expected output file: {}. Page may be blank or unreadable.",
"Tesseract did not create expected output file: {}. Page may be"
+ " blank or unreadable.",
pageOutputPath.getAbsolutePath());
// Save original page without OCR as fallback
try (PDDocument pageDoc = new PDDocument()) {
@@ -50,9 +50,10 @@ public class OverlayImageController {
summary = "Overlay image onto a PDF file",
description =
"This endpoint overlays an image onto a PDF file at the specified coordinates."
+ " Supports both raster formats (PNG, JPEG, etc.) and vector format (SVG). SVG"
+ " files are rendered as vector graphics for crisp output at any resolution. The"
+ " image can be overlaid on every page of the PDF if specified.")
+ " Supports both raster formats (PNG, JPEG, etc.) and vector format (SVG)."
+ " SVG files are rendered as vector graphics for crisp output at any"
+ " resolution. The image can be overlaid on every page of the PDF if"
+ " specified.")
public ResponseEntity<Resource> overlayImage(@ModelAttribute OverlayImageRequest request) {
MultipartFile pdfFile = request.getFileInput();
MultipartFile imageFile = request.getImageFile();
@@ -59,8 +59,9 @@ public class RepairController {
summary = "Repair a PDF file",
description =
"This endpoint repairs a given PDF file by running Ghostscript (primary), qpdf"
+ " (fallback), or PDFBox (if no external tools available). The PDF is first saved"
+ " to a temporary location, repaired, read back, and then returned as a response.")
+ " (fallback), or PDFBox (if no external tools available). The PDF is"
+ " first saved to a temporary location, repaired, read back, and then"
+ " returned as a response.")
public ResponseEntity<Resource> repairPdf(@ModelAttribute PDFFile file)
throws IOException, InterruptedException {
MultipartFile inputFile = file.getFileInput();
@@ -43,8 +43,8 @@ public class ReplaceAndInvertColorController {
summary = "Replace-Invert Color PDF",
description =
"This endpoint accepts a PDF file and provides options to invert all colors,"
+ " replace text and background colors, or convert to CMYK color space for"
+ " printing.")
+ " replace text and background colors, or convert to CMYK color space for"
+ " printing.")
public ResponseEntity<Resource> replaceAndInvertColor(
@ModelAttribute ReplaceAndInvertColorRequest request) throws IOException {
@@ -98,7 +98,8 @@ public class StampController {
summary = "Add stamp to a PDF file",
description =
"This endpoint adds a stamp to a given PDF file. Users can specify the stamp"
+ " type (text or image), rotation, opacity, width spacer, and height spacer.")
+ " type (text or image), rotation, opacity, width spacer, and height"
+ " spacer.")
public ResponseEntity<Resource> addStamp(@ModelAttribute AddStampRequest request)
throws IOException, Exception {
MultipartFile pdfFile = request.getFileInput();
@@ -58,8 +58,9 @@ public class PipelineController {
@Operation(
summary = "Execute automated PDF processing pipeline",
description =
"This endpoint processes multiple PDF files through a configurable pipeline of operations. "
+ "Users provide files and a JSON configuration defining the sequence of operations to perform.")
"This endpoint processes multiple PDF files through a configurable pipeline of"
+ " operations. Users provide files and a JSON configuration defining the"
+ " sequence of operations to perform.")
public ResponseEntity<Resource> handleData(@ModelAttribute HandleDataRequest request)
throws DatabindException, JacksonException {
MultipartFile[] files = request.getFileInput();
@@ -177,8 +177,8 @@ public class CertSignController {
summary = "Sign PDF with a Digital Certificate",
description =
"This endpoint accepts a PDF file, a digital certificate and related"
+ " information to sign the PDF. It then returns the digitally signed PDF"
+ " file.")
+ " information to sign the PDF. It then returns the digitally signed PDF"
+ " file.")
public ResponseEntity<Resource> signPDFWithCert(
@ModelAttribute SignPDFWithCertRequest request, HttpServletRequest httpRequest)
throws Exception {
@@ -99,8 +99,8 @@ public class RedactController {
summary = "Redacts areas and pages in a PDF document",
description =
"This endpoint redacts content from a PDF file based on manually specified"
+ " areas. Users can specify areas to redact and optionally convert the PDF to an"
+ " image.")
+ " areas. Users can specify areas to redact and optionally convert the PDF"
+ " to an image.")
public ResponseEntity<Resource> redactPDF(@ModelAttribute ManualRedactPdfRequest request)
throws IOException {
@@ -146,8 +146,8 @@ public class RedactController {
operationId = "redactPdfAuto",
description =
"This endpoint automatically redacts text from a PDF file based on specified"
+ " patterns. Users can provide text patterns to redact, with options for regex"
+ " and whole word matching.")
+ " patterns. Users can provide text patterns to redact, with options for"
+ " regex and whole word matching.")
public ResponseEntity<Resource> redactPdf(@ModelAttribute RedactPdfRequest request) {
if (request.getFileInput() == null || request.getFileInput().isEmpty()) {
log.error("File input is null or empty");
@@ -299,7 +299,7 @@ public class RedactController {
summary = "Execute a unified redaction plan on a PDF",
description =
"Unified redaction endpoint that accepts exact strings, regex patterns, and"
+ " page numbers in a single request. Supports execution strategy hints.")
+ " page numbers in a single request. Supports execution strategy hints.")
public ResponseEntity<Resource> executeRedaction(@ModelAttribute RedactExecuteRequest request)
throws IOException {
@@ -73,7 +73,8 @@ class RedactExecuteService {
boolean hasTextOps = !textValues.isEmpty() || !regexPatterns.isEmpty();
log.info(
"[redact/execute] strategy={} textValues={} regexPatterns={} wipePages={} ranges={} imageBoxes={} imagePages={}",
"[redact/execute] strategy={} textValues={} regexPatterns={} wipePages={} ranges={}"
+ " imageBoxes={} imagePages={}",
style.getStrategy(),
textValues.size(),
regexPatterns.size(),
@@ -107,7 +108,8 @@ class RedactExecuteService {
needsOverlayOnly = applyTextRemoval(document, request);
} else if (overlayOnly) {
log.info(
"[redact/execute] overlay-only mode requested — skipping content-stream rewriting");
"[redact/execute] overlay-only mode requested — skipping content-stream"
+ " rewriting");
}
// Reload fresh document on fallback so we overlay onto clean content.
@@ -458,7 +460,8 @@ class RedactExecuteService {
}
if (end == null) {
log.warn(
"[redact/execute] no end anchor after start at (page={}, col={}, y={}) — skipping",
"[redact/execute] no end anchor after start at (page={}, col={}, y={})"
+ " — skipping",
start.page + 1,
start.col,
start.y);
@@ -129,7 +129,8 @@ class TextRedactionService {
result != null ? result.totalMatches() : -1);
if (result == null) {
log.warn(
"JPDFium PdfRedactor.redact returned null result, falling back to box-only redaction mode");
"JPDFium PdfRedactor.redact returned null result, falling back to box-only"
+ " redaction mode");
return true;
}
@@ -153,7 +154,8 @@ class TextRedactionService {
return false;
} catch (Exception e) {
log.warn(
"JPDFium native text replacement failed, falling back to box-only redaction mode: {}",
"JPDFium native text replacement failed, falling back to box-only redaction"
+ " mode: {}",
e.getMessage());
return true;
} finally {
@@ -91,8 +91,8 @@ public class TimestampController {
summary = "Add RFC 3161 document timestamp to a PDF",
description =
"Contacts a trusted Time Stamp Authority (TSA) server and embeds an RFC 3161"
+ " document timestamp into the PDF. Only a SHA-256 hash of the document is sent"
+ " to the TSA - the PDF itself never leaves the server.")
+ " document timestamp into the PDF. Only a SHA-256 hash of the document is"
+ " sent to the TSA - the PDF itself never leaves the server.")
public ResponseEntity<Resource> timestampPdf(@ModelAttribute TimestampPdfRequest request)
throws Exception {
MultipartFile inputFile = request.getFileInput();
@@ -38,8 +38,8 @@ public class VerifyPDFController {
summary = "Verify PDF Standards Compliance",
description =
"Validates PDF files against the standards declared in their metadata."
+ " Automatically detects PDF/A, PDF/UA-1, PDF/UA-2, and WTPDF standards from the"
+ " document's XMP metadata and validates compliance.")
+ " Automatically detects PDF/A, PDF/UA-1, PDF/UA-2, and WTPDF standards"
+ " from the document's XMP metadata and validates compliance.")
@AutoJobPostMapping(
value = "/verify-pdf",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
@@ -81,8 +81,8 @@ public class WatermarkController {
summary = "Add watermark to a PDF file",
description =
"This endpoint adds a watermark to a given PDF file. Users can specify the"
+ " watermark type (text or image), rotation, opacity, width spacer, and height"
+ " spacer.")
+ " watermark type (text or image), rotation, opacity, width spacer, and"
+ " height spacer.")
public ResponseEntity<Resource> addWatermark(@Valid @ModelAttribute AddWatermarkRequest request)
throws IOException, Exception {
MultipartFile pdfFile = request.getFileInput();
@@ -58,7 +58,8 @@ public class MetricsController {
@Operation(
summary = "Application health check",
description =
"This endpoint returns the health status of the application and its version number. Mirrors /api/v1/info/status.")
"This endpoint returns the health status of the application and its version"
+ " number. Mirrors /api/v1/info/status.")
public ResponseEntity<?> getHealth() {
return getApplicationStatus();
}
@@ -91,7 +92,8 @@ public class MetricsController {
@Operation(
summary = "GET request count",
description =
"This endpoint returns the total count of GET requests for a specific endpoint or all endpoints.")
"This endpoint returns the total count of GET requests for a specific endpoint"
+ " or all endpoints.")
public ResponseEntity<?> getPageLoads(
@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint")
Optional<String> endpoint) {
@@ -110,7 +112,8 @@ public class MetricsController {
@Operation(
summary = "Unique users count for GET requests",
description =
"This endpoint returns the count of unique users for GET requests for a specific endpoint or all endpoints.")
"This endpoint returns the count of unique users for GET requests for a"
+ " specific endpoint or all endpoints.")
public ResponseEntity<?> getUniquePageLoads(
@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint")
Optional<String> endpoint) {
@@ -145,7 +148,8 @@ public class MetricsController {
@Operation(
summary = "Unique users count for GET requests for all endpoints",
description =
"This endpoint returns the count of unique users for GET requests for each endpoint.")
"This endpoint returns the count of unique users for GET requests for each"
+ " endpoint.")
public ResponseEntity<?> getAllUniqueEndpointLoads() {
if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
@@ -162,7 +166,8 @@ public class MetricsController {
@Operation(
summary = "POST request count",
description =
"This endpoint returns the total count of POST requests for a specific endpoint or all endpoints.")
"This endpoint returns the total count of POST requests for a specific endpoint"
+ " or all endpoints.")
public ResponseEntity<?> getTotalRequests(
@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint")
Optional<String> endpoint) {
@@ -181,7 +186,8 @@ public class MetricsController {
@Operation(
summary = "Unique users count for POST requests",
description =
"This endpoint returns the count of unique users for POST requests for a specific endpoint or all endpoints.")
"This endpoint returns the count of unique users for POST requests for a"
+ " specific endpoint or all endpoints.")
public ResponseEntity<?> getUniqueTotalRequests(
@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint")
Optional<String> endpoint) {
@@ -216,7 +222,8 @@ public class MetricsController {
@Operation(
summary = "Unique users count for POST requests for all endpoints",
description =
"This endpoint returns the count of unique users for POST requests for each endpoint.")
"This endpoint returns the count of unique users for POST requests for each"
+ " endpoint.")
public ResponseEntity<?> getAllUniquePostRequests() {
if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
@@ -397,7 +404,8 @@ public class MetricsController {
if (wauService.isEmpty()) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(
"WAU tracking is only available when security is disabled (no-login mode)");
"WAU tracking is only available when security is disabled (no-login"
+ " mode)");
}
WeeklyActiveUsersService service = wauService.get();
@@ -138,7 +138,8 @@ public class ReactRoutingController {
this.useExternalIndexHtml = false;
this.loggedMissingIndex = true;
log.warn(
"index.html not found in classpath or custom path; using lightweight fallback page");
"index.html not found in classpath or custom path; using lightweight fallback"
+ " page");
}
private String processIndexHtml() {
@@ -371,51 +372,51 @@ public class ReactRoutingController {
String serverUrl = "(window.location.origin + '" + escapedBaseUrlJs + "')";
return """
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<base href="%s" />
<title>Stirling PDF</title>
<script>
// Minimal handler for SSO callback when index.html is missing (desktop fallback)
(function() {
const baseUrl = '%s';
window.STIRLING_PDF_API_BASE_URL = baseUrl;
const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, ''));
const searchParams = new URLSearchParams(window.location.search);
const token = hashParams.get('access_token') || hashParams.get('token') || searchParams.get('access_token');
const serverUrl = %s;
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<base href="%s" />
<title>Stirling PDF</title>
<script>
// Minimal handler for SSO callback when index.html is missing (desktop fallback)
(function() {
const baseUrl = '%s';
window.STIRLING_PDF_API_BASE_URL = baseUrl;
const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, ''));
const searchParams = new URLSearchParams(window.location.search);
const token = hashParams.get('access_token') || hashParams.get('token') || searchParams.get('access_token');
const serverUrl = %s;
if (token) {
// Extract nonce from URL to send back to desktop app for validation
const nonceFromUrl = hashParams.get('nonce') || searchParams.get('nonce');
if (token) {
// Extract nonce from URL to send back to desktop app for validation
const nonceFromUrl = hashParams.get('nonce') || searchParams.get('nonce');
console.log('[Fallback Auth] Token received, sending to desktop app via deep link');
console.log('[Fallback Auth] Token received, sending to desktop app via deep link');
// Send token + nonce via deep link to desktop app
// Desktop app will validate nonce before accepting token
try {
const encodedToken = encodeURIComponent(token);
const encodedServer = encodeURIComponent(serverUrl);
const encodedNonce = nonceFromUrl ? encodeURIComponent(nonceFromUrl) : '';
const deepLink = `stirlingpdf://auth/sso-complete?server=${encodedServer}#access_token=${encodedToken}&nonce=${encodedNonce}&type=sso-selfhosted`;
window.location.href = deepLink;
return;
} catch (_) {
// ignore deep link errors
}
}
// Send token + nonce via deep link to desktop app
// Desktop app will validate nonce before accepting token
try {
const encodedToken = encodeURIComponent(token);
const encodedServer = encodeURIComponent(serverUrl);
const encodedNonce = nonceFromUrl ? encodeURIComponent(nonceFromUrl) : '';
const deepLink = `stirlingpdf://auth/sso-complete?server=${encodedServer}#access_token=${encodedToken}&nonce=${encodedNonce}&type=sso-selfhosted`;
window.location.href = deepLink;
return;
} catch (_) {
// ignore deep link errors
}
}
// No redirect to avoid loops when index.html is missing
})();
</script>
</head>
<body>
<p>Stirling PDF is running.</p>
</body>
</html>
"""
// No redirect to avoid loops when index.html is missing
})();
</script>
</head>
<body>
<p>Stirling PDF is running.</p>
</body>
</html>
"""
.formatted(escapedBaseUrlHtml, escapedBaseUrlJs, serverUrl);
}
@@ -430,238 +431,238 @@ public class ReactRoutingController {
String serverUrl = "(window.location.origin + '" + escapedBaseUrlJs + "')";
return """
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<base href="%s" />
<title>Authentication Complete</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<base href="%s" />
<title>Authentication Complete</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
text-align: center;
padding: 50px 20px;
background: #f5f5f5;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: #ffffff;
border-radius: 12px;
padding: 40px;
max-width: 420px;
width: 100%%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
color: #1a1a1a;
}
.icon {
font-size: 48px;
margin-bottom: 16px;
color: #2e7d32;
}
.icon.error {
color: #d32f2f;
}
h1 {
font-size: 24px;
font-weight: 600;
margin-bottom: 12px;
color: #1a1a1a;
}
p {
color: #666;
line-height: 1.6;
font-size: 15px;
}
.error-details {
background: #ffebee;
border: 1px solid #ffcdd2;
padding: 16px;
border-radius: 8px;
margin-top: 20px;
font-size: 14px;
color: #c62828;
word-break: break-word;
text-align: left;
line-height: 1.5;
display: none;
}
@media (prefers-color-scheme: dark) {
body {
background: #1a1a1a;
color: #e0e0e0;
}
.container {
background: #2d2d2d;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border-color: #374151;
color: #e5e7eb;
}
.icon {
color: #66bb6a;
}
.icon.error {
color: #ef5350;
}
h1 {
color: #f5f5f5;
}
p {
color: #b0b0b0;
}
.error-details {
background: #3d2020;
border: 1px solid #5d3030;
color: #ef9a9a;
}
}
@media (max-width: 480px) {
body {
padding: 20px 16px;
}
.container {
padding: 32px 24px;
}
h1 {
font-size: 20px;
}
.icon {
font-size: 40px;
}
}
</style>
<script>
(function() {
const run = () => {
const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, ''));
const searchParams = new URLSearchParams(window.location.search);
const token = hashParams.get('access_token') || hashParams.get('token') || searchParams.get('access_token');
const errorCode = searchParams.get('errorOAuth')
|| searchParams.get('error')
|| hashParams.get('error')
|| searchParams.get('error_description')
|| hashParams.get('error_description');
const serverUrl = %s;
const iconEl = document.getElementById('auth-icon');
const titleEl = document.getElementById('auth-title');
const messageEl = document.getElementById('auth-message');
const detailsEl = document.getElementById('auth-error-details');
const sendDeepLink = (type, value, key) => {
try {
const encodedValue = encodeURIComponent(value || '');
const encodedServer = encodeURIComponent(serverUrl);
const hashKey = key || 'access_token';
const deepLink = `stirlingpdf://auth/sso-complete?server=${encodedServer}#${hashKey}=${encodedValue}&type=${type}`;
window.location.href = deepLink;
} catch (_) {
// ignore deep link errors
}
};
const showError = (message, details) => {
if (iconEl) {
iconEl.textContent = '✗';
iconEl.classList.add('error');
}
if (titleEl) {
titleEl.textContent = 'Authentication failed';
}
if (messageEl) {
messageEl.textContent = message;
}
if (detailsEl && details) {
detailsEl.textContent = details;
detailsEl.style.display = 'block';
}
};
if (token) {
// Extract nonce from URL to send back to desktop app for validation
// (System browser doesn't have access to desktop app's sessionStorage)
const nonceFromUrl = hashParams.get('nonce') || searchParams.get('nonce');
console.log('[Auth Callback] Token received, sending to desktop app via deep link');
// Send token + nonce via deep link to desktop app
// Desktop app will validate nonce before accepting token
setTimeout(() => {
try {
const encodedToken = encodeURIComponent(token);
const encodedServer = encodeURIComponent(serverUrl);
const encodedNonce = nonceFromUrl ? encodeURIComponent(nonceFromUrl) : '';
const deepLink = `stirlingpdf://auth/sso-complete?server=${encodedServer}#access_token=${encodedToken}&nonce=${encodedNonce}&type=sso-selfhosted`;
window.location.href = deepLink;
} catch (err) {
console.error('[Auth Callback] Failed to trigger deep link:', err);
}
}, 200);
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
text-align: center;
padding: 50px 20px;
background: #f5f5f5;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
return;
}
.container {
background: #ffffff;
border-radius: 12px;
padding: 40px;
max-width: 420px;
width: 100%%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
color: #1a1a1a;
}
if (errorCode) {
const isCancelled = errorCode === 'access_denied';
sendDeepLink('sso-error', errorCode, 'error');
showError(
isCancelled
? 'Authentication was cancelled. You can close this window and return to the app.'
: 'Authentication was not successful. You can close this window and return to the app.',
errorCode
);
return;
}
.icon {
font-size: 48px;
margin-bottom: 16px;
color: #2e7d32;
}
showError(
'Authentication did not complete. You can close this window and try again.',
'missing_token'
);
};
.icon.error {
color: #d32f2f;
}
h1 {
font-size: 24px;
font-weight: 600;
margin-bottom: 12px;
color: #1a1a1a;
}
p {
color: #666;
line-height: 1.6;
font-size: 15px;
}
.error-details {
background: #ffebee;
border: 1px solid #ffcdd2;
padding: 16px;
border-radius: 8px;
margin-top: 20px;
font-size: 14px;
color: #c62828;
word-break: break-word;
text-align: left;
line-height: 1.5;
display: none;
}
@media (prefers-color-scheme: dark) {
body {
background: #1a1a1a;
color: #e0e0e0;
}
.container {
background: #2d2d2d;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border-color: #374151;
color: #e5e7eb;
}
.icon {
color: #66bb6a;
}
.icon.error {
color: #ef5350;
}
h1 {
color: #f5f5f5;
}
p {
color: #b0b0b0;
}
.error-details {
background: #3d2020;
border: 1px solid #5d3030;
color: #ef9a9a;
}
}
@media (max-width: 480px) {
body {
padding: 20px 16px;
}
.container {
padding: 32px 24px;
}
h1 {
font-size: 20px;
}
.icon {
font-size: 40px;
}
}
</style>
<script>
(function() {
const run = () => {
const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, ''));
const searchParams = new URLSearchParams(window.location.search);
const token = hashParams.get('access_token') || hashParams.get('token') || searchParams.get('access_token');
const errorCode = searchParams.get('errorOAuth')
|| searchParams.get('error')
|| hashParams.get('error')
|| searchParams.get('error_description')
|| hashParams.get('error_description');
const serverUrl = %s;
const iconEl = document.getElementById('auth-icon');
const titleEl = document.getElementById('auth-title');
const messageEl = document.getElementById('auth-message');
const detailsEl = document.getElementById('auth-error-details');
const sendDeepLink = (type, value, key) => {
try {
const encodedValue = encodeURIComponent(value || '');
const encodedServer = encodeURIComponent(serverUrl);
const hashKey = key || 'access_token';
const deepLink = `stirlingpdf://auth/sso-complete?server=${encodedServer}#${hashKey}=${encodedValue}&type=${type}`;
window.location.href = deepLink;
} catch (_) {
// ignore deep link errors
}
};
const showError = (message, details) => {
if (iconEl) {
iconEl.textContent = '✗';
iconEl.classList.add('error');
}
if (titleEl) {
titleEl.textContent = 'Authentication failed';
}
if (messageEl) {
messageEl.textContent = message;
}
if (detailsEl && details) {
detailsEl.textContent = details;
detailsEl.style.display = 'block';
}
};
if (token) {
// Extract nonce from URL to send back to desktop app for validation
// (System browser doesn't have access to desktop app's sessionStorage)
const nonceFromUrl = hashParams.get('nonce') || searchParams.get('nonce');
console.log('[Auth Callback] Token received, sending to desktop app via deep link');
// Send token + nonce via deep link to desktop app
// Desktop app will validate nonce before accepting token
setTimeout(() => {
try {
const encodedToken = encodeURIComponent(token);
const encodedServer = encodeURIComponent(serverUrl);
const encodedNonce = nonceFromUrl ? encodeURIComponent(nonceFromUrl) : '';
const deepLink = `stirlingpdf://auth/sso-complete?server=${encodedServer}#access_token=${encodedToken}&nonce=${encodedNonce}&type=sso-selfhosted`;
window.location.href = deepLink;
} catch (err) {
console.error('[Auth Callback] Failed to trigger deep link:', err);
}
}, 200);
return;
}
if (errorCode) {
const isCancelled = errorCode === 'access_denied';
sendDeepLink('sso-error', errorCode, 'error');
showError(
isCancelled
? 'Authentication was cancelled. You can close this window and return to the app.'
: 'Authentication was not successful. You can close this window and return to the app.',
errorCode
);
return;
}
showError(
'Authentication did not complete. You can close this window and try again.',
'missing_token'
);
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', run);
} else {
run();
}
})();
</script>
</head>
<body>
<div class="container">
<div class="icon" id="auth-icon">&#10003;</div>
<h1 id="auth-title">Authentication complete</h1>
<p id="auth-message">You can close this window and return to Stirling PDF.</p>
<div class="error-details" id="auth-error-details"></div>
</div>
</body>
</html>
"""
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', run);
} else {
run();
}
})();
</script>
</head>
<body>
<div class="container">
<div class="icon" id="auth-icon">&#10003;</div>
<h1 id="auth-title">Authentication complete</h1>
<p id="auth-message">You can close this window and return to Stirling PDF.</p>
<div class="error-details" id="auth-error-details"></div>
</div>
</body>
</html>
"""
.formatted(escapedBaseUrlHtml, serverUrl);
}
}

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