Compare commits

...
122 Commits
Author SHA1 Message Date
Anthony Stirling 3e061516a5 Libre threads (#5303)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-15 19:14:45 +00:00
Anthony Stirling 8afbd0afed Docker use latest libreoffice and fonts (#5482)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-15 15:01:10 +00:00
Krishna Prem 884801ea36 fix: update broken enterprise docs link from /Pro to /Paid-Offerings (#5471)
## Description
Fixed broken documentation link in the air-gapped setup guide that was
pointing to a non-existent `/Pro/#activation` page (returning 404).
Updated to the correct `/Paid-Offerings/#activating-your-license` URL
which returns 200 OK.

## Changes Made
- Updated link in `frontend/src/core/data/useTranslatedToolRegistry.tsx`
(line 838)
- Changed from `https://docs.stirlingpdf.com/Pro/#activation` to
`https://docs.stirlingpdf.com/Paid-Offerings/#activating-your-license`

## Testing
- Verified old link returns 404
- Verified new link returns 200 and has activation section
- Link now correctly points to the "Activating Your License" section of
the Paid Offerings documentation

## Related Issue
Resolves #5088

## Checklist
- [x] Code follows project style guidelines
- [x] Changes are limited to fixing the broken link only (atomic commit)
- [x] Link has been manually tested and verified working
2026-01-15 12:48:34 +00:00
Reece Browne 8fcee482f0 Make controls sticky (#5478) 2026-01-15 10:25:37 +00:00
Balázs Szücs dbf72cf053 fix(auth): remove unnecessary authentication requirement from SignatureController (#5477)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2026-01-14 21:30:16 +00:00
stirlingbot[bot] 84e23abddc 🌐 Sync Translations + Update README Progress Table (#5473)
### Description of Changes

This Pull Request was automatically generated to synchronize updates to
translation files and documentation. Below are the details of the
changes made:

#### **1. Synchronization of Translation Files**
- Updated translation files
(`frontend/public/locales/*/translation.toml`) to reflect changes in the
reference file `en-GB/translation.toml`.
- Ensured consistency and synchronization across all supported language
files.
- Highlighted any missing or incomplete translations.
- **Format**: TOML

#### **2. Update README.md**
- Generated the translation progress table in `README.md` using
`counter_translation_v3.py`.
- Added a summary of the current translation status for all supported
languages.
- Included up-to-date statistics on translation coverage.

#### **Why these changes are necessary**
- Keeps translation files aligned with the latest reference updates.
- Ensures the documentation reflects the current translation progress.

---

Auto-generated by [create-pull-request][1].

[1]: https://github.com/peter-evans/create-pull-request

---------

Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-01-14 12:12:43 +00:00
Anthony Stirling a7945da3b4 translations (#5469)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-14 11:26:45 +00:00
stirlingbot[bot]andLudy 72cc4e4963 Update Backend 3rd Party Licenses (#5458)
Auto-generated by stirlingbot[bot]

This PR updates the backend license report based on dependency changes.

Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: Ludy <Ludy87@users.noreply.github.com>
2026-01-14 07:34:08 +00:00
stirlingbot[bot] b7041cfc78 🤖 format everything with pre-commit by stirlingbot (#5462)
Auto-generated by [create-pull-request][1] with **stirlingbot**

[1]: https://github.com/peter-evans/create-pull-request

Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-01-14 01:09:41 +00:00
dependabot[bot] 8d6c8cbc11 build(deps): bump docker/login-action from 3.4.0 to 3.6.0 (#5464)
Bumps [docker/login-action](https://github.com/docker/login-action) from
3.4.0 to 3.6.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/docker/login-action/releases">docker/login-action's
releases</a>.</em></p>
<blockquote>
<h2>v3.6.0</h2>
<ul>
<li>Add <code>registry-auth</code> input for raw authentication to
registries by <a
href="https://github.com/crazy-max"><code>@​crazy-max</code></a> in <a
href="https://redirect.github.com/docker/login-action/pull/887">docker/login-action#887</a></li>
<li>Bump <code>@​aws-sdk/client-ecr</code> to 3.890.0 in <a
href="https://redirect.github.com/docker/login-action/pull/882">docker/login-action#882</a>
<a
href="https://redirect.github.com/docker/login-action/pull/890">docker/login-action#890</a></li>
<li>Bump <code>@​aws-sdk/client-ecr-public</code> to 3.890.0 in <a
href="https://redirect.github.com/docker/login-action/pull/882">docker/login-action#882</a>
<a
href="https://redirect.github.com/docker/login-action/pull/890">docker/login-action#890</a></li>
<li>Bump <code>@​docker/actions-toolkit</code> from 0.62.1 to 0.63.0 in
<a
href="https://redirect.github.com/docker/login-action/pull/883">docker/login-action#883</a></li>
<li>Bump brace-expansion from 1.1.11 to 1.1.12 in <a
href="https://redirect.github.com/docker/login-action/pull/880">docker/login-action#880</a></li>
<li>Bump undici from 5.28.4 to 5.29.0 in <a
href="https://redirect.github.com/docker/login-action/pull/879">docker/login-action#879</a></li>
<li>Bump tmp from 0.2.3 to 0.2.4 in <a
href="https://redirect.github.com/docker/login-action/pull/881">docker/login-action#881</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/docker/login-action/compare/v3.5.0...v3.6.0">https://github.com/docker/login-action/compare/v3.5.0...v3.6.0</a></p>
<h2>v3.5.0</h2>
<ul>
<li>Support dual-stack endpoints for AWS ECR by <a
href="https://github.com/Spacefish"><code>@​Spacefish</code></a> <a
href="https://github.com/crazy-max"><code>@​crazy-max</code></a> in <a
href="https://redirect.github.com/docker/login-action/pull/874">docker/login-action#874</a>
<a
href="https://redirect.github.com/docker/login-action/pull/876">docker/login-action#876</a></li>
<li>Bump <code>@​aws-sdk/client-ecr</code> to 3.859.0 in <a
href="https://redirect.github.com/docker/login-action/pull/860">docker/login-action#860</a>
<a
href="https://redirect.github.com/docker/login-action/pull/878">docker/login-action#878</a></li>
<li>Bump <code>@​aws-sdk/client-ecr-public</code> to 3.859.0 in <a
href="https://redirect.github.com/docker/login-action/pull/860">docker/login-action#860</a>
<a
href="https://redirect.github.com/docker/login-action/pull/878">docker/login-action#878</a></li>
<li>Bump <code>@​docker/actions-toolkit</code> from 0.57.0 to 0.62.1 in
<a
href="https://redirect.github.com/docker/login-action/pull/870">docker/login-action#870</a></li>
<li>Bump form-data from 2.5.1 to 2.5.5 in <a
href="https://redirect.github.com/docker/login-action/pull/875">docker/login-action#875</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/docker/login-action/compare/v3.4.0...v3.5.0">https://github.com/docker/login-action/compare/v3.4.0...v3.5.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/docker/login-action/commit/5e57cd118135c172c3672efd75eb46360885c0ef"><code>5e57cd1</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/login-action/issues/890">#890</a>
from docker/dependabot/npm_and_yarn/aws-sdk-dependenc...</li>
<li><a
href="https://github.com/docker/login-action/commit/97e31439e8b415da4e1322633630e1563c42c0f2"><code>97e3143</code></a>
chore: update generated content</li>
<li><a
href="https://github.com/docker/login-action/commit/3a0796b57f440ff1af59165907392527fa832e0c"><code>3a0796b</code></a>
build(deps): bump the aws-sdk-dependencies group with 2 updates</li>
<li><a
href="https://github.com/docker/login-action/commit/5b7b28b1cc417bbd34cd8c225a957c9ce9adf7f2"><code>5b7b28b</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/login-action/issues/882">#882</a>
from docker/dependabot/npm_and_yarn/aws-sdk-dependenc...</li>
<li><a
href="https://github.com/docker/login-action/commit/abc9fb3154ad354cf35d6c78a862bee018dd4cb8"><code>abc9fb3</code></a>
chore: update generated content</li>
<li><a
href="https://github.com/docker/login-action/commit/d46868881477a1d16bdcc80a5b2c05208b1befe4"><code>d468688</code></a>
build(deps): bump the aws-sdk-dependencies group with 2 updates</li>
<li><a
href="https://github.com/docker/login-action/commit/a99b2f88fc4efabea32b8ba09581cf535c1577e9"><code>a99b2f8</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/login-action/issues/883">#883</a>
from docker/dependabot/npm_and_yarn/docker/actions-to...</li>
<li><a
href="https://github.com/docker/login-action/commit/0d7fae8057d840a981e4132ce97862f6c8f48b42"><code>0d7fae8</code></a>
chore: update generated content</li>
<li><a
href="https://github.com/docker/login-action/commit/9832253cb7b14f93bd4134396bd26e855e8e4bd2"><code>9832253</code></a>
build(deps): bump <code>@​docker/actions-toolkit</code> from 0.62.1 to
0.63.0</li>
<li><a
href="https://github.com/docker/login-action/commit/09e05bbdf68bd9ce9eedefa6d2ebe03008c32b08"><code>09e05bb</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/login-action/issues/881">#881</a>
from docker/dependabot/npm_and_yarn/tmp-0.2.4</li>
<li>Additional commits viewable in <a
href="https://github.com/docker/login-action/compare/v3.4.0...5e57cd118135c172c3672efd75eb46360885c0ef">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=docker/login-action&package-manager=github_actions&previous-version=3.4.0&new-version=3.6.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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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-01-14 01:09:28 +00:00
dependabot[bot] 0e8992e3a5 build(deps): bump docker/setup-buildx-action from 3.11.1 to 3.12.0 (#5463)
Bumps
[docker/setup-buildx-action](https://github.com/docker/setup-buildx-action)
from 3.11.1 to 3.12.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>v3.12.0</h2>
<ul>
<li>Deprecate <code>install</code> input by <a
href="https://github.com/crazy-max"><code>@​crazy-max</code></a> in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/455">docker/setup-buildx-action#455</a></li>
<li>Bump <code>@​docker/actions-toolkit</code> from 0.62.1 to 0.63.0 in
<a
href="https://redirect.github.com/docker/setup-buildx-action/pull/434">docker/setup-buildx-action#434</a></li>
<li>Bump brace-expansion from 1.1.11 to 1.1.12 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/436">docker/setup-buildx-action#436</a></li>
<li>Bump form-data from 2.5.1 to 2.5.5 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/432">docker/setup-buildx-action#432</a></li>
<li>Bump undici from 5.28.4 to 5.29.0 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/435">docker/setup-buildx-action#435</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/docker/setup-buildx-action/compare/v3.11.1...v3.12.0">https://github.com/docker/setup-buildx-action/compare/v3.11.1...v3.12.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/8d2750c68a42422c14e847fe6c8ac0403b4cbd6f"><code>8d2750c</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/setup-buildx-action/issues/455">#455</a>
from crazy-max/install-deprecated</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/e81846bcc4416a0bf18b8389e4f6fce279619ddc"><code>e81846b</code></a>
deprecate install input</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/65d18f8f8a05aab1b2d761032bec9cd5578caadb"><code>65d18f8</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/setup-buildx-action/issues/454">#454</a>
from docker/dependabot/github_actions/actions/checkout-6</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/000d75d273dec231f74115df50b6ee04c9b25e55"><code>000d75d</code></a>
build(deps): bump actions/checkout from 5 to 6</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/1583c0f09d26c58c59d25b0eef29792b7ce99d9a"><code>1583c0f</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/setup-buildx-action/issues/443">#443</a>
from nicolasleger/patch-1</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/ed158e796328af702db08c75bebb92f2fb10be88"><code>ed158e7</code></a>
doc: bump actions/checkout from 4 to 5</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/4cc794f83e4b7488282e879f4469e86246e52ddd"><code>4cc794f</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/setup-buildx-action/issues/441">#441</a>
from docker/dependabot/github_actions/actions/checkout-5</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/4dfc3d6c5d9e9534040f8d8b55bd4a98459b62b7"><code>4dfc3d6</code></a>
build(deps): bump actions/checkout from 4 to 5</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/af1b253b8dc984466d22633f04ef341c1520ed2f"><code>af1b253</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/setup-buildx-action/issues/440">#440</a>
from crazy-max/k3s-build</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/3c6ab92b04d9f3e63233e0e0168ba114c6b448bc"><code>3c6ab92</code></a>
ci: k3s test with latest buildx</li>
<li>Additional commits viewable in <a
href="https://github.com/docker/setup-buildx-action/compare/e468171a9de216ec08956ac3ada2f0791b6bd435...8d2750c68a42422c14e847fe6c8ac0403b4cbd6f">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=docker/setup-buildx-action&package-manager=github_actions&previous-version=3.11.1&new-version=3.12.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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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-01-14 01:09:15 +00:00
dependabot[bot] aa39435303 build(deps): bump debian from 1c25564 to 449673e in /docker/embedded (#5357)
Bumps debian from `1c25564` to `449673e`.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=debian&package-manager=docker&previous-version=stable-slim&new-version=stable-slim)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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-01-14 00:40:24 +00:00
dependabot[bot] 3eaccc1e0e build(deps): bump alpine from 3.22.1 to 3.23.2 in /docker/embedded (#5319)
Bumps alpine from 3.22.1 to 3.23.2.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=alpine&package-manager=docker&previous-version=3.22.1&new-version=3.23.2)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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-01-14 00:37:53 +00:00
dependabot[bot] 1a7fd6ac4e build(deps): bump alpine from 3.22.2 to 3.23.2 in /docker/backend (#5316)
Bumps alpine from 3.22.2 to 3.23.2.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=alpine&package-manager=docker&previous-version=3.22.2&new-version=3.23.2)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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-01-14 00:37:34 +00:00
dependabot[bot] e49793a6b7 build(deps): bump urllib3 from 2.5.0 to 2.6.3 in /testing/cucumber in the pip group across 1 directory (#5456)
Bumps the pip group with 1 update in the /testing/cucumber directory:
[urllib3](https://github.com/urllib3/urllib3).

Updates `urllib3` from 2.5.0 to 2.6.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/urllib3/urllib3/releases">urllib3's
releases</a>.</em></p>
<blockquote>
<h2>2.6.3</h2>
<h2>🚀 urllib3 is fundraising for HTTP/2 support</h2>
<p><a
href="https://sethmlarson.dev/urllib3-is-fundraising-for-http2-support">urllib3
is raising ~$40,000 USD</a> to release HTTP/2 support and ensure
long-term sustainable maintenance of the project after a sharp decline
in financial support. If your company or organization uses Python and
would benefit from HTTP/2 support in Requests, pip, cloud SDKs, and
thousands of other projects <a
href="https://opencollective.com/urllib3">please consider contributing
financially</a> to ensure HTTP/2 support is developed sustainably and
maintained for the long-haul.</p>
<p>Thank you for your support.</p>
<h2>Changes</h2>
<ul>
<li>Fixed a security issue where decompression-bomb safeguards of the
streaming API were bypassed when HTTP redirects were followed.
(CVE-2026-21441 reported by <a
href="https://github.com/D47A"><code>@​D47A</code></a>, 8.9 High,
GHSA-38jv-5279-wg99)</li>
<li>Started treating <code>Retry-After</code> times greater than 6 hours
as 6 hours by default. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3743">urllib3/urllib3#3743</a>)</li>
<li>Fixed <code>urllib3.connection.VerifiedHTTPSConnection</code> on
Emscripten. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3752">urllib3/urllib3#3752</a>)</li>
</ul>
<h2>2.6.2</h2>
<h2>🚀 urllib3 is fundraising for HTTP/2 support</h2>
<p><a
href="https://sethmlarson.dev/urllib3-is-fundraising-for-http2-support">urllib3
is raising ~$40,000 USD</a> to release HTTP/2 support and ensure
long-term sustainable maintenance of the project after a sharp decline
in financial support. If your company or organization uses Python and
would benefit from HTTP/2 support in Requests, pip, cloud SDKs, and
thousands of other projects <a
href="https://opencollective.com/urllib3">please consider contributing
financially</a> to ensure HTTP/2 support is developed sustainably and
maintained for the long-haul.</p>
<p>Thank you for your support.</p>
<h2>Changes</h2>
<ul>
<li>Fixed <code>HTTPResponse.read_chunked()</code> to properly handle
leftover data in the decoder's buffer when reading compressed chunked
responses. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3734">urllib3/urllib3#3734</a>)</li>
</ul>
<h2>2.6.1</h2>
<h2>🚀 urllib3 is fundraising for HTTP/2 support</h2>
<p><a
href="https://sethmlarson.dev/urllib3-is-fundraising-for-http2-support">urllib3
is raising ~$40,000 USD</a> to release HTTP/2 support and ensure
long-term sustainable maintenance of the project after a sharp decline
in financial support. If your company or organization uses Python and
would benefit from HTTP/2 support in Requests, pip, cloud SDKs, and
thousands of other projects <a
href="https://opencollective.com/urllib3">please consider contributing
financially</a> to ensure HTTP/2 support is developed sustainably and
maintained for the long-haul.</p>
<p>Thank you for your support.</p>
<h2>Changes</h2>
<ul>
<li>Restore previously removed <code>HTTPResponse.getheaders()</code>
and <code>HTTPResponse.getheader()</code> methods. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3731">#3731</a>)</li>
</ul>
<h2>2.6.0</h2>
<h2>🚀 urllib3 is fundraising for HTTP/2 support</h2>
<p><a
href="https://sethmlarson.dev/urllib3-is-fundraising-for-http2-support">urllib3
is raising ~$40,000 USD</a> to release HTTP/2 support and ensure
long-term sustainable maintenance of the project after a sharp decline
in financial support. If your company or organization uses Python and
would benefit from HTTP/2 support in Requests, pip, cloud SDKs, and
thousands of other projects <a
href="https://opencollective.com/urllib3">please consider contributing
financially</a> to ensure HTTP/2 support is developed sustainably and
maintained for the long-haul.</p>
<p>Thank you for your support.</p>
<h2>Security</h2>
<ul>
<li>Fixed a security issue where streaming API could improperly handle
highly compressed HTTP content (&quot;decompression bombs&quot;) leading
to excessive resource consumption even when a small amount of data was
requested. Reading small chunks of compressed data is safer and much
more efficient now. (CVE-2025-66471 reported by <a
href="https://github.com/Cycloctane"><code>@​Cycloctane</code></a>, 8.9
High, GHSA-2xpw-w6gg-jr37)</li>
<li>Fixed a security issue where an attacker could compose an HTTP
response with virtually unlimited links in the
<code>Content-Encoding</code> header, potentially leading to a denial of
service (DoS) attack by exhausting system resources during decoding. The
number of allowed chained encodings is now limited to 5. (CVE-2025-66418
reported by <a
href="https://github.com/illia-v"><code>@​illia-v</code></a>, 8.9 High,
GHSA-gm62-xv2j-4w53)</li>
</ul>
<blockquote>
<p>[!IMPORTANT]</p>
<ul>
<li>If urllib3 is not installed with the optional
<code>urllib3[brotli]</code> extra, but your environment contains a
Brotli/brotlicffi/brotlipy package anyway, make sure to upgrade it to at
least Brotli 1.2.0 or brotlicffi 1.2.0.0 to benefit from the security
fixes and avoid warnings. Prefer using <code>urllib3[brotli]</code> to
install a compatible Brotli package automatically.</li>
</ul>
</blockquote>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/urllib3/urllib3/blob/main/CHANGES.rst">urllib3's
changelog</a>.</em></p>
<blockquote>
<h1>2.6.3 (2026-01-07)</h1>
<ul>
<li>Fixed a high-severity security issue where decompression-bomb
safeguards of
the streaming API were bypassed when HTTP redirects were followed.
(<code>GHSA-38jv-5279-wg99
&lt;https://github.com/urllib3/urllib3/security/advisories/GHSA-38jv-5279-wg99&gt;</code>__)</li>
<li>Started treating <code>Retry-After</code> times greater than 6 hours
as 6 hours by
default. (<code>[#3743](https://github.com/urllib3/urllib3/issues/3743)
&lt;https://github.com/urllib3/urllib3/issues/3743&gt;</code>__)</li>
<li>Fixed <code>urllib3.connection.VerifiedHTTPSConnection</code> on
Emscripten.
(<code>[#3752](https://github.com/urllib3/urllib3/issues/3752)
&lt;https://github.com/urllib3/urllib3/issues/3752&gt;</code>__)</li>
</ul>
<h1>2.6.2 (2025-12-11)</h1>
<ul>
<li>Fixed <code>HTTPResponse.read_chunked()</code> to properly handle
leftover data in
the decoder's buffer when reading compressed chunked responses.
(<code>[#3734](https://github.com/urllib3/urllib3/issues/3734)
&lt;https://github.com/urllib3/urllib3/issues/3734&gt;</code>__)</li>
</ul>
<h1>2.6.1 (2025-12-08)</h1>
<ul>
<li>Restore previously removed <code>HTTPResponse.getheaders()</code>
and
<code>HTTPResponse.getheader()</code> methods.
(<code>[#3731](https://github.com/urllib3/urllib3/issues/3731)
&lt;https://github.com/urllib3/urllib3/issues/3731&gt;</code>__)</li>
</ul>
<h1>2.6.0 (2025-12-05)</h1>
<h2>Security</h2>
<ul>
<li>Fixed a security issue where streaming API could improperly handle
highly
compressed HTTP content (&quot;decompression bombs&quot;) leading to
excessive resource
consumption even when a small amount of data was requested. Reading
small
chunks of compressed data is safer and much more efficient now.
(<code>GHSA-2xpw-w6gg-jr37
&lt;https://github.com/urllib3/urllib3/security/advisories/GHSA-2xpw-w6gg-jr37&gt;</code>__)</li>
<li>Fixed a security issue where an attacker could compose an HTTP
response with
virtually unlimited links in the <code>Content-Encoding</code> header,
potentially
leading to a denial of service (DoS) attack by exhausting system
resources
during decoding. The number of allowed chained encodings is now limited
to 5.
(<code>GHSA-gm62-xv2j-4w53
&lt;https://github.com/urllib3/urllib3/security/advisories/GHSA-gm62-xv2j-4w53&gt;</code>__)</li>
</ul>
<p>.. caution::</p>
<ul>
<li>If urllib3 is not installed with the optional
<code>urllib3[brotli]</code> extra, but
your environment contains a Brotli/brotlicffi/brotlipy package anyway,
make
sure to upgrade it to at least Brotli 1.2.0 or brotlicffi 1.2.0.0 to
benefit from the security fixes and avoid warnings. Prefer using</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/urllib3/urllib3/commit/0248277dd7ac0239204889ca991353ad3e3a1ddc"><code>0248277</code></a>
Release 2.6.3</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/8864ac407bba8607950025e0979c4c69bc7abc7b"><code>8864ac4</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/70cecb27ca99d56aaaeb63ac27ee270ef2b24c5c"><code>70cecb2</code></a>
Fix Scorecard issues related to vulnerable dev dependencies (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3755">#3755</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/41f249abe1ef3e20768588969c4035aba060a359"><code>41f249a</code></a>
Move &quot;v2.0 Migration Guide&quot; to the end of the table of
contents (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3747">#3747</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/fd4dffd2fc544166b76151a2fa3d7b7c0eab540c"><code>fd4dffd</code></a>
Patch <code>VerifiedHTTPSConnection</code> for Emscripten (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3752">#3752</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/13f0bfd55e4468fe1ea9c6f809d3a87b0f93ebab"><code>13f0bfd</code></a>
Handle massive values in Retry-After when calculating time to sleep for
(<a
href="https://redirect.github.com/urllib3/urllib3/issues/3743">#3743</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/8c480bf87bcefd321b3a1ae47f04e908b6b2ed7b"><code>8c480bf</code></a>
Bump actions/upload-artifact from 5.0.0 to 6.0.0 (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3748">#3748</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/4b40616e959c0a2c466e8075f2a785a9f99bb0c1"><code>4b40616</code></a>
Bump actions/cache from 4.3.0 to 5.0.1 (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3750">#3750</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/82b8479663d037d220c883f1584dd01a43bb273b"><code>82b8479</code></a>
Bump actions/download-artifact from 6.0.0 to 7.0.0 (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3749">#3749</a>)</li>
<li><a
href="https://github.com/urllib3/urllib3/commit/34284cb01700bb7d4fdd472f909e22393e9174e2"><code>34284cb</code></a>
Mention experimental features in the security policy (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3746">#3746</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/urllib3/urllib3/compare/2.5.0...2.6.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=urllib3&package-manager=pip&previous-version=2.5.0&new-version=2.6.3)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/Stirling-Tools/Stirling-PDF/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-14 00:37:18 +00:00
Ludy a366463496 ci(build): scope Docker layer cache per artifact to avoid cross-contamination (#5453)
This pull request updates the Docker build caching strategy in the
GitHub Actions workflow to improve cache isolation and efficiency. The
main change is the introduction of a cache `scope` based on the
`artifact-suffix` matrix value.

**Build caching improvements:**

*
[`.github/workflows/build.yml`](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L329-R330):
Updated `cache-from` and `cache-to` in the Docker build step to use a
scoped cache (`scope=${{ matrix.artifact-suffix }}`), enabling better
cache isolation across different build variants.
2026-01-14 00:36:41 +00:00
Ludy 472ee54098 fix(translations): improve translation merger CLI and sync missing UI strings across locales (#5309)
# Description of Changes

This pull request updates the Arabic translation file
(`frontend/public/locales/ar-AR/translation.toml`) with a large number
of new and improved strings, adding support for new features and
enhancing clarity and coverage across the application. Additionally, it
makes several improvements to the TOML language check script
(`.github/scripts/check_language_toml.py`) and updates the corresponding
GitHub Actions workflow to better track and validate translation
changes.

**Translation updates and enhancements:**

* Added translations for new features and UI elements, including
annotation tools, PDF/A-3b conversion, line art compression, background
removal, split modes, onboarding tours, and more.
[[1]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR343-R346)
[[2]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR442-R460)
[[3]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR514-R523)
[[4]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR739-R743)
[[5]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR1281-R1295)
[[6]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR1412-R1416)
[[7]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR2362-R2365)
[[8]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR2411-R2415)
[[9]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR2990)
[[10]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR3408-R3420)
[[11]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR3782-R3794)
[[12]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR3812-R3815)
[[13]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR3828-R3832)
[[14]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effL3974-R4157)
[[15]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR4208-R4221)
[[16]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR5247)
[[17]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR5414-R5423)
[[18]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR5444-R5447)
* Improved and expanded coverage for settings, security, onboarding, and
help menus, including detailed descriptions and tooltips for new and
existing features.
[[1]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR442-R460)
[[2]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR5247)
[[3]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR5414-R5423)
[[4]](diffhunk://#diff-460d5f61a7649a5b149373af2e52a8a87d9a1964cf54240a78ad4747e7233effR5444-R5447)

**TOML language check script improvements:**

* Increased the maximum allowed TOML file size from 500 KB to 570 KB to
accommodate larger translation files.
* Improved file validation logic to more accurately skip or process
files based on directory structure and file type, and added informative
print statements for skipped files.
* Enhanced reporting in the difference check: now, instead of raising
exceptions for unsafe files or oversized files, the script logs warnings
and continues processing, improving robustness and clarity in CI
reports.
* Adjusted the placement of file check report lines for clarity in the
generated report.

**Workflow and CI improvements:**

* Updated the GitHub Actions workflow
(`.github/workflows/check_toml.yml`) to trigger on changes to the
translation script and workflow files, in addition to translation TOMLs,
ensuring all relevant changes are validated.

These changes collectively improve the translation quality and coverage
for Arabic users, enhance the reliability and clarity of the translation
validation process, and ensure smoother CI/CD workflows for localization
updates.

<img width="654" height="133" alt="image"
src="https://github.com/user-attachments/assets/9f3e505d-927f-4dc0-9098-cee70bbe85ca"
/>


---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-14 00:31:05 +00:00
Ludy db049a3467 fix(core): reinstall frontend dependencies when iconify package is missing (#5432)
# Description of Changes

### What was changed
- Updated the Gradle `npmInstall` task to detect a missing required
devDependency (`@iconify-json/material-symbols`) even when
`node_modules` exists.
- If the package is missing, the task will no longer skip `npm install`;
it will force a reinstall instead.

### Why the change was made
- The frontend build (`npm run generate-icons`) can fail with `Cannot
find module '@iconify-json/material-symbols'` when `node_modules` is
present but incomplete/corrupted.
- The current "node_modules exists => skip install" shortcut is not
sufficient for this scenario, causing Gradle builds to fail during
`npmBuild`.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-14 00:22:05 +00:00
Balázs SzücsandAnthony Stirling b00bd760c8 feat(conversion): add PDF to EPUB/AZW3 conversion support and settings (#5434)
# Description of Changes


This pull request introduces support for converting PDF files to eBook
formats (EPUB and AZW3) in the frontend. It adds new user interface
options for PDF-to-eBook conversion, updates the conversion logic and
parameters, and ensures the new formats are integrated into the
conversion matrix and endpoints. The most important changes are grouped
below:

**PDF to eBook (EPUB/AZW3) Conversion Support**

* Added a new `ConvertToEpubSettings` component that provides UI
controls for PDF-to-eBook options, including chapter detection, target
device selection, and output format.
(`frontend/src/core/components/tools/convert/ConvertToEpubSettings.tsx`)
* Updated `ConvertSettings` to render the new eBook options when
converting from PDF to EPUB or AZW3, and set default values for these
options.
(`frontend/src/core/components/tools/convert/ConvertSettings.tsx`)
* Extended the `ConvertParameters` interface and default parameters to
include `epubOptions` for the new settings.
(`frontend/src/core/hooks/tools/convert/useConvertParameters.ts`)

**Conversion Logic and API Integration**

* Updated the conversion endpoints, endpoint names, and conversion
matrix to support PDF-to-EPUB/AZW3 conversions.
(`frontend/src/core/constants/convertConstants.ts`)
* Modified the conversion operation logic to handle `epubOptions` and
ensure that PDF-to-eBook conversions process each file separately and
send the correct options to the backend.
(`frontend/src/core/hooks/tools/convert/useConvertOperation.ts`)
**Localization and Tool Registry Updates**

* Added localization strings for the new eBook conversion options.
(`frontend/public/locales/en-GB/translation.toml`)
* Registered the new PDF-to-eBook operation in the tool catalog and test
helpers. (`frontend/src/core/data/useTranslatedToolRegistry.tsx`,
`frontend/src/core/tests/helpers/conversionEndpointDiscovery.ts`)



<img width="364" height="995" alt="image"
src="https://github.com/user-attachments/assets/c54c50c0-1b86-4074-aef8-b038c6caeb49"
/>

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-01-13 22:22:23 +00:00
e7b030e6b5 Add Telegram bot integration for pipeline processing (#5185)
# Description of Changes

This pull request introduces Telegram bot integration to the
application, enabling users to send files via Telegram for processing
through the pipeline. The main changes add configuration options,
dependency management, and a new service for handling Telegram
interactions.

**Telegram bot integration:**

* Added a new `TelegramPipelineBot` service (`TelegramPipelineBot.java`)
that listens for incoming Telegram messages, downloads attached files or
photos, places them in a pipeline inbox folder, waits for processing
results, and sends the output files back to the user. The service
includes error handling and status messaging.
* Introduced a `TelegramBotConfig` configuration class to initialize and
register the Telegram bot only when enabled via application properties.
* Added a new `Telegram` configuration section to
`ApplicationProperties` and the `settings.yml.template`, supporting
options like enabling/disabling the bot, bot token/username, pipeline
folder, processing timeout, and polling interval.
[[1]](diffhunk://#diff-1c357db0a3e88cf5bedd4a5852415fadad83b8b3b9eb56e67059d8b9d8b10702R63)
[[2]](diffhunk://#diff-1c357db0a3e88cf5bedd4a5852415fadad83b8b3b9eb56e67059d8b9d8b10702R580-R589)
[[3]](diffhunk://#diff-12f23603ae35319a3ea08f91b6340d5d935216941fda2e69d2df1b6cd22a63f2R108-R115)

**Dependency management:**

* Added the `org.telegram:telegrambots` library to the project
dependencies to support Telegram bot functionality.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-01-13 22:14:59 +00:00
Anthony Stirling 83e96a9aa3 Update user login conditions in workflow (#5457)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-13 22:00:06 +00:00
dependabot[bot] 392462a325 build(deps): bump globals from 16.5.0 to 17.0.0 in /frontend (#5413)
Bumps [globals](https://github.com/sindresorhus/globals) from 16.5.0 to
17.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/sindresorhus/globals/releases">globals's
releases</a>.</em></p>
<blockquote>
<h2>v17.0.0</h2>
<h3>Breaking</h3>
<ul>
<li>Split <code>audioWorklet</code> environment from
<code>browser</code> (<a
href="https://redirect.github.com/sindresorhus/globals/issues/320">#320</a>)
7bc293e</li>
</ul>
<h3>Improvements</h3>
<ul>
<li>Update globals (<a
href="https://redirect.github.com/sindresorhus/globals/issues/329">#329</a>)
ebe1063</li>
<li>Get all browser globals from both <code>chrome</code> and
<code>firefox</code> (<a
href="https://redirect.github.com/sindresorhus/globals/issues/321">#321</a>)
59ceff8</li>
<li>Add <code>bunBuiltin</code> environment (<a
href="https://redirect.github.com/sindresorhus/globals/issues/324">#324</a>)
1bc6e3b</li>
<li>Add <code>denoBuiltin</code> environment (<a
href="https://redirect.github.com/sindresorhus/globals/issues/324">#324</a>)
1bc6e3b</li>
<li>Add <code>paintWorklet</code> environment (<a
href="https://redirect.github.com/sindresorhus/globals/issues/323">#323</a>)
4b78f56</li>
<li>Add <code>sharedWorker</code> environment (<a
href="https://redirect.github.com/sindresorhus/globals/issues/322">#322</a>)
4a02a85</li>
</ul>
<hr />
<p><a
href="https://github.com/sindresorhus/globals/compare/v16.5.0...v17.0.0">https://github.com/sindresorhus/globals/compare/v16.5.0...v17.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/sindresorhus/globals/commit/bb89b18916b00f6c4f089baa0d66cb7f0771ab1b"><code>bb89b18</code></a>
17.0.0</li>
<li><a
href="https://github.com/sindresorhus/globals/commit/ebe106340137419b4ac0150f746b7567fb78ce03"><code>ebe1063</code></a>
Update globals (2026-01-01) (<a
href="https://redirect.github.com/sindresorhus/globals/issues/329">#329</a>)</li>
<li><a
href="https://github.com/sindresorhus/globals/commit/e3d8da349da72d66fcadf94d926d4ff96da3319c"><code>e3d8da3</code></a>
Revert &quot;Automate script for <code>paintWorklet</code> environment
(<a
href="https://redirect.github.com/sindresorhus/globals/issues/325">#325</a>)&quot;
(<a
href="https://redirect.github.com/sindresorhus/globals/issues/328">#328</a>)</li>
<li><a
href="https://github.com/sindresorhus/globals/commit/e7c0453f80840ebcef3a6bccaabc7a9dc0cb7a84"><code>e7c0453</code></a>
Automate script for <code>paintWorklet</code> environment (<a
href="https://redirect.github.com/sindresorhus/globals/issues/325">#325</a>)</li>
<li><a
href="https://github.com/sindresorhus/globals/commit/1bc6e3bc6658d4243a714bb7dd7401f206fd3bc5"><code>1bc6e3b</code></a>
Add <code>bunBuiltin</code> and <code>denoBuiltin</code> (<a
href="https://redirect.github.com/sindresorhus/globals/issues/324">#324</a>)</li>
<li><a
href="https://github.com/sindresorhus/globals/commit/4b78f56c1c305e34161183e8fd5ac89e92c72e84"><code>4b78f56</code></a>
Add <code>paintWorklet</code> environment (<a
href="https://redirect.github.com/sindresorhus/globals/issues/323">#323</a>)</li>
<li><a
href="https://github.com/sindresorhus/globals/commit/59ceff80cb22de31a78651acdda4db1a91eefb11"><code>59ceff8</code></a>
Get all browser globals from both <code>chrome</code> and
<code>firefox</code> (<a
href="https://redirect.github.com/sindresorhus/globals/issues/321">#321</a>)</li>
<li><a
href="https://github.com/sindresorhus/globals/commit/4a02a852463c03ae722f14f06944c5aa7e0e06e0"><code>4a02a85</code></a>
Add <code>sharedWorker</code> environment (<a
href="https://redirect.github.com/sindresorhus/globals/issues/322">#322</a>)</li>
<li><a
href="https://github.com/sindresorhus/globals/commit/7bc293ec2498be49293e3aec46186a98a76e5ecd"><code>7bc293e</code></a>
Split <code>audioWorklet</code> environment from <code>browser</code>
(<a
href="https://redirect.github.com/sindresorhus/globals/issues/320">#320</a>)</li>
<li><a
href="https://github.com/sindresorhus/globals/commit/814075ffd71e6b0599f2611cd445b00906ad2181"><code>814075f</code></a>
Fix browser launch (<a
href="https://redirect.github.com/sindresorhus/globals/issues/319">#319</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/sindresorhus/globals/compare/v16.5.0...v17.0.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=globals&package-manager=npm_and_yarn&previous-version=16.5.0&new-version=17.0.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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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-01-13 21:28:28 +00:00
dependabot[bot] 0a801fe3a6 build(deps): bump nginx from 8491795 to c083c37 in /docker/frontend (#5448)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps nginx from `8491795` to `c083c37`.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=nginx&package-manager=docker&previous-version=alpine&new-version=alpine)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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-01-13 21:26:40 +00:00
dependabot[bot] 646df1bfb5 build(deps): bump node from 20-alpine to 25-alpine in /docker/frontend (#5311)
Bumps node from 20-alpine to 25-alpine.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=node&package-manager=docker&previous-version=20-alpine&new-version=25-alpine)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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-01-13 21:26:06 +00:00
stirlingbot[bot] bfc029f993 Update Backend 3rd Party Licenses (#5363)
Auto-generated by stirlingbot[bot]

This PR updates the backend license report based on dependency changes.

Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-01-13 21:18:36 +00:00
LudyandJames Brunton b266b50bef fix(api): return JSON responses for admin settings + API key endpoints to prevent Tauri client parse errors (#5437)
# Description of Changes

```console
index-DsORDqQQ.js:124 \n [TauriHttpClient] Network error: \n{url: 'http://localhost:8080/api/v1/admin/settings', method: 'PUT', errorType: 'ERR_NETWORK', originalMessage: `Failed to execute 'close' on 'ReadableStreamDefaul…cted token 'S', "Successful"... is not valid JSON`, stack: `SyntaxError: Unexpected token 'S', "Successful"...…lback (<anonymous>:284:7)\n    at <anonymous>:1:28`}\nerrorType\n: \n"ERR_NETWORK"\nmethod\n: \n"PUT"\noriginalMessage\n: \n"Failed to execute 'close' on 'ReadableStreamDefaultController': Unexpected token 'S', \"Successful\"... is not valid JSON"\nstack\n: \n"SyntaxError: Unexpected token 'S', \"Successful\"... is not valid JSON\n    at A.onmessage (http://tauri.localhost/assets/index-DsORDqQQ.js:124:22714)\n    at http://tauri.localhost/assets/index-DsORDqQQ.js:124:20748\n    at <anonymous>:272:26\n    at Object.runCallback (<anonymous>:284:7)\n    at <anonymous>:1:28"\nurl\n: \n"http://localhost:8080/api/v1/admin/settings"

index-DXbk7lbS.js:124 \n [TauriHttpClient] Network error: \n{url: 'http://localhost:8080/api/v1/user/get-api-key', method: 'POST', errorType: 'ERR_NETWORK', originalMessage: `Failed to execute 'close' on 'ReadableStreamDefaul…cted token 'a', "a72f6b26-1"... is not valid JSON`, stack: `SyntaxError: Unexpected token 'a', "a72f6b26-1"...…lback (<anonymous>:284:7)\n    at <anonymous>:1:28`}\nerrorType\n: \n"ERR_NETWORK"\nmethod\n: \n"POST"\noriginalMessage\n: \n"Failed to execute 'close' on 'ReadableStreamDefaultController': Unexpected token 'a', \"a72f6b26-1\"... is not valid JSON"\nstack\n: \n"SyntaxError: Unexpected token 'a', \"a72f6b26-1\"... is not valid JSON\n    at A.onmessage (http://tauri.localhost/assets/index-DXbk7lbS.js:124:22714)\n    at http://tauri.localhost/assets/index-DXbk7lbS.js:124:20748\n    at <anonymous>:272:26\n    at Object.runCallback (<anonymous>:284:7)\n    at <anonymous>:1:28"\nurl\n: \n"http://localhost:8080/api/v1/user/get-api-key"
```


This pull request fixes a self-hosting issue where the Tauri HTTP client
fails with `Unexpected token ... is not valid JSON` because certain API
endpoints returned plain text responses.

## What was changed

- Updated `AdminSettingsController`:
- Changed `updateSettings` and `updateSettingsSection` to return
structured JSON objects instead of raw strings.
- Standardized success and error payloads using a `Map<String, Object>`
with keys like `message` and `error`.
- Updated `UserController`:
- Changed `/api/v1/user/get-api-key` and `/api/v1/user/update-api-key`
to return JSON objects (`{ "apiKey": "..." }`) and JSON error objects
instead of plain text.

## Why the change was made

- The Tauri client expects JSON responses and attempts to parse them.
Returning plain strings like `"Successful..."` or an API key string
causes JSON parsing to fail, resulting in network errors on self-hosted
setups.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: James Brunton <jbrunton96@gmail.com>
2026-01-13 21:18:09 +00:00
Balázs Szücs 84ed1d7ecb feat(conversion): refactor EML parser to use Simple Java Mail library and add MSG support (#5427)
# Description of Changes


Note on Simple Java Mail:
- SJM contains Angus/Jakarta Mail in it.
- SJM is a very thin layer on Angus Mail; see here:
https://github.com/bbottema/simple-java-mail
- SJM gives high level methods to more reliably parse in email via Angus
Mail, but also contains lots of other interesting features.
- SJM is Apache 2 licensed

This pull request updates the email processing utilities to add support
for parsing and validating Outlook MSG files, refactors the
`EmlProcessingUtils` utility class to use instance methods and improved
resource management, and enhances the handling and styling of generated
email HTML. The changes also introduce external CSS resource loading
with a fallback mechanism, and update dependencies to support MSG file
parsing.

**MSG file support and validation:**
- Added `simple-java-mail` and `outlook-module` dependencies to enable
EML and MSG file parsing, and updated validation logic to recognize and
accept MSG files by checking their magic bytes.
(`app/common/build.gradle`, `EmlProcessingUtils.java`)
**Refactoring and modernization of `EmlProcessingUtils`:**
- Converted static methods and fields in `EmlProcessingUtils` to
instance methods/fields, improving testability and future extensibility.
(`EmlProcessingUtils.java`)

**Enhanced HTML/CSS styling for email rendering:**
- Updated HTML generation to use consistent formatting and improved
style variable usage, and refactored CSS injection to load from an
external resource (`email-pdf-styles.css`) with a synchronized cache and
a minimal fallback if the resource is missing.
(`EmlProcessingUtils.java`)
**Attachment and content rendering improvements:**
- Improved the formatting of meta-information (e.g., CC, BCC, Date) and
attachment sections in generated email HTML, and ensured more robust
handling of empty or missing content. (`EmlProcessingUtils.java`)

**General code cleanup and logging:**
- Added SLF4J logging for error handling when loading CSS resources, and
cleaned up imports and method signatures for clarity and
maintainability. (`EmlProcessingUtils.java`)


<img width="367" height="991" alt="image"
src="https://github.com/user-attachments/assets/0cfb959c-da92-4cff-9e52-ff4ab7fa806e"
/>


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [X] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2026-01-13 21:17:40 +00:00
dependabot[bot] daf27b6128 build(deps): bump step-security/harden-runner from 2.12.1 to 2.14.0 (#5324)
Bumps
[step-security/harden-runner](https://github.com/step-security/harden-runner)
from 2.12.1 to 2.14.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.14.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Selective installation: Harden-Runner now skips installation on
GitHub-hosted runners when the repository has a custom property
skip_harden_runner, allowing organizations to opt out specific
repos.</li>
<li>Avoid double install: The action no longer installs Harden-Runner if
it’s already present on a GitHub-hosted runner, which could happen when
a composite action also installs it.</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/step-security/harden-runner/compare/v2.13.3...v2.14.0">https://github.com/step-security/harden-runner/compare/v2.13.3...v2.14.0</a></p>
<h2>v2.13.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Fixed an issue where process events were not uploaded in certain
edge cases.</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/step-security/harden-runner/compare/v2.13.2...v2.13.3">https://github.com/step-security/harden-runner/compare/v2.13.2...v2.13.3</a></p>
<h2>v2.13.2</h2>
<h2>What's Changed</h2>
<ul>
<li>Fixed an issue where there was a limit of 512 allowed endpoints when
using block egress policy. This restriction has been removed, allowing
for an unlimited number of endpoints to be configured.</li>
<li>Harden Runner now automatically detects if the agent is already
pre-installed on a custom VM image used by a GitHub-hosted runner. When
detected, the action will skip reinstallation and use the existing
agent.</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/step-security/harden-runner/compare/v2.13.1...v2.13.2">https://github.com/step-security/harden-runner/compare/v2.13.1...v2.13.2</a></p>
<h2>v2.13.1</h2>
<h2>What's Changed</h2>
<ul>
<li>
<p>Graceful handling of HTTP errors: Improved error handling when
fetching Harden Runner policies from the StepSecurity Policy Store API,
ensuring more reliable execution even in case of temporary network/API
issues.</p>
</li>
<li>
<p>Security updates for npm dependencies: Updated vulnerable npm package
dependencies to the latest secure versions.</p>
</li>
<li>
<p>Faster enterprise agent downloads: The enterprise agent is now
downloaded from GitHub Releases instead of packages.stepsecurity.io,
improving download speed and reliability.</p>
</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/step-security/harden-runner/compare/v2.13.0...v2.13.1">https://github.com/step-security/harden-runner/compare/v2.13.0...v2.13.1</a></p>
<h2>v2.13.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Improved job markdown summary</li>
<li>Https monitoring for all domains (included with the enterprise
tier)</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/step-security/harden-runner/compare/v2...v2.13.0">https://github.com/step-security/harden-runner/compare/v2...v2.13.0</a></p>
<h2>v2.12.2</h2>
<h2>What's Changed</h2>
<p>Added HTTPS Monitoring for additional destinations -
*.githubusercontent.com
Bug fixes:</p>
<ul>
<li>Implicitly allow local multicast, local unicast and broadcast IP
addresses in block mode</li>
<li>Increased policy map size for block mode</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/step-security/harden-runner/commit/20cf305ff2072d973412fa9b1e3a4f227bda3c76"><code>20cf305</code></a>
Merge pull request <a
href="https://redirect.github.com/step-security/harden-runner/issues/622">#622</a>
from step-security/feature/custom-property-skip</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/c51e8eeb6c4fdcd08f65e43a051dacdbfaa69702"><code>c51e8ee</code></a>
feat: skip agent install and post step on subsequent runs for
GitHub-hosted r...</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/e152b90204c3d85cefa1441b701a47a13ed28bd7"><code>e152b90</code></a>
feat: skip harden-runner based on repository custom property</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/ee1faec052d1000061fa79a13e030db11b3f86bd"><code>ee1faec</code></a>
feat: replace skip-harden-runner with skip-on-custom-property input</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/1dc7c1764659d537dab2a854b8e165a801103eb1"><code>1dc7c17</code></a>
feat: add skip-harden-runner input to conditionally skip execution</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/df199fb7be9f65074067a9eb93f12bb4c5547cf2"><code>df199fb</code></a>
Merge pull request <a
href="https://redirect.github.com/step-security/harden-runner/issues/620">#620</a>
from step-security/rc-29</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/03d096a772368b1f0222005a6899d3e35a7f62df"><code>03d096a</code></a>
update agent</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/40901073af04afd40408833437092a7467798f33"><code>4090107</code></a>
fix: update agent</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/95d9a5deda9de15063e7595e9719c11c38c90ae2"><code>95d9a5d</code></a>
Merge pull request <a
href="https://redirect.github.com/step-security/harden-runner/issues/606">#606</a>
from step-security/rc-28</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/87e429d3fb470bcc827f338e5cce1155ff99c6eb"><code>87e429d</code></a>
Update limitations.md</li>
<li>Additional commits viewable in <a
href="https://github.com/step-security/harden-runner/compare/v2.12.1...20cf305ff2072d973412fa9b1e3a4f227bda3c76">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.12.1&new-version=2.14.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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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-01-13 20:38:11 +00:00
dependabot[bot] 818c30cb55 build(deps): bump docker/setup-qemu-action from 3.6.0 to 3.7.0 (#5326)
Bumps
[docker/setup-qemu-action](https://github.com/docker/setup-qemu-action)
from 3.6.0 to 3.7.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/docker/setup-qemu-action/releases">docker/setup-qemu-action's
releases</a>.</em></p>
<blockquote>
<h2>v3.7.0</h2>
<ul>
<li>Bump <code>@​docker/actions-toolkit</code> from 0.56.0 to 0.67.0 in
<a
href="https://redirect.github.com/docker/setup-qemu-action/pull/217">docker/setup-qemu-action#217</a>
<a
href="https://redirect.github.com/docker/setup-qemu-action/pull/230">docker/setup-qemu-action#230</a></li>
<li>Bump brace-expansion from 1.1.11 to 1.1.12 in <a
href="https://redirect.github.com/docker/setup-qemu-action/pull/220">docker/setup-qemu-action#220</a></li>
<li>Bump form-data from 2.5.1 to 2.5.5 in <a
href="https://redirect.github.com/docker/setup-qemu-action/pull/218">docker/setup-qemu-action#218</a></li>
<li>Bump tmp from 0.2.3 to 0.2.4 in <a
href="https://redirect.github.com/docker/setup-qemu-action/pull/221">docker/setup-qemu-action#221</a></li>
<li>Bump undici from 5.28.4 to 5.29.0 in <a
href="https://redirect.github.com/docker/setup-qemu-action/pull/219">docker/setup-qemu-action#219</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/docker/setup-qemu-action/compare/v3.6.0...v3.7.0">https://github.com/docker/setup-qemu-action/compare/v3.6.0...v3.7.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/docker/setup-qemu-action/commit/c7c53464625b32c7a7e944ae62b3e17d2b600130"><code>c7c5346</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/setup-qemu-action/issues/230">#230</a>
from docker/dependabot/npm_and_yarn/docker/actions-to...</li>
<li><a
href="https://github.com/docker/setup-qemu-action/commit/3a517a1a6f815421d6c0f8ab069bd9cae3024828"><code>3a517a1</code></a>
chore: update generated content</li>
<li><a
href="https://github.com/docker/setup-qemu-action/commit/a5b45edf7e42dbe6d762ad8053bd35a7d336a850"><code>a5b45ed</code></a>
build(deps): bump <code>@​docker/actions-toolkit</code> from 0.62.1 to
0.67.0</li>
<li><a
href="https://github.com/docker/setup-qemu-action/commit/3a64278e93930d340f1caabd280b2e78b36b5032"><code>3a64278</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/setup-qemu-action/issues/220">#220</a>
from docker/dependabot/npm_and_yarn/brace-expansion-1...</li>
<li><a
href="https://github.com/docker/setup-qemu-action/commit/94906ba253608103ef32875025dd58d7f74e2716"><code>94906ba</code></a>
chore: update generated content</li>
<li><a
href="https://github.com/docker/setup-qemu-action/commit/4027abfd67ddf4bd8f75dbf3bb8bf3b69764ded6"><code>4027abf</code></a>
build(deps): bump brace-expansion from 1.1.11 to 1.1.12</li>
<li><a
href="https://github.com/docker/setup-qemu-action/commit/bee0aaad0f43394cdb4b8756cd994ae278b0667c"><code>bee0aaa</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/setup-qemu-action/issues/221">#221</a>
from docker/dependabot/npm_and_yarn/tmp-0.2.4</li>
<li><a
href="https://github.com/docker/setup-qemu-action/commit/0d7e25756e271776f152e7669522f64f54c5e3e2"><code>0d7e257</code></a>
chore: update generated content</li>
<li><a
href="https://github.com/docker/setup-qemu-action/commit/b86960130e28d1756dfa88e15e28ee5880e893de"><code>b869601</code></a>
build(deps): bump tmp from 0.2.3 to 0.2.4</li>
<li><a
href="https://github.com/docker/setup-qemu-action/commit/3a043edff31c211e56ebb1e00a76ce49a831bd24"><code>3a043ed</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/setup-qemu-action/issues/219">#219</a>
from docker/dependabot/npm_and_yarn/undici-5.29.0</li>
<li>Additional commits viewable in <a
href="https://github.com/docker/setup-qemu-action/compare/v3.6.0...c7c53464625b32c7a7e944ae62b3e17d2b600130">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=docker/setup-qemu-action&package-manager=github_actions&previous-version=3.6.0&new-version=3.7.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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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-01-13 20:35:11 +00:00
dependabot[bot]andLudy 7460e58abf build(deps): bump io.micrometer:micrometer-core from 1.16.0 to 1.16.1 (#5358)
Bumps
[io.micrometer:micrometer-core](https://github.com/micrometer-metrics/micrometer)
from 1.16.0 to 1.16.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/micrometer-metrics/micrometer/releases">io.micrometer:micrometer-core's
releases</a>.</em></p>
<blockquote>
<h2>1.16.1</h2>
<h2>🐞 Bug Fixes</h2>
<ul>
<li>Don't filter log events in LogbackMetricsBenchmark <a
href="https://redirect.github.com/micrometer-metrics/micrometer/pull/6891">#6891</a></li>
<li>Return value nullability is incorrect on function wrapper methods <a
href="https://redirect.github.com/micrometer-metrics/micrometer/issues/6869">#6869</a></li>
</ul>
<h2>📔 Documentation</h2>
<ul>
<li>Add link to the latest Micrometer Team talk <a
href="https://redirect.github.com/micrometer-metrics/micrometer/pull/6881">#6881</a></li>
<li>Document JSpecify dependency <a
href="https://redirect.github.com/micrometer-metrics/micrometer/issues/6886">#6886</a></li>
<li>Make cross-references more consistent in the docs <a
href="https://redirect.github.com/micrometer-metrics/micrometer/pull/6915">#6915</a></li>
</ul>
<h2>🔨 Dependency Upgrades</h2>
<ul>
<li>Bump ch.qos.logback:logback-classic from 1.5.20 to 1.5.21 <a
href="https://redirect.github.com/micrometer-metrics/micrometer/pull/6877">#6877</a></li>
<li>Bump com.netflix.spectator:spectator-reg-atlas from 1.9.1 to 1.9.2
<a
href="https://redirect.github.com/micrometer-metrics/micrometer/pull/6904">#6904</a></li>
<li>Bump com.uber.nullaway:nullaway from 0.12.12 to 0.12.14 <a
href="https://redirect.github.com/micrometer-metrics/micrometer/pull/6937">#6937</a></li>
<li>Bump grpc from 1.76.0 to 1.76.1 <a
href="https://redirect.github.com/micrometer-metrics/micrometer/pull/6902">#6902</a></li>
<li>Bump io.freefair.aspectj.post-compile-weaving from 8.14.2 to 8.14.3
<a
href="https://redirect.github.com/micrometer-metrics/micrometer/pull/6876">#6876</a></li>
<li>Bump io.prometheus:prometheus-metrics-bom from 1.4.2 to 1.4.3 <a
href="https://redirect.github.com/micrometer-metrics/micrometer/pull/6868">#6868</a></li>
<li>Bump spring6 from 6.2.12 to 6.2.14 <a
href="https://redirect.github.com/micrometer-metrics/micrometer/pull/6890">#6890</a></li>
</ul>
<h2>❤️ Contributors</h2>
<p>Thank you to all the contributors who worked on this release:</p>
<p><a href="https://github.com/MiLabuda"><code>@​MiLabuda</code></a>, <a
href="https://github.com/izeye"><code>@​izeye</code></a>, and <a
href="https://github.com/ngocnhan-tran1996"><code>@​ngocnhan-tran1996</code></a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/micrometer-metrics/micrometer/commit/0550e769cb9b4f15cff86c1e503fce8ae03ef84f"><code>0550e76</code></a>
Merge branch '1.15.x' into 1.16.x</li>
<li><a
href="https://github.com/micrometer-metrics/micrometer/commit/e8dd176ffe60d6ea068c7f536e5662278499cd8f"><code>e8dd176</code></a>
Merge branch '1.14.x' into 1.15.x</li>
<li><a
href="https://github.com/micrometer-metrics/micrometer/commit/d8a46b5a7080738e6f9afd623cfdfcf984f44185"><code>d8a46b5</code></a>
Fixed flaky test for mongo client (<a
href="https://redirect.github.com/micrometer-metrics/micrometer/issues/6924">#6924</a>)</li>
<li><a
href="https://github.com/micrometer-metrics/micrometer/commit/0bbe73b0f447ca5ca94e1d2b621eda83d7735477"><code>0bbe73b</code></a>
Bump com.uber.nullaway:nullaway from 0.12.13 to 0.12.14 (<a
href="https://redirect.github.com/micrometer-metrics/micrometer/issues/6937">#6937</a>)</li>
<li><a
href="https://github.com/micrometer-metrics/micrometer/commit/d0b368dbbc1cd8511f693e7be97810978f67787d"><code>d0b368d</code></a>
Polish <a
href="https://redirect.github.com/micrometer-metrics/micrometer/issues/6612">gh-6612</a>
(<a
href="https://redirect.github.com/micrometer-metrics/micrometer/issues/6932">#6932</a>)</li>
<li><a
href="https://github.com/micrometer-metrics/micrometer/commit/aa9eac0d31acb389a03be50f266bc968c8ef207f"><code>aa9eac0</code></a>
Bump com.uber.nullaway:nullaway from 0.12.12 to 0.12.13 (<a
href="https://redirect.github.com/micrometer-metrics/micrometer/issues/6930">#6930</a>)</li>
<li><a
href="https://github.com/micrometer-metrics/micrometer/commit/57bd4ff8ebb64d941ece6d9fb04d2c921bd77ac1"><code>57bd4ff</code></a>
Merge branch '1.15.x' into 1.16.x</li>
<li><a
href="https://github.com/micrometer-metrics/micrometer/commit/11d8fb1f8081098ad91efc40ea8902b9113d03d3"><code>11d8fb1</code></a>
Merge branch '1.14.x' into 1.15.x</li>
<li><a
href="https://github.com/micrometer-metrics/micrometer/commit/5779b09fcb1d5266c614b50ecefe1ad42264b3c2"><code>5779b09</code></a>
Backport fixing typos in docs</li>
<li><a
href="https://github.com/micrometer-metrics/micrometer/commit/8b88e43a98221f50f56d43cdd5b0eafb986e835b"><code>8b88e43</code></a>
Merge branch '1.15.x' into 1.16.x</li>
<li>Additional commits viewable in <a
href="https://github.com/micrometer-metrics/micrometer/compare/v1.16.0...v1.16.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=io.micrometer:micrometer-core&package-manager=gradle&previous-version=1.16.0&new-version=1.16.1)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
Co-authored-by: Ludy <Ludy87@users.noreply.github.com>
2026-01-13 20:34:36 +00:00
dependabot[bot] a9ee7f6a8e build(deps): bump pypdf from 6.4.0 to 6.6.0 in /testing/cucumber (#5422)
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.4.0 to 6.6.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/py-pdf/pypdf/releases">pypdf's
releases</a>.</em></p>
<blockquote>
<h2>Version 6.6.0, 2026-01-09</h2>
<h2>What's new</h2>
<h3>Security (SEC)</h3>
<ul>
<li>Improve handling of partially broken PDF files (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3594">#3594</a>)
by <a
href="https://github.com/stefan6419846"><code>@​stefan6419846</code></a></li>
</ul>
<h3>Deprecations (DEP)</h3>
<ul>
<li>Block common page content modifications when assigned to reader (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3582">#3582</a>)
by <a
href="https://github.com/stefan6419846"><code>@​stefan6419846</code></a></li>
</ul>
<h3>New Features (ENH)</h3>
<ul>
<li>Embellishments to generated text appearance streams (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3571">#3571</a>)
by <a href="https://github.com/PJBrs"><code>@​PJBrs</code></a></li>
</ul>
<h3>Bug Fixes (BUG)</h3>
<ul>
<li>Do not consider multi-byte BOM-like sequences as BOMs (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3589">#3589</a>)
by <a
href="https://github.com/stefan6419846"><code>@​stefan6419846</code></a></li>
</ul>
<h3>Robustness (ROB)</h3>
<ul>
<li>Avoid empty FlateDecode outputs without warning (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3579">#3579</a>)
by <a
href="https://github.com/stefan6419846"><code>@​stefan6419846</code></a></li>
</ul>
<h3>Documentation (DOC)</h3>
<ul>
<li>Add outlines documentation and link it in User Guide (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3511">#3511</a>)
by <a
href="https://github.com/mainuddin-md"><code>@​mainuddin-md</code></a></li>
</ul>
<h3>Developer Experience (DEV)</h3>
<ul>
<li>Add PyPy 3.11 to test matrix and benchmarks (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3574">#3574</a>)
by <a href="https://github.com/rassie"><code>@​rassie</code></a></li>
</ul>
<h3>Maintenance (MAINT)</h3>
<ul>
<li>Fix compatibility with Pillow &gt;= 12.1.0 (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3590">#3590</a>)
by <a
href="https://github.com/stefan6419846"><code>@​stefan6419846</code></a></li>
</ul>
<p><a href="https://github.com/py-pdf/pypdf/compare/6.5.0...6.6.0">Full
Changelog</a></p>
<h2>Version 6.5.0, 2025-12-21</h2>
<h2>What's new</h2>
<h3>New Features (ENH)</h3>
<ul>
<li>Limit jbig2dec memory usage (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3576">#3576</a>)
by <a
href="https://github.com/stefan6419846"><code>@​stefan6419846</code></a></li>
<li>FontDescriptor: Initiate from embedded font resource (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3551">#3551</a>)
by <a href="https://github.com/PJBrs"><code>@​PJBrs</code></a></li>
</ul>
<h3>Robustness (ROB)</h3>
<ul>
<li>Allow fallback to PBM files for jbig2dec without PNG support (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3567">#3567</a>)
by <a
href="https://github.com/stefan6419846"><code>@​stefan6419846</code></a></li>
<li>Use warning instead of error for early EOD for RunLengthDecode (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3548">#3548</a>)
by <a
href="https://github.com/stefan6419846"><code>@​stefan6419846</code></a></li>
</ul>
<h3>Developer Experience (DEV)</h3>
<ul>
<li>Test with macOS as well (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3401">#3401</a>)
by <a
href="https://github.com/stefan6419846"><code>@​stefan6419846</code></a></li>
</ul>
<p><a href="https://github.com/py-pdf/pypdf/compare/6.4.2...6.5.0">Full
Changelog</a></p>
<h2>Version 6.4.2, 2025-12-14</h2>
<h2>What's new</h2>
<h3>Bug Fixes (BUG)</h3>
<ul>
<li>Fix KeyError when flattening form field without /Font in resources
(<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3554">#3554</a>)
by <a
href="https://github.com/jgillard"><code>@​jgillard</code></a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md">pypdf's
changelog</a>.</em></p>
<blockquote>
<h2>Version 6.6.0, 2026-01-09</h2>
<h3>Security (SEC)</h3>
<ul>
<li>Improve handling of partially broken PDF files (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3594">#3594</a>)</li>
</ul>
<h3>Deprecations (DEP)</h3>
<ul>
<li>Block common page content modifications when assigned to reader (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3582">#3582</a>)</li>
</ul>
<h3>New Features (ENH)</h3>
<ul>
<li>Embellishments to generated text appearance streams (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3571">#3571</a>)</li>
</ul>
<h3>Bug Fixes (BUG)</h3>
<ul>
<li>Do not consider multi-byte BOM-like sequences as BOMs (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3589">#3589</a>)</li>
</ul>
<h3>Robustness (ROB)</h3>
<ul>
<li>Avoid empty FlateDecode outputs without warning (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3579">#3579</a>)</li>
</ul>
<h3>Documentation (DOC)</h3>
<ul>
<li>Add outlines documentation and link it in User Guide (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3511">#3511</a>)</li>
</ul>
<h3>Developer Experience (DEV)</h3>
<ul>
<li>Add PyPy 3.11 to test matrix and benchmarks (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3574">#3574</a>)</li>
</ul>
<h3>Maintenance (MAINT)</h3>
<ul>
<li>Fix compatibility with Pillow &gt;= 12.1.0 (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3590">#3590</a>)</li>
</ul>
<p><a href="https://github.com/py-pdf/pypdf/compare/6.5.0...6.6.0">Full
Changelog</a></p>
<h2>Version 6.5.0, 2025-12-21</h2>
<h3>New Features (ENH)</h3>
<ul>
<li>Limit jbig2dec memory usage (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3576">#3576</a>)</li>
<li>FontDescriptor: Initiate from embedded font resource (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3551">#3551</a>)</li>
</ul>
<h3>Robustness (ROB)</h3>
<ul>
<li>Allow fallback to PBM files for jbig2dec without PNG support (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3567">#3567</a>)</li>
<li>Use warning instead of error for early EOD for RunLengthDecode (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3548">#3548</a>)</li>
</ul>
<h3>Developer Experience (DEV)</h3>
<ul>
<li>Test with macOS as well (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3401">#3401</a>)</li>
</ul>
<p><a href="https://github.com/py-pdf/pypdf/compare/6.4.2...6.5.0">Full
Changelog</a></p>
<h2>Version 6.4.2, 2025-12-14</h2>
<h3>Bug Fixes (BUG)</h3>
<ul>
<li>Fix KeyError when flattening form field without /Font in resources
(<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3554">#3554</a>)</li>
</ul>
<h3>Robustness (ROB)</h3>
<ul>
<li>Allow deleting non-existent annotations (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3559">#3559</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/py-pdf/pypdf/commit/10df9c72fa7fb9ab14101b9cb911d66e680282a8"><code>10df9c7</code></a>
REL: 6.6.0</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/294165726b646bb7799be1cc787f593f2fdbcf45"><code>2941657</code></a>
SEC: Improve handling of partially broken PDF files (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3594">#3594</a>)</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/712688005e49d2d0a4427db0b16abbae160fe106"><code>7126880</code></a>
DEV: Update to urllib3 2.6.3 (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3593">#3593</a>)</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/f189f0755eb111564ae8667a990738a9e7ab4ba9"><code>f189f07</code></a>
DOC: Add outlines documentation and link it in User Guide (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3511">#3511</a>)</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/a29e5326f543f93c4c317cf9f92df54aff68c584"><code>a29e532</code></a>
BUG: Do not consider multi-byte BOM-like sequences as BOMs (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3589">#3589</a>)</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/d9ce594772717fdcfcd505d0309843461579bbf7"><code>d9ce594</code></a>
MAINT: Converge on one shared Font class for text extraction and
appearance s...</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/a65708c778467f1525662dfe5614a486adbd16b0"><code>a65708c</code></a>
DEV: Check for JavaScript library updates on GitHub Pages (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3592">#3592</a>)</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/6951bb7c039afd02bd273dda412c3f36df76eba3"><code>6951bb7</code></a>
MAINT: Fix compatibility with Pillow &gt;= 12.1.0 (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3590">#3590</a>)</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/97d47a001d574dfda54bf16c4dcee89ccbfabec8"><code>97d47a0</code></a>
TST: Improve test coverage (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3584">#3584</a>)</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/bda80a4846a8646419a04cc253d81b73773d3cec"><code>bda80a4</code></a>
DEV: Add PyPy 3.11 to test matrix and benchmarks (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3574">#3574</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/py-pdf/pypdf/compare/6.4.0...6.6.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pypdf&package-manager=pip&previous-version=6.4.0&new-version=6.6.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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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-01-13 20:23:55 +00:00
dependabot[bot] 36e0fd566a build(deps): bump github/codeql-action from 4.31.5 to 4.31.10 (#5449)
Bumps [github/codeql-action](https://github.com/github/codeql-action)
from 4.31.5 to 4.31.10.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/github/codeql-action/releases">github/codeql-action's
releases</a>.</em></p>
<blockquote>
<h2>v4.31.10</h2>
<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>4.31.10 - 12 Jan 2026</h2>
<ul>
<li>Update default CodeQL bundle version to 2.23.9. <a
href="https://redirect.github.com/github/codeql-action/pull/3393">#3393</a></li>
</ul>
<p>See the full <a
href="https://github.com/github/codeql-action/blob/v4.31.10/CHANGELOG.md">CHANGELOG.md</a>
for more information.</p>
<h2>v4.31.9</h2>
<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>4.31.9 - 16 Dec 2025</h2>
<p>No user facing changes.</p>
<p>See the full <a
href="https://github.com/github/codeql-action/blob/v4.31.9/CHANGELOG.md">CHANGELOG.md</a>
for more information.</p>
<h2>v4.31.8</h2>
<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>4.31.8 - 11 Dec 2025</h2>
<ul>
<li>Update default CodeQL bundle version to 2.23.8. <a
href="https://redirect.github.com/github/codeql-action/pull/3354">#3354</a></li>
</ul>
<p>See the full <a
href="https://github.com/github/codeql-action/blob/v4.31.8/CHANGELOG.md">CHANGELOG.md</a>
for more information.</p>
<h2>v4.31.7</h2>
<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>4.31.7 - 05 Dec 2025</h2>
<ul>
<li>Update default CodeQL bundle version to 2.23.7. <a
href="https://redirect.github.com/github/codeql-action/pull/3343">#3343</a></li>
</ul>
<p>See the full <a
href="https://github.com/github/codeql-action/blob/v4.31.7/CHANGELOG.md">CHANGELOG.md</a>
for more information.</p>
<h2>v4.31.6</h2>
<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>4.31.6 - 01 Dec 2025</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/github/codeql-action/blob/main/CHANGELOG.md">github/codeql-action'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.31.10 - 12 Jan 2026</h2>
<ul>
<li>Update default CodeQL bundle version to 2.23.9. <a
href="https://redirect.github.com/github/codeql-action/pull/3393">#3393</a></li>
</ul>
<h2>4.31.9 - 16 Dec 2025</h2>
<p>No user facing changes.</p>
<h2>4.31.8 - 11 Dec 2025</h2>
<ul>
<li>Update default CodeQL bundle version to 2.23.8. <a
href="https://redirect.github.com/github/codeql-action/pull/3354">#3354</a></li>
</ul>
<h2>4.31.7 - 05 Dec 2025</h2>
<ul>
<li>Update default CodeQL bundle version to 2.23.7. <a
href="https://redirect.github.com/github/codeql-action/pull/3343">#3343</a></li>
</ul>
<h2>4.31.6 - 01 Dec 2025</h2>
<p>No user facing changes.</p>
<h2>4.31.5 - 24 Nov 2025</h2>
<ul>
<li>Update default CodeQL bundle version to 2.23.6. <a
href="https://redirect.github.com/github/codeql-action/pull/3321">#3321</a></li>
</ul>
<h2>4.31.4 - 18 Nov 2025</h2>
<p>No user facing changes.</p>
<h2>4.31.3 - 13 Nov 2025</h2>
<ul>
<li>CodeQL Action v3 will be deprecated in December 2026. The Action now
logs a warning for customers who are running v3 but could be running v4.
For more information, see <a
href="https://github.blog/changelog/2025-10-28-upcoming-deprecation-of-codeql-action-v3/">Upcoming
deprecation of CodeQL Action v3</a>.</li>
<li>Update default CodeQL bundle version to 2.23.5. <a
href="https://redirect.github.com/github/codeql-action/pull/3288">#3288</a></li>
</ul>
<h2>4.31.2 - 30 Oct 2025</h2>
<p>No user facing changes.</p>
<h2>4.31.1 - 30 Oct 2025</h2>
<ul>
<li>The <code>add-snippets</code> input has been removed from the
<code>analyze</code> action. This input has been deprecated since CodeQL
Action 3.26.4 in August 2024 when this removal was announced.</li>
</ul>
<h2>4.31.0 - 24 Oct 2025</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/github/codeql-action/commit/cdefb33c0f6224e58673d9004f47f7cb3e328b89"><code>cdefb33</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3394">#3394</a>
from github/update-v4.31.10-0fa411efd</li>
<li><a
href="https://github.com/github/codeql-action/commit/cfa77c6b134886357b1c716fbe58a7708833bf31"><code>cfa77c6</code></a>
Update changelog for v4.31.10</li>
<li><a
href="https://github.com/github/codeql-action/commit/0fa411efd0628aefdf9d03a0faa20a1e0edafc4a"><code>0fa411e</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3393">#3393</a>
from github/update-bundle/codeql-bundle-v2.23.9</li>
<li><a
href="https://github.com/github/codeql-action/commit/c2843242125c2fb8dcd892f204eb2f8622886b78"><code>c284324</code></a>
Add changelog note</li>
<li><a
href="https://github.com/github/codeql-action/commit/83e7d0046cd548fe4cb5d55f5b2ce30b0de62304"><code>83e7d00</code></a>
Update default bundle to codeql-bundle-v2.23.9</li>
<li><a
href="https://github.com/github/codeql-action/commit/f6a16bef8e5c39e398e4da16862d381f76824ac6"><code>f6a16be</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3391">#3391</a>
from github/dependabot/npm_and_yarn/npm-minor-f1cdf5...</li>
<li><a
href="https://github.com/github/codeql-action/commit/c1f5f1a8b57e6da99af540e7c2f23ed33152e270"><code>c1f5f1a</code></a>
Rebuild</li>
<li><a
href="https://github.com/github/codeql-action/commit/1805d8d0a48bdde6eb34e4427b3c00c431427f89"><code>1805d8d</code></a>
Bump the npm-minor group with 2 updates</li>
<li><a
href="https://github.com/github/codeql-action/commit/b2951d2a1ed70de8ec57301118b487b35c13595a"><code>b2951d2</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3353">#3353</a>
from github/kaspersv/bump-min-cli-v-for-overlay</li>
<li><a
href="https://github.com/github/codeql-action/commit/41448d92b9e7bb3a481b3134031a56e52f85528f"><code>41448d9</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3287">#3287</a>
from github/henrymercer/generate-mergeback-last</li>
<li>Additional commits viewable in <a
href="https://github.com/github/codeql-action/compare/fdbfb4d2750291e159f0156def62b853c2798ca2...cdefb33c0f6224e58673d9004f47f7cb3e328b89">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github/codeql-action&package-manager=github_actions&previous-version=4.31.5&new-version=4.31.10)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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-01-13 20:23:33 +00:00
Ludy cfe90cff66 fix(ci): read workflow_dispatch PR number from inputs.pr in PR auto-deploy workflow (#5380)
# Description of Changes

This pull request makes a small update to the PR auto-deploy workflow.
The change ensures that the pull request number is correctly retrieved
from the workflow dispatch payload, improving reliability when the
workflow is manually triggered.

* Changed the PR number retrieval logic in
`.github/workflows/PR-Auto-Deploy-V2.yml` to use
`context.payload.inputs.pr` instead of `process.env.INPUT_PR` for
workflow dispatch events.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-13 16:58:50 +00:00
Ludy 65a5d05713 feat(ui): prevent self-modification in People management and highlight current user (#5441)
# Description of Changes

This PR improves the People management UI by preventing users from
modifying or deleting their own account and by visually highlighting the
currently logged-in user.

<img width="675" height="196" alt="image"
src="https://github.com/user-attachments/assets/c45fb0b6-c766-412c-a53b-b72aed2925d2"
/>


### What was changed
- Integrated session-based authentication context to identify the
currently logged-in user.
- Added a helper to detect the current user in the user list.
- Highlighted the current user's row with a subtle background color.
- Disabled self-actions:
  - Editing own role
  - Enabling/disabling own account
  - Deleting own account
- Kept password change available for the current user.

### Why the change was made
- Prevents accidental self-lockout or privilege removal.
- Aligns UI behavior with common security best practices.
- Improves clarity by visually distinguishing the active user account.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-13 16:13:28 +00:00
Aindriú Mac Giolla Eoin 8632ed9875 Updated ga-IE translations and added keys to ignore list (#5178)
# Description of Changes

Updated the Irish (ga-IE) translation by adding the 117 missing entries
and reviewing the remaining untranslated keys. After translation, some
34 items were identified as not requiring translation and have been
added to the ignore list (ignore_translation.toml).
2026-01-13 16:09:17 +00:00
stirlingbot[bot] 3af5970424 🤖 format everything with pre-commit by stirlingbot (#5426)
Auto-generated by [create-pull-request][1] with **stirlingbot**

[1]: https://github.com/peter-evans/create-pull-request

Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-01-13 16:02:25 +00:00
Ludy 3c8fb8ac96 build(ci): pin base container images, switch npm install to npm ci, and harden EML error handling (#5353)
# Description of Changes

This pull request introduces several improvements focused on security
and reliability in both the Docker build process and the backend API.
The most significant changes are the use of digest-pinned Docker base
images to ensure reproducible builds, safer handling of user-provided
filenames in error messages, and a switch to more reliable dependency
installation in CI workflows.

**Docker image security and reproducibility:**

* All Dockerfiles now use digest-pinned base images (e.g.,
`node:20-alpine@sha256:...`, `gradle:8.14-jdk21@sha256:...`,
`alpine:3.22.1@sha256:...`, `nginx:alpine@sha256:...`) to guarantee
build consistency and protect against upstream image changes.
[[1]](diffhunk://#diff-f8faae0938488156cf26e9322ffdf755deaa8770a7ac8c524dd6126c19548888L5-R5)
[[2]](diffhunk://#diff-f8faae0938488156cf26e9322ffdf755deaa8770a7ac8c524dd6126c19548888L18-R18)
[[3]](diffhunk://#diff-f8faae0938488156cf26e9322ffdf755deaa8770a7ac8c524dd6126c19548888L38-R38)
[[4]](diffhunk://#diff-2f5cd3ad965c86a7a5b4af6e0513ad294e0426644d9f5b5358dfb16a2ef995a7L5-R5)
[[5]](diffhunk://#diff-2f5cd3ad965c86a7a5b4af6e0513ad294e0426644d9f5b5358dfb16a2ef995a7L18-R18)
[[6]](diffhunk://#diff-2f5cd3ad965c86a7a5b4af6e0513ad294e0426644d9f5b5358dfb16a2ef995a7L37-R37)
[[7]](diffhunk://#diff-e9edf3a05475d0721a0e65be1ba0eeb162ae972891b0f6d7e1285687efab1de0L9-R9)
[[8]](diffhunk://#diff-fa0700cfd7d90d832649eb1d0503904564bb3b28c48972be7d9f17e4ce32a3dcL9-R9)
[[9]](diffhunk://#diff-2e766aaf0c87e7b8a62d2a2986f6999c38cc35f677479e31b77d1b427c7aeef7L5-R5)
[[10]](diffhunk://#diff-1726db0cbef194c9be3cba9825c0794802b154e15e4c892c1544d0aace03e037L5-R5)
[[11]](diffhunk://#diff-c1b6dd504a16fc68cd064baf9cf07d9dd31da56eb55de69601844ab03a5ae319L5-R5)
[[12]](diffhunk://#diff-2fc7fcfcfdbb617dd8fbb6b1a2ea5709f9018d618d13942cb33d3e0ed127df16L5-R5)
[[13]](diffhunk://#diff-2fc7fcfcfdbb617dd8fbb6b1a2ea5709f9018d618d13942cb33d3e0ed127df16L39-R39)
[[14]](diffhunk://#diff-759e94102d21fe6f9bde8ddb0b4f95b5d5cd214b0355ea0419d3ea6c09e8ffbfL2-R2)
[[15]](diffhunk://#diff-759e94102d21fe6f9bde8ddb0b4f95b5d5cd214b0355ea0419d3ea6c09e8ffbfL19-R19)

**Backend API security:**

* In `ConvertEmlToPDF.java`, error messages now escape user-provided
filenames using `HtmlUtils.htmlEscape`, preventing potential XSS
vulnerabilities when displaying error messages that include filenames.
[[1]](diffhunk://#diff-45d22a96bae3e8a746b7fb2c39e25c80aee0bf733b528a3517db8fdd2a3d25cdR13)
[[2]](diffhunk://#diff-45d22a96bae3e8a746b7fb2c39e25c80aee0bf733b528a3517db8fdd2a3d25cdR156-R170)

**CI/CD reliability:**

* All GitHub Actions workflows (`multiOSReleases.yml`,
`releaseArtifacts.yml`, `tauri-build.yml`) now use `npm ci` instead of
`npm install` for frontend dependency installation, ensuring clean,
reproducible installs that match the lockfile.
[[1]](diffhunk://#diff-895b214ee023c8c26048a2a3b946cfb1ebc4f26fbc8a9c2fa54b77c12e763b6bL271-R271)
[[2]](diffhunk://#diff-699ff98fe113446c403eb07daf16dd1966c2a047ab0b9f7e38fd695d079f7dddL177-R177)
[[3]](diffhunk://#diff-b34ab107dd4bc92075b2e89b6f16e4a2813e267ca7c2afebdb1931a0a3900d5aL177-R177)

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-13 13:59:59 +00:00
Ludy 251ad63ea6 feat(build): enhance JaCoCo reporting with coverage summary and enforce thresholds (#5352)
# Description of Changes

### What was changed
- Refactored Gradle task configuration to use `tasks.named` and
`configureEach` for better lazy configuration and compatibility.
- Centralized JaCoCo report handling by introducing a single
`jacocoReport` task reference.
- Added a post-processing step to the JaCoCo XML report to:
  - Parse coverage metrics (LINE, INSTRUCTION, BRANCH).
  - Calculate coverage ratios.
  - Print a formatted coverage summary table directly to the build logs.
- Enabled and aligned `jacocoTestCoverageVerification` rules with
defined minimum coverage thresholds.
- Ensured the `build` task depends on the JaCoCo report to always
generate coverage output.

### Why the change was made
- To improve visibility of test coverage results directly in CI and
local builds without manually opening the HTML report.
- To enforce consistent and explicit coverage thresholds for key
metrics.
- To modernize Gradle task configuration and avoid eager task
realization.

---

> Task :proprietary:jacocoTestReport

==== JaCoCo Coverage Summary ====
Metric      | Coverage | Covered/Total | Status | Target   
------------|----------|---------------|--------|----------
LINE        | 9.01%    | 759/8426      | FAIL   | >= 16.00%
INSTRUCTION | 8.41%    | 2741/32590    | FAIL   | >= 14.00%
BRANCH      | 6.04%    | 248/4103      | FAIL   | >= 9.00% 

---

> Task :common:jacocoTestReport

==== JaCoCo Coverage Summary ====
| Metric      | Coverage | Covered/Total | Status | Target   
|------------|----------|---------------|--------|----------
LINE        | 39.47%   | 2996/7591     | PASS   | >= 16.00%
INSTRUCTION | 41.05%   | 12868/31345   | PASS   | >= 14.00%
BRANCH      | 33.43%   | 1166/3488     | PASS   | >= 9.00% 

---

> Task :stirling-pdf:jacocoTestReport

==== JaCoCo Coverage Summary ====
Metric      | Coverage | Covered/Total | Status | Target   
------------|----------|---------------|--------|----------
LINE        | 13.63%   | 2554/18741    | FAIL   | >= 16.00%
INSTRUCTION | 14.59%   | 11459/78532   | PASS   | >= 14.00%
BRANCH      | 10.68%   | 868/8124      | PASS   | >= 9.00% 

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-12 22:17:01 +00:00
Balázs Szücs c7b713ac80 fix(verify-pdf): verification to properly detect non-PDF/A documents with XMP metadata (#5397)
# Description of Changes

Fixed an issue where PDFs containing XMP metadata but lacking PDF/A
identification schema were incorrectly being validated as PDF/A
documents and reporting "PDF/A-1b with errors" instead of "NOT PDF/A".

### Changes Made
- Improved the PDF/A detection logic in `VeraPDFService.java` to check
for both missing XMP metadata and missing PDF/A identification schema
- Added validation for clause 6.7.11 (PDF/A Identification extension
schema requirement) in addition to clause 6.7.2 (XMP metadata presence)
- Documents with XMP metadata but without proper PDF/A identification
now correctly return "NOT PDF/A"

### Root Cause
The previous implementation only checked for missing XMP metadata
(clause 6.7.2) but didn't verify that the XMP contained the required
PDF/A identification schema (clause 6.7.11). This caused documents with
generic XMP metadata to be incorrectly treated as declared PDF/A files.

Fixes issue where non-PDF/A documents with XMP metadata were incorrectly
showing PDF/A validation errors.









<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2026-01-12 19:39:54 +00:00
Anthony Stirling d4d4538630 hide login if login type disabled (#5438)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-12 19:38:02 +00:00
Anthony Stirling d2677e64dd OCR fix and Mobile QR changes (#5433)
# Description of Changes
## OCR / Tesseract path handling

Makes tessDataPath resolution deterministic with priority: config >
TESSDATA_PREFIX env > default.
Updates language discovery to use runtimePathConfig.getTessDataPath()
instead of raw config value.
Ensure default OCR dir is debian based not alpine

## Mobile scanner: feature gating + new conversion settings
Adds system.mobileScannerSettings (convert-to-PDF + resolution + page
format + stretch) exposed via backend config and configurable in the
proprietary admin UI.
Enforces enableMobileScanner on the MobileScannerController endpoints
(403 when disabled).
Frontend mobile upload flow can now optionally convert received images
to PDF (pdf-lib + canvas).

## Desktop/Tauri connectivity work
Expands tauri-plugin-http permissions and enables dangerous-settings.
Adds a very comprehensive multi-stage server connection diagnostic
routine (with lots of logging).


<img width="688" height="475" alt="image"
src="https://github.com/user-attachments/assets/6f9c1aec-58c7-449b-96b0-52f25430d741"
/>


---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-12 11:18:37 +00:00
Anthony Stirling 0ae108ca11 new VPS (#5430)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-10 23:50:18 +00:00
stirlingbot[bot] 5236c1ccf6 🤖 format everything with pre-commit by stirlingbot (#5367)
Auto-generated by [create-pull-request][1] with **stirlingbot**

[1]: https://github.com/peter-evans/create-pull-request

Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-01-09 23:08:49 +00:00
ConnorYohandAnthony Stirling 72389f5872 Desktop self-hosted connection logging (#5410)
Also added in automotic protocol addition if missing. Defaults to
https://

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-01-09 23:01:31 +00:00
Ludy a284dbfc15 Update CODEOWNERS for backend and frontend (#5415)
# Description of Changes

This pull request updates the `.github/CODEOWNERS` file to reflect
changes in code ownership. The main updates are the addition of
`@balazs-szucs` as a code owner for several directories and some cleanup
of existing owner assignments.

Ownership updates:

* Added `@balazs-szucs` as a code owner for `/app/**`, `/frontend/**`,
`/app/core/src/main/resources/static/**`, and `/.github/**`.

Cleanup:

* Removed duplicate owner from `/docker/backend/**` entry.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-09 19:05:59 +00:00
Anthony Stirling ff078b9d88 push docker (#5421)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-09 19:04:41 +00:00
James BruntonandAnthony Stirling 18be8f4692 Self-hosted desktop SSO (#5265)
# Description of Changes
Support SSO in self-hosted desktop app.

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-01-09 18:21:16 +00:00
Anthony Stirling dd09f7b7cf minor changes (#5419)
# Description of Changes

OLD
(MEMBER tag)
<img width="690" height="167" alt="image"
src="https://github.com/user-attachments/assets/079a32b6-2483-46a6-a307-8cacb664cbc8"
/>
<img width="719" height="158" alt="image"
src="https://github.com/user-attachments/assets/cba625f6-56de-4b32-b6ea-22cee59fffbd"
/>

NEW
<img width="748" height="248" alt="image"
src="https://github.com/user-attachments/assets/d3c556da-1859-4241-89c6-d0b96fd6072a"
/>
<img width="752" height="416" alt="image"
src="https://github.com/user-attachments/assets/ddeb7f0d-cccb-4690-b05a-7243695e9b61"
/>


---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-09 12:28:15 +00:00
ConnorYoh 8394b71014 Login-colour-fix-v2 (#5418) 2026-01-09 10:30:34 +00:00
James Brunton b4dd7afe03 Optimise Tauri builds (#5404)
# Description of Changes
- Just build proper installers in CI for each platform
- Provide commands to build just the bundled apps without the need for
installers locally
  - `tauri-dev` - Builds quickly for an unoptimised version of the app
  - `tauri-build` - Builds the full optimised app installer for release
- `tauri-build-dev` - Builds an optimised app with no bundling (builds
to a folder on Mac; raw `.exe` on Windows; etc.)
- `tauri-build-dev-mac` - Builds an optimised bundled Mac app with no
installer (as an `.app` file)
- `tauri-build-dev-windows` - Builds an optimised bundled Windows app as
an `.nsis` installer
- `tauri-build-dev-linux` - Builds an optimised bundled Linux app as an
`.appimage`
2026-01-09 10:23:17 +00:00
James Brunton ef7a3fa933 Fix PDF editor (#5393)
# Description of Changes
PDF editor has been impossible to access since #5169. This fixes the bug
introduced in that which made it impossible to access. The main issue
was the refactor removed the `setLeftPanelView` call.
2026-01-08 09:27:55 +00:00
Anthony Stirling 772ed6f52b OOM logs (#5405)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-07 17:35:45 +00:00
James Brunton 4a1039fa97 Update frontend packages to latest minor version (#5401)
# Description of Changes
Fixes vulnerability warnings in dependencies
2026-01-06 17:06:42 +00:00
Reece BrowneandJames Brunton c158664d2c Chore/v2/improve uploads (#5351)
Co-authored-by: James Brunton <jbrunton96@gmail.com>
2026-01-06 16:54:14 +00:00
EthanHealy01andJames Brunton d9caa3482c Stop the "Open Files" button from disappearing at certain zoom levels (#5384)
I have been trying to get this perfect for a while, but this is the best
I can do. I tried to get the open files button to be sticky at the
bottom, but beyond certain zoom levels, this causes it to disappear. Not
worth spending more time on it right now.

Co-authored-by: James Brunton <jbrunton96@gmail.com>
2026-01-06 16:53:16 +00:00
James Brunton c44b7e229c Make stamp preview content private (#5392)
# Description of Changes
Currently when applying stamps, the preview of the PDF is not being
treated as private content. This correctly marks the content as private
2026-01-06 16:52:47 +00:00
Ludy f38e3d2b2c fix(Cert): ensure incremental save after PDF signing in CertSignController (#5390)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-06 00:01:31 +00:00
Balázs Szücs b6e675fab3 fix(conversion): improve PDF/A conversion quality, color accuracy, and compliance (#5396)
# Description of Changes


* Transparency Fix: Implemented a pre-processing step that adds an
opaque white background to pages before conversion. This ensures that
transparent elements are correctly
flattened against white rather than defaulting to black (a common issue
in Ghostscript flattening).
* Color Distortion Fix: Removed a misconfigured -sDefaultCMYKProfile
setting in the Ghostscript command that was incorrectly pointing to an
RGB profile. This resolves the
"dark/black" color corruption previously seen in print-ready CMYK PDFs.
   * PDF/A Compliance Improvements:
* Font Handling: Updated fixType1FontCharSet to only add a standard
CharSet if it is missing or empty. This prevents validation errors where
subsetted fonts were being forced
         to declare glyphs they did not contain.
* Spot Color Unification: Added fixSeparationColorSpaces to detect and
unify TintTransform objects for Separation colors with the same colorant
name, ensuring consistency
         across document resources.
* OCG Naming: Ensured all Optional Content Groups have a valid Name
entry.





<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2026-01-05 23:53:39 +00:00
Balázs Szücs 91bf9abbaa refactor(pdf): improve resource management, memory usage, and exception safety across controllers and utilities (#5379)
# Description of Changes

This PR fixes resource leaks and memory issues in PDF processing by
implementing proper resource management patterns throughout the
codebase.

## Key Changes

### Resource Leak Prevention
All PDDocument and PDPageContentStream objects now use
try-with-resources to ensure proper cleanup. Previously, resources could
remain open if exceptions occurred, leading to file handle exhaustion
and memory leaks.

### Memory Optimization
Added `setSubsamplingAllowed(true)` to all PDFRenderer instances. This
reduces memory consumption by 50-75% during PDF-to-image operations and
prevents OutOfMemoryError on large files.

**Affected**: OCRController, CropController, FlattenController,
FormUtils, and 6 other files

### Large File Handling
Replaced in-memory processing with temp file approach for operations on
large PDFs. This prevents loading entire documents into memory.

**Example (GetInfoOnPDF.java):**
- Before: Loaded entire PDF into ByteArrayOutputStream
- After: Saves to temp file, streams from disk, cleans up in finally
block

**Also changed**: PrintFileController, SplitPdfBySizeController

### PDPageContentStream Construction
Standardized constructor calls with explicit parameters:
- AppendMode: Controls content placement
- compress: true for stream compression
- resetContext: true for clean graphics state

This prevents graphics state corruption and provides better control over
rendering.

### Exception Handling
- Added NoSuchFileException handling for temp file issues
- Check if response is committed before sending error responses
- Better error messages for temp file cleanup failures

### Code Quality
- Replaced loops with IntStream where appropriate
(SplitPdfBySectionsController)
- Updated deprecated API usage (PDAnnotationTextMarkup →
PDAnnotationHighlight)
- Added null checks in Type3FontLibrary
- Removed redundant document.close() calls

### Dependencies
Added `org.apache.pdfbox:pdfbox-io` dependency for proper I/O handling.






<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2026-01-05 23:43:16 +00:00
Ludy faf0a3555e fix(certSign): accept .pfx/.p12 uploads for PKCS12 and ensure PFX files are included in form data (#5391)
# Description of Changes

This pull request improves the handling of PKCS12 and PFX certificate
file types in the certificate signing tool. The main changes unify and
simplify the logic for uploading and processing these file types,
ensuring consistent behavior and reducing code duplication.

**Certificate file upload improvements:**

* Updated the `CertificateFilesSettings` component to handle both
`PKCS12` and `PFX` certificate types in a single file upload input,
allowing selection of either `.p12` or `.pfx` files and dynamically
adjusting the placeholder text.

**Certificate signing logic updates:**

* Modified the `buildCertSignFormData` function to append the uploaded
file for both `PKCS12` and `PFX` certificate types, ensuring both are
supported during form data construction.

<img width="1916" height="463" alt="image"
src="https://github.com/user-attachments/assets/51dbe130-c25b-4b77-aecd-f3d55d5c62ae"
/>


---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-05 23:38:27 +00:00
EthanHealy01andAnthony Stirling fbc5d91c2c adding opacity to the PDFs so we can see the numbers and stamps better on the add PDF numbers and the add stamp tools (#5383)
<img width="301" height="450" alt="Screenshot 2026-01-02 at 2 17 19 PM"
src="https://github.com/user-attachments/assets/f7a1fd6d-722e-454a-a63d-d43709dd4e23"
/>

<img width="304" height="612" alt="Screenshot 2026-01-02 at 2 17 53 PM"
src="https://github.com/user-attachments/assets/907ba03c-b95a-49e2-8285-8ccaffaf067f"
/>

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-01-05 23:25:31 +00:00
EthanHealy01 0c96133544 Feature/v2/redact (#5249)
# Description of Changes

- Add manual redaction and added it to the right rail in the viewer

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-05 15:12:14 +00:00
Ludy 991d158cb8 chore(ci): skip license bot actions for Dependabot PRs (#5368)
# Description of Changes

### What was changed
- Added explicit conditions to exclude `dependabot[bot]` from:
  - GitHub App bot setup
  - Deleting previous license check comments
  - Posting license check comments on pull requests
- Adjusted the summary step logic so Dependabot PRs are treated like
fork PRs (summary-only, no comments).
- Refactored the generated PR body formatting to use proper multi-line
strings for improved readability.

### Why the change was made
- Dependabot PRs cannot use repository GitHub App credentials, causing
unnecessary failures in bot-related steps.
- Avoids redundant or failing comment actions on automated dependency
update PRs.
- Improves clarity and robustness of the CI workflow when handling
different PR actors.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-31 18:09:58 +00:00
Balázs Szücs 02f9785212 refactor(resource): improve resource management and exception safety across controllers and utilities (#5350)
TLDR:
- Use try-with-resources for InputStreams, PDDocument, TempFile, and
ByteArrayOutputStream to ensure proper cleanup
- Refactor PDF manipulation methods to handle exceptions and resource
closure more robustly
- Simplify session sorting logic in SessionPersistentRegistry
- Add missing resource cleanup in MergeController and
SplitPdfBySectionsController
- Update PrintFileController to close streams and documents safely
- Add unit test for SplitPdfBySizeController

# Description of Changes

This pull request introduces several improvements to resource management
and error handling throughout the codebase, especially for temporary
files and PDF document objects. The changes help prevent resource leaks
by ensuring files and documents are properly closed or deleted in the
event of errors or after use. Notable updates include the use of
try-with-resources, custom AutoCloseable wrappers, and enhanced error
handling logic.

**Resource Management and Error Handling Improvements**

* Added try-catch blocks to delete temporary files if an exception
occurs during file conversion in `GeneralUtils.java`, ensuring no
orphaned temp files are left behind
* Introduced the `TempFile` AutoCloseable helper class in
`InvertFullColorStrategy.java`, and refactored the PDF processing logic
to use try-with-resources for both temporary files and `PDDocument`
objects, ensuring proper cleanup
* Improved error handling in `MergeController.java` by ensuring that
partially created merged documents are closed if an error occurs during
the merge process
* Enhanced the splitting logic in `SplitPdfBySectionsController.java` to
close any partially created `PDDocument` objects if an exception is
thrown, preventing resource leaks

**Refactoring for Safer Document Handling**

* Refactored `handleSplitBySize` in `SplitPdfBySizeController.java` to
use a custom `DocHolder` AutoCloseable wrapper for managing the
lifecycle of `PDDocument` objects, and updated all related logic to use
this wrapper, improving code safety and clarity
* Updated `handleSplitByPageCount` in `SplitPdfBySizeController.java` to
ensure `PDDocument` objects are set to null after being saved and
closed, reducing the risk of operating on closed resources

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-31 18:09:29 +00:00
Ludy 3fd0398648 chore(ci): streamline GitHub workflows, labels, and license automation for main (#5356)
# Description of Changes

This pull request makes several updates to the repository’s CI/CD
configuration, focusing on improving workflow automation, updating Java
versions, enhancing labeling and file ownership, and cleaning up unused
files. The most significant changes involve migrating workflows and
labels to support a new `V3` branch, updating build environments to use
JDK 21, and expanding automated dependency update coverage.

**Key changes include:**

### CI/CD Workflow Updates

* Updated all GitHub Actions workflows (`build.yml`,
`PR-Demo-Comment-with-react.yml`, etc.) to use JDK 21 instead of JDK 17
for Java builds, ensuring compatibility with newer Java features and
dependencies.
[[1]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L120-R125)
[[2]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721R180-R187)
[[3]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L226-R232)
[[4]](diffhunk://#diff-8d23782ae5caff72d55828bb25814854f5f2523f299d7dbcda4a3537dd84c5c3L154-R154)
[[5]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L288-L298)
* Removed the now-unused `codeql.yml-disabled` file, cleaning up
deprecated static analysis configuration.
* Added a dedicated `frontend-validation` job in `build.yml` that only
runs when frontend files change, improving CI efficiency.
* Minor workflow improvements: consistent quoting in config paths,
improved cache settings, and small cleanups in deploy scripts.
[[1]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L150-R152)
[[2]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L244-R247)
[[3]](diffhunk://#diff-7cdd3ccec44c8ba176bdc3b9ef54c3f56aa210a1a4e2bb5f79d87b1e50314a18L29-R29)
[[4]](diffhunk://#diff-f8b6ec3c0af9cd2d8dffef6f3def2be6357fe596a606850ca7f5d799e1349069L87)
[[5]](diffhunk://#diff-f8b6ec3c0af9cd2d8dffef6f3def2be6357fe596a606850ca7f5d799e1349069L120-L127)
[[6]](diffhunk://#diff-f8b6ec3c0af9cd2d8dffef6f3def2be6357fe596a606850ca7f5d799e1349069L189)
[[7]](diffhunk://#diff-26fc40a450703e6602af586a24594196fb10e132de14a9a488ae64ee8cc51166L99)
[[8]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L30-R39)
[[9]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L69-R75)
[[10]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721R93)

### Branch and Label Management

* Migrated auto-labeler and build workflows to track the `V3` branch
instead of `V2`, and updated labels and labeler config to use `v3`
instead of `v2`.
[[1]](diffhunk://#diff-cfe84f4bb9657c721ff741644ee0bce45aa81aaef9dea1ea8741c946984e9722L7-R7)
[[2]](diffhunk://#diff-d3d79c492fbafebc87fbb739c553afea093a79344ff78b88fe785ac3ca7e7b3dL49-R75)
[[3]](diffhunk://#diff-080b7ef0dc11b28f262ea02985043c6229e353ecfdd5920b4d3b19f369f85dc6R87-R89)
* Added new labels for `v3`, `Rust`, `Tauri`, and
`license-review-required` to improve PR categorization and review
processes.

### Dependency Automation

* Expanded `dependabot.yml` to update Gradle dependencies in all
relevant subdirectories (`/`, `/app/common`, `/app/core`,
`/app/proprietary`) and ensured all ecosystems use the `rebase-strategy:
auto` setting for more reliable PR updates.

### File Ownership and Labeling

* Enhanced `.github/config/.files.yaml` to include new scripts and
frontend-related files under the correct project and frontend
categories, and introduced license file groups for both frontend and
backend.
[[1]](diffhunk://#diff-16e8af5ab290e6fdcf843429e4970b7dee6721897ad02c5291f259f47e0978deL5-R7)
[[2]](diffhunk://#diff-16e8af5ab290e6fdcf843429e4970b7dee6721897ad02c5291f259f47e0978deR34-R60)
* Improved labeler configuration to better match new frontend and
translation file patterns, and introduced a new label group for
Tauri-related files.

---

These changes collectively modernize the repository’s automation,
streamline maintenance, and prepare the project for ongoing work on the
`V3` branch.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-30 18:56:09 +00:00
Anthony Stirling 70fc6348f3 photo scan V2 (#5255)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-30 18:55:56 +00:00
dependabot[bot] 8f1af5f967 build(deps-dev): bump stylelint from 16.26.0 to 16.26.1 in /devTools (#5314)
Bumps [stylelint](https://github.com/stylelint/stylelint) from 16.26.0
to 16.26.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/stylelint/stylelint/releases">stylelint's
releases</a>.</em></p>
<blockquote>
<h2>16.26.1</h2>
<p>It fixes numerous false positive bugs, including many in the
<code>declaration-property-value-no-unknown</code> rule for the latest
CSS specifications.</p>
<ul>
<li>Fixed: <code>*-no-unknown</code> false positives for latest specs by
integrating <code>@csstools/css-syntax-patches-for-csstree</code> (<a
href="https://redirect.github.com/stylelint/stylelint/issues/8850">#8850</a>)
(<a
href="https://github.com/romainmenke"><code>@​romainmenke</code></a>).</li>
<li>Fixed: <code>at-rule-no-unknown</code> false positives for
<code>@function</code> (<a
href="https://redirect.github.com/stylelint/stylelint/issues/8851">#8851</a>)
(<a href="https://github.com/jeddy3"><code>@​jeddy3</code></a>).</li>
<li>Fixed: <code>declaration-property-value-no-unknown</code> false
positives for <code>attr()</code>, <code>if()</code> and custom
functions (<a
href="https://redirect.github.com/stylelint/stylelint/issues/8853">#8853</a>)
(<a href="https://github.com/jeddy3"><code>@​jeddy3</code></a>).</li>
<li>Fixed: <code>function-url-quotes</code> false positives when URLs
require quoting (<a
href="https://redirect.github.com/stylelint/stylelint/issues/8804">#8804</a>)
(<a href="https://github.com/taearls"><code>@​taearls</code></a>).</li>
<li>Fixed: <code>selector-pseudo-element-no-unknown</code> false
positives for <code>::scroll-button()</code> (<a
href="https://redirect.github.com/stylelint/stylelint/issues/8856">#8856</a>)
(<a
href="https://github.com/Mouvedia"><code>@​Mouvedia</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md">stylelint's
changelog</a>.</em></p>
<blockquote>
<h2>16.26.1 - 2025-11-28</h2>
<p>It fixes numerous false positive bugs, including many in the
<code>declaration-property-value-no-unknown</code> rule for the latest
CSS specifications.</p>
<ul>
<li>Fixed: <code>*-no-unknown</code> false positives for latest specs by
integrating <code>@csstools/css-syntax-patches-for-csstree</code> (<a
href="https://redirect.github.com/stylelint/stylelint/pull/8850">#8850</a>)
(<a
href="https://github.com/romainmenke"><code>@​romainmenke</code></a>).</li>
<li>Fixed: <code>at-rule-no-unknown</code> false positives for
<code>@function</code> (<a
href="https://redirect.github.com/stylelint/stylelint/pull/8851">#8851</a>)
(<a href="https://github.com/jeddy3"><code>@​jeddy3</code></a>).</li>
<li>Fixed: <code>declaration-property-value-no-unknown</code> false
positives for <code>attr()</code>, <code>if()</code> and custom
functions (<a
href="https://redirect.github.com/stylelint/stylelint/pull/8853">#8853</a>)
(<a href="https://github.com/jeddy3"><code>@​jeddy3</code></a>).</li>
<li>Fixed: <code>function-url-quotes</code> false positives when URLs
require quoting (<a
href="https://redirect.github.com/stylelint/stylelint/pull/8804">#8804</a>)
(<a href="https://github.com/taearls"><code>@​taearls</code></a>).</li>
<li>Fixed: <code>selector-pseudo-element-no-unknown</code> false
positives for <code>::scroll-button()</code> (<a
href="https://redirect.github.com/stylelint/stylelint/pull/8856">#8856</a>)
(<a
href="https://github.com/Mouvedia"><code>@​Mouvedia</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/stylelint/stylelint/commit/b96814344b7d1088e3459c44dcafebfbdabff412"><code>b968143</code></a>
Release 16.26.1 (<a
href="https://redirect.github.com/stylelint/stylelint/issues/8857">#8857</a>)</li>
<li><a
href="https://github.com/stylelint/stylelint/commit/2b24b9cd5030b4ef6726d575ea71d34005dd9929"><code>2b24b9c</code></a>
Fix <code>selector-pseudo-element-no-unknown</code> false positives for
`::scroll-button...</li>
<li><a
href="https://github.com/stylelint/stylelint/commit/f152564f037047a4f1a40c812fba77dde05d0062"><code>f152564</code></a>
Fix <code>*-no-unknown</code> false positives for latest specs by
integrating `@csstools...</li>
<li><a
href="https://github.com/stylelint/stylelint/commit/431cb53c0a181eaacc3b208a71c0e765c14faedf"><code>431cb53</code></a>
Fix <code>at-rule-no-unknown</code> false positives for
<code>@function</code> (<a
href="https://redirect.github.com/stylelint/stylelint/issues/8851">#8851</a>)</li>
<li><a
href="https://github.com/stylelint/stylelint/commit/119097ea694cca6bf477ac534fd02c39c8b37c8e"><code>119097e</code></a>
Fix <code>declaration-property-value-no-unknown</code> false positives
for <code>attr()</code> and ...</li>
<li><a
href="https://github.com/stylelint/stylelint/commit/4b9c68be0763a87df187a7fc9de00bced940d916"><code>4b9c68b</code></a>
Fix <code>function-url-quotes</code> false positives when URLs require
quoting (<a
href="https://redirect.github.com/stylelint/stylelint/issues/8804">#8804</a>)</li>
<li><a
href="https://github.com/stylelint/stylelint/commit/8cc4ced2e8938785aa29559609984df8c4d83431"><code>8cc4ced</code></a>
Bump rollup from 4.52.5 to 4.53.2 (<a
href="https://redirect.github.com/stylelint/stylelint/issues/8848">#8848</a>)</li>
<li><a
href="https://github.com/stylelint/stylelint/commit/4383feb6dfacb57fc334ab6441ba32e7ea4e3008"><code>4383feb</code></a>
Bump file-entry-cache from 11.1.0 to 11.1.1 (<a
href="https://redirect.github.com/stylelint/stylelint/issues/8846">#8846</a>)</li>
<li><a
href="https://github.com/stylelint/stylelint/commit/a8a7560c49f78ce1baaa1fd182c03685c12c7b37"><code>a8a7560</code></a>
Bump the eslint group with 2 updates (<a
href="https://redirect.github.com/stylelint/stylelint/issues/8845">#8845</a>)</li>
<li><a
href="https://github.com/stylelint/stylelint/commit/947ad33c1562b03e54b440693db69c5fbb4b39fb"><code>947ad33</code></a>
Fix <code>patch-package</code> warning about mismatched
<code>@types/css-tree</code> version (<a
href="https://redirect.github.com/stylelint/stylelint/issues/8844">#8844</a>)</li>
<li>See full diff in <a
href="https://github.com/stylelint/stylelint/compare/16.26.0...16.26.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=stylelint&package-manager=npm_and_yarn&previous-version=16.26.0&new-version=16.26.1)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-12-29 15:53:41 +00:00
dependabot[bot] 16cd453870 build(deps): bump org.sonarqube from 7.1.0.6387 to 7.2.2.6593 (#5313)
Bumps org.sonarqube from 7.1.0.6387 to 7.2.2.6593.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.sonarqube&package-manager=gradle&previous-version=7.1.0.6387&new-version=7.2.2.6593)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-12-29 15:53:23 +00:00
Ludy 915a33bbc2 fix(ci): correct pip --require-hashes usage in TOML check workflow (#5336)
# Description of Changes

This pull request updates the Python dependency installation step in the
`.github/workflows/check_toml.yml` workflow. Instead of installing a
single dependency directly, it now installs all dependencies listed in a
requirements file, which helps centralize and manage dependencies more
effectively.

Dependency management improvements:
* Changed the Python dependency installation to use the
`requirements_sync_readme.txt` file, allowing for easier updates and
consistent dependency management. (.github/workflows/check_toml.yml)

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-29 10:33:57 +00:00
dependabot[bot] 42f9384ece build(deps): bump debian from 7cb087f to 1c25564 in /docker/embedded (#5310)
Bumps debian from `7cb087f` to `1c25564`.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=debian&package-manager=docker&previous-version=stable-slim&new-version=stable-slim)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-12-28 12:42:04 +00:00
Ludy 804f1d8975 deps(ci): update Dependabot, pre-commit tooling, and testing dependencies (#5170)
# Description of Changes

This pull request updates dependency management and CI/CD configurations
to improve automation, security, and maintainability. The most
significant changes include expanding Dependabot coverage to more
directories and ecosystems, updating pre-commit and Python dependency
versions, and pinning action versions in GitHub workflows for better
reproducibility and security.

**Dependency Management Improvements:**

* Expanded Dependabot configuration in `.github/dependabot.yml` to
include additional directories and package ecosystems such as npm,
docker, cargo, and pip, ensuring automated dependency updates across
more parts of the project.
* Updated Python dependencies in
`.github/scripts/requirements_pre_commit.txt` to newer versions for
`cfgv`, `filelock`, `platformdirs`, `pre-commit`, and `virtualenv`,
improving compatibility and security.
[[1]](diffhunk://#diff-4b865d764c6955aa3ab06c7beff7c08a122e5145c1f0fecd7b4fd4575848b598L7-R17)
[[2]](diffhunk://#diff-4b865d764c6955aa3ab06c7beff7c08a122e5145c1f0fecd7b4fd4575848b598L27-R33)
[[3]](diffhunk://#diff-4b865d764c6955aa3ab06c7beff7c08a122e5145c1f0fecd7b4fd4575848b598L110-R112)
* Added `tomli-w` to `.github/scripts/requirements_sync_readme.in` and
`.github/scripts/requirements_sync_readme.txt` for TOML file writing
support.
[[1]](diffhunk://#diff-e359c7d332d374a67300c004d7bab6c37cb16b5e1b9c8cd63adf2b59462c1f06R2)
[[2]](diffhunk://#diff-cf0fa825b1295e115dbbe842a6f179ed0c72dd80b758d3238ab792cdd0013a4cR7-R10)

**CI/CD Workflow Enhancements:**

* Updated installation commands in `.github/workflows/check_toml.yml`
and `.github/workflows/sync_files_v2.yml` to use hashed and
version-pinned dependencies, improving reproducibility and security.
Also removed redundant dependency installation in the sync workflow.
[[1]](diffhunk://#diff-3117b4a93711d37b0a9a1668272eec716fea0b4f57dde16a85e7ab3f569c455dL203-R203)
[[2]](diffhunk://#diff-b1acd58f6bdc16d0f02514058f8842a8ec3c90e8771f6a1e83801fa14ee5041cL56-R56)
[[3]](diffhunk://#diff-b1acd58f6bdc16d0f02514058f8842a8ec3c90e8771f6a1e83801fa14ee5041cL68-L70)
* Pinned GitHub Actions versions in
`.github/workflows/deploy-on-v2-commit.yml` by using commit SHAs for
actions such as `actions/checkout`, `docker/setup-buildx-action`,
`docker/login-action`, and `docker/build-push-action`, ensuring builds
use known-good versions.
[[1]](diffhunk://#diff-f8b6ec3c0af9cd2d8dffef6f3def2be6357fe596a606850ca7f5d799e1349069L26-R29)
[[2]](diffhunk://#diff-f8b6ec3c0af9cd2d8dffef6f3def2be6357fe596a606850ca7f5d799e1349069L89-R96)
[[3]](diffhunk://#diff-f8b6ec3c0af9cd2d8dffef6f3def2be6357fe596a606850ca7f5d799e1349069L109-R109)

**Pre-commit Configuration Updates:**

* Updated hooks in `.pre-commit-config.yaml` to newer versions for
`ruff-pre-commit`, `gitleaks`, and `pre-commit-hooks`, providing
enhanced linting and security scanning.
[[1]](diffhunk://#diff-63a9c44a44acf85fea213a857769990937107cf072831e1a26808cfde9d096b9L3-R3)
[[2]](diffhunk://#diff-63a9c44a44acf85fea213a857769990937107cf072831e1a26808cfde9d096b9L25-R29)

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-27 23:56:57 +00:00
182eb504de [V2] feat(convert): add support for CBR to PDF and PDF to CBR conversions (#4833)
# Description of Changes
TLDR:
- Introduced `ConvertFromCbrSettings` and `ConvertToCbrSettings`
components for handling conversion-specific settings.
- Updated `ConvertSettings` to include CBR-related configurations.
- Updated `useConvertParameters` with new parameters: `cbrOptions` and
`pdfToCbrOptions`.
- Updated locales with CBR-related translations.
- Added endpoints and updated conversion matrix for CBR formats in
`convertConstants`.
- Modified `useConvertOperation` to handle CBR-specific parameters
during form data preparation.


For backend see this PR: #4581

This pull request adds support for converting between CBR (Comic Book
Archive) and PDF formats in the frontend. It introduces new UI
components for configuring conversion options, updates the conversion
constants and logic, and ensures parameters for these conversions are
handled correctly throughout the app.

**CBR/PDF Conversion Features:**

* Added new UI components: `ConvertFromCbrSettings` for CBR to PDF
conversions (option to optimize for ebook readers), and
`ConvertToCbrSettings` for PDF to CBR conversions (DPI selection for
image rendering). These are conditionally rendered in
`ConvertSettings.tsx` based on selected formats.

**Conversion Logic and Constants:**

* Extended conversion endpoints, format options, conversion matrix, and
endpoint mappings to support CBR to PDF and PDF to CBR conversions.
* Updated `shouldProcessFilesSeparately` logic to handle PDF to CBR
conversions, ensuring each PDF generates its own archive.

**Parameter Handling:**

* Added new parameters (`cbrOptions` and `pdfToCbrOptions`) to the
conversion parameter interface and default values, ensuring they are
set/reset appropriately during format changes
* Modified form data construction to include new options for CBR/PDF
conversions (optimize for ebook, DPI).

### Frontend
<img width="1291" height="861" alt="image"
src="https://github.com/user-attachments/assets/fb63be66-6f40-4dde-8235-86c9ddfa1f7c"
/>


<img width="1411" height="1000" alt="image"
src="https://github.com/user-attachments/assets/529593c4-6f32-4b11-9754-7f334f40d32e"
/>


Note on RAR5. You can go back-and-forth with this converter due to the
fact on default pdf to cbr makes RAR5 meanwhile, cbr to pdf can only
RAR4 and below. This is unfortunate limitation of JunRAR library. In the
real world, generally RAR5s are not that common, and they mostly
unsupported by open-source software.

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2025-12-27 01:32:55 +00:00
Balázs Szücs 98fa5dfcc1 [V2] feat(sign): add automatic white background removal for signature images (#5210)
# Description of Changes



This pull request adds a new feature that allows users to automatically
remove the white background from uploaded signature images, making them
transparent. The changes include UI improvements for image uploading,
new image processing utilities, and updates to translation files for
better user feedback.

### Image Upload & Processing Enhancements

* Added a `removeWhiteBackground` utility function in
`imageTransparency.ts` to process images and remove white backgrounds,
with options for automatic corner color detection and tolerance
settings.
* Updated the `ImageUploader` component to support background removal,
including a checkbox for toggling the feature, processing state
feedback, and integration with the new utility

### Signature Tool Integration

* Modified `SignSettings.tsx` to enable the background removal feature
for signature images, handle processed image data, and ensure signature
placement logic works seamlessly with the new feature.

### UI & Localization Updates

* Added new translation strings for background removal and image
processing feedback in the English locale file.



https://github.com/user-attachments/assets/28263940-1756-4f0e-9bfb-5603a6fb8a2c


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-26 23:50:21 +00:00
albanobattistella 429520d3f9 Update translation.toml (#5308)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [x] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-26 22:37:59 +00:00
stirlingbot[bot] fbfe1da7da 🤖 format everything with pre-commit by stirlingbot (#5306)
Auto-generated by [create-pull-request][1] with **stirlingbot**

[1]: https://github.com/peter-evans/create-pull-request

Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2025-12-25 23:39:05 +00:00
Ludy c047bdda8c feat(translations): add command to remove unused translation keys (#5292)
# Description of Changes

This pull request adds a new feature to the translation management
script, allowing users to automatically remove unused translations that
are not present in the `en-GB` (golden truth) file. The update includes
both the implementation in `translation_merger.py` and documentation in
`README.md`. The most important changes are grouped below:

**New Feature: Remove Unused Translations**

* Added a `remove-unused` command to `translation_merger.py`, which
removes translation keys from a target file if they are not present in
the `en-GB` reference. This includes backup support and a CLI interface.
[[1]](diffhunk://#diff-bb7f09f590ccabb456565d52afd66c1421525ddb1b6b65c0985aaab5a3c20ae7R282-R307)
[[2]](diffhunk://#diff-bb7f09f590ccabb456565d52afd66c1421525ddb1b6b65c0985aaab5a3c20ae7R397-R404)
[[3]](diffhunk://#diff-bb7f09f590ccabb456565d52afd66c1421525ddb1b6b65c0985aaab5a3c20ae7R464-R471)
* Implemented helper methods `_delete_nested_key` (for deleting nested
keys and cleaning up empty branches) and `get_unused_keys` (for
identifying unused keys) in the translation merger logic.
[[1]](diffhunk://#diff-bb7f09f590ccabb456565d52afd66c1421525ddb1b6b65c0985aaab5a3c20ae7R117-R139)
[[2]](diffhunk://#diff-bb7f09f590ccabb456565d52afd66c1421525ddb1b6b65c0985aaab5a3c20ae7R156-R166)

**Documentation Updates**

* Updated `README.md` to document the new `remove-unused` command and
describe its purpose and usage.
[[1]](diffhunk://#diff-aeee54608ec45b06f4d52a4afcfbc7e6b08dee75f4328207cec32b8ca02fe0c6R185-R187)
[[2]](diffhunk://#diff-aeee54608ec45b06f4d52a4afcfbc7e6b08dee75f4328207cec32b8ca02fe0c6R196)

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-25 23:24:30 +00:00
Balázs Szücs a33e22559b [V2] feat(convert): add eBook formats (EPUB, MOBI, AZW3, FB2) to supported formats list (#5307)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-25 20:02:58 +00:00
Balázs Szücs 1318604f32 [V2] feat(convert): add eBook (EPUB, MOBI, AZW3, FB2) to PDF conversion options and UI (#5291)
# Description of Changes

This pull request adds support for converting eBook formats (EPUB, MOBI,
AZW3, FB2) to PDF, including a new set of user-configurable options for
the conversion process. It introduces frontend UI components, updates
configuration and constants, and ensures that the backend can recognize
and handle these new formats and options.

**eBook to PDF Conversion Support**

- Added support for converting eBook formats (`epub`, `mobi`, `azw3`,
`fb2`) to PDF, including updates to conversion endpoints, extension
mappings, and conversion matrices in `convertConstants.ts` to route
these conversions through the new `ebook-to-pdf` endpoint.
- Updated the file processing logic so that eBook-to-PDF conversions are
processed separately for each file, similar to web file conversions.

**Frontend: User Options for eBook Conversion**

- Added a new UI component `ConvertFromEbookSettings.tsx` that allows
users to configure options for eBook-to-PDF conversion: embedding all
fonts, including a table of contents, adding page numbers, and
optimizing for eBook readers. This component is conditionally rendered
in the conversion settings when an eBook format is selected as the
source and PDF as the target.
- Integrated the new eBook options into the conversion parameters and
ensured they are initialized/reset appropriately in the conversion
settings.
**Form Data and Backend Integration**

- Modified the form data builder to append the new eBook options to the
request payload when performing eBook-to-PDF conversions, ensuring these
settings are sent to the backend.

**Localization and Configuration**

- Added localization strings for all new eBook conversion options in the
English translation file, providing user-friendly labels and
descriptions in the UI.
- Updated backend configuration logic to recognize `Calibre` and
`FFmpeg` as tool groups, ensuring correct enablement/disablement
behavior for these tools.

<img width="366" height="997" alt="image"
src="https://github.com/user-attachments/assets/44d3308c-ea49-4874-8f5e-c7d617a37489"
/>

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-25 14:58:10 +00:00
Balázs Szücs 43a5f72b01 [V2] feat(crop): add auto-crop whitespace option to crop tool UI (#5275)
# Description of Changes

This pull request adds an auto-crop feature to the PDF crop tool,
allowing users to automatically crop whitespace from PDFs. The UI now
includes an "Auto-crop whitespace" checkbox, and when enabled, manual
crop controls are hidden. The crop operation logic and form data
submission have been updated to support this new option.

**Auto-crop Feature Implementation**

* Added an `autoCrop` boolean parameter to the `CropParameters`
interface and set its default value to `false` in
`useCropParameters.ts`.
* Updated the crop operation logic in `useCropOperation.ts` to include
the `autoCrop` parameter in the form data and only send manual crop
coordinates if `autoCrop` is disabled.

**User Interface Updates**

* Added an "Auto-crop whitespace" checkbox to the crop settings UI in
`CropSettings.tsx`, which toggles the auto-crop feature
* Modified the crop settings UI to hide manual crop controls and
validation alerts when auto-crop is enabled

**Localization**

* Added a new translation string for "Auto-crop whitespace" in the
English locale file `translation.toml`.



<img width="363" height="998" alt="image"
src="https://github.com/user-attachments/assets/a92988b8-eea0-47e7-961f-b4a6e018ff2f"
/>

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-25 14:22:46 +00:00
dependabot[bot] d97820c232 build(deps): bump docker/metadata-action from 5.8.0 to 5.10.0 (#5299)
Bumps
[docker/metadata-action](https://github.com/docker/metadata-action) from
5.8.0 to 5.10.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/docker/metadata-action/releases">docker/metadata-action's
releases</a>.</em></p>
<blockquote>
<h2>v5.10.0</h2>
<ul>
<li>Bump <code>@​docker/actions-toolkit</code> from 0.66.0 to 0.68.0 in
<a
href="https://redirect.github.com/docker/metadata-action/pull/559">docker/metadata-action#559</a>
<a
href="https://redirect.github.com/docker/metadata-action/pull/569">docker/metadata-action#569</a></li>
<li>Bump js-yaml from 3.14.1 to 3.14.2 in <a
href="https://redirect.github.com/docker/metadata-action/pull/564">docker/metadata-action#564</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/docker/metadata-action/compare/v5.9.0...v5.10.0">https://github.com/docker/metadata-action/compare/v5.9.0...v5.10.0</a></p>
<h2>v5.9.0</h2>
<ul>
<li>Add <code>tag-names</code> output to return tag names without image
base name by <a
href="https://github.com/crazy-max"><code>@​crazy-max</code></a> in <a
href="https://redirect.github.com/docker/metadata-action/pull/553">docker/metadata-action#553</a></li>
<li>Bump <code>@​babel/runtime-corejs3</code> from 7.14.7 to 7.28.2 in
<a
href="https://redirect.github.com/docker/metadata-action/pull/539">docker/metadata-action#539</a></li>
<li>Bump <code>@​docker/actions-toolkit</code> from 0.62.1 to 0.66.0 in
<a
href="https://redirect.github.com/docker/metadata-action/pull/555">docker/metadata-action#555</a></li>
<li>Bump brace-expansion from 1.1.11 to 1.1.12 in <a
href="https://redirect.github.com/docker/metadata-action/pull/540">docker/metadata-action#540</a></li>
<li>Bump csv-parse from 5.6.0 to 6.1.0 in <a
href="https://redirect.github.com/docker/metadata-action/pull/532">docker/metadata-action#532</a></li>
<li>Bump semver from 7.7.2 to 7.7.3 in in <a
href="https://redirect.github.com/docker/metadata-action/pull/554">docker/metadata-action#554</a></li>
<li>Bump tmp from 0.2.3 to 0.2.5 in <a
href="https://redirect.github.com/docker/metadata-action/pull/541">docker/metadata-action#541</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/docker/metadata-action/compare/v5.8.0...v5.9.0">https://github.com/docker/metadata-action/compare/v5.8.0...v5.9.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/docker/metadata-action/commit/c299e40c65443455700f0fdfc63efafe5b349051"><code>c299e40</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/metadata-action/issues/569">#569</a>
from docker/dependabot/npm_and_yarn/docker/actions-to...</li>
<li><a
href="https://github.com/docker/metadata-action/commit/f015d7914a06d5c4931affc0780479c2336fd8e3"><code>f015d79</code></a>
chore: update generated content</li>
<li><a
href="https://github.com/docker/metadata-action/commit/121bcc2ca8f5f246e9af338aeb41e55825fe7c88"><code>121bcc2</code></a>
chore(deps): Bump <code>@​docker/actions-toolkit</code> from 0.67.0 to
0.68.0</li>
<li><a
href="https://github.com/docker/metadata-action/commit/f7b6bf41b94feca9834c527e3bd584efa2e7280b"><code>f7b6bf4</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/metadata-action/issues/564">#564</a>
from docker/dependabot/npm_and_yarn/js-yaml-3.14.2</li>
<li><a
href="https://github.com/docker/metadata-action/commit/0b95c6b8604d853d90ab386caf3a1e754d9697f7"><code>0b95c6b</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/metadata-action/issues/565">#565</a>
from docker/dependabot/github_actions/actions/checkout-6</li>
<li><a
href="https://github.com/docker/metadata-action/commit/17f70d7525d8de2c783e41c092e28219c8fc2a67"><code>17f70d7</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/metadata-action/issues/568">#568</a>
from motoki317/docs/fix-to-24h-schedule-pattern</li>
<li><a
href="https://github.com/docker/metadata-action/commit/afd7e6d7bbf70ea7bc2e4f3c782fe1feabe42d88"><code>afd7e6d</code></a>
docs(README): Fix date format from 12h to 24h in schedule pattern</li>
<li><a
href="https://github.com/docker/metadata-action/commit/602aff8e11accbaf5f2233069201ffe332de3d5e"><code>602aff8</code></a>
chore(deps): Bump actions/checkout from 5 to 6</li>
<li><a
href="https://github.com/docker/metadata-action/commit/aecb1a49a52523dc3b4dcab352cae7572eb61c87"><code>aecb1a4</code></a>
chore(deps): Bump js-yaml from 3.14.1 to 3.14.2</li>
<li><a
href="https://github.com/docker/metadata-action/commit/8d8c7c12f7b958582a5cb82ba16d5903cb27976a"><code>8d8c7c1</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/metadata-action/issues/559">#559</a>
from docker/dependabot/npm_and_yarn/docker/actions-to...</li>
<li>Additional commits viewable in <a
href="https://github.com/docker/metadata-action/compare/v5.8.0...c299e40c65443455700f0fdfc63efafe5b349051">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=docker/metadata-action&package-manager=github_actions&previous-version=5.8.0&new-version=5.10.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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-12-25 13:49:29 +00:00
dependabot[bot]andAnthony Stirling 03cd7c1456 build(deps): bump peter-evans/create-pull-request from 7.0.8 to 8.0.0 (#5300)
Bumps
[peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request)
from 7.0.8 to 8.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/peter-evans/create-pull-request/releases">peter-evans/create-pull-request's
releases</a>.</em></p>
<blockquote>
<h2>Create Pull Request v8.0.0</h2>
<h2>What's new in v8</h2>
<ul>
<li>Requires <a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Actions
Runner v2.327.1</a> or later if you are using a self-hosted runner for
Node 24 support.</li>
</ul>
<h2>What's Changed</h2>
<ul>
<li>chore: Update checkout action version to v6 by <a
href="https://github.com/yonas"><code>@​yonas</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4258">peter-evans/create-pull-request#4258</a></li>
<li>Update actions/checkout references to <a
href="https://github.com/v6"><code>@​v6</code></a> in docs by <a
href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4259">peter-evans/create-pull-request#4259</a></li>
<li>feat: v8 by <a
href="https://github.com/peter-evans"><code>@​peter-evans</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4260">peter-evans/create-pull-request#4260</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/yonas"><code>@​yonas</code></a> made
their first contribution in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4258">peter-evans/create-pull-request#4258</a></li>
<li><a href="https://github.com/Copilot"><code>@​Copilot</code></a> made
their first contribution in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4259">peter-evans/create-pull-request#4259</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/peter-evans/create-pull-request/compare/v7.0.11...v8.0.0">https://github.com/peter-evans/create-pull-request/compare/v7.0.11...v8.0.0</a></p>
<h2>Create Pull Request v7.0.11</h2>
<h2>What's Changed</h2>
<ul>
<li>fix: restrict remote prune to self-hosted runners by <a
href="https://github.com/peter-evans"><code>@​peter-evans</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4250">peter-evans/create-pull-request#4250</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/peter-evans/create-pull-request/compare/v7.0.10...v7.0.11">https://github.com/peter-evans/create-pull-request/compare/v7.0.10...v7.0.11</a></p>
<h2>Create Pull Request v7.0.10</h2>
<p>⚙️ Fixes an issue where updating a pull request failed when targeting
a forked repository with the same owner as its parent.</p>
<h2>What's Changed</h2>
<ul>
<li>build(deps): bump the github-actions group with 2 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4235">peter-evans/create-pull-request#4235</a></li>
<li>build(deps-dev): bump prettier from 3.6.2 to 3.7.3 in the npm group
by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4240">peter-evans/create-pull-request#4240</a></li>
<li>fix: provider list pulls fallback for multi fork same owner by <a
href="https://github.com/peter-evans"><code>@​peter-evans</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4245">peter-evans/create-pull-request#4245</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/obnyis"><code>@​obnyis</code></a> made
their first contribution in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4064">peter-evans/create-pull-request#4064</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/peter-evans/create-pull-request/compare/v7.0.9...v7.0.10">https://github.com/peter-evans/create-pull-request/compare/v7.0.9...v7.0.10</a></p>
<h2>Create Pull Request v7.0.9</h2>
<p>⚙️ Fixes an <a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4228">incompatibility</a>
with the recently released <code>actions/checkout@v6</code>.</p>
<h2>What's Changed</h2>
<ul>
<li>~70 dependency updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
<li>docs: fix workaround description about <code>ready_for_review</code>
by <a href="https://github.com/ybiquitous"><code>@​ybiquitous</code></a>
in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3939">peter-evans/create-pull-request#3939</a></li>
<li>Docs: <code>add-paths</code> default behavior by <a
href="https://github.com/joeflack4"><code>@​joeflack4</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3928">peter-evans/create-pull-request#3928</a></li>
<li>docs: update to create-github-app-token v2 by <a
href="https://github.com/Goooler"><code>@​Goooler</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4063">peter-evans/create-pull-request#4063</a></li>
<li>Fix compatibility with actions/checkout@v6 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4230">peter-evans/create-pull-request#4230</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/joeflack4"><code>@​joeflack4</code></a>
made their first contribution in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3928">peter-evans/create-pull-request#3928</a></li>
<li><a href="https://github.com/Goooler"><code>@​Goooler</code></a> made
their first contribution in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4063">peter-evans/create-pull-request#4063</a></li>
<li><a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> made
their first contribution in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4230">peter-evans/create-pull-request#4230</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/98357b18bf14b5342f975ff684046ec3b2a07725"><code>98357b1</code></a>
feat: v8 (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4260">#4260</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/41c0e4b7899a4a0922bf899d64c5f25738cfe356"><code>41c0e4b</code></a>
Update actions/checkout references to <a
href="https://github.com/v6"><code>@​v6</code></a> in docs (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4259">#4259</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/994332de4c8124517167807167073cf397678768"><code>994332d</code></a>
chore: Update checkout action version to v6 (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4258">#4258</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/22a9089034f40e5a961c8808d113e2c98fb63676"><code>22a9089</code></a>
fix: restrict remote prune to self-hosted runners (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4250">#4250</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/d4f3be6ce6f4083b7ac7490ab98b48a62db1ee41"><code>d4f3be6</code></a>
fix: provider list pulls fallback for multi fork same owner (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4245">#4245</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/bc8a47f5657f110049f4afd030c95529a9c62b76"><code>bc8a47f</code></a>
build(deps-dev): bump prettier from 3.6.2 to 3.7.3 in the npm group (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4240">#4240</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/a67ef28ca5df73d51a15007068e5931257943b0d"><code>a67ef28</code></a>
build(deps): bump the github-actions group with 2 updates (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4235">#4235</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/84ae59a2cdc2258d6fa0732dd66352dddae2a412"><code>84ae59a</code></a>
fix: compatibility with actions/checkout@v6 (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4230">#4230</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/b4733b9419fd47bbfa1807b15627e17cd70b5b22"><code>b4733b9</code></a>
build(deps-dev): bump js-yaml from 4.1.0 to 4.1.1 (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4222">#4222</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/0edc001d28a2959cd7a6b505629f1d82f0a6e67d"><code>0edc001</code></a>
build(deps-dev): bump the npm group with 2 updates (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4201">#4201</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/peter-evans/create-pull-request/compare/v7.0.8...98357b18bf14b5342f975ff684046ec3b2a07725">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=peter-evans/create-pull-request&package-manager=github_actions&previous-version=7.0.8&new-version=8.0.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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2025-12-25 13:40:33 +00:00
dependabot[bot] b1b37366e8 build(deps): bump actions/github-script from 7.0.1 to 8.0.0 (#5302)
Bumps [actions/github-script](https://github.com/actions/github-script)
from 7.0.1 to 8.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/github-script/releases">actions/github-script's
releases</a>.</em></p>
<blockquote>
<h2>v8.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update Node.js version support to 24.x by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/637">actions/github-script#637</a></li>
<li>README for updating actions/github-script from v7 to v8 by <a
href="https://github.com/sneha-krip"><code>@​sneha-krip</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/653">actions/github-script#653</a></li>
</ul>
<h2>⚠️ Minimum Compatible Runner Version</h2>
<p><strong>v2.327.1</strong><br />
<a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Release
Notes</a></p>
<p>Make sure your runner is updated to this version or newer to use this
release.</p>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/637">actions/github-script#637</a></li>
<li><a
href="https://github.com/sneha-krip"><code>@​sneha-krip</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/653">actions/github-script#653</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/github-script/compare/v7.1.0...v8.0.0">https://github.com/actions/github-script/compare/v7.1.0...v8.0.0</a></p>
<h2>v7.1.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Upgrade husky to v9 by <a
href="https://github.com/benelan"><code>@​benelan</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/482">actions/github-script#482</a></li>
<li>Add workflow file for publishing releases to immutable action
package by <a
href="https://github.com/Jcambass"><code>@​Jcambass</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/485">actions/github-script#485</a></li>
<li>Upgrade IA Publish by <a
href="https://github.com/Jcambass"><code>@​Jcambass</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/486">actions/github-script#486</a></li>
<li>Fix workflow status badges by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/497">actions/github-script#497</a></li>
<li>Update usage of <code>actions/upload-artifact</code> by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/512">actions/github-script#512</a></li>
<li>Clear up package name confusion by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/514">actions/github-script#514</a></li>
<li>Update dependencies with <code>npm audit fix</code> by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/515">actions/github-script#515</a></li>
<li>Specify that the used script is JavaScript by <a
href="https://github.com/timotk"><code>@​timotk</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/478">actions/github-script#478</a></li>
<li>chore: Add Dependabot for NPM and Actions by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/472">actions/github-script#472</a></li>
<li>Define <code>permissions</code> in workflows and update actions by
<a href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in
<a
href="https://redirect.github.com/actions/github-script/pull/531">actions/github-script#531</a></li>
<li>chore: Add Dependabot for .github/actions/install-dependencies by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/532">actions/github-script#532</a></li>
<li>chore: Remove .vscode settings by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/533">actions/github-script#533</a></li>
<li>ci: Use github/setup-licensed by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/473">actions/github-script#473</a></li>
<li>make octokit instance available as octokit on top of github, to make
it easier to seamlessly copy examples from GitHub rest api or octokit
documentations by <a
href="https://github.com/iamstarkov"><code>@​iamstarkov</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/508">actions/github-script#508</a></li>
<li>Remove <code>octokit</code> README updates for v7 by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/557">actions/github-script#557</a></li>
<li>docs: add &quot;exec&quot; usage examples by <a
href="https://github.com/neilime"><code>@​neilime</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/546">actions/github-script#546</a></li>
<li>Bump ruby/setup-ruby from 1.213.0 to 1.222.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/github-script/pull/563">actions/github-script#563</a></li>
<li>Bump ruby/setup-ruby from 1.222.0 to 1.229.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/github-script/pull/575">actions/github-script#575</a></li>
<li>Clearly document passing inputs to the <code>script</code> by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/603">actions/github-script#603</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/610">actions/github-script#610</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/benelan"><code>@​benelan</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/482">actions/github-script#482</a></li>
<li><a href="https://github.com/Jcambass"><code>@​Jcambass</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/485">actions/github-script#485</a></li>
<li><a href="https://github.com/timotk"><code>@​timotk</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/478">actions/github-script#478</a></li>
<li><a
href="https://github.com/iamstarkov"><code>@​iamstarkov</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/508">actions/github-script#508</a></li>
<li><a href="https://github.com/neilime"><code>@​neilime</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/546">actions/github-script#546</a></li>
<li><a href="https://github.com/nebuk89"><code>@​nebuk89</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/610">actions/github-script#610</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/github-script/compare/v7...v7.1.0">https://github.com/actions/github-script/compare/v7...v7.1.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/github-script/commit/ed597411d8f924073f98dfc5c65a23a2325f34cd"><code>ed59741</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/github-script/issues/653">#653</a>
from actions/sneha-krip/readme-for-v8</li>
<li><a
href="https://github.com/actions/github-script/commit/2dc352e4baefd91bec0d06f6ae2f1045d1687ca3"><code>2dc352e</code></a>
Bold minimum Actions Runner version in README</li>
<li><a
href="https://github.com/actions/github-script/commit/01e118c8d0d22115597e46514b5794e7bc3d56f1"><code>01e118c</code></a>
Update README for Node 24 runtime requirements</li>
<li><a
href="https://github.com/actions/github-script/commit/8b222ac82eda86dcad7795c9d49b839f7bf5b18b"><code>8b222ac</code></a>
Apply suggestion from <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a></li>
<li><a
href="https://github.com/actions/github-script/commit/adc0eeac992408a7b276994ca87edde1c8ce4d25"><code>adc0eea</code></a>
README for updating actions/github-script from v7 to v8</li>
<li><a
href="https://github.com/actions/github-script/commit/20fe497b3fe0c7be8aae5c9df711ac716dc9c425"><code>20fe497</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/github-script/issues/637">#637</a>
from actions/node24</li>
<li><a
href="https://github.com/actions/github-script/commit/e7b7f222b11a03e8b695c4c7afba89a02ea20164"><code>e7b7f22</code></a>
update licenses</li>
<li><a
href="https://github.com/actions/github-script/commit/2c81ba05f308415d095291e6eeffe983d822345b"><code>2c81ba0</code></a>
Update Node.js version support to 24.x</li>
<li><a
href="https://github.com/actions/github-script/commit/f28e40c7f34bde8b3046d885e986cb6290c5673b"><code>f28e40c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/github-script/issues/610">#610</a>
from actions/nebuk89-patch-1</li>
<li><a
href="https://github.com/actions/github-script/commit/1ae9958572fde544457e4d51aed5ea044e8936f3"><code>1ae9958</code></a>
Update README.md</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/github-script/compare/v7.0.1...ed597411d8f924073f98dfc5c65a23a2325f34cd">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/github-script&package-manager=github_actions&previous-version=7.0.1&new-version=8.0.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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-12-25 13:39:19 +00:00
Balázs SzücsandAnthony Stirling 5cd9377bba [V2] feat(flatten): add optional rendering DPI parameter to settings in flatten (#5276)
# Description of Changes

This pull request adds support for specifying a custom rendering DPI for
PDF flattening, allowing users to control the output sharpness and file
size. The changes update the UI to include a new DPI input (when
appropriate), extend parameter handling, and ensure the DPI value is
passed to the backend if provided.

**User interface enhancements:**

* Added a `NumberInput` field in `FlattenSettings.tsx` for users to
specify the rendering DPI, shown only when "Flatten only forms" is not
selected. The input includes label, help text, placeholder, and value
constraints.
* Updated the English locale file to add translation strings for the
rendering DPI input label, help text, and placeholder.

**Parameter and data handling:**

* Extended the `FlattenParameters` interface and default parameters to
include an optional `renderDpi` property.
* Modified the form data builder in `useFlattenOperation.ts` to append
the `renderDpi` value to the request if specified.
* Imported `NumberInput` from the Mantine UI library to support the new
DPI input field in the settings component.
<img width="363" height="998" alt="image"
src="https://github.com/user-attachments/assets/9a59f379-f1d3-4827-8a9c-f34c0bfcfada"
/>


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2025-12-25 01:27:53 +00:00
Balázs SzücsandReece Browne a3e1cc8393 [V2] refactor(ui): replace native inputs with Mantine components (#4898)
# Description of Changes

#### Before
<img width="779" height="768" alt="image"
src="https://github.com/user-attachments/assets/a6d63bac-e27d-4bd8-b44e-fa72333f4c59"
/>


#### After
<img width="779" height="768" alt="image"
src="https://github.com/user-attachments/assets/f39d8d92-802a-40ff-a7db-2b6a187b6847"
/>


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2025-12-25 01:23:49 +00:00
Balázs Szücs 1810a12cf3 feat(rotate): add keyboard shortcuts for rotating with arrow keys (#5294)
# Description of Changes

This pull request adds keyboard shortcut support to the `RotateSettings`
component, allowing users to rotate using the left and right arrow keys.
This improves accessibility and user experience when interacting with
rotation controls.

**Enhanced keyboard interaction:**

* Added a `useEffect` hook to listen for `ArrowLeft` and `ArrowRight`
key presses, triggering the `rotateAnticlockwise` and `rotateClockwise`
methods on the `parameters` object, respectively.


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-24 22:31:41 +00:00
dependabot[bot] c3cd8c48b5 build(deps): bump imageioVersion from 3.12.0 to 3.13.0 (#5295)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps `imageioVersion` from 3.12.0 to 3.13.0.
Updates `com.twelvemonkeys.imageio:imageio-batik` from 3.12.0 to 3.13.0

Updates `com.twelvemonkeys.imageio:imageio-bmp` from 3.12.0 to 3.13.0

Updates `com.twelvemonkeys.imageio:imageio-jpeg` from 3.12.0 to 3.13.0

Updates `com.twelvemonkeys.imageio:imageio-tiff` from 3.12.0 to 3.13.0

Updates `com.twelvemonkeys.imageio:imageio-webp` from 3.12.0 to 3.13.0

Updates `com.twelvemonkeys.imageio:imageio-psd` from 3.12.0 to 3.13.0


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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-12-24 22:13:38 +00:00
dependabot[bot] 2502a8def0 build(deps): bump logback from 1.5.22 to 1.5.23 (#5298)
Bumps `logback` from 1.5.22 to 1.5.23.
Updates `ch.qos.logback:logback-core` from 1.5.22 to 1.5.23
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/qos-ch/logback/releases">ch.qos.logback:logback-core's
releases</a>.</em></p>
<blockquote>
<h2>Logback 1.5.23</h2>
<p><strong>2025-12-21 Release of logback version 1.5.23</strong></p>
<p>• In response to <a
href="https://redirect.github.com/qos-ch/logback/issues/959">issues/959</a>
file name collisions are detected at configuration time by analyzing the
configuration file and no longer at run time. This avoids the
<code>ConcurrentModificationException</code> reported in the issue.</p>
<p>• ZIP and XZ compression now use a <code>BufferedOutputStream</code>
when writing to the compressed file. This issue was reported in <a
href="https://redirect.github.com/qos-ch/logback/issues/988">issues/988</a>.</p>
<p>• A bit-wise identical binary of this version can be reproduced by
building from source code at commit
0bcc3feb54a6d99caac70969ee5f8334aad1fbaf associated with the tag
v_1.5.23. Release built using Java &quot;21&quot; 2023-10-17 LTS build
21.0.1.+12-LTS-29 under Linux Debian 11.6.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/qos-ch/logback/commit/0bcc3feb54a6d99caac70969ee5f8334aad1fbaf"><code>0bcc3fe</code></a>
prepare release 1.5.23</li>
<li><a
href="https://github.com/qos-ch/logback/commit/4627dbd618cbb2365a09c8013ec3fc00d349743e"><code>4627dbd</code></a>
better to use BufferedOutputStream during ZIP and XZ compression,
especially ...</li>
<li><a
href="https://github.com/qos-ch/logback/commit/299f091d3211ad38869aadadbf7b2f66f231ad52"><code>299f091</code></a>
add collision test in presence of conditional processing</li>
<li><a
href="https://github.com/qos-ch/logback/commit/b446f3f06188f4041cea827832ffb8a90fb07241"><code>b446f3f</code></a>
In Context, remove collision map</li>
<li><a
href="https://github.com/qos-ch/logback/commit/a3eb14df4886ce8c398f6486b22ea77ad45ba9af"><code>a3eb14d</code></a>
in response to issues/959, collision detection is now done by
FileCollisionAn...</li>
<li><a
href="https://github.com/qos-ch/logback/commit/681b2be7e11dc46f883ead7ed4603dbad92812ae"><code>681b2be</code></a>
remove unused method, minor comment edits</li>
<li><a
href="https://github.com/qos-ch/logback/commit/17a3edfccc6021cfdd6f0ff52bfd2ae8bcc2c393"><code>17a3edf</code></a>
start work on 1.5.23-SNAPSHOT</li>
<li>See full diff in <a
href="https://github.com/qos-ch/logback/compare/v_1.5.22...v_1.5.23">compare
view</a></li>
</ul>
</details>
<br />

Updates `ch.qos.logback:logback-classic` from 1.5.22 to 1.5.23
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/qos-ch/logback/releases">ch.qos.logback:logback-classic's
releases</a>.</em></p>
<blockquote>
<h2>Logback 1.5.23</h2>
<p><strong>2025-12-21 Release of logback version 1.5.23</strong></p>
<p>• In response to <a
href="https://redirect.github.com/qos-ch/logback/issues/959">issues/959</a>
file name collisions are detected at configuration time by analyzing the
configuration file and no longer at run time. This avoids the
<code>ConcurrentModificationException</code> reported in the issue.</p>
<p>• ZIP and XZ compression now use a <code>BufferedOutputStream</code>
when writing to the compressed file. This issue was reported in <a
href="https://redirect.github.com/qos-ch/logback/issues/988">issues/988</a>.</p>
<p>• A bit-wise identical binary of this version can be reproduced by
building from source code at commit
0bcc3feb54a6d99caac70969ee5f8334aad1fbaf associated with the tag
v_1.5.23. Release built using Java &quot;21&quot; 2023-10-17 LTS build
21.0.1.+12-LTS-29 under Linux Debian 11.6.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/qos-ch/logback/commit/0bcc3feb54a6d99caac70969ee5f8334aad1fbaf"><code>0bcc3fe</code></a>
prepare release 1.5.23</li>
<li><a
href="https://github.com/qos-ch/logback/commit/4627dbd618cbb2365a09c8013ec3fc00d349743e"><code>4627dbd</code></a>
better to use BufferedOutputStream during ZIP and XZ compression,
especially ...</li>
<li><a
href="https://github.com/qos-ch/logback/commit/299f091d3211ad38869aadadbf7b2f66f231ad52"><code>299f091</code></a>
add collision test in presence of conditional processing</li>
<li><a
href="https://github.com/qos-ch/logback/commit/b446f3f06188f4041cea827832ffb8a90fb07241"><code>b446f3f</code></a>
In Context, remove collision map</li>
<li><a
href="https://github.com/qos-ch/logback/commit/a3eb14df4886ce8c398f6486b22ea77ad45ba9af"><code>a3eb14d</code></a>
in response to issues/959, collision detection is now done by
FileCollisionAn...</li>
<li><a
href="https://github.com/qos-ch/logback/commit/681b2be7e11dc46f883ead7ed4603dbad92812ae"><code>681b2be</code></a>
remove unused method, minor comment edits</li>
<li><a
href="https://github.com/qos-ch/logback/commit/17a3edfccc6021cfdd6f0ff52bfd2ae8bcc2c393"><code>17a3edf</code></a>
start work on 1.5.23-SNAPSHOT</li>
<li>See full diff in <a
href="https://github.com/qos-ch/logback/compare/v_1.5.22...v_1.5.23">compare
view</a></li>
</ul>
</details>
<br />


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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-12-24 22:12:10 +00:00
Balázs Szücs 54cd804319 [V2] feat(attachments): add PDF/A-3b conversion, attachment listing, renaming, and deletion (#5304)
# Description of Changes

This pull request introduces major improvements to the PDF attachment
API, adding new endpoints for listing, renaming, and deleting
attachments in PDFs, as well as improving error handling and content
negotiation. It also adds support for converting PDFs to PDF/A-3b format
when adding attachments and introduces stricter validation for
attachment uploads. The exception handling is improved to ensure
consistent JSON error responses, even when the client requests a PDF.

**API Feature Additions:**

* Added new endpoints in `AttachmentController` for listing
(`/list-attachments`), renaming (`/rename-attachment`), and deleting
(`/delete-attachment`) PDF attachments, with corresponding request and
response models: `ListAttachmentsRequest`, `RenameAttachmentRequest`,
`DeleteAttachmentRequest`, and `AttachmentInfo`.

* Enhanced the `/add-attachments` endpoint to optionally convert the
resulting PDF to PDF/A-3b format, controlled by a new `convertToPdfA3b`
flag in `AddAttachmentRequest`.

**Validation and Robustness:**

* Introduced strict validation for attachment uploads, enforcing
non-empty attachments, a maximum size per attachment (50 MB), and a
total size limit (200 MB).

**Content Negotiation:**

* Updated `WebMvcConfig` to configure content negotiation, allowing both
PDF and JSON responses, and preventing 406 errors when clients request
PDFs but errors must be returned as JSON.


<img width="370" height="997" alt="image"
src="https://github.com/user-attachments/assets/571504d4-e97e-4b30-ae97-3defba217b47"
/>
<img width="1415" height="649" alt="image"
src="https://github.com/user-attachments/assets/bb8863fc-0be8-4bf2-af7d-73a229010f9a"
/>
<img width="1415" height="649" alt="image"
src="https://github.com/user-attachments/assets/68092672-5be5-4ef7-9cbc-1fb008b728e1"
/>
<img width="1415" height="649" alt="image"
src="https://github.com/user-attachments/assets/c4b0eda5-2573-4e38-8284-c077acb83f7f"
/>



<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-24 21:35:36 +00:00
1b0a1e938e added a flag to hide settings (#5263)
Added an optional flag in settings.yml to hide the settings button in no
login servers. When hidden, users can no longer:

- Open the Settings modal at all (gear button is hidden)
- Change General preferences (tool picker mode, hide unavailable
tools/conversions, auto‑unzip and file limit)
- Configure keyboard shortcuts (Hotkeys / Keyboard Shortcuts section)
- Use the in‑app update checker UI (see current/latest version, check
for updates, view update details)
- Note: When enableLogin === true, the flag is ignored and the Settings
button remains visible.

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2025-12-24 20:56:22 +00:00
Balázs Szücs 69b035c6e1 [V2] feat(security): add PDF standards verification feature using veraPDF (#4874)
# Description of Changes
- Implemented `PDFVerificationRequest` and `PDFVerificationResult`
models for validation requests and responses
- Developed `VeraPDFService` to validate PDFs against specific or
auto-detected standards
- Added `VerifyPDFController` with an endpoint for PDF verification
- Integrated veraPDF dependencies into project build file
- Deprecated unused `/verify-pdf` form in `SecurityWebController`
- Updated `EndpointConfiguration` to include the new `verify-pdf`
endpoint

This PR introduces a PDF standards verification feature to Stirling-PDF,
powered by the industry-standard veraPDF validation library. This
feature enables users to validate PDF files against multiple PDF
standards including PDF/A (archival), PDF/UA (accessibility), and WTPDF
standards.

### 1. PDF Standards Verification Endpoint
- New API Endpoint: `/api/v1/security/verify-pdf`
- Validates PDF files against multiple standards:
  - PDF/A (1b, 2a, 2b, 2u, 3a, 3b, 3u, 4, 4e, 4f) - Archival standards
  - PDF/UA-1 and PDF/UA-2 - Universal Accessibility standards
  - WTPDF - Well-Tagged PDF standards
- Auto-detection: Automatically detects and validates all standards
declared in the PDF's XMP metadata


### 2. Validation Results
The verification returns detailed JSON results including:
- Compliance status: Whether the PDF meets the standard requirements
- Declared vs validated standards: Shows what the PDF claims to be vs
what it actually is
- Categorized issues:
  - Errors: Critical compliance failures that prevent certification
  - Warnings: Non-critical issues and recommendations
- Detailed issue information:
  - Rule IDs from the specification
  - Descriptive error messages
  - Location within the PDF where the issue occurs
  - Specification references (clause numbers, test numbers)

### 3. Detect Issue Classification
Implements intelligent classification of validation issues:
- Errors: Issues that prevent standard compliance (font problems, color
space issues, structural problems)
- Warnings: Recommended but not required elements (metadata
recommendations, optional features)
- Classification based on:
  - Rule ID patterns
  - Clause number prefixes
  - Message content analysis

### New Files Added

#### Controllers
- VerifyPDFController.java: REST API controller handling PDF
verification requests
  - Handles multipart file uploads
  - Supports both single-standard and auto-detection modes
- Comprehensive error handling for encrypted PDFs, parsing errors, and
validation failures

#### Models
- PDFVerificationRequest.java: Request model for verification API
  - Extends standard PDFFile model
  - Optional `standard` parameter for manual standard selection
  
- PDFVerificationResult.java: Response model containing validation
results
  - Includes standard information and validation profile details
  - Separate lists for errors and warnings
  - Nested `ValidationIssue` class for detailed issue reporting

#### Services
- VeraPDFService.java: Core service implementing veraPDF integration
  - Initializes veraPDF Greenfield engine
  - Extracts declared PDF/A standards from XMP metadata
  - Performs validation against specified or detected standards
  - Converts veraPDF results to application-specific format
  - Implements smart issue classification logic


### Endpoint Configuration Updates

#### EndpointConfiguration.java
- Added `verify-pdf` to the Security group
- Added `verify-pdf` to the Java group (no external tools required)
- Created new veraPDF dependency group for endpoint availability
tracking
- Updated `isToolGroup()` method to recognize veraPDF as a tool
dependency


### Supported Standards

#### PDF/A (Archival)
- PDF/A-1 (a, b): ISO 19005-1:2005
- PDF/A-2 (a, b, u): ISO 19005-2:2011
- PDF/A-3 (a, b, u): ISO 19005-3:2012
- PDF/A-4 (standard, e, f): ISO 19005-4:2020

#### PDF/UA (Universal Accessibility)
- PDF/UA-1: ISO 14289-1:2014
- PDF/UA-2: ISO 14289-2 (latest)

#### WTPDF (Well-Tagged PDF)
- WTPDF 1.0: Tagged PDF for accessibility and structure

### Security Considerations

The following test scenarios should be validated:

1. Valid PDF/A documents (should return compliant)
2. Non-compliant PDF/A documents (should return errors)
3. PDFs without PDF/A declaration (should detect and report)
4. PDF/UA documents (should validate accessibility)
5. Encrypted PDFs (should return appropriate error)
6. Mixed standards (PDF/A + PDF/UA) (should validate both)
7. Empty standard parameter (should auto-detect)
8. Invalid standard parameter (should return error)

### API Usage Examples

```bash
curl -X POST http://localhost:8080/api/v1/security/verify-pdf \
  -F "fileInput=@document.pdf"
```


### Example Response
```json
[
  {
    "standard": "3b",
    "standardName": "PDF/A-ISO 19005-3:2012B compliant",
    "validationProfile": "3b",
    "validationProfileName": "PDF/A-ISO 19005-3:2012B",
    "complianceSummary": "PDF/A-ISO 19005-3:2012B compliant",
    "declaredPdfa": true,
    "compliant": true,
    "totalFailures": 0,
    "totalWarnings": 0,
    "failures": [],
    "warnings": []
  }
]
```

```json
[
  {
    "standard": "2b",
    "standardName": "PDF/A-ISO 19005-2:2011B with errors",
    "validationProfile": "2b",
    "validationProfileName": "PDF/A-ISO 19005-2:2011B",
    "complianceSummary": "PDF/A-ISO 19005-2:2011B with errors",
    "declaredPdfa": true,
    "compliant": false,
    "totalFailures": 2,
    "totalWarnings": 0,
    "failures": [
      {
        "ruleId": "RuleId [specification=ISO 19005-2:2011, clause=6.2.11.4.1, testNumber=1]",
        "message": "The font programs for all fonts used for rendering within a conforming file shall be embedded within that file, as defined in ISO 32000-1:2008, 9.9",
        "location": "Location [level=CosDocument, context=root/document[0]/pages[0](3 0 obj PDPage)/contentStream[0](105 0 obj PDContentStream)/operators[60]/font[0](ArialMT)]",
        "specification": "ISO 19005-2:2011",
        "clause": "6.2.11.4.1",
        "testNumber": "1"
      },
      {
        "ruleId": "RuleId [specification=ISO 19005-2:2011, clause=6.3.2, testNumber=1]",
        "message": "Except for annotation dictionaries whose Subtype value is Popup, all annotation dictionaries shall contain the F key",
        "location": "Location [level=CosDocument, context=root/document[0]/pages[0](3 0 obj PDPage)/annots[4](107 0 obj PDLinkAnnot)]",
        "specification": "ISO 19005-2:2011",
        "clause": "6.3.2",
        "testNumber": "1"
      }
    ],
    "warnings": []
  }
]
```

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-23 22:29:30 +00:00
Balázs SzücsandCopilot 8164c56d33 [V2] feat(ocr): add option to invalidate digital signatures with warning tooltip (#5148)
# Description of Changes
This pull request adds support for invalidating digital signatures
during the OCR process, allowing users to process signed PDFs with OCR
at the cost of breaking their digital signatures. The feature is
integrated across the backend, frontend, and user interface, including
appropriate warnings to inform users of the consequences.

**Backend support for digital signature invalidation:**

- Added a new `invalidateDigitalSignatures` boolean field to the
`ProcessPdfWithOcrRequest` model, and updated the `OCRController` to
accept, pass, and handle this parameter. If enabled, the backend adds
the `--invalidate-digital-signatures` flag to the OCR command,
invalidating any digital signatures present in the PDF.

**Frontend and UI integration:**

- Added an "Invalidate digital signatures" option to the advanced OCR
settings in the UI, including a warning tooltip to inform users that
enabling this option will make the document's digital signatures
invalid.
- Updated the OCR form data builder to include the
`invalidateDigitalSignatures` parameter when submitting OCR requests.

**Localization and tooltips:**

- Added new translation strings and warning messages for the "Invalidate
digital signatures" option and its tooltip, ensuring users are clearly
informed about the consequences of enabling this feature.

<img width="649" height="994" alt="image"
src="https://github.com/user-attachments/assets/8d7f42cb-fc9e-4864-ad09-09ccb56347a1"
/>

Closes: #5142
<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-23 10:54:53 +00:00
Balázs Szücs 098f2badec [V2] feat(getPdfInfo): add attachment, embedded file, and image info display (#5278)
# Description of Changes

This pull request enhances the display of PDF metadata in the "Other"
and "Per Page" sections by introducing more detailed and user-friendly
rendering of attachments, embedded files, and images. The changes add
new helper functions for rendering these lists and extend the type
definitions to support richer information, improving the clarity and
usefulness of the UI.

**Improvements to PDF metadata rendering:**

* Added `renderAttachmentsList` and `renderEmbeddedFilesList` helper
functions in `OtherSection.tsx` to display detailed information about
attachments and embedded files, including name, description, file size,
MIME type, and dates. These replace the generic list renderer for these
fields.
* Added `renderImagesList` helper function in `PerPageSection.tsx` to
display images with their name, dimensions, and color space, replacing
the generic list renderer for images.

**Type definition enhancements:**

* Extended `PdfEmbeddedFileInfo` and `PdfAttachmentInfo` in
`getPdfInfo.ts` to include additional fields: `FileSize`, `MimeType`,
`CreationDate`, and `ModificationDate` for embedded files, and
`FileSize` for attachments.

<img width="1920" height="998" alt="image"
src="https://github.com/user-attachments/assets/249409c1-4fd5-4599-b8f9-07093034cb55"
/>

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-22 23:05:52 +00:00
Anthony Stirling d2a4054120 Revert "[ImgBot] Optimize images" (#5293)
Reverts Stirling-Tools/Stirling-PDF#5290
2025-12-22 22:10:23 +00:00
Balázs Szücs 7baf503d60 [V2] feat(split): add split mode options and custom page selection to section split (#5277)
# Description of Changes


This pull request adds a new "Split Mode" feature to the "Split by
Sections" tool, allowing users to customize how pages are split,
including options to exclude the first/last pages or specify custom
pages. The changes include updates to the UI, form data handling,
parameter definitions, and validation logic to support these new
options.

**Split by Sections: New Split Mode Feature**

*UI Enhancements:*
- Added a `Radio.Group` in `SplitSettings.tsx` to let users choose the
split mode (split all, exclude first/last, or custom pages), and a
conditional `TextInput` for custom page numbers when "Custom" is
selected.

*Localization:*
- Updated `translation.toml` with new labels and descriptions for the
split mode options and custom page numbers.

*Parameter Handling:*
- Extended the `SplitParameters` interface to include `splitMode` and
`customPages`, and set their default values.

*Form Submission:*
- Modified `buildSplitFormData` to include the selected split mode and
custom page numbers (if applicable) in the form data sent to the
backend.

*Validation Logic:*
- Updated the validation logic in `useSplitParameters` to require custom
page numbers when the custom split mode is selected.

<img width="363" height="998" alt="image"
src="https://github.com/user-attachments/assets/c9f781a6-cf7e-4f3d-8514-57cfe468e0c8"
/>




<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-22 19:28:00 +00:00
EthanHealy01 d9622d0fac Cleanup/v2/text editor (#5169)
# Description of Changes

<img width="2056" height="1075" alt="Screenshot 2025-12-04 at 8 16
28 PM"
src="https://github.com/user-attachments/assets/9d2deacb-e471-4711-b9ac-9ef5a64b6f23"
/>
<img width="2056" height="1070" alt="Screenshot 2025-12-04 at 8 16
51 PM"
src="https://github.com/user-attachments/assets/07f07835-c973-4f56-80fb-5efc00ff1651"
/>
<img width="728" height="517" alt="Screenshot 2025-12-04 at 8 17 04 PM"
src="https://github.com/user-attachments/assets/8c59fae0-93a8-4da5-b2e4-9a89be651b7f"
/>
<img width="388" height="218" alt="Screenshot 2025-12-04 at 8 17 29 PM"
src="https://github.com/user-attachments/assets/997e9a68-6a3b-4033-bc45-04e3945f23d5"
/>


---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-22 18:25:25 +00:00
imgbot[bot]andImgBotApp ddba5a3eab [ImgBot] Optimize images (#5290)
## Beep boop. Your images are optimized!

Your image file size has been reduced by **20%** 🎉

<details>
<summary>
Details
</summary>

| File | Before | After | Percent reduction |
|:--|:--|:--|:--|
| /frontend/public/Login/LoginBackgroundPanel.png | 5.30kb | 2.21kb |
58.24% |
| /frontend/src-tauri/icons/icon.png | 19.17kb | 8.78kb | 54.22% |
| /frontend/public/modern-logo/logo512.png | 7.96kb | 4.49kb | 43.61% |
| /images/custom-home.png | 543.80kb | 329.50kb | 39.41% |
| /images/custom-home-dark.png | 456.09kb | 287.99kb | 36.86% |
| /frontend/public/modern-logo/logo192.png | 3.09kb | 2.06kb | 33.22% |
| /images/home.png | 98.13kb | 69.74kb | 28.93% |
| /frontend/public/classic-logo/logo512.png | 99.72kb | 78.32kb | 21.47%
|
| /frontend/public/modern-logo/Firstpage.png | 210.88kb | 169.27kb |
19.73% |
| /frontend/public/Login/Firstpage.png | 210.88kb | 169.27kb | 19.73% |
| /images/home-light.png | 56.34kb | 47.79kb | 15.19% |
| /frontend/public/Login/AddToPDF.png | 716.99kb | 627.65kb | 12.46% |
| /frontend/public/classic-logo/Firstpage.png | 405.84kb | 355.87kb |
12.31% |
| /frontend/public/classic-logo/logo192.png | 23.45kb | 21.19kb | 9.62%
|
| /frontend/src-tauri/icons/128x128@2x.png | 19.47kb | 18.27kb | 6.16% |
|
/frontend/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
| 26.17kb | 24.76kb | 5.38% |
| /frontend/src-tauri/icons/Square284x284Logo.png | 22.54kb | 21.41kb |
5.02% |
| /frontend/src-tauri/icons/Square310x310Logo.png | 24.80kb | 23.74kb |
4.26% |
|
/frontend/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png
| 35.56kb | 34.13kb | 4.03% |
| /frontend/src-tauri/icons/ios/AppIcon-512@2x.png | 111.12kb | 108.22kb
| 2.61% |
|
/frontend/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
| 16.67kb | 16.29kb | 2.28% |
| /frontend/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png | 11.38kb |
11.12kb | 2.21% |
| /frontend/src-tauri/icons/ios/AppIcon-60x60@3x.png | 12.33kb | 12.06kb
| 2.19% |
| /frontend/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
| 14.63kb | 14.40kb | 1.60% |
| /frontend/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png |
14.63kb | 14.40kb | 1.60% |
| /frontend/src-tauri/icons/ios/AppIcon-76x76@2x.png | 10.34kb | 10.18kb
| 1.55% |
|
/frontend/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
| 12.01kb | 11.88kb | 1.05% |
| /frontend/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
| 10.59kb | 10.48kb | 0.99% |
| /frontend/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png |
10.59kb | 10.48kb | 0.99% |
| /frontend/src-tauri/icons/Square150x150Logo.png | 10.96kb | 10.87kb |
0.82% |
| /frontend/src-tauri/icons/ios/AppIcon-40x40@3x.png | 8.10kb | 8.04kb |
0.81% |
| /frontend/src-tauri/icons/ios/AppIcon-60x60@2x.png | 8.10kb | 8.04kb |
0.81% |
| /frontend/public/Login/SecurePDF.png | 228.99kb | 227.15kb | 0.80% |
| /frontend/src-tauri/icons/Square142x142Logo.png | 10.38kb | 10.33kb |
0.46% |
| /app/core/src/main/resources/static/api-wordmark.svg | 6.39kb | 6.39kb
| 0.11% |
| /frontend/public/modern-logo/StirlingPDFLogoGreyText.svg | 6.39kb |
6.39kb | 0.11% |
| | | | |
| **Total :** | **3,489.78kb** | **2,793.14kb** | **19.96%** |
</details>

---

[📝 docs](https://imgbot.net/docs) | [:octocat:
repo](https://github.com/imgbot/ImgBot) | [🙋🏾
issues](https://github.com/imgbot/ImgBot/issues) | [🏪
marketplace](https://github.com/marketplace/imgbot)

<i>~Imgbot - Part of [Optimole](https://optimole.com/) family</i>

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
2025-12-22 16:00:54 +00:00
Anthony Stirling 0bfb90cfc1 API mode UI (#5287)
# Description of Changes


<img width="1945" height="1895" alt="image"
src="https://github.com/user-attachments/assets/4ace2535-d0ec-44a7-b57c-df9bcdd5c5d7"
/>

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-22 15:49:27 +00:00
Balázs Szücs c454a72f08 [V2] feat(compress): add linearize option for fast web viewing (#5280)
# Description of Changes

This pull request adds a new "Linearize PDF" option to the PDF
compression tool, enabling users to optimize PDFs for fast web viewing.
The change includes updates to the user interface, parameters, and form
data handling to support this feature.

**User Interface Updates:**
- Added a new checkbox labeled "Linearize PDF for fast web viewing" to
the compression settings UI, allowing users to enable or disable PDF
linearization.

**Localization:**
- Introduced a new localization string for the "Linearize PDF for fast
web viewing" label in the English (UK) translation file.

**Parameter Handling:**
- Updated the `CompressParameters` interface and default parameters to
include a `linearize` boolean flag.
- Modified the form data builder to append the `linearize` parameter
when submitting compression requests.

<img width="366" height="998" alt="image"
src="https://github.com/user-attachments/assets/f1efcdd5-fb91-4232-94f7-0a72de86739f"
/>


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-22 15:47:12 +00:00
Balázs Szücs 34fd328444 [V2] feat(sign): add SVG support for signature image uploads (#5279)
# Description of Changes


This pull request updates the `ImageUploader` component to improve SVG
file support. The main changes ensure that SVG files can be uploaded and
are properly validated, even if their MIME type is not set as an image.

**SVG File Support Improvements:**

* Updated the file type validation in the `handleImageChange` function
to allow files ending with `.svg`, even if their MIME type is not
`image/*`.
* Modified the `accept` attribute in the `FileInput` component to
explicitly include `.svg` files, making SVG selection possible in the
file picker.

### Sign with SVG pics

<img width="1920" height="998" alt="image"
src="https://github.com/user-attachments/assets/9e24d83e-304c-4c7b-85b3-82798616ac5a"
/>


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-22 15:46:26 +00:00
Anthony Stirling 738dd0645d Revert "Revert "Sync provider settings with settings.yml in config UI"" (#5273)
Reverts Stirling-Tools/Stirling-PDF#5271
2025-12-22 15:45:55 +00:00
dependabot[bot] e997f0d4ff build(deps): bump com.squareup.okhttp3:okhttp-bom from 5.3.1 to 5.3.2 (#4961)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps
[com.squareup.okhttp3:okhttp-bom](https://github.com/square/okhttp) from
5.3.1 to 5.3.2.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/square/okhttp/blob/master/CHANGELOG.md">com.squareup.okhttp3:okhttp-bom's
changelog</a>.</em></p>
<blockquote>
<h2>Version 5.3.2</h2>
<p><em>2025-11-18</em></p>
<ul>
<li>
<p>Fix: Don't delay triggering timeouts. In Okio 3.16.0 we introduced a
regression that caused
timeouts to fire later than they were supposed to.</p>
</li>
<li>
<p>Upgrade: [Okio 3.16.4][okio_3_16_4].</p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/square/okhttp/commit/75b9c267744cdd2e8b222d247052748ff0954304"><code>75b9c26</code></a>
Prepare for release 5.3.2.</li>
<li><a
href="https://github.com/square/okhttp/commit/ab48e5d86d0c0b16b3679b9b9522acf65db73da4"><code>ab48e5d</code></a>
Okio 3.16.4 (<a
href="https://redirect.github.com/square/okhttp/issues/9200">#9200</a>)</li>
<li><a
href="https://github.com/square/okhttp/commit/a9a4638b3d38b83782a959514b4183ddea0f071f"><code>a9a4638</code></a>
Prepare next development version.</li>
<li>See full diff in <a
href="https://github.com/square/okhttp/compare/parent-5.3.1...parent-5.3.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.squareup.okhttp3:okhttp-bom&package-manager=gradle&previous-version=5.3.1&new-version=5.3.2)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-22 15:45:30 +00:00
dependabot[bot] e137436656 build(deps): bump bouncycastleVersion from 1.82 to 1.83 (#5111)
Bumps `bouncycastleVersion` from 1.82 to 1.83.
Updates `org.bouncycastle:bcprov-jdk18on` from 1.82 to 1.83
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html">org.bouncycastle:bcprov-jdk18on's
changelog</a>.</em></p>
<blockquote>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<p><!-- raw HTML omitted --><!-- raw HTML omitted -->2.1.1 Version<!--
raw HTML omitted --><!-- raw HTML omitted -->
Release: 1.83<!-- raw HTML omitted -->
Date:      2025, November 27th.</p>
<!-- raw HTML omitted -->
<p><!-- raw HTML omitted --><!-- raw HTML omitted -->2.2.1 Version<!--
raw HTML omitted --><!-- raw HTML omitted -->
Release: 1.82<!-- raw HTML omitted -->
Date:      2025, 17th September.</p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/bcgit/bc-java/commits">compare view</a></li>
</ul>
</details>
<br />

Updates `org.bouncycastle:bcpkix-jdk18on` from 1.82 to 1.83
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html">org.bouncycastle:bcpkix-jdk18on's
changelog</a>.</em></p>
<blockquote>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<p><!-- raw HTML omitted --><!-- raw HTML omitted -->2.1.1 Version<!--
raw HTML omitted --><!-- raw HTML omitted -->
Release: 1.83<!-- raw HTML omitted -->
Date:      2025, November 27th.</p>
<!-- raw HTML omitted -->
<p><!-- raw HTML omitted --><!-- raw HTML omitted -->2.2.1 Version<!--
raw HTML omitted --><!-- raw HTML omitted -->
Release: 1.82<!-- raw HTML omitted -->
Date:      2025, 17th September.</p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/bcgit/bc-java/commits">compare view</a></li>
</ul>
</details>
<br />


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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-12-22 15:45:11 +00:00
dependabot[bot] 1649b9bffb build(deps): bump softprops/action-gh-release from 2.4.2 to 2.5.0 (#5134)
Bumps
[softprops/action-gh-release](https://github.com/softprops/action-gh-release)
from 2.4.2 to 2.5.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/softprops/action-gh-release/releases">softprops/action-gh-release's
releases</a>.</em></p>
<blockquote>
<h2>v2.5.0</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<h3>Exciting New Features 🎉</h3>
<ul>
<li>feat: mark release as draft until all artifacts are uploaded by <a
href="https://github.com/dumbmoron"><code>@​dumbmoron</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/692">softprops/action-gh-release#692</a></li>
</ul>
<h3>Other Changes 🔄</h3>
<ul>
<li>chore(deps): bump the npm group across 1 directory with 5 updates by
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/697">softprops/action-gh-release#697</a></li>
<li>chore(deps): bump actions/checkout from 5.0.0 to 5.0.1 in the
github-actions group by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/689">softprops/action-gh-release#689</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/dumbmoron"><code>@​dumbmoron</code></a>
made their first contribution in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/692">softprops/action-gh-release#692</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/softprops/action-gh-release/compare/v2.4.2...v2.5.0">https://github.com/softprops/action-gh-release/compare/v2.4.2...v2.5.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md">softprops/action-gh-release's
changelog</a>.</em></p>
<blockquote>
<h2>2.5.0</h2>
<h2>What's Changed</h2>
<h3>Exciting New Features 🎉</h3>
<ul>
<li>feat: mark release as draft until all artifacts are uploaded by <a
href="https://github.com/dumbmoron"><code>@​dumbmoron</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/692">softprops/action-gh-release#692</a></li>
</ul>
<h3>Other Changes 🔄</h3>
<ul>
<li>dependency updates</li>
</ul>
<h2>2.4.2</h2>
<h2>What's Changed</h2>
<h3>Exciting New Features 🎉</h3>
<ul>
<li>feat: Ensure generated release notes cannot be over 125000
characters by <a
href="https://github.com/BeryJu"><code>@​BeryJu</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/684">softprops/action-gh-release#684</a></li>
</ul>
<h3>Other Changes 🔄</h3>
<ul>
<li>dependency updates</li>
</ul>
<h2>2.4.1</h2>
<h2>What's Changed</h2>
<h3>Other Changes 🔄</h3>
<ul>
<li>fix(util): support brace expansion globs containing commas in
parseInputFiles by <a
href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/672">softprops/action-gh-release#672</a></li>
<li>fix: gracefully fallback to body when body_path cannot be read by <a
href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/671">softprops/action-gh-release#671</a></li>
</ul>
<h2>2.4.0</h2>
<h2>What's Changed</h2>
<h3>Exciting New Features 🎉</h3>
<ul>
<li>feat(action): respect working_directory for files globs by <a
href="https://github.com/stephenway"><code>@​stephenway</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/667">softprops/action-gh-release#667</a></li>
</ul>
<h2>2.3.4</h2>
<h2>What's Changed</h2>
<h3>Bug fixes 🐛</h3>
<ul>
<li>fix(action): handle 422 already_exists race condition by <a
href="https://github.com/stephenway"><code>@​stephenway</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/665">softprops/action-gh-release#665</a></li>
</ul>
<h3>Other Changes 🔄</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/softprops/action-gh-release/commit/a06a81a03ee405af7f2048a818ed3f03bbf83c7b"><code>a06a81a</code></a>
release 2.5.0</li>
<li><a
href="https://github.com/softprops/action-gh-release/commit/7da8983734babbd3165bced7ac7add895656129f"><code>7da8983</code></a>
feat: mark release as draft until all artifacts are uploaded (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/692">#692</a>)</li>
<li><a
href="https://github.com/softprops/action-gh-release/commit/87973286a4d52a3789332acb79caa97720ecde8d"><code>8797328</code></a>
chore(deps): bump actions/checkout in the github-actions group (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/689">#689</a>)</li>
<li><a
href="https://github.com/softprops/action-gh-release/commit/1bfc62a71b435bb6eb12e454577ad67e07938581"><code>1bfc62a</code></a>
chore(deps): bump the npm group across 1 directory with 5 updates (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/697">#697</a>)</li>
<li>See full diff in <a
href="https://github.com/softprops/action-gh-release/compare/5be0e66d93ac7ed76da52eca8bb058f665c3a5fe...a06a81a03ee405af7f2048a818ed3f03bbf83c7b">compare
view</a></li>
</ul>
</details>
<br />

<details>
<summary>Most Recent Ignore Conditions Applied to This Pull
Request</summary>

| Dependency Name | Ignore Conditions |
| --- | --- |
| softprops/action-gh-release | [>= 2.2.a, < 2.3] |
</details>


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=softprops/action-gh-release&package-manager=github_actions&previous-version=2.4.2&new-version=2.5.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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-12-22 15:44:56 +00:00
stirlingbot[bot] c990ab3216 🤖 format everything with pre-commit by stirlingbot (#5144)
Auto-generated by [create-pull-request][1] with **stirlingbot**

[1]: https://github.com/peter-evans/create-pull-request

Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2025-12-22 15:44:38 +00:00
dependabot[bot] e6d3f20c36 build(deps): bump actions/stale from 10.1.0 to 10.1.1 (#5133)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps [actions/stale](https://github.com/actions/stale) from 10.1.0 to
10.1.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/stale/releases">actions/stale's
releases</a>.</em></p>
<blockquote>
<h2>v10.1.1</h2>
<h2>What's Changed</h2>
<h3>Bug Fix</h3>
<ul>
<li>Add Missing Input Reading for <code>only-issue-types</code> by <a
href="https://github.com/Bibo-Joshi"><code>@​Bibo-Joshi</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1298">actions/stale#1298</a></li>
</ul>
<h3>Improvement</h3>
<ul>
<li>Improves error handling when rate limiting is disabled on GHES. by
<a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
in <a
href="https://redirect.github.com/actions/stale/pull/1300">actions/stale#1300</a></li>
</ul>
<h3>Dependency Upgrades</h3>
<ul>
<li>Upgrade eslint-config-prettier from 8.10.0 to 10.1.8 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1276">actions/stale#1276</a></li>
<li>Upgrade <code>@​types/node</code> from 20.10.3 to 24.2.0 and
document breaking changes in v10 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1280">actions/stale#1280</a></li>
<li>Upgrade actions/publish-action from 0.3.0 to 0.4.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1291">actions/stale#1291</a></li>
<li>Upgrade actions/checkout from 4 to 6 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1306">actions/stale#1306</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/stale/pull/1300">actions/stale#1300</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/stale/compare/v10...v10.1.1">https://github.com/actions/stale/compare/v10...v10.1.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/stale/commit/997185467fa4f803885201cee163a9f38240193d"><code>9971854</code></a>
build(deps): bump actions/checkout from 4 to 6 (<a
href="https://redirect.github.com/actions/stale/issues/1306">#1306</a>)</li>
<li><a
href="https://github.com/actions/stale/commit/5611b9defa6b7799a950489b00163db69f7a3ece"><code>5611b9d</code></a>
build(deps): bump actions/publish-action from 0.3.0 to 0.4.0 (<a
href="https://redirect.github.com/actions/stale/issues/1291">#1291</a>)</li>
<li><a
href="https://github.com/actions/stale/commit/fad0de84e50d1aba7b0236cdaf0ea98a43286849"><code>fad0de8</code></a>
Improves error handling when rate limiting is disabled on GHES. (<a
href="https://redirect.github.com/actions/stale/issues/1300">#1300</a>)</li>
<li><a
href="https://github.com/actions/stale/commit/39bea7de61dd70ce4705a976f904f33d5e1e0f49"><code>39bea7d</code></a>
Add Missing Input Reading for <code>only-issue-types</code> (<a
href="https://redirect.github.com/actions/stale/issues/1298">#1298</a>)</li>
<li><a
href="https://github.com/actions/stale/commit/e46bbabb3ede15841d25946157759558dd16306e"><code>e46bbab</code></a>
build(deps-dev): bump <code>@​types/node</code> from 20.10.3 to 24.2.0
and document breakin...</li>
<li><a
href="https://github.com/actions/stale/commit/65d1d4804d3060875fff9f9fa8a49e27f71ce7f0"><code>65d1d48</code></a>
build(deps-dev): bump eslint-config-prettier from 8.10.0 to 10.1.8 (<a
href="https://redirect.github.com/actions/stale/issues/1276">#1276</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/stale/compare/5f858e3efba33a5ca4407a664cc011ad407f2008...997185467fa4f803885201cee163a9f38240193d">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/stale&package-manager=github_actions&previous-version=10.1.0&new-version=10.1.1)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-12-22 15:22:21 +00:00
dependabot[bot] 79d5bc942d build(deps): bump actions/setup-python from 6.0.0 to 6.1.0 (#4992)
Bumps [actions/setup-python](https://github.com/actions/setup-python)
from 6.0.0 to 6.1.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/setup-python/releases">actions/setup-python's
releases</a>.</em></p>
<blockquote>
<h2>v6.1.0</h2>
<h2>What's Changed</h2>
<h3>Enhancements:</h3>
<ul>
<li>Add support for <code>pip-install</code> input by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-python/pull/1201">actions/setup-python#1201</a></li>
<li>Add graalpy early-access and windows builds by <a
href="https://github.com/timfel"><code>@​timfel</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/880">actions/setup-python#880</a></li>
</ul>
<h3>Dependency and Documentation updates:</h3>
<ul>
<li>Enhanced wording and updated example usage for
<code>allow-prereleases</code> by <a
href="https://github.com/yarikoptic"><code>@​yarikoptic</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/979">actions/setup-python#979</a></li>
<li>Upgrade urllib3 from 1.26.19 to 2.5.0 and document breaking changes
in v6 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1139">actions/setup-python#1139</a></li>
<li>Upgrade typescript from 5.4.2 to 5.9.3 and Documentation update by
<a href="https://github.com/dependabot"><code>@​dependabot</code></a> in
<a
href="https://redirect.github.com/actions/setup-python/pull/1094">actions/setup-python#1094</a></li>
<li>Upgrade actions/publish-action from 0.3.0 to 0.4.0 &amp;
Documentation update for pip-install input by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1199">actions/setup-python#1199</a></li>
<li>Upgrade requests from 2.32.2 to 2.32.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1130">actions/setup-python#1130</a></li>
<li>Upgrade prettier from 3.5.3 to 3.6.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1234">actions/setup-python#1234</a></li>
<li>Upgrade <code>@​types/node</code> from 24.1.0 to 24.9.1 and update
macos-13 to macos-15-intel by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1235">actions/setup-python#1235</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/yarikoptic"><code>@​yarikoptic</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-python/pull/979">actions/setup-python#979</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-python/compare/v6...v6.1.0">https://github.com/actions/setup-python/compare/v6...v6.1.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/setup-python/commit/83679a892e2d95755f2dac6acb0bfd1e9ac5d548"><code>83679a8</code></a>
Bump <code>@​types/node</code> from 24.1.0 to 24.9.1 and update macos-13
to macos-15-intel ...</li>
<li><a
href="https://github.com/actions/setup-python/commit/bfc4944b43a5d84377eca3cf6ab5b7992ba61923"><code>bfc4944</code></a>
Bump prettier from 3.5.3 to 3.6.2 (<a
href="https://redirect.github.com/actions/setup-python/issues/1234">#1234</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/97aeb3efb8a852c559869050c7fb175b4efcc8cf"><code>97aeb3e</code></a>
Bump requests from 2.32.2 to 2.32.4 in /<strong>tests</strong>/data (<a
href="https://redirect.github.com/actions/setup-python/issues/1130">#1130</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/443da59188462e2402e2942686db5aa6723f4bed"><code>443da59</code></a>
Bump actions/publish-action from 0.3.0 to 0.4.0 &amp; Documentation
update for pi...</li>
<li><a
href="https://github.com/actions/setup-python/commit/cfd55ca82492758d853442341ad4d8010466803a"><code>cfd55ca</code></a>
graalpy: add graalpy early-access and windows builds (<a
href="https://redirect.github.com/actions/setup-python/issues/880">#880</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/bba65e51ff35d50c6dbaaacd8a4681db13aa7cb4"><code>bba65e5</code></a>
Bump typescript from 5.4.2 to 5.9.3 and update docs/advanced-usage.md
(<a
href="https://redirect.github.com/actions/setup-python/issues/1094">#1094</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/18566f86b301499665bd3eb1a2247e0849c64fa5"><code>18566f8</code></a>
Improve wording and &quot;fix example&quot; (remove 3.13) on testing
against pre-releas...</li>
<li><a
href="https://github.com/actions/setup-python/commit/2e3e4b15a884dc73a63f962bff250a855150a234"><code>2e3e4b1</code></a>
Add support for pip-install input (<a
href="https://redirect.github.com/actions/setup-python/issues/1201">#1201</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/4267e283df95c05d9f16ece6624106f44613b489"><code>4267e28</code></a>
Bump urllib3 from 1.26.19 to 2.5.0 in /<strong>tests</strong>/data and
document breaking c...</li>
<li>See full diff in <a
href="https://github.com/actions/setup-python/compare/e797f83bcb11b83ae66e0230d6156d7c80228e7c...83679a892e2d95755f2dac6acb0bfd1e9ac5d548">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=6.0.0&new-version=6.1.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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-12-22 15:21:50 +00:00
dependabot[bot] d652fc1d1c build(deps): bump actions/ai-inference from 1.2.8 to 2.0.4 (#5132)
Bumps [actions/ai-inference](https://github.com/actions/ai-inference)
from 1.2.8 to 2.0.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/ai-inference/releases">actions/ai-inference's
releases</a>.</em></p>
<blockquote>
<h2>v2.0.4</h2>
<h2>What's Changed</h2>
<ul>
<li>Clarify token requirements for MCP integration by <a
href="https://github.com/GulerSevil"><code>@​GulerSevil</code></a> in <a
href="https://redirect.github.com/actions/ai-inference/pull/139">actions/ai-inference#139</a></li>
<li>Pass GitHub MCP Tools by <a
href="https://github.com/maartenvandiemen"><code>@​maartenvandiemen</code></a>
in <a
href="https://redirect.github.com/actions/ai-inference/pull/142">actions/ai-inference#142</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/GulerSevil"><code>@​GulerSevil</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/ai-inference/pull/139">actions/ai-inference#139</a></li>
<li><a
href="https://github.com/maartenvandiemen"><code>@​maartenvandiemen</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/ai-inference/pull/142">actions/ai-inference#142</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/ai-inference/compare/v2...v2.0.4">https://github.com/actions/ai-inference/compare/v2...v2.0.4</a></p>
<h2>v2.0.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Mock inference in CI by <a
href="https://github.com/sgoedecke"><code>@​sgoedecke</code></a> in <a
href="https://redirect.github.com/actions/ai-inference/pull/150">actions/ai-inference#150</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/ai-inference/compare/v2...v2.0.3">https://github.com/actions/ai-inference/compare/v2...v2.0.3</a></p>
<h2>v2.0.2</h2>
<h2>What's Changed</h2>
<ul>
<li>chore(deps): bump actions/checkout from 4 to 5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/ai-inference/pull/94">actions/ai-inference#94</a></li>
<li>docs: update documentation on mcp usage by <a
href="https://github.com/FidelusAleksander"><code>@​FidelusAleksander</code></a>
in <a
href="https://redirect.github.com/actions/ai-inference/pull/93">actions/ai-inference#93</a></li>
<li>Clarify PAT requirement for github-mcp-token by <a
href="https://github.com/srt32"><code>@​srt32</code></a> in <a
href="https://redirect.github.com/actions/ai-inference/pull/147">actions/ai-inference#147</a></li>
<li>fix: do template substition after parsing prompt YAML by <a
href="https://github.com/dsanders11"><code>@​dsanders11</code></a> in <a
href="https://redirect.github.com/actions/ai-inference/pull/136">actions/ai-inference#136</a></li>
<li>feat: support modelParameters in prompt.yaml files by <a
href="https://github.com/dsanders11"><code>@​dsanders11</code></a> in <a
href="https://redirect.github.com/actions/ai-inference/pull/148">actions/ai-inference#148</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/FidelusAleksander"><code>@​FidelusAleksander</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/ai-inference/pull/93">actions/ai-inference#93</a></li>
<li><a href="https://github.com/srt32"><code>@​srt32</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/ai-inference/pull/147">actions/ai-inference#147</a></li>
<li><a
href="https://github.com/dsanders11"><code>@​dsanders11</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/ai-inference/pull/136">actions/ai-inference#136</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/ai-inference/compare/v2...v2.0.2">https://github.com/actions/ai-inference/compare/v2...v2.0.2</a></p>
<h2>v2.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Parse inference response format defensively by <a
href="https://github.com/sgoedecke"><code>@​sgoedecke</code></a> in <a
href="https://redirect.github.com/actions/ai-inference/pull/97">actions/ai-inference#97</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/ai-inference/compare/v2...v2.0.1">https://github.com/actions/ai-inference/compare/v2...v2.0.1</a></p>
<h2>v2.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/ai-inference/pull/79">actions/ai-inference#79</a></li>
<li>Pin two imported actions to a set sha by <a
href="https://github.com/garman"><code>@​garman</code></a> in <a
href="https://redirect.github.com/actions/ai-inference/pull/90">actions/ai-inference#90</a></li>
<li>Uses tmp library to ensure more secure tmp file creation by <a
href="https://github.com/JessRudder"><code>@​JessRudder</code></a> in <a
href="https://redirect.github.com/actions/ai-inference/pull/91">actions/ai-inference#91</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/ai-inference/pull/79">actions/ai-inference#79</a></li>
<li><a href="https://github.com/garman"><code>@​garman</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/ai-inference/pull/90">actions/ai-inference#90</a></li>
<li><a
href="https://github.com/JessRudder"><code>@​JessRudder</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/ai-inference/pull/91">actions/ai-inference#91</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/ai-inference/commit/334892bb203895caaed82ec52d23c1ed9385151e"><code>334892b</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/ai-inference/issues/142">#142</a>
from maartenvandiemen/feature/pass-toolsets</li>
<li><a
href="https://github.com/actions/ai-inference/commit/bbe0ccb2444c1e35852645a7e65bf1d83941efdd"><code>bbe0ccb</code></a>
Fix failing tests</li>
<li><a
href="https://github.com/actions/ai-inference/commit/ca3b99ea743cc306cdaa68d07f817e930927c489"><code>ca3b99e</code></a>
Undo changes in tests.</li>
<li><a
href="https://github.com/actions/ai-inference/commit/8a5d2ea4a1b866720d3acc8bbbccd159e6ca7c3a"><code>8a5d2ea</code></a>
Merge branch 'main' into feature/pass-toolsets</li>
<li><a
href="https://github.com/actions/ai-inference/commit/112739fb15acc0a01a0d9ffc4637a0f5ca7dcc50"><code>112739f</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/ai-inference/issues/139">#139</a>
from GulerSevil/patch-1</li>
<li><a
href="https://github.com/actions/ai-inference/commit/f95554969e9a265f90a0c97938d04a6f319db7f3"><code>f955549</code></a>
Merge branch 'main' into patch-1</li>
<li><a
href="https://github.com/actions/ai-inference/commit/9e60aa0a3f27cab0e62286d74d9c77b04b1d0596"><code>9e60aa0</code></a>
Lint fix</li>
<li><a
href="https://github.com/actions/ai-inference/commit/02c6cc30ae592ce65ee356387748dfc2fd5f7993"><code>02c6cc3</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/ai-inference/issues/150">#150</a>
from actions/sgoedecke/mock-inference-in-ci</li>
<li><a
href="https://github.com/actions/ai-inference/commit/18d468666d48874954c853d63c0d9091bdfeb3b2"><code>18d4686</code></a>
fix: keep response-file temp file for downstream steps</li>
<li><a
href="https://github.com/actions/ai-inference/commit/fd73d0264cd240d895c114657b06fd1b952b97b6"><code>fd73d02</code></a>
Mock inference in CI</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/ai-inference/compare/b81b2afb8390ee6839b494a404766bef6493c7d9...334892bb203895caaed82ec52d23c1ed9385151e">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/ai-inference&package-manager=github_actions&previous-version=1.2.8&new-version=2.0.4)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-12-22 15:21:27 +00:00
Balázs Szücs 22a1412242 [V2] feat(pdfa): add PDF/A-3b option with updated descriptions and translations (#5286)
# Description of Changes


This pull request adds support for the PDF/A-3b format in the PDF/A
conversion tool, updates the user interface to reflect this new option,
and enhances the translation strings to clarify the differences between
the PDF/A variants.

**PDF/A-3b support and UI update:**

* Added PDF/A-3b as an option in the PDF/A output format dropdown in
`ConvertToPdfaSettings.tsx`.
* Updated the PDF/A options note to mention that PDF/A-3b supports
embedded files, improving user guidance in both the UI and translation
strings.

**Localization improvements:**

* Added and updated relevant translation keys and values for PDF/A-3b
support and improved descriptions in both the TOML and JavaScript i18n
config files.


<img width="366" height="998" alt="image"
src="https://github.com/user-attachments/assets/c6db557a-2a9c-436d-bfb5-7360b33b0d36"
/>

Closes: #5173


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-22 15:17:47 +00:00
dependabot[bot] 0c3c0765a3 Bump logback from 1.5.21 to 1.5.22 (#5281)
Bumps `logback` from 1.5.21 to 1.5.22.
Updates `ch.qos.logback:logback-core` from 1.5.21 to 1.5.22
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/qos-ch/logback/releases">ch.qos.logback:logback-core's
releases</a>.</em></p>
<blockquote>
<h2>Logback 1.5.22</h2>
<p><strong>2025-12-11 Release of logback version 1.5.22</strong></p>
<p>• In order to prevent involuntary information leakage, Logback will
no longer output the value of a substituted variable, if the variable
name contains any of the case-insensitive strings &quot;password&quot;,
&quot;secret&quot; or &quot;confidential&quot;. This problem was
reported by Chintan Rohila in <a
href="https://redirect.github.com/qos-ch/logback/issues/986">issues/986</a>.</p>
<p>• Logback now takes the overridden <code>toString()</code> method of
<code>Throwable</code> subclasses into account when printing stack
traces. This issue was reported in <a
href="https://jira.qos.ch/browse/LOGBACK-543">LOGBACK-543</a> by Alvin
Chee, with a fix provided in <a
href="https://redirect.github.com/qos-ch/logback/pull/404">PR 404</a> by
Brett Kail.</p>
<p>• Instead of limit-counting guard, Logback now uses a tumbling-window
guard to rate limit internal error messages.</p>
<p>• A bit-wise identical binary of this version can be reproduced by
building from source code at commit
572379aabd2f672b49593e4020696c624541e5b0 associated with the tag
v_1.5.22. Release built using Java &quot;21&quot; 2023-10-17 LTS build
21.0.1.+12-LTS-29 under Linux Debian 11.6.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/qos-ch/logback/commit/572379aabd2f672b49593e4020696c624541e5b0"><code>572379a</code></a>
prepare release 1.5.22</li>
<li><a
href="https://github.com/qos-ch/logback/commit/39d17ea3b3381d08b181c300e27ca0713ffc20b8"><code>39d17ea</code></a>
fix status printing of variable substitution when the variable name
contains ...</li>
<li><a
href="https://github.com/qos-ch/logback/commit/75509a918665cc16a8d35ee4024be03e17c7147a"><code>75509a9</code></a>
fix PR 404, LOGBACK-543</li>
<li><a
href="https://github.com/qos-ch/logback/commit/8eb93569728ab33c50b963d42ea9fcd4269c502f"><code>8eb9356</code></a>
remove unused import</li>
<li><a
href="https://github.com/qos-ch/logback/commit/6131a3ad0af65a72df2e78d56424d9ac0fed8935"><code>6131a3a</code></a>
use a slightly more sophisticated guard for printing status
messages</li>
<li><a
href="https://github.com/qos-ch/logback/commit/9efca21c6e07feefa2a6ffb6b9b3807f357515e8"><code>9efca21</code></a>
add no-args constructor to support various serialization frameworks</li>
<li><a
href="https://github.com/qos-ch/logback/commit/1bea5804f8329a7e49a4197e34cc297ad46a597c"><code>1bea580</code></a>
minor comment edits</li>
<li><a
href="https://github.com/qos-ch/logback/commit/bd07fddf12b8b74d28d313a56e7357f6202d2449"><code>bd07fdd</code></a>
update angus, greenmail versions</li>
<li><a
href="https://github.com/qos-ch/logback/commit/aef993c64b4a7119f9e831fd4acaa7e470e267ca"><code>aef993c</code></a>
start work on 1.5.22-SNAPSHOT</li>
<li>See full diff in <a
href="https://github.com/qos-ch/logback/compare/v_1.5.21...v_1.5.22">compare
view</a></li>
</ul>
</details>
<br />

Updates `ch.qos.logback:logback-classic` from 1.5.21 to 1.5.22
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/qos-ch/logback/releases">ch.qos.logback:logback-classic's
releases</a>.</em></p>
<blockquote>
<h2>Logback 1.5.22</h2>
<p><strong>2025-12-11 Release of logback version 1.5.22</strong></p>
<p>• In order to prevent involuntary information leakage, Logback will
no longer output the value of a substituted variable, if the variable
name contains any of the case-insensitive strings &quot;password&quot;,
&quot;secret&quot; or &quot;confidential&quot;. This problem was
reported by Chintan Rohila in <a
href="https://redirect.github.com/qos-ch/logback/issues/986">issues/986</a>.</p>
<p>• Logback now takes the overridden <code>toString()</code> method of
<code>Throwable</code> subclasses into account when printing stack
traces. This issue was reported in <a
href="https://jira.qos.ch/browse/LOGBACK-543">LOGBACK-543</a> by Alvin
Chee, with a fix provided in <a
href="https://redirect.github.com/qos-ch/logback/pull/404">PR 404</a> by
Brett Kail.</p>
<p>• Instead of limit-counting guard, Logback now uses a tumbling-window
guard to rate limit internal error messages.</p>
<p>• A bit-wise identical binary of this version can be reproduced by
building from source code at commit
572379aabd2f672b49593e4020696c624541e5b0 associated with the tag
v_1.5.22. Release built using Java &quot;21&quot; 2023-10-17 LTS build
21.0.1.+12-LTS-29 under Linux Debian 11.6.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/qos-ch/logback/commit/572379aabd2f672b49593e4020696c624541e5b0"><code>572379a</code></a>
prepare release 1.5.22</li>
<li><a
href="https://github.com/qos-ch/logback/commit/39d17ea3b3381d08b181c300e27ca0713ffc20b8"><code>39d17ea</code></a>
fix status printing of variable substitution when the variable name
contains ...</li>
<li><a
href="https://github.com/qos-ch/logback/commit/75509a918665cc16a8d35ee4024be03e17c7147a"><code>75509a9</code></a>
fix PR 404, LOGBACK-543</li>
<li><a
href="https://github.com/qos-ch/logback/commit/8eb93569728ab33c50b963d42ea9fcd4269c502f"><code>8eb9356</code></a>
remove unused import</li>
<li><a
href="https://github.com/qos-ch/logback/commit/6131a3ad0af65a72df2e78d56424d9ac0fed8935"><code>6131a3a</code></a>
use a slightly more sophisticated guard for printing status
messages</li>
<li><a
href="https://github.com/qos-ch/logback/commit/9efca21c6e07feefa2a6ffb6b9b3807f357515e8"><code>9efca21</code></a>
add no-args constructor to support various serialization frameworks</li>
<li><a
href="https://github.com/qos-ch/logback/commit/1bea5804f8329a7e49a4197e34cc297ad46a597c"><code>1bea580</code></a>
minor comment edits</li>
<li><a
href="https://github.com/qos-ch/logback/commit/bd07fddf12b8b74d28d313a56e7357f6202d2449"><code>bd07fdd</code></a>
update angus, greenmail versions</li>
<li><a
href="https://github.com/qos-ch/logback/commit/aef993c64b4a7119f9e831fd4acaa7e470e267ca"><code>aef993c</code></a>
start work on 1.5.22-SNAPSHOT</li>
<li>See full diff in <a
href="https://github.com/qos-ch/logback/compare/v_1.5.21...v_1.5.22">compare
view</a></li>
</ul>
</details>
<br />


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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-12-22 15:15:40 +00:00
+17 68ed54e398 V1 merge (#5193)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
Co-authored-by: OUNZAR Aymane <aymane.ounzar@imt-atlantique.net>
Co-authored-by: YAOU Reda <yaoureda24@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: Balázs Szücs <127139797+balazs-szucs@users.noreply.github.com>
Co-authored-by: Ludy <Ludy87@users.noreply.github.com>
Co-authored-by: tkymmm <136296842+tkymmm@users.noreply.github.com>
Co-authored-by: Peter Dave Hello <hsu@peterdavehello.org>
Co-authored-by: albanobattistella <34811668+albanobattistella@users.noreply.github.com>
Co-authored-by: PingLin8888 <88387490+PingLin8888@users.noreply.github.com>
Co-authored-by: FdaSilvaYY <FdaSilvaYY@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: OteJlo <106060728+OteJlo@users.noreply.github.com>
Co-authored-by: Angel <41905618+TheShadowAngel@users.noreply.github.com>
Co-authored-by: Ricardo Catarino <ricardomicc@gmail.com>
Co-authored-by: Luis Antonio Argüelles González <luis.arguelles@encora.com>
Co-authored-by: Dawid Urbański <31166488+urbaned121@users.noreply.github.com>
Co-authored-by: Stephan Paternotte <Stephan-P@users.noreply.github.com>
Co-authored-by: Leonardo Santos Paulucio <leonardo.paulucio@hotmail.com>
Co-authored-by: hamza khalem <72972114+hamzakhalem@users.noreply.github.com>
Co-authored-by: IT Creativity + Art Team <admin@it-playground.net>
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
Co-authored-by: James Brunton <jbrunton96@gmail.com>
Co-authored-by: Victor Villarreal <133383186+vvillarreal-cfee@users.noreply.github.com>
2025-12-21 10:40:32 +00:00
Anthony Stirling a5dcdd5bd9 Revert "Sync provider settings with settings.yml in config UI" (#5271)
Reverts Stirling-Tools/Stirling-PDF#5270
2025-12-20 22:21:45 +00:00
Ludy 5eafbf9e51 Sync provider settings with settings.yml in config UI (#5270)
# Description of Changes

This pull request improves the handling of provider settings in the
`ProviderCard` component to ensure settings are always in sync with the
latest values from `settings.yml`, and enhances how default values are
initialized for provider fields. It also updates the usage of
`ProviderCard` in the admin connections section to explicitly pass
provider settings.

**Provider settings synchronization and initialization:**

* Added a `useEffect` in `ProviderCard` to keep `localSettings` in sync
with incoming `settings` prop, ensuring that changes from `settings.yml`
are reflected in the UI.
* Improved default value initialization in `ProviderCard`: when opening
an unconfigured provider, it now merges defaults with any existing
values from `settings.yml`, rather than overwriting them.

**Integration with AdminConnectionsSection:**

* Updated `AdminConnectionsSection` to explicitly pass provider settings
to `ProviderCard` via the `settings` prop, ensuring the component always
receives the correct configuration.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-20 22:08:35 +00:00
Dario Ghunney Ware a1317f0e01 Bumping version (#5268) 2025-12-18 20:43:10 +00:00
Reece Browne 3529849bca Feature/annotations (#5260) 2025-12-18 15:47:54 +00:00
Stephan Paternotte 49bea34576 Update translation.toml (#5254)
Updated translations of latest additions,
Saintized lots of accented letters,
Brought consistency in formal addressing of the user
2025-12-17 13:37:47 +00:00
Anthony StirlingandConnorYoh f9a44c4da4 Saml fixes (#5256)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
2025-12-17 10:52:48 +00:00
Anthony Stirling 4ec75d4d8c allow static overrides (#5258)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-17 10:07:51 +00:00
Reece Browne 93ed05b054 Only allow dual page view when there is more than one page, update to… (#5246) 2025-12-16 16:02:22 +00:00
Reece BrowneandClaude Sonnet 4.5 195b1472e4 Bug/v2/viewer annotations (#5245)
Show uneditable annotations on viewer
show editable annotations layer when in annotation tools (sign, add
image, add text)
Remove draw tool from viewer (this is replaced wholesale in an upcoming
PR so it wasn't worth doing the work to ensure it worked with the new
annotation layer set up_)
refactoring work, mostly renaming variables we can use for all
annotation based tools that had sign specific names.
remove "tools" tooltip

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 15:52:47 +00:00
James Brunton 340006ceea Add Sign Up functionality to desktop app (#5244)
# Description of Changes
Adds Sign Up with email to desktop app. SSO sign up will come in a
future PR.
2025-12-16 14:55:53 +00:00
Anthony Stirling d80e627899 Cache fix issues V2 (#5237)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-15 23:54:25 +00:00
Anthony Stirling 336ec34125 V2 Handle SSO account restrictions in account settings (#5225)
## Summary
- hide password and username update controls for SSO accounts and show a
managed-account notice
- prevent account update handlers from calling APIs when the user
authenticates via SSO
- expose authenticationType on the user session model and add
translations for new SSO messaging

## Testing
- not run (not requested)


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_693ae8144148832888ecf128e66cd3ca)
2025-12-15 21:55:58 +00:00
687 changed files with 79996 additions and 16569 deletions
+5 -8
View File
@@ -2,20 +2,17 @@
* @Frooodle @Ludy87 @jbrunton96 @ConnorYoh
# Backend
/app/** @DarioGii @Frooodle @Ludy87 @jbrunton96 @ConnorYoh
#V1 frontend
/app/core/src/main/resources/static/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87
/app/core/src/main/resources/templates/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87
/app/** @DarioGii @Frooodle @Ludy87 @jbrunton96 @ConnorYoh @balazs-szucs
#V2 frontend
/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle
/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @balazs-szucs
/app/core/src/main/resources/static/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87 @balazs-szucs
#V2 docker
/docker/backend/** @Frooodle @Ludy87 @DarioGii @Ludy87
/docker/backend/** @Frooodle @Ludy87 @DarioGii
/docker/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87
/docker/compose/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 @Frooodle @Ludy87
#GHA (All users)
/.github/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 @Frooodle @Ludy87
/.github/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 @Frooodle @Ludy87 @balazs-szucs
+37 -15
View File
@@ -2,26 +2,28 @@ build: &build
- build.gradle
- app/(common|core|proprietary)/build.gradle
app: &app
- app/(common|core|proprietary)/src/main/java/**
openapi: &openapi
- build.gradle
- app/(common|core|proprietary)/build.gradle
- *build
- app/(common|core|proprietary)/src/main/java/**
project: &project
- app/(common|core|proprietary)/src/(main|test)/java/**
- app/(common|core|proprietary)/build.gradle
- 'app/(common|core|proprietary)/src/(main|test)/resources/**/!(messages_*.properties|*.md)*'
- exampleYmlFiles/**
- gradle/**
- libs/**
- testing/**
- build.gradle
docker: &docker
- Dockerfile
- Dockerfile.fat
- Dockerfile.ultra-lite
- ".github/workflows/build.yml"
- scripts/init.sh
- scripts/init-without-ocr.sh
- exampleYmlFiles/**
project: &project
- app/(common|core|proprietary)/src/(main|test)/java/**
- *build
- "app/(common|core|proprietary)/src/(main|test)/resources/**/!(messages_*.properties|*.md)*"
- exampleYmlFiles/**
- gradle/**
- libs/**
- "testing/**/!(requirements*.txt|requirements*.in)*"
- *docker
- gradle.properties
- gradlew
- gradlew.bat
@@ -29,10 +31,30 @@ project: &project
- settings.gradle
- frontend/**
- docker/**
- testing/**
- scripts/RestartHelper.java
frontend: &frontend
- frontend/**
- .github/workflows/testdriver.yml
- testing/**
- docker/**
- scripts/translations/*.py
- scripts/build-tauri-jlink.bat
- scripts/build-tauri-jlink.sh
- scripts/convert_cff_to_ttf.py
- scripts/harvest_type3_fonts.py
- scripts/ignore_translation.toml
- scripts/index_type3_catalogue.py
- scripts/summarize_type3_signatures.py
- scripts/type3_to_cff.py
- scripts/update_type3_library.py
licenses-frontend: &licenses-frontend
- ".github/workflows/frontend-backend-licenses-update.yml"
- "frontend/package.json"
- "frontend/package-lock.json"
- "frontend/scripts/generate-licenses.js"
licenses-backend: &licenses-backend
- ".github/workflows/frontend-backend-licenses-update.yml"
- *build
+1 -1
View File
@@ -1 +1 @@
allow-ghsas: GHSA-wrw7-89jp-8q8g
allow-ghsas: GHSA-wrw7-89jp-8q8g
+8 -1
View File
@@ -1,4 +1,9 @@
{
"label_changer": [
"Frooodle",
"Ludy87",
"balazs-szucs"
],
"repo_devs": [
"Frooodle",
"sf298",
@@ -8,6 +13,8 @@
"reecebrowne",
"DarioGii",
"ConnorYoh",
"EthanHealy01"
"EthanHealy01",
"jbrunton96",
"balazs-szucs"
]
}
+49 -2
View File
@@ -6,18 +6,65 @@
version: 2
updates:
- package-ecosystem: "gradle" # See documentation for possible values
directory: "/" # Location of package manifests
directories:
- "/" # Location of package manifests
- "/app/common"
- "/app/core"
- "/app/proprietary"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
rebase-strategy: "auto"
- package-ecosystem: "docker"
directory: "/" # Location of Dockerfile
schedule:
interval: "weekly"
rebase-strategy: "auto"
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
rebase-strategy: "auto"
- package-ecosystem: npm
directory: /devTools
schedule:
interval: "weekly"
rebase-strategy: "auto"
- package-ecosystem: docker
directory: /docker/backend
schedule:
interval: "weekly"
rebase-strategy: "auto"
- package-ecosystem: docker
directory: /docker/embedded
schedule:
interval: "weekly"
rebase-strategy: "auto"
- package-ecosystem: docker
directory: /docker/frontend
schedule:
interval: "weekly"
rebase-strategy: "auto"
- package-ecosystem: npm
directory: /frontend
schedule:
interval: "weekly"
rebase-strategy: "auto"
- package-ecosystem: cargo
directory: /frontend/src-tauri
schedule:
interval: "weekly"
rebase-strategy: "auto"
- package-ecosystem: pip
directory: /testing/cucumber
schedule:
interval: "weekly"
rebase-strategy: "auto"
+15 -7
View File
@@ -46,26 +46,34 @@ labels:
- label: 'API'
title: '.*openapi.*|.*swagger.*|.*api.*'
- label: 'v2'
base-branch: 'V2'
- label: 'v3'
base-branch: 'V3'
- label: 'Translation'
files:
- 'app/core/src/main/resources/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}.properties'
- 'frontend/public/locales/[a-zA-Z]{2}-[a-zA-Z\-]{2,7}/translation.toml'
- 'scripts/ignore_translation.toml'
- 'app/core/src/main/resources/templates/fragments/languages.html'
- '.github/scripts/check_language_properties.py'
- 'scripts/remove_translation_keys.sh'
- 'scripts/replace_translation_line.sh'
- 'scripts/translations/.*'
- '.github/scripts/check_language_toml.py'
- 'scripts/counter_translation_v3.py'
- label: 'Front End'
files:
- 'app/core/src/main/resources/templates/.*'
- 'app/proprietary/src/main/resources/templates/.*'
- 'app/core/src/main/resources/static/.*'
- 'app/proprietary/src/main/resources/static/.*'
- 'app/core/src/main/java/stirling/software/SPDF/controller/web/.*'
- 'app/core/src/main/java/stirling/software/SPDF/UI/.*'
- 'app/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/.*'
- 'frontend/**'
- 'frontend/.*'
- 'frontend/**/.*'
- label: 'Tauri'
files:
- 'frontend/src-tauri/**'
- 'frontend/src-tauri/.*'
- label: 'Java'
files:
+17
View File
@@ -84,6 +84,9 @@
- name: "v2"
color: "FFFF00"
description: "Issues or pull requests related to the v2 branch"
- name: "v3"
color: "FFA500"
description: "Issues or pull requests related to the v3 branch"
- name: "wontfix"
description: "This will not be worked on"
color: "FFFFFF"
@@ -184,3 +187,17 @@
- name: "codex"
color: "ededed"
description: "chatgpt AI generated code"
- name: "break-change"
color: "FF0000"
description: "This PR introduces a breaking API change."
- name: "Rust"
color: "DEA584"
description: "Pull requests that update Rust code"
from_name: "rust"
- name: "Tauri"
color: "24C8FF"
description: "Pull requests that update Tauri code"
from_name: "tauri"
- name: "license-review-required"
color: "EDEDED"
description: "This PR requires a license review"
+4
View File
@@ -27,6 +27,10 @@ Closes #(issue_number)
- [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed)
- [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only)
### Translations (if applicable)
- [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)
### UI Changes (if applicable)
- [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR)
+4
View File
@@ -1,5 +1,9 @@
changelog:
categories:
- title: Breaking Changes
labels:
- break-change
- title: Bug Fixes
labels:
- Bug
+57 -38
View File
@@ -11,15 +11,16 @@ adjusting the format.
Usage:
python check_language_toml.py --reference-file <path_to_reference_file> --branch <branch_name> [--actor <actor_name>] [--files <list_of_changed_files>]
"""
# Sample for Windows:
# python .github/scripts/check_language_toml.py --reference-file frontend/public/locales/en-GB/translation.toml --branch "" --files frontend/public/locales/de-DE/translation.toml frontend/public/locales/fr-FR/translation.toml
import copy
import argparse
import glob
import os
import argparse
import re
import json
from pathlib import Path
import tomllib # Python 3.11+ (stdlib)
import tomli_w # For writing TOML files
@@ -38,7 +39,8 @@ def find_duplicate_keys(file_path, keys=None, prefix=""):
duplicates = []
# Load TOML file
with open(file_path, 'rb') as file:
file_path = Path(file_path)
with file_path.open("rb") as file:
data = tomllib.load(file)
def process_dict(obj, current_prefix=""):
@@ -57,8 +59,8 @@ def find_duplicate_keys(file_path, keys=None, prefix=""):
return duplicates
# Maximum size for TOML files (e.g., 500 KB)
MAX_FILE_SIZE = 500 * 1024
# Maximum size for TOML files (e.g., 570 KB)
MAX_FILE_SIZE = 570 * 1024
def parse_toml_file(file_path):
@@ -67,7 +69,8 @@ def parse_toml_file(file_path):
:param file_path: Path to the TOML file.
:return: Dictionary with flattened keys.
"""
with open(file_path, 'rb') as file:
file_path = Path(file_path)
with file_path.open("rb") as file:
data = tomllib.load(file)
def flatten_dict(d, parent_key="", sep="."):
@@ -110,7 +113,8 @@ def write_toml_file(file_path, updated_properties):
"""
nested_data = unflatten_dict(updated_properties)
with open(file_path, "wb") as file:
file_path = Path(file_path)
with file_path.open("wb") as file:
tomli_w.dump(nested_data, file)
@@ -121,18 +125,23 @@ def update_missing_keys(reference_file, file_list, branch=""):
:param file_list: List of translation files to update.
:param branch: Branch where the files are located.
"""
reference_file = Path(reference_file)
reference_properties = parse_toml_file(reference_file)
branch_path = Path(branch) if branch else Path()
for file_path in file_list:
basename_current_file = os.path.basename(os.path.join(branch, file_path))
file_path = Path(file_path)
language_dir = file_path.parent.name
reference_lang_dir = reference_file.parent.name
if (
basename_current_file == os.path.basename(reference_file)
or not file_path.endswith(".toml")
or not os.path.dirname(file_path).endswith("locales")
language_dir == reference_lang_dir
or file_path.suffix != ".toml"
or file_path.parents[1].name != "locales"
):
print(f"Skipping file: {file_path}")
continue
current_properties = parse_toml_file(os.path.join(branch, file_path))
current_properties = parse_toml_file(branch_path / file_path)
updated_properties = {}
for ref_key, ref_value in reference_properties.items():
@@ -143,7 +152,7 @@ def update_missing_keys(reference_file, file_list, branch=""):
# Add missing key with reference value
updated_properties[ref_key] = ref_value
write_toml_file(os.path.join(branch, file_path), updated_properties)
write_toml_file(branch_path / file_path, updated_properties)
def check_for_missing_keys(reference_file, file_list, branch):
@@ -151,14 +160,17 @@ def check_for_missing_keys(reference_file, file_list, branch):
def read_toml_keys(file_path):
if os.path.isfile(file_path) and os.path.exists(file_path):
file_path = Path(file_path)
if file_path.is_file():
return parse_toml_file(file_path)
return {}
def check_for_differences(reference_file, file_list, branch, actor):
reference_branch = branch
basename_reference_file = os.path.basename(reference_file)
reference_file = Path(reference_file)
basename_reference_file = reference_file.name
branch_path = Path(branch) if branch else Path()
report = []
report.append(f"#### 🔄 Reference Branch: `{reference_branch}`")
@@ -172,39 +184,44 @@ def check_for_differences(reference_file, file_list, branch, actor):
if len(file_list) == 1:
file_arr = file_list[0].split()
base_dir = os.path.abspath(
os.path.join(os.getcwd(), "frontend", "public", "locales")
)
base_dir = Path.cwd() / "frontend" / "public" / "locales"
for file_path in file_arr:
file_normpath = os.path.normpath(file_path)
absolute_path = os.path.abspath(file_normpath)
file_path = Path(file_path)
file_normpath = file_path
absolute_path = file_normpath.resolve()
basename_current_file = (branch_path / file_normpath).name
locale_dir = file_normpath.parent.name
report.append(f"#### 📃 **File Check:** `{locale_dir}/{basename_current_file}`")
# Verify that file is within the expected directory
if not absolute_path.startswith(base_dir):
raise ValueError(f"Unsafe file found: {file_normpath}")
if not absolute_path.is_relative_to(base_dir):
has_differences = True
report.append(
f"\n⚠️ Unsafe file found: `{locale_dir}/{basename_current_file}`\n\n---\n"
)
continue
# Verify file size before processing
if os.path.getsize(os.path.join(branch, file_normpath)) > MAX_FILE_SIZE:
raise ValueError(
f"The file {file_normpath} is too large and could pose a security risk."
if (branch_path / file_normpath).stat().st_size > MAX_FILE_SIZE:
has_differences = True
report.append(
f"\n⚠️ The file `{locale_dir}/{basename_current_file}` is too large and could pose a security risk.\n\n---\n"
)
continue
basename_current_file = os.path.basename(os.path.join(branch, file_normpath))
locale_dir = os.path.basename(os.path.dirname(file_normpath))
if basename_current_file == basename_reference_file and locale_dir == "en-GB":
continue
if (
basename_current_file == basename_reference_file
and locale_dir == "en-GB"
file_normpath.suffix != ".toml"
or basename_current_file != "translation.toml"
):
continue
if not file_normpath.endswith(".toml") or basename_current_file != "translation.toml":
continue
only_reference_file = False
report.append(f"#### 📃 **File Check:** `{locale_dir}/{basename_current_file}`")
current_keys = read_toml_keys(os.path.join(branch, file_path))
current_keys = read_toml_keys(branch_path / file_path)
reference_key_count = len(reference_keys)
current_key_count = len(current_keys)
@@ -249,13 +266,13 @@ def check_for_differences(reference_file, file_list, branch, actor):
else:
report.append("2. **Test Status:** ✅ **_Passed_**")
if find_duplicate_keys(os.path.join(branch, file_normpath)):
if find_duplicate_keys(branch_path / file_normpath):
has_differences = True
output = "\n".join(
[
f" - `{key}`: first at {first}, duplicate at `{duplicate}`"
for key, first, duplicate in find_duplicate_keys(
os.path.join(branch, file_normpath)
branch_path / file_normpath
)
]
)
@@ -288,7 +305,9 @@ def check_for_differences(reference_file, file_list, branch, actor):
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Find missing keys in TOML translation files")
parser = argparse.ArgumentParser(
description="Find missing keys in TOML translation files"
)
parser.add_argument(
"--actor",
required=False,
+1
View File
@@ -6,3 +6,4 @@ pillow
unoserver
opencv-python-headless
pre-commit
brotli @ git+https://github.com/google/brotli.git@028fb5a23661f123017c060daa546b55cf4bde29
+338 -420
View File
@@ -4,201 +4,98 @@
#
# pip-compile --allow-unsafe --generate-hashes --output-file='.github\scripts\requirements_dev.txt' --strip-extras '.github\scripts\requirements_dev.in'
#
brotli==1.1.0 \
--hash=sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208 \
--hash=sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48 \
--hash=sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354 \
--hash=sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419 \
--hash=sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a \
--hash=sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128 \
--hash=sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c \
--hash=sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088 \
--hash=sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9 \
--hash=sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a \
--hash=sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3 \
--hash=sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757 \
--hash=sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2 \
--hash=sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438 \
--hash=sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578 \
--hash=sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b \
--hash=sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b \
--hash=sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68 \
--hash=sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0 \
--hash=sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d \
--hash=sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943 \
--hash=sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd \
--hash=sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409 \
--hash=sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28 \
--hash=sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da \
--hash=sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50 \
--hash=sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f \
--hash=sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0 \
--hash=sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547 \
--hash=sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180 \
--hash=sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0 \
--hash=sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d \
--hash=sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a \
--hash=sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb \
--hash=sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112 \
--hash=sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc \
--hash=sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2 \
--hash=sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265 \
--hash=sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327 \
--hash=sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95 \
--hash=sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec \
--hash=sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd \
--hash=sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c \
--hash=sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38 \
--hash=sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914 \
--hash=sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0 \
--hash=sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a \
--hash=sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7 \
--hash=sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368 \
--hash=sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c \
--hash=sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0 \
--hash=sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f \
--hash=sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451 \
--hash=sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f \
--hash=sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8 \
--hash=sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e \
--hash=sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248 \
--hash=sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c \
--hash=sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91 \
--hash=sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724 \
--hash=sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7 \
--hash=sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966 \
--hash=sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9 \
--hash=sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97 \
--hash=sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d \
--hash=sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5 \
--hash=sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf \
--hash=sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac \
--hash=sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b \
--hash=sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951 \
--hash=sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74 \
--hash=sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648 \
--hash=sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60 \
--hash=sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c \
--hash=sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1 \
--hash=sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8 \
--hash=sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d \
--hash=sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc \
--hash=sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61 \
--hash=sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460 \
--hash=sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751 \
--hash=sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9 \
--hash=sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2 \
--hash=sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0 \
--hash=sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1 \
--hash=sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474 \
--hash=sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75 \
--hash=sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5 \
--hash=sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f \
--hash=sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2 \
--hash=sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f \
--hash=sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb \
--hash=sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6 \
--hash=sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9 \
--hash=sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111 \
--hash=sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2 \
--hash=sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01 \
--hash=sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467 \
--hash=sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619 \
--hash=sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf \
--hash=sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408 \
--hash=sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579 \
--hash=sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84 \
--hash=sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7 \
--hash=sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c \
--hash=sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284 \
--hash=sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52 \
--hash=sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b \
--hash=sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59 \
--hash=sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752 \
--hash=sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1 \
--hash=sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80 \
--hash=sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839 \
--hash=sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0 \
--hash=sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2 \
--hash=sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3 \
--hash=sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64 \
--hash=sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089 \
--hash=sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643 \
--hash=sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b \
--hash=sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e \
--hash=sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985 \
--hash=sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596 \
--hash=sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2 \
--hash=sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064
# via fonttools
cffi==1.17.1 \
--hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \
--hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \
--hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \
--hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \
--hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \
--hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \
--hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \
--hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \
--hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \
--hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \
--hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \
--hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \
--hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \
--hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \
--hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \
--hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \
--hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \
--hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \
--hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \
--hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \
--hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \
--hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \
--hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \
--hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \
--hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \
--hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \
--hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \
--hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \
--hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \
--hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \
--hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \
--hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \
--hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \
--hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \
--hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \
--hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \
--hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \
--hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \
--hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \
--hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \
--hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \
--hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \
--hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \
--hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \
--hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \
--hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \
--hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \
--hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \
--hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \
--hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \
--hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \
--hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \
--hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \
--hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \
--hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \
--hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \
--hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \
--hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \
--hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \
--hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \
--hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \
--hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \
--hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \
--hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \
--hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \
--hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \
--hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b
# WARNING: pip install will require the following package to be hashed.
# Consider using a hashable URL like https://github.com/jazzband/pip-tools/archive/SOMECOMMIT.zip
# CVE-2025-6176 mitigation: pin brotli to a specific commit
brotli @ git+https://github.com/google/brotli.git@028fb5a23661f123017c060daa546b55cf4bde29
# via
# -r .github/scripts/requirements_dev.in
# fonttools
cffi==2.0.0 \
--hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \
--hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \
--hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \
--hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \
--hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \
--hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \
--hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \
--hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \
--hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \
--hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \
--hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \
--hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \
--hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \
--hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \
--hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \
--hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \
--hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \
--hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \
--hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \
--hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \
--hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \
--hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \
--hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \
--hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \
--hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \
--hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \
--hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \
--hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \
--hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \
--hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \
--hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \
--hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \
--hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \
--hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \
--hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \
--hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \
--hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \
--hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \
--hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \
--hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \
--hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \
--hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \
--hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \
--hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \
--hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \
--hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \
--hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \
--hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \
--hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \
--hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \
--hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \
--hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \
--hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \
--hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \
--hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \
--hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \
--hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \
--hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \
--hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \
--hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \
--hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \
--hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \
--hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \
--hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \
--hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \
--hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \
--hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \
--hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \
--hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \
--hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \
--hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \
--hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \
--hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \
--hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \
--hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \
--hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \
--hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \
--hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \
--hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \
--hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \
--hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \
--hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \
--hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \
--hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf
# via weasyprint
cfgv==3.4.0 \
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
@@ -212,57 +109,73 @@ distlib==0.4.0 \
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
# via virtualenv
filelock==3.18.0 \
--hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \
--hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de
filelock==3.20.0 \
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
# via virtualenv
fonttools==4.59.0 \
--hash=sha256:052444a5d0151878e87e3e512a1aa1a0ab35ee4c28afde0a778e23b0ace4a7de \
--hash=sha256:169b99a2553a227f7b5fea8d9ecd673aa258617f466b2abc6091fe4512a0dcd0 \
--hash=sha256:209b75943d158f610b78320eacb5539aa9e920bee2c775445b2846c65d20e19d \
--hash=sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df \
--hash=sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d \
--hash=sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe \
--hash=sha256:2e7cf8044ce2598bb87e44ba1d2c6e45d7a8decf56055b92906dc53f67c76d64 \
--hash=sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e \
--hash=sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01 \
--hash=sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705 \
--hash=sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c \
--hash=sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2 \
--hash=sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b \
--hash=sha256:4c908a7036f0f3677f8afa577bcd973e3e20ddd2f7c42a33208d18bee95cdb6f \
--hash=sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97 \
--hash=sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96 \
--hash=sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2 \
--hash=sha256:60f6665579e909b618282f3c14fa0b80570fbf1ee0e67678b9a9d43aa5d67a37 \
--hash=sha256:62224a9bb85b4b66d1b46d45cbe43d71cbf8f527d332b177e3b96191ffbc1e64 \
--hash=sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757 \
--hash=sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e \
--hash=sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3 \
--hash=sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2 \
--hash=sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c \
--hash=sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0 \
--hash=sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1 \
--hash=sha256:8b4309a2775e4feee7356e63b163969a215d663399cce1b3d3b65e7ec2d9680e \
--hash=sha256:8d77f92438daeaddc05682f0f3dac90c5b9829bcac75b57e8ce09cb67786073c \
--hash=sha256:902425f5afe28572d65d2bf9c33edd5265c612ff82c69e6f83ea13eafc0dcbea \
--hash=sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5 \
--hash=sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6 \
--hash=sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c \
--hash=sha256:b818db35879d2edf7f46c7e729c700a0bce03b61b9412f5a7118406687cb151d \
--hash=sha256:b8974b2a266b54c96709bd5e239979cddfd2dbceed331aa567ea1d7c4a2202db \
--hash=sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14 \
--hash=sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38 \
--hash=sha256:d40dcf533ca481355aa7b682e9e079f766f35715defa4929aeb5597f9604272e \
--hash=sha256:e93df708c69a193fc7987192f94df250f83f3851fda49413f02ba5dded639482 \
--hash=sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4 \
--hash=sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b \
--hash=sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464 \
--hash=sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b
fonttools==4.60.1 \
--hash=sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c \
--hash=sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc \
--hash=sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a \
--hash=sha256:0eae96373e4b7c9e45d099d7a523444e3554360927225c1cdae221a58a45b856 \
--hash=sha256:122e1a8ada290423c493491d002f622b1992b1ab0b488c68e31c413390dc7eb2 \
--hash=sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259 \
--hash=sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c \
--hash=sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce \
--hash=sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003 \
--hash=sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272 \
--hash=sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77 \
--hash=sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038 \
--hash=sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea \
--hash=sha256:2ee06fc57512144d8b0445194c2da9f190f61ad51e230f14836286470c99f854 \
--hash=sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2 \
--hash=sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258 \
--hash=sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652 \
--hash=sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08 \
--hash=sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99 \
--hash=sha256:596ecaca36367027d525b3b426d8a8208169d09edcf8c7506aceb3a38bfb55c7 \
--hash=sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914 \
--hash=sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6 \
--hash=sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed \
--hash=sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb \
--hash=sha256:7473a8ed9ed09aeaa191301244a5a9dbe46fe0bf54f9d6cd21d83044c3321217 \
--hash=sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc \
--hash=sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f \
--hash=sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c \
--hash=sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877 \
--hash=sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801 \
--hash=sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85 \
--hash=sha256:8b4eb332f9501cb1cd3d4d099374a1e1306783ff95489a1026bde9eb02ccc34a \
--hash=sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb \
--hash=sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383 \
--hash=sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401 \
--hash=sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28 \
--hash=sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01 \
--hash=sha256:a140761c4ff63d0cb9256ac752f230460ee225ccef4ad8f68affc723c88e2036 \
--hash=sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc \
--hash=sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac \
--hash=sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903 \
--hash=sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3 \
--hash=sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6 \
--hash=sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c \
--hash=sha256:b42d86938e8dda1cd9a1a87a6d82f1818eaf933348429653559a458d027446da \
--hash=sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299 \
--hash=sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15 \
--hash=sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199 \
--hash=sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf \
--hash=sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1 \
--hash=sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537 \
--hash=sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d \
--hash=sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c \
--hash=sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4 \
--hash=sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9 \
--hash=sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed \
--hash=sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987 \
--hash=sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa
# via weasyprint
identify==2.6.13 \
--hash=sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b \
--hash=sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32
identify==2.6.15 \
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
--hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf
# via pre-commit
nodeenv==1.9.1 \
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
@@ -338,128 +251,113 @@ pdf2image==1.17.0 \
--hash=sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57 \
--hash=sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2
# via -r .github/scripts/requirements_dev.in
pillow==11.3.0 \
--hash=sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2 \
--hash=sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214 \
--hash=sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e \
--hash=sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59 \
--hash=sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50 \
--hash=sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632 \
--hash=sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06 \
--hash=sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a \
--hash=sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51 \
--hash=sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced \
--hash=sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f \
--hash=sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12 \
--hash=sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8 \
--hash=sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6 \
--hash=sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580 \
--hash=sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f \
--hash=sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac \
--hash=sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860 \
--hash=sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd \
--hash=sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722 \
--hash=sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8 \
--hash=sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4 \
--hash=sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673 \
--hash=sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788 \
--hash=sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542 \
--hash=sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e \
--hash=sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd \
--hash=sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8 \
--hash=sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523 \
--hash=sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967 \
--hash=sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809 \
--hash=sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477 \
--hash=sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027 \
--hash=sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae \
--hash=sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b \
--hash=sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c \
--hash=sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f \
--hash=sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e \
--hash=sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b \
--hash=sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7 \
--hash=sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27 \
--hash=sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361 \
--hash=sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae \
--hash=sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d \
--hash=sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc \
--hash=sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58 \
--hash=sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad \
--hash=sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6 \
--hash=sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024 \
--hash=sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978 \
--hash=sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb \
--hash=sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d \
--hash=sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0 \
--hash=sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9 \
--hash=sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f \
--hash=sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874 \
--hash=sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa \
--hash=sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081 \
--hash=sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149 \
--hash=sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6 \
--hash=sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d \
--hash=sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd \
--hash=sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f \
--hash=sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c \
--hash=sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31 \
--hash=sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e \
--hash=sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db \
--hash=sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6 \
--hash=sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f \
--hash=sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494 \
--hash=sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69 \
--hash=sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94 \
--hash=sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77 \
--hash=sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d \
--hash=sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7 \
--hash=sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a \
--hash=sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438 \
--hash=sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288 \
--hash=sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b \
--hash=sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635 \
--hash=sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3 \
--hash=sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d \
--hash=sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe \
--hash=sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0 \
--hash=sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe \
--hash=sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a \
--hash=sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805 \
--hash=sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8 \
--hash=sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36 \
--hash=sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a \
--hash=sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b \
--hash=sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e \
--hash=sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25 \
--hash=sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12 \
--hash=sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada \
--hash=sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c \
--hash=sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71 \
--hash=sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d \
--hash=sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c \
--hash=sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6 \
--hash=sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1 \
--hash=sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50 \
--hash=sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653 \
--hash=sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c \
--hash=sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4 \
--hash=sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3
pillow==12.0.0 \
--hash=sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643 \
--hash=sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e \
--hash=sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e \
--hash=sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc \
--hash=sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642 \
--hash=sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6 \
--hash=sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1 \
--hash=sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b \
--hash=sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399 \
--hash=sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba \
--hash=sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad \
--hash=sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47 \
--hash=sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739 \
--hash=sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b \
--hash=sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f \
--hash=sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10 \
--hash=sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52 \
--hash=sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d \
--hash=sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b \
--hash=sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a \
--hash=sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9 \
--hash=sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d \
--hash=sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098 \
--hash=sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905 \
--hash=sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b \
--hash=sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3 \
--hash=sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371 \
--hash=sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953 \
--hash=sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01 \
--hash=sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca \
--hash=sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e \
--hash=sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7 \
--hash=sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27 \
--hash=sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082 \
--hash=sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e \
--hash=sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d \
--hash=sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8 \
--hash=sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a \
--hash=sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad \
--hash=sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3 \
--hash=sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a \
--hash=sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d \
--hash=sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353 \
--hash=sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee \
--hash=sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b \
--hash=sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b \
--hash=sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a \
--hash=sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7 \
--hash=sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef \
--hash=sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a \
--hash=sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a \
--hash=sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257 \
--hash=sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07 \
--hash=sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4 \
--hash=sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c \
--hash=sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c \
--hash=sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4 \
--hash=sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe \
--hash=sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8 \
--hash=sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5 \
--hash=sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6 \
--hash=sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e \
--hash=sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8 \
--hash=sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e \
--hash=sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275 \
--hash=sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3 \
--hash=sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76 \
--hash=sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227 \
--hash=sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9 \
--hash=sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5 \
--hash=sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79 \
--hash=sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca \
--hash=sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa \
--hash=sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b \
--hash=sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e \
--hash=sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197 \
--hash=sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab \
--hash=sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79 \
--hash=sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2 \
--hash=sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363 \
--hash=sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0 \
--hash=sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e \
--hash=sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782 \
--hash=sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925 \
--hash=sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0 \
--hash=sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b \
--hash=sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced \
--hash=sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c \
--hash=sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344 \
--hash=sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9 \
--hash=sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1
# via
# -r .github/scripts/requirements_dev.in
# pdf2image
# weasyprint
platformdirs==4.3.8 \
--hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \
--hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4
platformdirs==4.5.0 \
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
# via virtualenv
pre-commit==4.3.0 \
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
--hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16
# via -r .github/scripts/requirements_dev.in
pycparser==2.22 \
--hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
--hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
pycparser==2.23 \
--hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \
--hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934
# via cffi
pydyf==0.11.0 \
--hash=sha256:0aaf9e2ebbe786ec7a78ec3fbffa4cdcecde53fd6f563221d53c6bc1328848a3 \
@@ -469,60 +367,80 @@ pyphen==0.17.2 \
--hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \
--hash=sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3
# via weasyprint
pyyaml==6.0.2 \
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
--hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
--hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \
--hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
--hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
--hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
--hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
--hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
--hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \
--hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \
--hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \
--hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \
--hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \
--hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
--hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \
--hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
--hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \
--hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
--hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
--hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
--hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \
--hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \
--hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
--hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
--hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \
--hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
--hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
--hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \
--hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
--hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \
--hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
--hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \
--hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \
--hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
--hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \
--hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \
--hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \
--hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
--hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
--hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
--hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
--hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \
--hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \
--hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \
--hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
--hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \
--hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
--hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
pyyaml==6.0.3 \
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
--hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
--hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
--hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \
--hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \
--hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \
--hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \
--hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \
--hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \
--hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \
--hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \
--hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \
--hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \
--hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \
--hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \
--hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \
--hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \
--hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \
--hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \
--hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
--hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \
--hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \
--hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \
--hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \
--hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \
--hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \
--hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \
--hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \
--hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \
--hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \
--hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
--hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
--hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \
--hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
--hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \
--hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \
--hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \
--hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \
--hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \
--hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \
--hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
--hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \
--hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \
--hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \
--hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \
--hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
--hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
--hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \
--hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
--hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \
--hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \
--hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \
--hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \
--hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \
--hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \
--hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \
--hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \
--hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \
--hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
--hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \
--hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \
--hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \
--hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \
--hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \
--hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \
--hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
--hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \
--hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \
--hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
--hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
# via pre-commit
tinycss2==1.4.0 \
--hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \
@@ -534,13 +452,13 @@ tinyhtml5==2.0.0 \
--hash=sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc \
--hash=sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e
# via weasyprint
unoserver==3.3.2 \
--hash=sha256:1eeb7467cf6b56b8eff3b576e2d1b2b2ff4e0eb2052e995ac80a1456de300639 \
--hash=sha256:87e144f903ee21951b2e06a97549450c13ed7eca5bcebad942d3352d4e882616
unoserver==3.4 \
--hash=sha256:3dcf2204013def1d1ddd3671f38b11346bdf349fef9728277462666a8a634419 \
--hash=sha256:64c24d33d4f65d680a2d9f676518cb28e7fd6c1f9d9a745c33e4a4cb59afdfcd
# via -r .github/scripts/requirements_dev.in
virtualenv==20.33.1 \
--hash=sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67 \
--hash=sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8
virtualenv==20.35.4 \
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
# via pre-commit
weasyprint==66.0 \
--hash=sha256:82b0783b726fcd318e2c977dcdddca76515b30044bc7a830cc4fbe717582a6d0 \
@@ -628,9 +546,9 @@ zopfli==0.2.3.post1 \
# via fonttools
# The following packages are considered to be unsafe in a requirements file:
pip==25.2 \
--hash=sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2 \
--hash=sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717
pip==25.3 \
--hash=sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343 \
--hash=sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd
# via -r .github/scripts/requirements_dev.in
setuptools==80.9.0 \
--hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \
+15 -15
View File
@@ -4,17 +4,17 @@
#
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_pre_commit.txt' --strip-extras '.github\scripts\requirements_pre_commit.in'
#
cfgv==3.4.0 \
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
--hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
cfgv==3.5.0 \
--hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \
--hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132
# via pre-commit
distlib==0.4.0 \
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
# via virtualenv
filelock==3.19.1 \
--hash=sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58 \
--hash=sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d
filelock==3.20.0 \
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
# via virtualenv
identify==2.6.15 \
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
@@ -24,13 +24,13 @@ nodeenv==1.9.1 \
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
# via pre-commit
platformdirs==4.4.0 \
--hash=sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85 \
--hash=sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf
platformdirs==4.5.0 \
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
# via virtualenv
pre-commit==4.3.0 \
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
--hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16
pre-commit==4.5.0 \
--hash=sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1 \
--hash=sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b
# via -r .github/scripts/requirements_pre_commit.in
pyyaml==6.0.3 \
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
@@ -107,7 +107,7 @@ pyyaml==6.0.3 \
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
# via pre-commit
virtualenv==20.34.0 \
--hash=sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026 \
--hash=sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a
virtualenv==20.35.4 \
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
# via pre-commit
@@ -1 +1,2 @@
tomlkit
tomli-w
@@ -4,6 +4,10 @@
#
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_sync_readme.txt' --strip-extras '.github\scripts\requirements_sync_readme.in'
#
tomli-w==1.2.0 \
--hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \
--hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021
# via -r .github/scripts/requirements_sync_readme.in
tomlkit==0.13.3 \
--hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \
--hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0
+63 -109
View File
@@ -11,6 +11,10 @@ on:
allow_fork:
description: "Allow deploying fork PR?"
required: false
type: choice
options:
- "true"
- "false"
default: "false"
permissions:
@@ -31,18 +35,18 @@ jobs:
pr_ref: ${{ steps.resolve.outputs.ref }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Resolve PR info
id: resolve
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const { owner, repo } = context.repo;
let prNumber = context.eventName === 'workflow_dispatch'
? parseInt(process.env.INPUT_PR, 10)
? parseInt(context.payload.inputs.pr, 10)
: context.payload.number;
if (!Number.isInteger(prNumber)) { core.setFailed('Invalid PR number'); return; }
@@ -107,7 +111,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
@@ -128,13 +132,13 @@ jobs:
- name: Add deployment started comment
id: deployment-started
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
const { owner, repo } = context.repo;
const prNumber = ${{ needs.check-pr.outputs.pr_number }};
// Delete previous V2 deployment comments to avoid clutter
const { data: comments } = await github.rest.issues.listComments({
owner,
@@ -157,7 +161,7 @@ jobs:
comment_id: comment.id
});
}
// Create new deployment started comment
const { data: newComment } = await github.rest.issues.createComment({
owner,
@@ -173,10 +177,10 @@ jobs:
repository: ${{ needs.check-pr.outputs.pr_repository }}
ref: ${{ needs.check-pr.outputs.pr_ref }}
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0 # Fetch full history for commit hash detection
fetch-depth: 0 # Fetch full history for commit hash detection
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Get version number
id: versionNumber
@@ -185,93 +189,56 @@ jobs:
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
- name: Login to Docker Hub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Get commit hashes for frontend and backend
id: commit-hashes
- name: Get commit hash for app
id: commit-hash
run: |
# Get last commit that touched the frontend folder, docker/frontend, or docker/compose
FRONTEND_HASH=$(git log -1 --format="%H" -- frontend/ docker/frontend/ docker/compose/ 2>/dev/null || echo "")
if [ -z "$FRONTEND_HASH" ]; then
FRONTEND_HASH="no-frontend-changes"
# Get last commit that touched the application code
APP_HASH=$(git log -1 --format="%H" -- . 2>/dev/null || echo "")
if [ -z "$APP_HASH" ]; then
APP_HASH="no-changes"
fi
# Get last commit that touched backend code, docker/backend, or docker/compose
BACKEND_HASH=$(git log -1 --format="%H" -- app/ docker/backend/ docker/compose/ 2>/dev/null || echo "")
if [ -z "$BACKEND_HASH" ]; then
BACKEND_HASH="no-backend-changes"
fi
echo "Frontend hash: $FRONTEND_HASH"
echo "Backend hash: $BACKEND_HASH"
echo "frontend_hash=$FRONTEND_HASH" >> $GITHUB_OUTPUT
echo "backend_hash=$BACKEND_HASH" >> $GITHUB_OUTPUT
# Short hashes for tags
if [ "$FRONTEND_HASH" = "no-frontend-changes" ]; then
echo "frontend_short=no-frontend" >> $GITHUB_OUTPUT
echo "App hash: $APP_HASH"
echo "app_hash=$APP_HASH" >> $GITHUB_OUTPUT
# Short hash for tags
if [ "$APP_HASH" = "no-changes" ]; then
echo "app_short=no-changes" >> $GITHUB_OUTPUT
else
echo "frontend_short=${FRONTEND_HASH:0:8}" >> $GITHUB_OUTPUT
fi
if [ "$BACKEND_HASH" = "no-backend-changes" ]; then
echo "backend_short=no-backend" >> $GITHUB_OUTPUT
else
echo "backend_short=${BACKEND_HASH:0:8}" >> $GITHUB_OUTPUT
echo "app_short=${APP_HASH:0:8}" >> $GITHUB_OUTPUT
fi
- name: Check if frontend image exists
id: check-frontend
- name: Check if image exists
id: check-image
run: |
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} >/dev/null 2>&1; then
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }} >/dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "Frontend image already exists, skipping build"
echo "Image already exists, skipping build"
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "Frontend image needs to be built"
echo "Image needs to be built"
fi
- name: Check if backend image exists
id: check-backend
run: |
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} >/dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "Backend image already exists, skipping build"
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "Backend image needs to be built"
fi
- name: Build and push V2 frontend image
if: steps.check-frontend.outputs.exists == 'false'
- name: Build and push V2 image
if: steps.check-image.outputs.exists == 'false'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: ./docker/frontend/Dockerfile
file: ./docker/embedded/Dockerfile
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
build-args: VERSION_TAG=v2-alpha
platforms: linux/amd64
- name: Build and push V2 backend image
if: steps.check-backend.outputs.exists == 'false'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: ./docker/backend/Dockerfile
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }}
build-args: VERSION_TAG=v2-alpha
platforms: linux/amd64
- name: Set up SSH
run: |
mkdir -p ~/.ssh/
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
sudo chmod 600 ../private.key
- name: Deploy V2 to VPS
@@ -279,17 +246,16 @@ jobs:
run: |
# Use same port strategy as regular PRs - just the PR number
V2_PORT=${{ needs.check-pr.outputs.pr_number }}
BACKEND_PORT=$((V2_PORT + 10000)) # Backend on higher port to avoid conflicts
# Create docker-compose for V2 with separate frontend and backend
# Create docker-compose for V2 with unified embedded image
cat > docker-compose.yml << EOF
version: '3.3'
services:
stirling-pdf-v2-backend:
container_name: stirling-pdf-v2-backend-pr-${{ needs.check-pr.outputs.pr_number }}
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
stirling-pdf-v2:
container_name: stirling-pdf-v2-pr-${{ needs.check-pr.outputs.pr_number }}
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }}
ports:
- "${BACKEND_PORT}:8080" # Backend API port
- "${V2_PORT}:8080"
volumes:
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/data:/usr/share/tessdata:rw
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/config:/configs:rw
@@ -301,7 +267,7 @@ jobs:
SECURITY_INITIALLOGIN_PASSWORD: "${{ secrets.TEST_LOGIN_PASSWORD }}"
SYSTEM_DEFAULTLOCALE: en-GB
UI_APPNAME: "Stirling-PDF V2 PR#${{ needs.check-pr.outputs.pr_number }}"
UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Frontend/Backend Split Architecture"
UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Embedded Architecture"
UI_APPNAMENAVBAR: "V2 PR#${{ needs.check-pr.outputs.pr_number }}"
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
@@ -309,23 +275,12 @@ jobs:
SWAGGER_SERVER_URL: "https://${V2_PORT}.ssl.stirlingpdf.cloud"
baseUrl: "https://${V2_PORT}.ssl.stirlingpdf.cloud"
restart: on-failure:5
stirling-pdf-v2-frontend:
container_name: stirling-pdf-v2-frontend-pr-${{ needs.check-pr.outputs.pr_number }}
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
ports:
- "${V2_PORT}:80" # Frontend port (same as regular PRs)
environment:
VITE_API_BASE_URL: "http://${{ secrets.VPS_HOST }}:${BACKEND_PORT}"
depends_on:
- stirling-pdf-v2-backend
restart: on-failure:5
EOF
# Deploy to VPS
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/docker-compose-v2.yml
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/docker-compose-v2.yml
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << ENDSSH
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << ENDSSH
# Create V2 PR-specific directories
mkdir -p /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/{data,config,logs}
@@ -335,15 +290,15 @@ jobs:
# Stop any existing container and clean up
cd /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}
docker-compose down --remove-orphans 2>/dev/null || true
# Start the new container
docker-compose pull
docker-compose up -d
# Clean up unused Docker resources to save space
docker system prune -af --volumes || true
# Clean up old backend/frontend images (older than 2 weeks)
# Clean up old images (older than 2 weeks)
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
ENDSSH
@@ -352,14 +307,14 @@ jobs:
- name: Post V2 deployment URL to PR
if: success()
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
const { owner, repo } = context.repo;
const prNumber = ${{ needs.check-pr.outputs.pr_number }};
const v2Port = ${{ steps.deploy.outputs.v2_port }};
// Delete the "deploying..." comment since we're posting the final result
const deploymentStartedId = ${{ steps.deployment-started.outputs.result }};
if (deploymentStartedId) {
@@ -374,12 +329,12 @@ jobs:
console.log(`Could not delete deployment started comment: ${error.message}`);
}
}
const deploymentUrl = `http://${{ secrets.VPS_HOST }}:${v2Port}`;
const deploymentUrl = `http://${{ secrets.NEW_VPS_HOST }}:${v2Port}`;
const httpsUrl = `https://${v2Port}.ssl.stirlingpdf.cloud`;
const commentBody = `## 🚀 V2 Auto-Deployment Complete!\n\n` +
`Your V2 PR with the new frontend/backend split architecture has been deployed!\n\n` +
`Your V2 PR with embedded architecture has been deployed!\n\n` +
`🔗 **Direct Test URL (non-SSL)** [${deploymentUrl}](${deploymentUrl})\n\n` +
`🔐 **Secure HTTPS URL**: [${httpsUrl}](${httpsUrl})\n\n` +
`_This deployment will be automatically cleaned up when the PR is closed._\n\n` +
@@ -402,7 +357,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
@@ -419,25 +374,25 @@ jobs:
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Clean up V2 deployment comments
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
const { owner, repo } = context.repo;
const prNumber = ${{ github.event.pull_request.number }};
// Find and delete V2 deployment comments
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: prNumber
});
const v2Comments = comments.filter(c =>
c.body?.includes("## 🚀 V2 Auto-Deployment Complete!") &&
c.user?.type === "Bot"
);
for (const comment of v2Comments) {
await github.rest.issues.deleteComment({
owner,
@@ -450,12 +405,12 @@ jobs:
- name: Set up SSH
run: |
mkdir -p ~/.ssh/
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
sudo chmod 600 ../private.key
- name: Cleanup V2 deployment
run: |
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << 'ENDSSH'
if [ -d "/stirling/V2-PR-${{ github.event.pull_request.number }}" ]; then
echo "Found V2 PR directory, proceeding with cleanup..."
@@ -469,9 +424,8 @@ jobs:
# Remove V2 PR-specific directories
rm -rf /stirling/V2-PR-${{ github.event.pull_request.number }}
# Clean up V2 containers by name (in case compose cleanup missed them)
docker rm -f stirling-pdf-v2-frontend-pr-${{ github.event.pull_request.number }} || true
docker rm -f stirling-pdf-v2-backend-pr-${{ github.event.pull_request.number }} || true
# Clean up V2 container by name (in case compose cleanup missed it)
docker rm -f stirling-pdf-v2-pr-${{ github.event.pull_request.number }} || true
echo "V2 cleanup completed"
else
+158 -13
View File
@@ -25,8 +25,7 @@ jobs:
github.event.comment.user.login == 'frooodle' ||
github.event.comment.user.login == 'sf298' ||
github.event.comment.user.login == 'Ludy87' ||
github.event.comment.user.login == 'LaserKaspar' ||
github.event.comment.user.login == 'sbplat' ||
github.event.comment.user.login == 'balazs-szucs' ||
github.event.comment.user.login == 'reecebrowne' ||
github.event.comment.user.login == 'DarioGii' ||
github.event.comment.user.login == 'EthanHealy01' ||
@@ -41,12 +40,12 @@ jobs:
enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout PR
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup GitHub App Bot
if: github.actor != 'dependabot[bot]'
@@ -129,12 +128,12 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout PR
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup GitHub App Bot
if: github.actor != 'dependabot[bot]'
@@ -146,12 +145,12 @@ jobs:
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Checkout PR
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge
token: ${{ steps.setup-bot.outputs.token }}
- name: Set up JDK
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "17"
@@ -169,7 +168,7 @@ jobs:
STIRLING_PDF_DESKTOP_UI: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
@@ -190,7 +189,7 @@ jobs:
- name: Set up SSH
run: |
mkdir -p ~/.ssh/
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
sudo chmod 600 ../private.key
- name: Deploy to VPS
@@ -252,9 +251,9 @@ jobs:
EOF
# Then copy the file and execute commands
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/docker-compose.yml
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/docker-compose.yml
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << ENDSSH
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << ENDSSH
# Create PR-specific directories
mkdir -p /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/{data,config,logs}
@@ -336,7 +335,7 @@ jobs:
const prNumber = ${{ needs.check-comment.outputs.pr_number }};
const securityStatus = process.env.security_status || "Security Disabled";
const deploymentUrl = `http://${{ secrets.VPS_HOST }}:${prNumber}`;
const deploymentUrl = `http://${{ secrets.NEW_VPS_HOST }}:${prNumber}`;
const commentBody = `## 🚀 PR Test Deployment\n\n` +
`Your PR has been deployed for testing!\n\n` +
`🔗 **Test URL:** [${deploymentUrl}](${deploymentUrl})\n` +
@@ -357,3 +356,149 @@ jobs:
rm -f ../private.key docker-compose.yml
echo "Cleanup complete."
continue-on-error: true
handle-label-commands:
if: ${{ github.event.issue.pull_request != null }}
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Check out the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup GitHub App Bot
id: setup-bot
uses: ./.github/actions/setup-bot
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Apply label commands
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
const fs = require('fs');
const path = require('path');
const { comment, issue } = context.payload;
const commentBody = comment?.body ?? '';
if (!commentBody.includes('::label::')) {
core.info('No label commands detected in comment.');
return;
}
const configPath = path.join(process.env.GITHUB_WORKSPACE, '.github', 'config', 'repo_devs.json');
const repoDevsConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const label_changer = (repoDevsConfig.label_changer || []).map((login) => login.toLowerCase());
const commenter = (comment?.user?.login || '').toLowerCase();
if (!label_changer.includes(commenter)) {
core.info(`User ${commenter} is not authorized to manage labels.`);
return;
}
const labelsConfigPath = path.join(process.env.GITHUB_WORKSPACE, '.github', 'labels.yml');
const labelsFile = fs.readFileSync(labelsConfigPath, 'utf8');
const labelNameMap = new Map();
for (const match of labelsFile.matchAll(/-\s+name:\s*(?:"([^"]+)"|'([^']+)'|([^\n]+))/g)) {
const labelName = (match[1] ?? match[2] ?? match[3] ?? '').trim();
if (!labelName) {
continue;
}
const normalized = labelName.toLowerCase();
if (!labelNameMap.has(normalized)) {
labelNameMap.set(normalized, labelName);
}
}
if (!labelNameMap.size) {
core.warning('No labels could be read from .github/labels.yml; aborting label commands.');
return;
}
let allowedLabelNames = new Set(labelNameMap.values());
const labelsToAdd = new Set();
const labelsToRemove = new Set();
const commandRegex = /^(\w+)::(label)::"([^"]+)"/gim;
let match;
while ((match = commandRegex.exec(commentBody)) !== null) {
core.info(`Found label command: ${match[0]} (action: ${match[1]}, label: ${match[2]}, labelName: ${match[3]})`);
const action = match[1].toLowerCase();
const labelName = match[3].trim();
if (!labelName) {
continue;
}
const normalized = labelName.toLowerCase();
const resolvedLabelName = labelNameMap.get(normalized);
if (action === 'add') {
if (!resolvedLabelName) {
core.warning(`Label "${labelName}" is not defined in .github/labels.yml and cannot be added.`);
continue;
}
if (!allowedLabelNames.has(resolvedLabelName)) {
core.warning(`Label "${resolvedLabelName}" is not allowed for add commands and will be skipped.`);
continue;
}
labelsToAdd.add(resolvedLabelName);
} else if (action === 'rm') {
const labelToRemove = resolvedLabelName ?? labelName;
if (!resolvedLabelName) {
core.warning(`Label "${labelName}" is not defined in .github/labels.yml; attempting to remove as provided.`);
}
labelsToRemove.add(labelToRemove);
}
}
const addLabels = Array.from(labelsToAdd);
const removeLabels = Array.from(labelsToRemove);
if (!addLabels.length && !removeLabels.length) {
core.info('No valid label commands found after parsing.');
return;
}
const issueParams = {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
};
if (addLabels.length) {
core.info(`Adding labels: ${addLabels.join(', ')}`);
await github.rest.issues.addLabels({
...issueParams,
labels: addLabels,
});
}
for (const labelName of removeLabels) {
core.info(`Removing label: ${labelName}`);
try {
await github.rest.issues.removeLabel({
...issueParams,
name: labelName,
});
} catch (error) {
if (error.status === 404) {
core.warning(`Label "${labelName}" was not present on the pull request.`);
} else {
throw error;
}
}
}
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: comment.id,
});
core.info('Processed label commands and deleted the comment.');
+5 -6
View File
@@ -8,7 +8,7 @@ permissions:
contents: read
env:
SERVER_IP: ${{ secrets.VPS_IP }} # Add this to your GitHub secrets
SERVER_IP: ${{ secrets.NEW_VPS_IP }} # Add this to your GitHub secrets
CLEANUP_PERFORMED: "false" # Add flag to track if cleanup occurred
jobs:
@@ -21,12 +21,12 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout PR
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup GitHub App Bot
if: github.actor != 'dependabot[bot]'
@@ -96,19 +96,18 @@ jobs:
const hasDeploymentComment = deploymentComments.length > 0;
core.setOutput('present', (hasLabel || hasDeploymentComment) ? 'true' : 'false');
- name: Set up SSH
if: steps.remove-label-comment.outputs.present == 'true'
run: |
mkdir -p ~/.ssh/
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
sudo chmod 600 ../private.key
- name: Cleanup PR deployment
if: steps.remove-label-comment.outputs.present == 'true'
id: cleanup
run: |
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << 'ENDSSH'
if [ -d "/stirling/PR-${{ github.event.pull_request.number }}" ]; then
echo "Found PR directory, proceeding with cleanup..."
+4 -4
View File
@@ -5,7 +5,7 @@ on:
types: [opened, edited]
branches: [main]
permissions: # required for secure-repo hardening
permissions: # required for secure-repo hardening
contents: read
jobs:
@@ -19,11 +19,11 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
@@ -87,7 +87,7 @@ jobs:
- name: AI PR Title Analysis
if: steps.actor.outputs.is_repo_dev == 'true'
id: ai-title-analysis
uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1.2.8
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
with:
model: openai/gpt-4o
system-prompt-file: ".github/config/system-prompt.txt"
+5 -2
View File
@@ -2,6 +2,9 @@ name: "Auto Pull Request Labeler V2"
on:
pull_request_target:
types: [opened, synchronize]
branches:
- main
- V3
permissions:
contents: read
@@ -13,11 +16,11 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup GitHub App Bot
id: setup-bot
+49 -39
View File
@@ -2,7 +2,7 @@ name: Build and Test Workflow
on:
pull_request:
branches: ["main", "V2", "V2-gha"]
branches: ["main"]
workflow_dispatch:
# cancel in-progress jobs if a new job is triggered
@@ -27,15 +27,16 @@ jobs:
timeout-minutes: 3
outputs:
build: ${{ steps.changes.outputs.build }}
app: ${{ steps.changes.outputs.app }}
project: ${{ steps.changes.outputs.project }}
openapi: ${{ steps.changes.outputs.openapi }}
frontend: ${{ steps.changes.outputs.frontend }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Check for file changes
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
@@ -55,7 +56,7 @@ jobs:
spring-security: [true, false]
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout repository
@@ -66,14 +67,12 @@ jobs:
with:
java-version: ${{ matrix.jdk-version }}
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Build with Gradle and spring security ${{ matrix.spring-security }}
run: ./gradlew clean build -PnoSpotless
env:
DISABLE_ADDITIONAL_FEATURES: ${{ matrix.spring-security }}
- name: Check Test Reports Exist
if: always()
run: |
@@ -91,12 +90,14 @@ jobs:
exit 1
fi
done
- name: Upload Test Reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: test-reports-jdk-${{ matrix.jdk-version }}-spring-security-${{ matrix.spring-security }}
path: |
app/**/build/reports/jacoco/test
app/**/build/reports/tests/
app/**/build/test-results/
app/**/build/reports/problems/
@@ -104,31 +105,41 @@ jobs:
retention-days: 3
if-no-files-found: warn
- name: Add coverage to PR with spring security ${{ matrix.spring-security }} and JDK ${{ matrix.jdk-version }}
id: jacoco
uses: madrapps/jacoco-report@50d3aff4548aa991e6753342d9ba291084e63848 # v1.7.2
with:
paths: |
${{ github.workspace }}/**/build/reports/jacoco/test/jacocoTestReport.xml
token: ${{ secrets.GITHUB_TOKEN }}
min-coverage-overall: 10
min-coverage-changed-files: 0
comment-type: summary
check-generateOpenApiDocs:
if: needs.files-changed.outputs.openapi == 'true'
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up JDK 17
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "17"
java-version: "21"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
- name: Generate OpenAPI documentation
run: ./gradlew :stirling-pdf:generateOpenApiDocs
env:
DISABLE_ADDITIONAL_FEATURES: true
- name: Upload OpenAPI Documentation
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
@@ -136,10 +147,12 @@ jobs:
path: ./SwaggerDoc.json
frontend-validation:
if: needs.files-changed.outputs.frontend == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout repository
@@ -147,8 +160,8 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: '22'
cache: 'npm'
node-version: "22"
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Install frontend dependencies
run: cd frontend && npm ci
@@ -173,15 +186,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up JDK 17
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "17"
java-version: "21"
distribution: "temurin"
- name: check the licenses for compatibility
@@ -216,21 +231,21 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout Repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Java 17
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "17"
java-version: "21"
distribution: "temurin"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Install Docker Compose
run: |
@@ -238,10 +253,10 @@ jobs:
sudo chmod +x /usr/local/bin/docker-compose
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.12"
cache: 'pip' # caching pip dependencies
cache: "pip" # caching pip dependencies
cache-dependency-path: ./testing/cucumber/requirements.txt
- name: Pip requirements
@@ -271,7 +286,7 @@ jobs:
artifact-suffix: Dockerfile.fat
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
@@ -285,17 +300,12 @@ jobs:
docker system prune -af || true
echo "Disk space after cleanup:" && df -h
- name: Set up JDK 17
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "17"
java-version: "21"
distribution: "temurin"
- name: Set up Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Build application
run: ./gradlew clean build
env:
@@ -303,11 +313,11 @@ jobs:
STIRLING_PDF_DESKTOP_UI: false
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Build ${{ matrix.docker-rev }}
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
@@ -316,8 +326,8 @@ jobs:
context: .
file: ./${{ matrix.docker-rev }}
push: false
cache-from: type=gha
cache-to: type=gha,mode=max
cache-from: type=gha,scope=${{ matrix.artifact-suffix }}
cache-to: type=gha,mode=max,scope=${{ matrix.artifact-suffix }}
platforms: linux/amd64,linux/arm64/v8
provenance: true
sbom: true
+6 -5
View File
@@ -7,6 +7,8 @@ on:
types: [opened, synchronize, reopened]
paths:
- "frontend/public/locales/*/translation.toml"
- ".github/scripts/check_language_toml.py"
- ".github/workflows/check_toml.yml"
# cancel in-progress jobs if a new job is triggered
concurrency:
@@ -25,12 +27,12 @@ jobs:
pull-requests: write # Allow writing to pull requests
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout main branch first
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup GitHub App Bot
id: setup-bot
@@ -194,13 +196,12 @@ jobs:
core.exportVariable("REFERENCE_FILE", referenceFilePath);
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.12"
- name: Install Python dependencies
run: |
pip install tomli-w
run: pip install --require-hashes -r ./.github/scripts/requirements_sync_readme.txt
- name: Run Python script to check files
id: run-check
-79
View File
@@ -1,79 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
#disable for now
#on:
# push:
# branches: ["main"]
# pull_request:
# The branches below must be a subset of the branches above
# branches: ["main"]
# schedule:
# - cron: "0 0 * * 1"
permissions:
contents: read
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ["java"]
# CodeQL supports [ $supported-codeql-languages ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Harden Runner
uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
with:
category: "/language:${{matrix.language}}"
+4 -4
View File
@@ -17,13 +17,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: "Checkout Repository"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: "Dependency Review"
uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
with:
config-file: './.github/config/dependency-review-config.yml'
config-file: "./.github/config/dependency-review-config.yml"
+20 -24
View File
@@ -18,15 +18,15 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Get commit hashes for frontend and backend
id: commit-hashes
@@ -36,26 +36,26 @@ jobs:
if [ -z "$FRONTEND_HASH" ]; then
FRONTEND_HASH="no-frontend-changes"
fi
# Get last commit that touched backend code, docker/backend, or docker/compose
BACKEND_HASH=$(git log -1 --format="%H" -- app/ docker/backend/ docker/compose/ 2>/dev/null || echo "")
if [ -z "$BACKEND_HASH" ]; then
BACKEND_HASH="no-backend-changes"
fi
echo "Frontend hash: $FRONTEND_HASH"
echo "Backend hash: $BACKEND_HASH"
echo "frontend_hash=$FRONTEND_HASH" >> $GITHUB_OUTPUT
echo "backend_hash=$BACKEND_HASH" >> $GITHUB_OUTPUT
# Short hashes for tags
if [ "$FRONTEND_HASH" = "no-frontend-changes" ]; then
echo "frontend_short=no-frontend" >> $GITHUB_OUTPUT
else
echo "frontend_short=${FRONTEND_HASH:0:8}" >> $GITHUB_OUTPUT
fi
if [ "$BACKEND_HASH" = "no-backend-changes" ]; then
echo "backend_short=no-backend" >> $GITHUB_OUTPUT
else
@@ -84,16 +84,15 @@ jobs:
echo "Backend image needs to be built"
fi
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Build and push frontend image
if: steps.check-frontend.outputs.exists == 'false'
uses: docker/build-push-action@v6
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: ./docker/frontend/Dockerfile
@@ -103,10 +102,10 @@ jobs:
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-latest
build-args: VERSION_TAG=v2-alpha
platforms: linux/amd64
- name: Build and push backend image
if: steps.check-backend.outputs.exists == 'false'
uses: docker/build-push-action@v6
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: ./docker/backend/Dockerfile
@@ -117,18 +116,16 @@ jobs:
build-args: VERSION_TAG=v2-alpha
platforms: linux/amd64
- name: Set up SSH
run: |
mkdir -p ~/.ssh/
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
chmod 600 ../private.key
- name: Deploy to VPS on port 3000
run: |
export UNIQUE_NAME=docker-compose-v2-$GITHUB_RUN_ID.yml
cat > $UNIQUE_NAME << EOF
version: '3.3'
services:
@@ -154,24 +151,24 @@ jobs:
SWAGGER_SERVER_URL: "https://demo.stirlingpdf.cloud"
baseUrl: "https://demo.stirlingpdf.cloud"
restart: on-failure:5
frontend:
container_name: stirling-v2-frontend
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
ports:
- "3000:80"
environment:
VITE_API_BASE_URL: "http://${{ secrets.VPS_HOST }}:13000"
VITE_API_BASE_URL: "http://${{ secrets.NEW_VPS_HOST }}:13000"
depends_on:
- backend
restart: on-failure:5
EOF
# Copy to remote with unique name
scp -i ../private.key -o StrictHostKeyChecking=no $UNIQUE_NAME ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/$UNIQUE_NAME
scp -i ../private.key -o StrictHostKeyChecking=no $UNIQUE_NAME ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/$UNIQUE_NAME
# SSH and rename/move atomically to avoid interference
ssh -i ../private.key -o StrictHostKeyChecking=no ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << ENDSSH
ssh -i ../private.key -o StrictHostKeyChecking=no ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << ENDSSH
mkdir -p /stirling/V2/{data,config,logs}
mv /tmp/$UNIQUE_NAME /stirling/V2/docker-compose.yml
cd /stirling/V2
@@ -186,4 +183,3 @@ jobs:
if: always()
run: |
rm -f ../private.key
@@ -0,0 +1,510 @@
name: License Report Workflow
on:
push:
branches:
- main
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
files-changed:
name: detect what files changed
runs-on: ubuntu-latest
timeout-minutes: 3
outputs:
licenses-frontend: ${{ steps.changes.outputs.licenses-frontend }}
licenses-backend: ${{ steps.changes.outputs.licenses-backend }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Check for file changes
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: changes
with:
filters: .github/config/.files.yaml
generate-frontend-license-report:
if: needs.files-changed.outputs.licenses-frontend == 'true'
name: Generate Frontend License Report
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
repository-projects: write # Required for enabling automerge
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout PR head (default)
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Setup GitHub App Bot
if: (github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)) && github.actor != 'dependabot[bot]'
id: setup-bot
uses: ./.github/actions/setup-bot
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Checkout BASE branch (safe script)
if: github.event_name == 'pull_request'
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
ref: ${{ github.event.pull_request.base.sha }}
path: base
fetch-depth: 1
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: "22"
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Install frontend dependencies
working-directory: frontend
env:
NPM_CONFIG_IGNORE_SCRIPTS: "true"
run: npm ci --ignore-scripts --audit=false --fund=false
- name: Generate frontend license report (internal PR)
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
working-directory: frontend
env:
PR_IS_FORK: "false"
run: npm run generate-licenses
- name: Generate frontend license report (fork PRs, pinned)
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
env:
NPM_CONFIG_IGNORE_SCRIPTS: "true"
working-directory: frontend
run: |
mkdir -p src/assets
npx --yes license-report --only=prod --output=json > src/assets/3rdPartyLicenses.json
- name: Postprocess with project script (BASE version)
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
env:
PR_IS_FORK: "true"
run: |
node base/frontend/scripts/generate-licenses.js \
--input frontend/src/assets/3rdPartyLicenses.json
- name: Copy postprocessed artifacts back (fork PRs)
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
run: |
mkdir -p frontend/src/assets
if [ -f "base/frontend/src/assets/3rdPartyLicenses.json" ]; then
cp base/frontend/src/assets/3rdPartyLicenses.json frontend/src/assets/3rdPartyLicenses.json
fi
if [ -f "base/frontend/src/assets/license-warnings.json" ]; then
cp base/frontend/src/assets/license-warnings.json frontend/src/assets/license-warnings.json
fi
- name: Check for license warnings
run: |
if [ -f "frontend/src/assets/license-warnings.json" ]; then
echo "LICENSE_WARNINGS_EXIST=true" >> $GITHUB_ENV
else
echo "LICENSE_WARNINGS_EXIST=false" >> $GITHUB_ENV
fi
# PR Event: Check licenses and comment on PR
- name: Delete previous license check comments
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) && github.actor != 'dependabot[bot]'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
const { owner, repo } = context.repo;
const prNumber = context.issue.number;
// Get all comments on the PR
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: prNumber,
per_page: 100
});
// Filter for license check comments
const licenseComments = comments.filter(comment =>
comment.body.includes('## ✅ Frontend License Check Passed') ||
comment.body.includes('## ❌ Frontend License Check Failed')
);
// Delete old license check comments
for (const comment of licenseComments) {
console.log(`Deleting old license check comment: ${comment.id}`);
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: comment.id
});
}
- name: Summarize results (fork PRs)
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) || github.actor == 'dependabot[bot]'
run: |
{
echo "## Frontend License Check"
echo ""
if [ "${LICENSE_WARNINGS_EXIST}" = "true" ]; then
echo "❌ **Failed** incompatible or unknown licenses found."
if [ -f "frontend/src/assets/license-warnings.json" ]; then
echo ""
echo "### Warnings"
jq -r '.warnings[] | "- \(.message)"' frontend/src/assets/license-warnings.json || true
fi
else
echo "✅ **Passed** no license warnings detected."
fi
echo ""
echo "_Note: This is a fork PR. PR comments are disabled; use this summary._"
} >> "$GITHUB_STEP_SUMMARY"
- name: Comment on PR - License Check Results
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) && github.actor != 'dependabot[bot]'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
const { owner, repo } = context.repo;
const prNumber = context.issue.number;
const hasWarnings = process.env.LICENSE_WARNINGS_EXIST === 'true';
let commentBody;
if (hasWarnings) {
// Read warnings file to get specific issues
const fs = require('fs');
let warningDetails = '';
try {
const warnings = JSON.parse(fs.readFileSync('frontend/src/assets/license-warnings.json', 'utf8'));
warningDetails = warnings.warnings.map(w => `- ${w.message}`).join('\n');
} catch (e) {
warningDetails = 'Unable to read warning details';
}
commentBody = `## ❌ Frontend License Check Failed
The frontend license check has detected compatibility warnings that require review:
${warningDetails}
**Action Required:** Please review these licenses to ensure they are acceptable for your use case before merging.
_This check will fail the PR until license issues are resolved._`;
} else {
commentBody = `## ✅ Frontend License Check Passed
All frontend licenses have been validated and no compatibility warnings were detected.
The frontend license report has been updated successfully.`;
}
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: commentBody
});
- name: Fail workflow if license warnings exist (PR only)
if: github.event_name == 'pull_request' && env.LICENSE_WARNINGS_EXIST == 'true'
run: |
echo "❌ License warnings detected. Failing the workflow."
exit 1
# Push Event: Commit license files and create PR
- name: Commit changes (Push only)
if: github.event_name == 'push'
run: |
git add frontend/src/assets/3rdPartyLicenses.json
# Note: Do NOT commit license-warnings.json - it's only for PR review
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
- name: Prepare PR body (Push only)
if: github.event_name == 'push'
run: |
PR_BODY="Auto-generated by ${{ steps.setup-bot.outputs.app-slug }}[bot]
This PR updates the frontend license report based on changes to package.json dependencies."
if [ "${{ env.LICENSE_WARNINGS_EXIST }}" = "true" ]; then
PR_BODY="$PR_BODY
## ⚠️ License Compatibility Warnings
The following licenses may require review for corporate compatibility:
$(cat frontend/src/assets/license-warnings.json | jq -r '.warnings[].message')
Please review these licenses to ensure they are acceptable for your use case."
fi
echo "PR_BODY<<EOF" >> $GITHUB_ENV
echo "$PR_BODY" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Create Pull Request (Push only)
id: cpr
if: github.event_name == 'push' && env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
token: ${{ steps.setup-bot.outputs.token }}
commit-message: "Update Frontend 3rd Party Licenses"
committer: ${{ steps.setup-bot.outputs.committer }}
author: ${{ steps.setup-bot.outputs.committer }}
signoff: true
branch: update-frontend-3rd-party-licenses
base: main
title: "Update Frontend 3rd Party Licenses"
body: ${{ env.PR_BODY }}
labels: Licenses,github-actions,frontend
draft: false
delete-branch: true
sign-commits: true
- name: Enable Pull Request Automerge (Push only)
if: github.event_name == 'push' && steps.cpr.outputs.pull-request-operation == 'created' && env.LICENSE_WARNINGS_EXIST == 'false'
run: gh pr merge --squash --auto "${{ steps.cpr.outputs.pull-request-number }}"
env:
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
- name: Add review required label (Push only)
if: github.event_name == 'push' && steps.cpr.outputs.pull-request-operation == 'created' && env.LICENSE_WARNINGS_EXIST == 'true'
run: gh pr edit "${{ steps.cpr.outputs.pull-request-number }}" --add-label "license-review-required"
env:
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
generate-backend-license-report:
if: needs.files-changed.outputs.licenses-backend == 'true'
needs: files-changed
name: Generate Backend License Report
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
repository-projects: write # Required for enabling automerge
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Setup GitHub App Bot
if: (github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)) && github.actor != 'dependabot[bot]'
id: setup-bot
uses: ./.github/actions/setup-bot
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "21"
distribution: "temurin"
- name: Check licenses and generate report
id: license-check
run: |
./gradlew clean checkLicense generateLicenseReport || echo "LICENSE_CHECK_FAILED=true" >> $GITHUB_ENV
env:
DISABLE_ADDITIONAL_FEATURES: false
STIRLING_PDF_DESKTOP_UI: true
- name: Check for license compatibility issues
run: |
if [ -f build/reports/dependency-license/dependencies-without-allowed-license.json ] && \
jq '.dependenciesWithoutAllowedLicenses | length > 0' build/reports/dependency-license/dependencies-without-allowed-license.json | grep -q true; then
echo "LICENSE_WARNINGS_EXIST=true" >> $GITHUB_ENV
else
echo "LICENSE_WARNINGS_EXIST=false" >> $GITHUB_ENV
fi
if: always()
- name: Upload artifact on license issues
if: env.LICENSE_WARNINGS_EXIST == 'true'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: backend-dependencies-without-allowed-license.json
path: build/reports/dependency-license/dependencies-without-allowed-license.json
- name: Move license file
if: env.LICENSE_CHECK_FAILED != 'true' && env.LICENSE_WARNINGS_EXIST == 'false'
run: |
mkdir -p app/core/src/main/resources/static
cp build/reports/dependency-license/index.json app/core/src/main/resources/static/3rdPartyLicenses.json
- name: Delete previous backend license check comments
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) && github.actor != 'dependabot[bot]'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
const { owner, repo } = context.repo;
const prNumber = context.issue.number;
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: prNumber,
per_page: 100
});
const backendLicenseComments = comments.filter(comment =>
comment.body.includes('## ✅ Backend License Check Passed') ||
comment.body.includes('## ❌ Backend License Check Failed')
);
for (const comment of backendLicenseComments) {
console.log(`Deleting old backend license comment: ${comment.id}`);
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: comment.id
});
}
- name: Comment on PR - Backend License Check Results
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) && github.actor != 'dependabot[bot]'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
const hasWarnings = process.env.LICENSE_WARNINGS_EXIST === 'true';
const fs = require('fs');
let warningDetails = '';
if (hasWarnings) {
try {
const warningsFile = 'build/reports/dependency-license/dependencies-without-allowed-license.json';
if (fs.existsSync(warningsFile)) {
const data = JSON.parse(fs.readFileSync(warningsFile, 'utf8'));
if (data.length > 0) {
warningDetails = data.map(dep => `- **${dep.moduleName}@${dep.moduleVersion}** ${dep.moduleLicenses.map(l => l.licenseName).join(', ')}`).join('\n');
}
}
} catch (e) {
warningDetails = 'Unable to parse warning details.';
}
}
let commentBody;
if (hasWarnings) {
commentBody = `## ❌ Backend License Check Failed
The backend license check has detected dependencies with incompatible or unallowed licenses:
${warningDetails || 'See uploaded artifact for details.'}
**Action Required:** Please review these licenses and resolve before merging.
_This check will fail the PR until license issues are resolved._`;
} else {
commentBody = `## ✅ Backend License Check Passed
All backend dependencies have valid and allowed licenses.
The backend license report has been updated successfully.`;
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: commentBody
});
- name: Fail workflow if license warnings exist (PR only)
if: github.event_name == 'pull_request' && env.LICENSE_WARNINGS_EXIST == 'true'
run: |
echo "❌ Backend license warnings detected. Failing the workflow."
exit 1
- name: Commit changes (push only)
if: github.event_name == 'push' && env.LICENSE_WARNINGS_EXIST == 'false'
run: |
git config user.name "${{ steps.setup-bot.outputs.committer }}"
git config user.email "${{ steps.setup-bot.outputs.committer-email || 'bot@github.com' }}"
git add app/core/src/main/resources/static/3rdPartyLicenses.json
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
- name: Prepare PR body (push only)
if: github.event_name == 'push' && env.CHANGES_DETECTED == 'true'
run: |
PR_BODY="Auto-generated by ${{ steps.setup-bot.outputs.app-slug }}[bot]
This PR updates the backend license report based on dependency changes."
if [ "${{ env.LICENSE_WARNINGS_EXIST }}" = "true" ]; then
PR_BODY="$PR_BODY
## ⚠️ License Compatibility Warnings
Incompatible licenses detected manual review required before merge."
fi
echo "PR_BODY<<EOF" >> $GITHUB_ENV
echo "$PR_BODY" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Create Pull Request (push only)
if: github.event_name == 'push' && env.CHANGES_DETECTED == 'true'
id: cpr
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
token: ${{ steps.setup-bot.outputs.token }}
commit-message: "Update Backend 3rd Party Licenses"
committer: ${{ steps.setup-bot.outputs.committer }}
author: ${{ steps.setup-bot.outputs.committer }}
signoff: true
branch: update-backend-3rd-party-licenses
base: main
title: "Update Backend 3rd Party Licenses"
body: ${{ env.PR_BODY }}
labels: Licenses,github-actions,backend
delete-branch: true
sign-commits: true
- name: Enable Pull Request Automerge (push only, no warnings)
if: github.event_name == 'push' && steps.cpr.outputs.pull-request-operation == 'created' && env.LICENSE_WARNINGS_EXIST == 'false'
run: gh pr merge --squash --auto "${{ steps.cpr.outputs.pull-request-number }}"
env:
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
- name: Add review required label (push only, with warnings)
if: github.event_name == 'push' && steps.cpr.outputs.pull-request-operation == 'created' && env.LICENSE_WARNINGS_EXIST == 'true'
run: gh pr edit "${{ steps.cpr.outputs.pull-request-number }}" --add-label "license-review-required"
env:
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
@@ -1,282 +0,0 @@
name: Frontend License Report Workflow
on:
push:
branches:
- V2
paths:
- "frontend/package.json"
- "frontend/package-lock.json"
- "frontend/scripts/generate-licenses.js"
pull_request:
branches:
- V2
paths:
- ".github/workflows/frontend-licenses-update.yml"
- "frontend/package.json"
- "frontend/package-lock.json"
- "frontend/scripts/generate-licenses.js"
permissions:
contents: read
jobs:
generate-frontend-license-report:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
repository-projects: write # Required for enabling automerge
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: Checkout PR head (default)
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Setup GitHub App Bot
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)
id: setup-bot
uses: ./.github/actions/setup-bot
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Checkout BASE branch (safe script)
if: github.event_name == 'pull_request'
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ github.event.pull_request.base.sha }}
path: base
fetch-depth: 1
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install frontend dependencies
working-directory: frontend
env:
NPM_CONFIG_IGNORE_SCRIPTS: "true"
run: npm ci --ignore-scripts --audit=false --fund=false
- name: Generate frontend license report (internal PR)
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
working-directory: frontend
env:
PR_IS_FORK: "false"
run: npm run generate-licenses
- name: Generate frontend license report (fork PRs, pinned)
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
env:
NPM_CONFIG_IGNORE_SCRIPTS: "true"
working-directory: frontend
run: |
mkdir -p src/assets
npx --yes license-report --only=prod --output=json > src/assets/3rdPartyLicenses.json
- name: Postprocess with project script (BASE version)
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
env:
PR_IS_FORK: "true"
run: |
node base/frontend/scripts/generate-licenses.js \
--input frontend/src/assets/3rdPartyLicenses.json
- name: Copy postprocessed artifacts back (fork PRs)
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
run: |
mkdir -p frontend/src/assets
if [ -f "base/frontend/src/assets/3rdPartyLicenses.json" ]; then
cp base/frontend/src/assets/3rdPartyLicenses.json frontend/src/assets/3rdPartyLicenses.json
fi
if [ -f "base/frontend/src/assets/license-warnings.json" ]; then
cp base/frontend/src/assets/license-warnings.json frontend/src/assets/license-warnings.json
fi
- name: Check for license warnings
run: |
if [ -f "frontend/src/assets/license-warnings.json" ]; then
echo "LICENSE_WARNINGS_EXIST=true" >> $GITHUB_ENV
else
echo "LICENSE_WARNINGS_EXIST=false" >> $GITHUB_ENV
fi
# PR Event: Check licenses and comment on PR
- name: Delete previous license check comments
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
const { owner, repo } = context.repo;
const prNumber = context.issue.number;
// Get all comments on the PR
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: prNumber,
per_page: 100
});
// Filter for license check comments
const licenseComments = comments.filter(comment =>
comment.body.includes('## ✅ Frontend License Check Passed') ||
comment.body.includes('## ❌ Frontend License Check Failed')
);
// Delete old license check comments
for (const comment of licenseComments) {
console.log(`Deleting old license check comment: ${comment.id}`);
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: comment.id
});
}
- name: Summarize results (fork PRs)
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
run: |
{
echo "## Frontend License Check"
echo ""
if [ "${LICENSE_WARNINGS_EXIST}" = "true" ]; then
echo "❌ **Failed** incompatible or unknown licenses found."
if [ -f "frontend/src/assets/license-warnings.json" ]; then
echo ""
echo "### Warnings"
jq -r '.warnings[] | "- \(.message)"' frontend/src/assets/license-warnings.json || true
fi
else
echo "✅ **Passed** no license warnings detected."
fi
echo ""
echo "_Note: This is a fork PR. PR comments are disabled; use this summary._"
} >> "$GITHUB_STEP_SUMMARY"
- name: Comment on PR - License Check Results
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
const { owner, repo } = context.repo;
const prNumber = context.issue.number;
const hasWarnings = process.env.LICENSE_WARNINGS_EXIST === 'true';
let commentBody;
if (hasWarnings) {
// Read warnings file to get specific issues
const fs = require('fs');
let warningDetails = '';
try {
const warnings = JSON.parse(fs.readFileSync('frontend/src/assets/license-warnings.json', 'utf8'));
warningDetails = warnings.warnings.map(w => `- ${w.message}`).join('\n');
} catch (e) {
warningDetails = 'Unable to read warning details';
}
commentBody = `## ❌ Frontend License Check Failed
The frontend license check has detected compatibility warnings that require review:
${warningDetails}
**Action Required:** Please review these licenses to ensure they are acceptable for your use case before merging.
_This check will fail the PR until license issues are resolved._`;
} else {
commentBody = `## ✅ Frontend License Check Passed
All frontend licenses have been validated and no compatibility warnings were detected.
The frontend license report has been updated successfully.`;
}
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: commentBody
});
- name: Fail workflow if license warnings exist (PR only)
if: github.event_name == 'pull_request' && env.LICENSE_WARNINGS_EXIST == 'true'
run: |
echo "❌ License warnings detected. Failing the workflow."
exit 1
# Push Event: Commit license files and create PR
- name: Commit changes (Push only)
if: github.event_name == 'push'
run: |
git add frontend/src/assets/3rdPartyLicenses.json
# Note: Do NOT commit license-warnings.json - it's only for PR review
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
- name: Prepare PR body (Push only)
if: github.event_name == 'push'
run: |
PR_BODY="Auto-generated by ${{ steps.setup-bot.outputs.app-slug }}[bot]
This PR updates the frontend license report based on changes to package.json dependencies."
if [ "${{ env.LICENSE_WARNINGS_EXIST }}" = "true" ]; then
PR_BODY="$PR_BODY
## ⚠️ License Compatibility Warnings
The following licenses may require review for corporate compatibility:
$(cat frontend/src/assets/license-warnings.json | jq -r '.warnings[].message')
Please review these licenses to ensure they are acceptable for your use case."
fi
echo "PR_BODY<<EOF" >> $GITHUB_ENV
echo "$PR_BODY" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Create Pull Request (Push only)
id: cpr
if: github.event_name == 'push' && env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
token: ${{ steps.setup-bot.outputs.token }}
commit-message: "Update Frontend 3rd Party Licenses"
committer: ${{ steps.setup-bot.outputs.committer }}
author: ${{ steps.setup-bot.outputs.committer }}
signoff: true
branch: update-frontend-3rd-party-licenses
base: V2
title: "Update Frontend 3rd Party Licenses"
body: ${{ env.PR_BODY }}
labels: Licenses,github-actions,frontend
draft: false
delete-branch: true
sign-commits: true
- name: Enable Pull Request Automerge (Push only)
if: github.event_name == 'push' && steps.cpr.outputs.pull-request-operation == 'created' && env.LICENSE_WARNINGS_EXIST == 'false'
run: gh pr merge --squash --auto "${{ steps.cpr.outputs.pull-request-number }}"
env:
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
- name: Add review required label (Push only)
if: github.event_name == 'push' && steps.cpr.outputs.pull-request-operation == 'created' && env.LICENSE_WARNINGS_EXIST == 'true'
run: gh pr edit "${{ steps.cpr.outputs.pull-request-number }}" --add-label "license-review-required"
env:
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
-105
View File
@@ -1,105 +0,0 @@
name: License Report Workflow
on:
push:
branches:
- main
paths:
- "build.gradle"
# cancel in-progress jobs if a new job is triggered
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
# or a pull request is updated.
# It helps to save resources and time by ensuring that only the latest commit is built and tested
# This is particularly useful for long-running jobs that may take a while to complete.
# The `group` is set to a combination of the workflow name, event name, and branch name.
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
generate-license-report:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
repository-projects: write # Required for enabling automerge
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: Check out code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- name: Setup GitHub App Bot
id: setup-bot
uses: ./.github/actions/setup-bot
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Set up JDK 17
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "17"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
- name: Check licenses for compatibility
run: ./gradlew clean checkLicense
env:
DISABLE_ADDITIONAL_FEATURES: false
STIRLING_PDF_DESKTOP_UI: true
- name: Upload artifact on failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: dependencies-without-allowed-license.json
path: build/reports/dependency-license/dependencies-without-allowed-license.json
retention-days: 3
- name: Move and rename license file
run: |
mv build/reports/dependency-license/index.json app/core/src/main/resources/static/3rdPartyLicenses.json
- name: Commit changes
run: |
git add app/core/src/main/resources/static/3rdPartyLicenses.json
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
- name: Create Pull Request
id: cpr
if: env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
token: ${{ steps.setup-bot.outputs.token }}
commit-message: "Update 3rd Party Licenses"
committer: ${{ steps.setup-bot.outputs.committer }}
author: ${{ steps.setup-bot.outputs.committer }}
signoff: true
branch: update-3rd-party-licenses
title: "Update 3rd Party Licenses"
body: |
Auto-generated by ${{ steps.setup-bot.outputs.app-slug }}[bot]
labels: Licenses,github-actions
draft: false
delete-branch: true
sign-commits: true
- name: Enable Pull Request Automerge
if: steps.cpr.outputs.pull-request-operation == 'created'
run: gh pr merge --squash --auto "${{ steps.cpr.outputs.pull-request-number }}"
env:
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
+2 -2
View File
@@ -15,12 +15,12 @@ jobs:
issues: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Check out the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Run Labeler
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0
+22 -22
View File
@@ -22,7 +22,7 @@ on:
- macos
- linux
push:
branches: [main, V2, V2-demo, V2-master]
branches: [main, V2-master]
release:
types: [created]
@@ -38,14 +38,14 @@ jobs:
version: ${{ steps.versionNumber.outputs.versionNumber }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up JDK 21
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "21"
distribution: "temurin"
@@ -106,14 +106,14 @@ jobs:
file_suffix: "-server"
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up JDK 21
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "21"
distribution: "temurin"
@@ -126,8 +126,8 @@ jobs:
if: matrix.variant.build_frontend == true
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
node-version: 22
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Build JAR
@@ -145,7 +145,7 @@ jobs:
cp app/core/build/libs/stirling-pdf-${{ needs.determine-matrix.outputs.version }}.jar ./jar-dist/Stirling-PDF${{ matrix.variant.file_suffix }}.jar
- name: Upload JAR artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: jar${{ matrix.variant.file_suffix }}
path: ./jar-dist/*.jar
@@ -162,7 +162,7 @@ jobs:
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
@@ -178,8 +178,8 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
node-version: 22
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Setup Rust
@@ -189,7 +189,7 @@ jobs:
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Set up JDK 21
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "21"
distribution: "temurin"
@@ -268,7 +268,7 @@ jobs:
- name: Install frontend dependencies
working-directory: ./frontend
run: npm install
run: npm ci
# DigiCert KeyLocker Setup (Cloud HSM)
- name: Setup DigiCert KeyLocker
@@ -516,7 +516,7 @@ jobs:
fi
- name: Upload build artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: Stirling-PDF-${{ matrix.name }}
path: ./dist/*
@@ -530,30 +530,30 @@ jobs:
contents: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Download all Tauri artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
pattern: Stirling-PDF-*
path: ./artifacts/tauri
- name: Download JAR artifact (default)
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: jar
path: ./artifacts/jars
- name: Download JAR artifact (with login)
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: jar-with-login
path: ./artifacts/jars
- name: Download JAR artifact (server only)
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: jar-server
path: ./artifacts/jars
@@ -562,7 +562,7 @@ jobs:
run: ls -R ./artifacts
- name: Upload binaries to Release
uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
tag_name: v${{ needs.determine-matrix.outputs.version }}
generate_release_notes: true
+7 -7
View File
@@ -21,12 +21,12 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
@@ -38,10 +38,10 @@ jobs:
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: 3.12
cache: 'pip' # caching pip dependencies
cache: "pip" # caching pip dependencies
cache-dependency-path: ./.github/scripts/requirements_pre_commit.txt
- name: Run Pre-Commit Hooks
@@ -51,10 +51,10 @@ jobs:
- run: pre-commit run --all-files -c .pre-commit-config.yaml
continue-on-error: true
- name: Set up JDK
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: 17
java-version: 21
distribution: "temurin"
- name: Build with Gradle
@@ -67,7 +67,7 @@ jobs:
- name: Create Pull Request
if: env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
token: ${{ steps.setup-bot.outputs.token }}
commit-message: ":file_folder: pre-commit"
-246
View File
@@ -1,246 +0,0 @@
name: Push Docker Image - V2 Branch
on:
workflow_dispatch:
push:
branches:
- V2-master
- alljavadocker
# cancel in-progress jobs if a new job is triggered
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
# or a pull request is updated.
# It helps to save resources and time by ensuring that only the latest commit is built and tested
# This is particularly useful for long-running jobs that may take a while to complete.
# The `group` is set to a combination of the workflow name, event name, and branch name.
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
push:
if: ${{ vars.CI_PROFILE != 'lite' }}
runs-on: ubuntu-24.04-8core
permissions:
packages: write
id-token: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Get version number
id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
- name: Install cosign
if: github.ref == 'refs/heads/V2-master'
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
with:
cosign-release: "v2.4.1"
- name: Login to Docker Hub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Login to GitHub Container Registry
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- name: Convert repository owner to lowercase
id: repoowner
run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT
- name: Generate tags for latest (V2-master branch - production)
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
if: github.ref == 'refs/heads/V2-master'
with:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}
type=raw,value=latest
- name: Generate tags for latest (alljavadocker branch - test)
id: meta-test
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
if: github.ref == 'refs/heads/alljavadocker'
with:
images: |
ghcr.io/stirling-tools/stirling-pdf-test
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}
type=raw,value=latest
- name: Build and push Unified Dockerfile (latest variant)
id: build-push-latest
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./docker/embedded/Dockerfile
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ github.ref == 'refs/heads/V2-master' && steps.meta.outputs.tags || steps.meta-test.outputs.tags }}
labels: ${{ github.ref == 'refs/heads/V2-master' && steps.meta.outputs.labels || steps.meta-test.outputs.labels }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8
provenance: true
sbom: true
- name: Sign regular images
if: github.ref == 'refs/heads/V2-master'
env:
DIGEST: ${{ steps.build-push-latest.outputs.digest }}
TAGS: ${{ steps.meta.outputs.tags }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
echo "$TAGS" | tr ',' '\n' | while read -r tag; do
cosign sign --yes \
--key env://COSIGN_PRIVATE_KEY \
"${tag}@${DIGEST}"
done
- name: Generate tags for latest-fat (V2-master branch - production)
id: meta-fat
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
if: github.ref == 'refs/heads/V2-master'
with:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat
type=raw,value=latest-fat
- name: Generate tags for latest-fat (alljavadocker branch - test)
id: meta-fat-test
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
if: github.ref == 'refs/heads/alljavadocker'
with:
images: |
ghcr.io/stirling-tools/stirling-pdf-test
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat
type=raw,value=latest-fat
- name: Build and push Unified Dockerfile (fat variant)
id: build-push-fat
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./docker/embedded/Dockerfile.fat
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ github.ref == 'refs/heads/V2-master' && steps.meta-fat.outputs.tags || steps.meta-fat-test.outputs.tags }}
labels: ${{ github.ref == 'refs/heads/V2-master' && steps.meta-fat.outputs.labels || steps.meta-fat-test.outputs.labels }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8
provenance: true
sbom: true
- name: Sign fat images
if: github.ref == 'refs/heads/V2-master'
env:
DIGEST: ${{ steps.build-push-fat.outputs.digest }}
TAGS: ${{ steps.meta-fat.outputs.tags }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
echo "$TAGS" | tr ',' '\n' | while read -r tag; do
cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${tag}@${DIGEST}"
done
- name: Generate tags for ultra-lite (V2-master branch - production)
id: meta-lite
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
if: github.ref == 'refs/heads/V2-master'
with:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite
type=raw,value=latest-ultra-lite
- name: Generate tags for ultra-lite (alljavadocker branch - test)
id: meta-lite-test
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
if: github.ref == 'refs/heads/alljavadocker'
with:
images: |
ghcr.io/stirling-tools/stirling-pdf-test
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite
type=raw,value=latest-ultra-lite
- name: Build and push Unified Dockerfile (ultra-lite variant)
id: build-push-lite
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./docker/embedded/Dockerfile.ultra-lite
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ github.ref == 'refs/heads/V2-master' && steps.meta-lite.outputs.tags || steps.meta-lite-test.outputs.tags }}
labels: ${{ github.ref == 'refs/heads/V2-master' && steps.meta-lite.outputs.labels || steps.meta-lite-test.outputs.labels }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8
provenance: true
sbom: true
- name: Sign ultra-lite images
if: github.ref == 'refs/heads/V2-master'
env:
DIGEST: ${{ steps.build-push-lite.outputs.digest }}
TAGS: ${{ steps.meta-lite.outputs.tags }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
echo "$TAGS" | tr ',' '\n' | while read -r tag; do
cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${tag}@${DIGEST}"
done
+80 -71
View File
@@ -6,6 +6,8 @@ on:
branches:
- master
- main
- V2-master
- testMain
# cancel in-progress jobs if a new job is triggered
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
@@ -25,48 +27,42 @@ permissions:
jobs:
push:
if: ${{ vars.CI_PROFILE != 'lite' }}
runs-on: ubuntu-latest
runs-on: ubuntu-24.04-8core
permissions:
packages: write
id-token: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up JDK 17
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "17"
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Run Gradle Command
run: ./gradlew clean build
env:
DISABLE_ADDITIONAL_FEATURES: true
STIRLING_PDF_DESKTOP_UI: false
- name: Install cosign
if: github.ref == 'refs/heads/master'
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
with:
cosign-release: "v2.4.1"
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Get version number
id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
- name: Install cosign
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
with:
cosign-release: "v2.4.1"
- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
@@ -81,16 +77,15 @@ jobs:
password: ${{ github.token }}
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Convert repository owner to lowercase
id: repoowner
run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT
- name: Generate tags
- name: Generate tags for latest
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
if: github.ref != 'refs/heads/main'
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
@@ -98,13 +93,13 @@ jobs:
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' }}
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/testMain' }}
- name: Build and push main Dockerfile
id: build-push-regular
- name: Build and push Unified Dockerfile (latest variant)
id: build-push-latest
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
if: github.ref != 'refs/heads/main'
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
@@ -120,9 +115,9 @@ jobs:
sbom: true
- name: Sign regular images
if: github.ref == 'refs/heads/master'
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
env:
DIGEST: ${{ steps.build-push-regular.outputs.digest }}
DIGEST: ${{ steps.build-push-latest.outputs.digest }}
TAGS: ${{ steps.meta.outputs.tags }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
@@ -133,10 +128,10 @@ jobs:
"${tag}@${DIGEST}"
done
- name: Generate tags ultra-lite
id: meta2
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
if: github.ref != 'refs/heads/main'
- name: Generate tags for latest-fat
id: meta-fat
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain'
with:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
@@ -144,43 +139,13 @@ jobs:
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
- name: Build and push Dockerfile-ultra-lite
id: build-push-lite
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
if: github.ref != 'refs/heads/main'
with:
context: .
file: ./docker/embedded/Dockerfile.ultra-lite
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ steps.meta2.outputs.tags }}
labels: ${{ steps.meta2.outputs.labels }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8
provenance: true
sbom: true
- name: Generate tags fat
id: meta3
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
with:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' }}
type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' }}
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push main Dockerfile fat
- name: Build and push Unified Dockerfile (fat variant)
id: build-push-fat
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain'
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
@@ -188,18 +153,62 @@ jobs:
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ steps.meta3.outputs.tags }}
labels: ${{ steps.meta3.outputs.labels }}
tags: ${{ steps.meta-fat.outputs.tags }}
labels: ${{ steps.meta-fat.outputs.labels }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8
provenance: true
sbom: true
- name: Sign fat images
if: github.ref == 'refs/heads/master'
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
env:
DIGEST: ${{ steps.build-push-fat.outputs.digest }}
TAGS: ${{ steps.meta3.outputs.tags }}
TAGS: ${{ steps.meta-fat.outputs.tags }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
echo "$TAGS" | tr ',' '\n' | while read -r tag; do
cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${tag}@${DIGEST}"
done
- name: Generate tags for ultra-lite
id: meta-lite
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain'
with:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
- name: Build and push Unified Dockerfile (ultra-lite variant)
id: build-push-lite
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain'
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./docker/embedded/Dockerfile.ultra-lite
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ steps.meta-lite.outputs.tags }}
labels: ${{ steps.meta-lite.outputs.labels }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8
provenance: true
sbom: true
- name: Sign ultra-lite images
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
env:
DIGEST: ${{ steps.build-push-lite.outputs.digest }}
TAGS: ${{ steps.meta-lite.outputs.tags }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
-535
View File
@@ -1,535 +0,0 @@
name: Release Artifacts
on:
workflow_dispatch:
inputs:
platform:
description: "Platform to build (windows, macos, linux, or all)"
required: true
default: "all"
type: choice
options:
- all
- windows
- macos
- linux
push:
branches: [main, V2, V2-demo]
permissions:
contents: read
jobs:
determine-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
version: ${{ steps.versionNumber.outputs.versionNumber }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Get version number
id: versionNumber
run: |
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
- name: Determine build matrix
id: set-matrix
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
case "${{ github.event.inputs.platform }}" in
"windows")
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"}]}' >> $GITHUB_OUTPUT
;;
"macos")
echo 'matrix={"include":[{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}]}' >> $GITHUB_OUTPUT
;;
"linux")
echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
;;
*)
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
;;
esac
else
# For push events, build all platforms
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
fi
build:
needs: determine-matrix
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.determine-matrix.outputs.matrix) }}
runs-on: ${{ matrix.platform }}
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Set up JDK 21
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
java-version: "21"
distribution: "temurin"
- name: Build Java backend with JLink
working-directory: ./
shell: bash
run: |
chmod +x ./gradlew
echo "🔧 Building Stirling-PDF JAR..."
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
# Find the built JAR
STIRLING_JAR=$(ls app/core/build/libs/stirling-pdf-*.jar | head -n 1)
echo "✅ Built JAR: $STIRLING_JAR"
# Create Tauri directories
mkdir -p ./frontend/src-tauri/libs
mkdir -p ./frontend/src-tauri/runtime
# Copy JAR to Tauri libs
cp "$STIRLING_JAR" ./frontend/src-tauri/libs/
echo "✅ JAR copied to Tauri libs"
# Analyze JAR dependencies for jlink modules
echo "🔍 Analyzing JAR dependencies..."
if command -v jdeps &> /dev/null; then
DETECTED_MODULES=$(jdeps --print-module-deps --ignore-missing-deps "$STIRLING_JAR" 2>/dev/null || echo "")
if [ -n "$DETECTED_MODULES" ]; then
echo "📋 jdeps detected modules: $DETECTED_MODULES"
MODULES="$DETECTED_MODULES,java.compiler,java.instrument,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
else
echo "⚠️ jdeps analysis failed, using predefined modules"
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
fi
else
echo "⚠️ jdeps not available, using predefined modules"
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
fi
# Create custom JRE with jlink
echo "🔧 Creating custom JRE with jlink..."
echo "📋 Using modules: $MODULES"
# Remove any existing JRE
rm -rf ./frontend/src-tauri/runtime/jre
# Create the custom JRE
jlink \
--add-modules "$MODULES" \
--strip-debug \
--compress=2 \
--no-header-files \
--no-man-pages \
--output ./frontend/src-tauri/runtime/jre
if [ ! -d "./frontend/src-tauri/runtime/jre" ]; then
echo "❌ Failed to create JLink runtime"
exit 1
fi
# Test the bundled runtime
if [ -f "./frontend/src-tauri/runtime/jre/bin/java" ]; then
RUNTIME_VERSION=$(./frontend/src-tauri/runtime/jre/bin/java --version 2>&1 | head -n 1)
echo "✅ Custom JRE created successfully: $RUNTIME_VERSION"
else
echo "❌ Custom JRE executable not found"
exit 1
fi
# Calculate runtime size
RUNTIME_SIZE=$(du -sh ./frontend/src-tauri/runtime/jre | cut -f1)
echo "📊 Custom JRE size: $RUNTIME_SIZE"
env:
DISABLE_ADDITIONAL_FEATURES: true
- name: Install frontend dependencies
working-directory: ./frontend
run: npm install
# DigiCert KeyLocker Setup (Cloud HSM)
- name: Setup DigiCert KeyLocker
id: digicert-setup
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
uses: digicert/ssm-code-signing@v1.1.0
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
SM_KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
SM_HOST: ${{ secrets.SM_HOST }}
- name: Setup DigiCert KeyLocker Certificate
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
shell: pwsh
run: |
Write-Host "Setting up DigiCert KeyLocker environment..."
# Decode client certificate
$certBytes = [Convert]::FromBase64String("${{ secrets.SM_CLIENT_CERT_FILE_B64 }}")
$certPath = "D:\Certificate_pkcs12.p12"
[IO.File]::WriteAllBytes($certPath, $certBytes)
# Set environment variables
echo "SM_CLIENT_CERT_FILE=D:\Certificate_pkcs12.p12" >> $env:GITHUB_ENV
echo "SM_HOST=${{ secrets.SM_HOST }}" >> $env:GITHUB_ENV
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> $env:GITHUB_ENV
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> $env:GITHUB_ENV
echo "SM_KEYPAIR_ALIAS=${{ secrets.SM_KEYPAIR_ALIAS }}" >> $env:GITHUB_ENV
# Get PKCS11 config path from DigiCert action
$pkcs11Config = $env:PKCS11_CONFIG
if ($pkcs11Config) {
Write-Host "Found PKCS11_CONFIG: $pkcs11Config"
echo "PKCS11_CONFIG=$pkcs11Config" >> $env:GITHUB_ENV
} else {
Write-Host "PKCS11_CONFIG not set by DigiCert action, using default path"
$defaultPath = "C:\Users\RUNNER~1\AppData\Local\Temp\smtools-windows-x64\pkcs11properties.cfg"
if (Test-Path $defaultPath) {
Write-Host "Found config at default path: $defaultPath"
echo "PKCS11_CONFIG=$defaultPath" >> $env:GITHUB_ENV
} else {
Write-Host "Warning: Could not find PKCS11 config file"
}
}
# Traditional PFX Certificate Import (fallback if KeyLocker not configured)
- name: Import Windows Code Signing Certificate
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY == '' && github.ref == 'refs/heads/main' }}
env:
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
shell: powershell
run: |
if ($env:WINDOWS_CERTIFICATE) {
Write-Host "Importing Windows Code Signing Certificate..."
# Decode base64 certificate and save to file
$certBytes = [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE)
$certPath = Join-Path $env:RUNNER_TEMP "certificate.pfx"
[IO.File]::WriteAllBytes($certPath, $certBytes)
# Import certificate to CurrentUser\My store
$cert = Import-PfxCertificate -FilePath $certPath -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force)
# Extract and set thumbprint as environment variable
$thumbprint = $cert.Thumbprint
Write-Host "Certificate imported with thumbprint: $thumbprint"
echo "WINDOWS_CERTIFICATE_THUMBPRINT=$thumbprint" >> $env:GITHUB_ENV
# Clean up certificate file
Remove-Item $certPath
Write-Host "Windows certificate import completed."
} else {
Write-Host "⚠️ WINDOWS_CERTIFICATE secret not set - building unsigned binary"
}
- name: Import Apple Developer Certificate
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
echo "Importing Apple Developer Certificate..."
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
# Create temporary keychain
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# Import certificate
security import certificate.p12 -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# Clean up
rm certificate.p12
- name: Verify Certificate
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
run: |
echo "Verifying Apple Developer Certificate..."
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
CERT_INFO=$(security find-identity -v -p codesigning $KEYCHAIN_PATH | grep "Developer ID Application")
echo "Certificate Info: $CERT_INFO"
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
echo "Certificate ID: $CERT_ID"
echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> $GITHUB_ENV
echo "Certificate imported successfully."
- name: Build Tauri app
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.APPIMAGETOOL_SIGN_PASSPHRASE }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY }}
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL }}
# Only enable Windows signing in Tauri when on main
SIGN: ${{ github.ref == 'refs/heads/main' && (env.SM_API_KEY == '' && env.WINDOWS_CERTIFICATE != '') && '1' || '0' }}
CI: true
with:
projectPath: ./frontend
tauriScript: npx tauri
args: ${{ matrix.args }}
# Sign with DigiCert KeyLocker (post-build)
- name: Sign Windows binaries with DigiCert KeyLocker
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
shell: pwsh
run: |
Write-Host "=== DigiCert KeyLocker Signing ==="
# Test smctl connectivity first
Write-Host "Testing smctl connection..."
$healthCheck = & smctl healthcheck 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "[SUCCESS] Connected to DigiCert KeyLocker"
} else {
Write-Host "[ERROR] Failed to connect to DigiCert KeyLocker"
Write-Host $healthCheck
exit 1
}
Write-Host ""
# Sync certificates to Windows certificate store
Write-Host "Syncing certificates to Windows certificate store..."
$syncOutput = & smctl windows certsync 2>&1
Write-Host "Cert sync result: $syncOutput"
Write-Host ""
# Find only the files we need to sign (not build scripts)
$filesToSign = @()
# Main application executable
$mainExe = Get-ChildItem -Path "./frontend/src-tauri/target/x86_64-pc-windows-msvc/release" -Filter "stirling-pdf.exe" -File -ErrorAction SilentlyContinue
if ($mainExe) { $filesToSign += $mainExe }
# MSI installer
$msiFiles = Get-ChildItem -Path "./frontend/src-tauri/target" -Filter "*.msi" -Recurse -File
$filesToSign += $msiFiles
if ($filesToSign.Count -eq 0) {
Write-Host "[ERROR] No files found to sign"
exit 1
}
Write-Host "Found $($filesToSign.Count) files to sign:"
foreach ($f in $filesToSign) { Write-Host " - $($f.Name)" }
Write-Host ""
$signedCount = 0
foreach ($file in $filesToSign) {
Write-Host "Signing: $($file.Name)"
# Get PKCS11 config file path (set by DigiCert action)
$pkcs11Config = $env:PKCS11_CONFIG
if (-not $pkcs11Config) {
Write-Host "[ERROR] PKCS11_CONFIG environment variable not set"
Write-Host "DigiCert KeyLocker action may not have run correctly"
exit 1
}
Write-Host "Using PKCS11 config: $pkcs11Config"
# Try signing with certificate fingerprint first (if available)
$fingerprint = "${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}"
if ($fingerprint -and $fingerprint -ne "") {
Write-Host "Attempting to sign with certificate fingerprint..."
$output = & smctl sign --fingerprint "$fingerprint" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
$exitCode = $LASTEXITCODE
} else {
Write-Host "No fingerprint provided, using keypair alias..."
# Use smctl to sign with keypair alias
$output = & smctl sign --keypair-alias "${{ secrets.SM_KEYPAIR_ALIAS }}" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
$exitCode = $LASTEXITCODE
}
Write-Host "Exit code: $exitCode"
Write-Host "Output: $output"
# Check if output contains "FAILED" even with exit code 0
if ($output -match "FAILED" -or $output -match "error" -or $output -match "Error") {
Write-Host ""
Write-Host "[ERROR] Signing failed for $($file.Name)"
Write-Host "[ERROR] smctl returned success but output indicates failure"
exit 1
}
if ($exitCode -ne 0) {
Write-Host "[ERROR] Failed to sign $($file.Name)"
Write-Host "Full error output:"
Write-Host $output
exit 1
}
$signedCount++
Write-Host "[SUCCESS] Signed: $($file.Name)"
Write-Host ""
}
Write-Host "=== Summary ==="
Write-Host "[SUCCESS] Signed $signedCount/$($filesToSign.Count) files successfully"
- name: Rename artifacts
shell: bash
run: |
mkdir -p ./dist
cd ./frontend/src-tauri/target
# Find and rename artifacts based on platform
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
find . -name "*.exe" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.exe" \;
find . -name "*.msi" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.msi" \;
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \;
find . -name "*.app" -exec cp -r {} "../../../dist/Stirling-PDF-${{ matrix.name }}.app" \;
else
find . -name "*.deb" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.deb" \;
find . -name "*.AppImage" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.AppImage" \;
fi
- name: Upload build artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: Stirling-PDF-${{ matrix.name }}
path: ./dist/*
retention-days: 30
sign_verify:
needs: build
runs-on: ubuntu-latest
if: success()
strategy:
fail-fast: false
matrix:
name: [windows-x86_64, macos-aarch64, macos-x86_64, linux-x86_64]
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: Download build artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: Stirling-PDF-${{ matrix.name }}
- name: Display structure of downloaded files
run: ls -R
- name: Install Cosign
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
- name: Generate key pair
run: cosign generate-key-pair
- name: Sign and generate attestations
shell: bash
run: |
# Sign all artifacts for this platform
for file in *; do
if [ -f "$file" ] && [[ ! "$file" =~ \.(sig|intoto\.jsonl)$ ]]; then
echo "Signing: $file"
# Sign the artifact
cosign sign-blob \
--key ./cosign.key \
--yes \
--output-signature "${file}.sig" \
"$file"
# Generate attestation
cosign attest-blob \
--predicate - \
--key ./cosign.key \
--yes \
--output-attestation "${file}.intoto.jsonl" \
"$file"
# Verify the signature
cosign verify-blob \
--key ./cosign.pub \
--signature "${file}.sig" \
"$file"
echo "✅ Signed and verified: $file"
fi
done
- name: Upload signed artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: Stirling-PDF-${{ matrix.name }}-signed
path: |
*
!cosign.key
!cosign.pub
retention-days: 30
release:
needs: [determine-matrix, build, sign_verify]
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main'
permissions:
contents: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: Download all signed artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
pattern: Stirling-PDF-*-signed
path: ./artifacts
- name: Display structure of downloaded files
run: ls -R ./artifacts
- name: Create GitHub Release
uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4
with:
tag_name: v${{ needs.determine-matrix.outputs.version }}
generate_release_notes: true
files: ./artifacts/**/*
draft: false
prerelease: false
+4 -4
View File
@@ -35,12 +35,12 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: "Checkout code"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
@@ -67,7 +67,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: SARIF file
path: results.sarif
@@ -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@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.29.5
uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3.29.5
with:
sarif_file: results.sarif
-76
View File
@@ -1,76 +0,0 @@
name: Run Sonarqube
on:
push:
branches:
- master
pull_request_target:
branches:
- main
workflow_dispatch:
# cancel in-progress jobs if a new job is triggered
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
# or a pull request is updated.
# It helps to save resources and time by ensuring that only the latest commit is built and tested
# This is particularly useful for long-running jobs that may take a while to complete.
# The `group` is set to a combination of the workflow name, event name, and branch name.
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref_name || github.ref }}
cancel-in-progress: true
permissions:
pull-requests: read
actions: read
jobs:
sonarqube:
if: ${{ vars.CI_PROFILE != 'lite' }}
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
- name: Build and analyze with Gradle
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
DISABLE_ADDITIONAL_FEATURES: false
STIRLING_PDF_DESKTOP_UI: true
run: |
./gradlew clean build sonar \
-Dsonar.projectKey=Stirling-Tools_Stirling-PDF \
-Dsonar.organization=stirling-tools \
-Dsonar.host.url=https://sonarcloud.io \
-Dsonar.login=${SONAR_TOKEN} \
-Dsonar.log.level=DEBUG \
--info
- name: Upload Problems Report on Failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: gradle-problems-report
path: build/reports/problems/problems-report.html
retention-days: 7
- name: Upload Sonar Logs on Failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: sonar-logs
path: |
.scannerwork/report-task.txt
build/sonar/
retention-days: 7
+2 -2
View File
@@ -17,12 +17,12 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: 30 days stale issues
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 30
+4 -4
View File
@@ -27,16 +27,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up JDK 17
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "17"
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
+6 -11
View File
@@ -5,7 +5,6 @@ on:
push:
branches:
- main
- syncLangTest
paths:
- "build.gradle"
- "README.md"
@@ -33,11 +32,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup GitHub App Bot
id: setup-bot
@@ -47,14 +46,13 @@ jobs:
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.12"
cache: "pip" # caching pip dependencies
- name: Install Python dependencies
run: |
pip install tomli-w
run: pip install --require-hashes -r ./.github/scripts/requirements_sync_readme.txt
- name: Sync translation TOML files
run: |
@@ -65,9 +63,6 @@ jobs:
git add frontend/public/locales/*/translation.toml
git diff --staged --quiet || git commit -m ":memo: Sync translation files (TOML)" || echo "No changes detected"
- name: Install README dependencies
run: pip install --require-hashes -r ./.github/scripts/requirements_sync_readme.txt
- name: Sync README.md
run: |
python scripts/counter_translation_v3.py
@@ -79,7 +74,7 @@ jobs:
- name: Create Pull Request
if: always()
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
token: ${{ steps.setup-bot.outputs.token }}
commit-message: Update files
@@ -121,4 +116,4 @@ jobs:
add-paths: |
README.md
frontend/public/locales/*/translation.toml
scripts/ignore_translation.toml
scripts/ignore_translation.toml
+22 -23
View File
@@ -14,14 +14,14 @@ on:
- macos
- linux
pull_request:
branches: [main, V2, V2-tauri-windows]
branches: [main, V2-tauri-windows]
paths:
- 'frontend/src-tauri/**'
- 'frontend/src/desktop/**'
- 'frontend/tsconfig.desktop.json'
- '.github/workflows/tauri-build.yml'
- "frontend/src-tauri/**"
- "frontend/src/desktop/**"
- "frontend/tsconfig.desktop.json"
- ".github/workflows/tauri-build.yml"
push:
branches: [main, V2]
branches: [main]
permissions:
contents: read
@@ -67,7 +67,7 @@ jobs:
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
@@ -83,8 +83,8 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
node-version: 22
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Setup Rust
@@ -111,15 +111,15 @@ jobs:
# Find the built JAR
STIRLING_JAR=$(ls app/core/build/libs/stirling-pdf-*.jar | head -n 1)
echo "✅ Built JAR: $STIRLING_JAR"
# Create Tauri directories
mkdir -p ./frontend/src-tauri/libs
mkdir -p ./frontend/src-tauri/runtime
# Copy JAR to Tauri libs
cp "$STIRLING_JAR" ./frontend/src-tauri/libs/
echo "✅ JAR copied to Tauri libs"
# Analyze JAR dependencies for jlink modules
echo "🔍 Analyzing JAR dependencies..."
if command -v jdeps &> /dev/null; then
@@ -135,14 +135,14 @@ jobs:
echo "⚠️ jdeps not available, using predefined modules"
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
fi
# Create custom JRE with jlink (always rebuild)
echo "🔧 Creating custom JRE with jlink..."
echo "📋 Using modules: $MODULES"
# Remove any existing JRE
rm -rf ./frontend/src-tauri/runtime/jre
# Create the custom JRE
jlink \
--add-modules "$MODULES" \
@@ -151,12 +151,12 @@ jobs:
--no-header-files \
--no-man-pages \
--output ./frontend/src-tauri/runtime/jre
if [ ! -d "./frontend/src-tauri/runtime/jre" ]; then
echo "❌ Failed to create JLink runtime"
exit 1
fi
# Test the bundled runtime
if [ -f "./frontend/src-tauri/runtime/jre/bin/java" ]; then
RUNTIME_VERSION=$(./frontend/src-tauri/runtime/jre/bin/java --version 2>&1 | head -n 1)
@@ -165,7 +165,7 @@ jobs:
echo "❌ Custom JRE executable not found"
exit 1
fi
# Calculate runtime size
RUNTIME_SIZE=$(du -sh ./frontend/src-tauri/runtime/jre | cut -f1)
echo "📊 Custom JRE size: $RUNTIME_SIZE"
@@ -174,7 +174,7 @@ jobs:
- name: Install frontend dependencies
working-directory: ./frontend
run: npm install
run: npm ci
# DigiCert KeyLocker Setup (Cloud HSM)
- name: Setup DigiCert KeyLocker
@@ -507,7 +507,6 @@ jobs:
find . -name "*.msi" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.msi" \;
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \;
find . -name "*.app" -exec cp -r {} "../../../dist/Stirling-PDF-${{ matrix.name }}.app" \;
else
find . -name "*.deb" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.deb" \;
find . -name "*.AppImage" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.AppImage" \;
@@ -579,7 +578,7 @@ jobs:
name: Stirling-PDF-${{ matrix.name }}
path: ./dist/*
retention-days: 7
- name: Verify build artifacts
shell: bash
run: |
@@ -595,8 +594,8 @@ jobs:
fi
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
echo "Checking for macOS artifacts..."
find . -name "*.dmg" -o -name "*.app" | head -5
if [ $(find . -name "*.dmg" -o -name "*.app" | wc -l) -eq 0 ]; then
find . -name "*.dmg" | head -5
if [ $(find . -name "*.dmg" | wc -l) -eq 0 ]; then
echo "❌ No macOS artifacts found"
exit 1
fi
+18 -18
View File
@@ -25,18 +25,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up JDK
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: '17'
distribution: 'temurin'
java-version: "21"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
@@ -49,7 +49,7 @@ jobs:
DISABLE_ADDITIONAL_FEATURES: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Get version number
id: versionNumber
@@ -76,7 +76,7 @@ jobs:
- name: Set up SSH
run: |
mkdir -p ~/.ssh/
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
sudo chmod 600 ../private.key
- name: Deploy to VPS
@@ -107,9 +107,9 @@ jobs:
restart: on-failure:5
EOF
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/docker-compose.yml
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/docker-compose.yml
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << EOF
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << EOF
mkdir -p /stirling/test-${{ github.sha }}/{data,config,logs}
mv /tmp/docker-compose.yml /stirling/test-${{ github.sha }}/docker-compose.yml
cd /stirling/test-${{ github.sha }}
@@ -125,7 +125,7 @@ jobs:
outputs:
frontend: ${{ steps.changes.outputs.frontend }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Check for file changes
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
@@ -140,16 +140,16 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
cache: 'npm'
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Run TestDriver.ai
@@ -161,7 +161,7 @@ jobs:
npm install
npm run build
npm install dashcam-chrome --save
Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList "--start-maximized", "--load-extension=$(pwd)/node_modules/dashcam-chrome/build", "http://${{ secrets.VPS_HOST }}:1337"
Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList "--start-maximized", "--load-extension=$(pwd)/node_modules/dashcam-chrome/build", "http://${{ secrets.NEW_VPS_HOST }}:1337"
Start-Sleep -Seconds 20
prompt: |
1. /run testing/testdriver/test.yml
@@ -176,20 +176,20 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Set up SSH
run: |
mkdir -p ~/.ssh/
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
sudo chmod 600 ../private.key
- name: Cleanup deployment
if: always()
run: |
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << EOF
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << EOF
cd /stirling/test-${{ github.sha }}
docker-compose down
cd /stirling
+2
View File
@@ -43,6 +43,8 @@ app/core/src/main/resources/static/og_images/
app/core/src/main/resources/static/samples/
app/core/src/main/resources/static/manifest-classic.json
app/core/src/main/resources/static/robots.txt
app/core/src/main/resources/static/pdfium/
app/core/src/main/resources/static/vendor/
# Note: Keep backend-managed files like fonts/, css/, js/, pdfjs/, etc.
# Gradle
+3 -3
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.7
rev: v0.14.8
hooks:
- id: ruff
args:
@@ -22,11 +22,11 @@ repos:
files: \.(html|css|js|py|md)$
exclude: (.vscode|.devcontainer|app/core/src/main/resources|app/proprietary/src/main/resources|Dockerfile|.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js)
- repo: https://github.com/gitleaks/gitleaks
rev: v8.28.0
rev: v8.30.0
hooks:
- id: gitleaks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: end-of-file-fixer
files: ^.*(\.js|\.java|\.py|\.yml)$
+16
View File
@@ -96,6 +96,22 @@
"moduleName": ".*",
"moduleLicense": "MPL 2.0"
},
{
"moduleName": ".*",
"moduleLicense": "Mozilla Public License, Version 2.0"
},
{
"moduleName": ".*",
"moduleLicense": "Mozilla Public License 2.0 (MPL-2.0)"
},
{
"moduleName": ".*",
"moduleLicense": "CDDL+GPL License"
},
{
"moduleName": ".*",
"moduleLicense": "BSD"
},
{
"moduleName": ".*",
"moduleLicense": "UnboundID SCIM2 SDK Free Use License"
+7 -3
View File
@@ -33,16 +33,20 @@ dependencies {
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
api 'com.fathzer:javaluator:3.0.6'
api 'com.posthog.java:posthog:1.2.0'
api 'org.apache.commons:commons-lang3:3.19.0'
api 'org.apache.commons:commons-lang3:3.20.0'
api 'com.drewnoakes:metadata-extractor:2.19.0' // Image metadata extractor
api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8'
api "org.apache.pdfbox:pdfbox:$pdfboxVersion"
api "org.apache.pdfbox:pdfbox-io:$pdfboxVersion"
api "org.apache.pdfbox:xmpbox:$pdfboxVersion"
api "org.apache.pdfbox:preflight:$pdfboxVersion"
api 'com.github.junrar:junrar:7.5.5' // RAR archive support for CBR files
api 'com.github.junrar:junrar:7.5.7' // RAR archive support for CBR files
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
api 'org.snakeyaml:snakeyaml-engine:2.10'
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13"
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.14"
// Simple Java Mail for EML/MSG parsing (replaces direct Angus Mail usage)
api 'org.simplejavamail:simple-java-mail:8.12.6'
api 'org.simplejavamail:outlook-module:8.12.6' // MSG file support
api 'jakarta.mail:jakarta.mail-api:2.1.5'
runtimeOnly 'org.eclipse.angus:angus-mail:2.0.5'
}
@@ -156,19 +156,19 @@ public class EndpointConfiguration {
return false;
}
// Rule 2: For tool groups, they're enabled unless explicitly disabled (handled above)
if (isToolGroup(group)) {
log.debug("isGroupEnabled('{}') -> true (tool group not disabled)", group);
return true;
}
// Rule 3: For functional groups, check if all endpoints are enabled
Set<String> endpoints = endpointGroups.get(group);
if (endpoints == null || endpoints.isEmpty()) {
log.debug("isGroupEnabled('{}') -> false (no endpoints)", group);
return false;
}
// Rule 2: For functional groups, check if all endpoints are enabled
// Rule 3: For tool groups, they're enabled unless explicitly disabled (handled above)
if (isToolGroup(group)) {
log.debug("isGroupEnabled('{}') -> true (tool group not disabled)", group);
return true;
}
// For functional groups, check each endpoint individually
for (String endpoint : endpoints) {
if (!isEndpointEnabledDirectly(endpoint)) {
@@ -334,6 +334,9 @@ public class EndpointConfiguration {
addEndpointToGroup("Convert", "pdf-to-csv");
addEndpointToGroup("Convert", "pdf-to-markdown");
addEndpointToGroup("Convert", "eml-to-pdf");
addEndpointToGroup("Convert", "pdf-to-vector");
addEndpointToGroup("Convert", "vector-to-pdf");
addEndpointToGroup("Convert", "pdf-to-video");
addEndpointToGroup("Convert", "cbz-to-pdf");
addEndpointToGroup("Convert", "pdf-to-cbz");
addEndpointToGroup("Convert", "pdf-to-json");
@@ -350,6 +353,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Security", "auto-redact");
addEndpointToGroup("Security", "redact");
addEndpointToGroup("Security", "validate-signature");
addEndpointToGroup("Security", "verify-pdf");
addEndpointToGroup("Security", "stamp");
addEndpointToGroup("Security", "sign");
@@ -476,6 +480,8 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "pdf-to-json");
addEndpointToGroup("Java", "json-to-pdf");
addEndpointToGroup("rar", "pdf-to-cbr");
addEndpointToGroup("Java", "pdf-to-video");
addEndpointToGroup("Java", "verify-pdf");
// Javascript
addEndpointToGroup("Javascript", "pdf-organizer");
@@ -490,6 +496,10 @@ public class EndpointConfiguration {
/* Ghostscript */
addEndpointToGroup("Ghostscript", "repair");
addEndpointToGroup("Ghostscript", "compress-pdf");
addEndpointToGroup("Ghostscript", "crop");
addEndpointToGroup("Ghostscript", "replace-invert-pdf");
addEndpointToGroup("Ghostscript", "pdf-to-vector");
addEndpointToGroup("Ghostscript", "vector-to-pdf");
/* ImageMagick */
addEndpointToGroup("ImageMagick", "compress-pdf");
@@ -528,6 +538,9 @@ public class EndpointConfiguration {
addEndpointToGroup("Weasyprint", "markdown-to-pdf");
addEndpointToGroup("Weasyprint", "eml-to-pdf");
// veraPDF dependent endpoints
addEndpointToGroup("veraPDF", "verify-pdf");
// Pdftohtml dependent endpoints
addEndpointToGroup("Pdftohtml", "pdf-to-html");
addEndpointToGroup("Pdftohtml", "pdf-to-markdown");
@@ -554,7 +567,7 @@ public class EndpointConfiguration {
disableGroup("enterprise");
}
if (!applicationProperties.getSystem().getEnableUrlToPDF()) {
if (!applicationProperties.getSystem().isEnableUrlToPDF()) {
disableEndpoint("url-to-pdf");
}
}
@@ -578,7 +591,10 @@ public class EndpointConfiguration {
|| "Weasyprint".equals(group)
|| "Pdftohtml".equals(group)
|| "ImageMagick".equals(group)
|| "rar".equals(group);
|| "rar".equals(group)
|| "Calibre".equals(group)
|| "FFmpeg".equals(group)
|| "veraPDF".equals(group);
}
private boolean isEndpointEnabledDirectly(String endpoint) {
@@ -37,7 +37,7 @@ public class AutoJobAspect {
@Around("@annotation(autoJobPostMapping)")
public Object wrapWithJobExecution(
ProceedingJoinPoint joinPoint, AutoJobPostMapping autoJobPostMapping) {
ProceedingJoinPoint joinPoint, AutoJobPostMapping autoJobPostMapping) throws Exception {
// This aspect will run before any audit aspects due to @Order(0)
// Extract parameters from the request and annotation
boolean async = Boolean.parseBoolean(request.getParameter("async"));
@@ -81,6 +81,12 @@ public class AutoJobAspect {
"AutoJobAspect caught exception during job execution: {}",
ex.getMessage(),
ex);
// Rethrow RuntimeException as-is to preserve exception type
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap checked exceptions - GlobalExceptionHandler will unwrap
// BaseAppException
throw new RuntimeException(ex);
}
},
@@ -109,7 +115,8 @@ public class AutoJobAspect {
int maxRetries,
boolean trackProgress,
boolean queueable,
int resourceWeight) {
int resourceWeight)
throws Exception {
// Keep jobId reference for progress tracking in TaskManager
AtomicReference<String> jobIdRef = new AtomicReference<>();
@@ -207,6 +214,12 @@ public class AutoJobAspect {
// If we get here, all retries failed
if (lastException != null) {
// Rethrow RuntimeException as-is to preserve exception type
if (lastException instanceof RuntimeException) {
throw (RuntimeException) lastException;
}
// Wrap checked exceptions - GlobalExceptionHandler will unwrap
// BaseAppException
throw new RuntimeException(
"Job failed after "
+ maxRetries
@@ -37,10 +37,6 @@ public class AppConfig {
private final ApplicationProperties applicationProperties;
@Getter
@Value("${baseUrl:http://localhost}")
private String baseUrl;
@Getter
@Value("${server.servlet.context-path:/}")
private String contextPath;
@@ -49,6 +45,17 @@ public class AppConfig {
@Value("${server.port:8080}")
private String serverPort;
/**
* Get the backend URL from system configuration. Falls back to http://localhost if not
* configured.
*
* @return The backend base URL for SAML/OAuth/API callbacks
*/
public String getBackendUrl() {
String backendUrl = applicationProperties.getSystem().getBackendUrl();
return (backendUrl != null && !backendUrl.isBlank()) ? backendUrl : "http://localhost";
}
@Value("${v2}")
public boolean v2Enabled;
@@ -69,7 +76,7 @@ public class AppConfig {
@Bean(name = "loginEnabled")
public boolean loginEnabled() {
return applicationProperties.getSecurity().getEnableLogin();
return applicationProperties.getSecurity().isEnableLogin();
}
@Bean(name = "appName")
@@ -113,9 +120,7 @@ public class AppConfig {
@Bean(name = "enableAlphaFunctionality")
public boolean enableAlphaFunctionality() {
return applicationProperties.getSystem().getEnableAlphaFunctionality() != null
? applicationProperties.getSystem().getEnableAlphaFunctionality()
: false;
return applicationProperties.getSystem().isEnableAlphaFunctionality();
}
@Bean(name = "rateLimit")
@@ -258,9 +263,14 @@ public class AppConfig {
return "NORMAL";
}
@Bean(name = "disablePixel")
public boolean disablePixel() {
return Boolean.parseBoolean(env.getProperty("DISABLE_PIXEL", "false"));
@Bean(name = "scarfEnabled")
public boolean scarfEnabled() {
return applicationProperties.getSystem().isScarfEnabled();
}
@Bean(name = "posthogEnabled")
public boolean posthogEnabled() {
return applicationProperties.getSystem().isPosthogEnabled();
}
@Bean(name = "machineType")
@@ -2,6 +2,7 @@ package stirling.software.common.configuration;
import java.io.File;
import java.nio.file.Paths;
import java.util.Locale;
import lombok.extern.slf4j.Slf4j;
@@ -61,7 +62,7 @@ public class InstallationPathConfig {
private static String initializeBasePath() {
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
String os = System.getProperty("os.name").toLowerCase();
String os = System.getProperty("os.name").toLowerCase(Locale.ROOT);
if (os.contains("win")) {
return Paths.get(
System.getenv("APPDATA"), // parent path
@@ -2,6 +2,9 @@ package stirling.software.common.configuration;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Configuration;
@@ -10,8 +13,12 @@ import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.ApplicationProperties.CustomPaths;
import stirling.software.common.model.ApplicationProperties.CustomPaths.Operations;
import stirling.software.common.model.ApplicationProperties.CustomPaths.Pipeline;
import stirling.software.common.model.ApplicationProperties.System;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.UnoServerPool;
@Slf4j
@Configuration
@@ -19,8 +26,18 @@ import stirling.software.common.model.ApplicationProperties.CustomPaths.Pipeline
public class RuntimePathConfig {
private final ApplicationProperties properties;
private final String basePath;
// Operation paths
private final String weasyPrintPath;
private final String unoConvertPath;
private final String calibrePath;
private final String ocrMyPdfPath;
private final String sOfficePath;
// Tesseract data path
private final String tessDataPath;
private final List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> unoServerEndpoints;
// Pipeline paths
private final String pipelineWatchedFoldersPath;
@@ -37,7 +54,10 @@ public class RuntimePathConfig {
String defaultFinishedFolders = Path.of(this.pipelinePath, "finishedFolders").toString();
String defaultWebUIConfigs = Path.of(this.pipelinePath, "defaultWebUIConfigs").toString();
Pipeline pipeline = properties.getSystem().getCustomPaths().getPipeline();
System system = properties.getSystem();
CustomPaths customPaths = system.getCustomPaths();
Pipeline pipeline = customPaths.getPipeline();
this.pipelineWatchedFoldersPath =
resolvePath(
@@ -57,8 +77,11 @@ public class RuntimePathConfig {
// Initialize Operation paths
String defaultWeasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint";
String defaultUnoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert";
String defaultCalibrePath = isDocker ? "/opt/calibre/ebook-convert" : "ebook-convert";
String defaultOcrMyPdfPath = isDocker ? "/usr/bin/ocrmypdf" : "ocrmypdf";
String defaultSOfficePath = isDocker ? "/usr/bin/soffice" : "soffice";
Operations operations = properties.getSystem().getCustomPaths().getOperations();
Operations operations = customPaths.getOperations();
this.weasyPrintPath =
resolvePath(
defaultWeasyPrintPath,
@@ -67,6 +90,39 @@ public class RuntimePathConfig {
resolvePath(
defaultUnoConvertPath,
operations != null ? operations.getUnoconvert() : null);
this.calibrePath =
resolvePath(
defaultCalibrePath, operations != null ? operations.getCalibre() : null);
this.ocrMyPdfPath =
resolvePath(
defaultOcrMyPdfPath, operations != null ? operations.getOcrmypdf() : null);
this.sOfficePath =
resolvePath(
defaultSOfficePath, operations != null ? operations.getSoffice() : null);
// Initialize Tesseract data path
// Priority: config setting > TESSDATA_PREFIX env var > default path
String tessPath = system.getTessdataDir();
String tessdataPrefix = java.lang.System.getenv("TESSDATA_PREFIX");
String defaultPath = "/usr/share/tesseract-ocr/5/tessdata";
if (tessPath != null && !tessPath.isEmpty()) {
this.tessDataPath = tessPath;
} else if (tessdataPrefix != null && !tessdataPrefix.isEmpty()) {
this.tessDataPath = tessdataPrefix;
} else {
this.tessDataPath = defaultPath;
}
log.info("Using Tesseract data path: {}", this.tessDataPath);
ApplicationProperties.ProcessExecutor processExecutor = properties.getProcessExecutor();
int libreOfficeLimit = 1;
if (processExecutor != null && processExecutor.getSessionLimit() != null) {
libreOfficeLimit = processExecutor.getSessionLimit().getLibreOfficeSessionLimit();
}
this.unoServerEndpoints = buildUnoServerEndpoints(processExecutor, libreOfficeLimit);
ProcessExecutor.setUnoServerPool(new UnoServerPool(this.unoServerEndpoints));
}
private String resolvePath(String defaultPath, String customPath) {
@@ -76,4 +132,69 @@ public class RuntimePathConfig {
private boolean isRunningInDocker() {
return Files.exists(Path.of("/.dockerenv"));
}
private List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> buildUnoServerEndpoints(
ApplicationProperties.ProcessExecutor processExecutor, int sessionLimit) {
if (processExecutor == null) {
log.warn("ProcessExecutor config missing; defaulting to a single UNO endpoint.");
return Collections.singletonList(
new ApplicationProperties.ProcessExecutor.UnoServerEndpoint());
}
if (!processExecutor.isAutoUnoServer()) {
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> configured =
sanitizeUnoServerEndpoints(processExecutor.getUnoServerEndpoints());
if (!configured.isEmpty()) {
// 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.",
configured.size(),
sessionLimit);
}
return configured;
}
log.warn(
"autoUnoServer disabled but no unoServerEndpoints configured; defaulting to 127.0.0.1:2003.");
return Collections.singletonList(
new ApplicationProperties.ProcessExecutor.UnoServerEndpoint());
}
int count = sessionLimit > 0 ? sessionLimit : 1;
return buildAutoUnoServerEndpoints(count);
}
private List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint>
buildAutoUnoServerEndpoints(int count) {
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints = new ArrayList<>();
int basePort = 2003;
for (int i = 0; i < count; i++) {
ApplicationProperties.ProcessExecutor.UnoServerEndpoint endpoint =
new ApplicationProperties.ProcessExecutor.UnoServerEndpoint();
endpoint.setHost("127.0.0.1");
endpoint.setPort(basePort + (i * 2));
endpoints.add(endpoint);
}
return endpoints;
}
private List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint>
sanitizeUnoServerEndpoints(
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints) {
if (endpoints == null || endpoints.isEmpty()) {
return Collections.emptyList();
}
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> sanitized = new ArrayList<>();
for (ApplicationProperties.ProcessExecutor.UnoServerEndpoint endpoint : endpoints) {
if (endpoint == null) {
continue;
}
String host = endpoint.getHost();
int port = endpoint.getPort();
if (host == null || host.isBlank() || port <= 0) {
continue;
}
sanitized.add(endpoint);
}
return sanitized;
}
}
@@ -12,6 +12,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
@@ -60,6 +61,7 @@ public class ApplicationProperties {
private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated();
private Mail mail = new Mail();
private Telegram telegram = new Telegram();
private Premium premium = new Premium();
@@ -68,6 +70,7 @@ public class ApplicationProperties {
private AutoPipeline autoPipeline = new AutoPipeline();
private ProcessExecutor processExecutor = new ProcessExecutor();
private PdfEditor pdfEditor = new PdfEditor();
@Bean
public PropertySource<?> dynamicYamlPropertySource(ConfigurableEnvironment environment)
@@ -100,6 +103,46 @@ public class ApplicationProperties {
private String outputFolder;
}
@Data
public static class PdfEditor {
private Cache cache = new Cache();
private FontNormalization fontNormalization = new FontNormalization();
private CffConverter cffConverter = new CffConverter();
private Type3 type3 = new Type3();
private String fallbackFont = "classpath:/static/fonts/NotoSans-Regular.ttf";
@Data
public static class Cache {
private long maxBytes = -1;
private int maxPercent = 20;
}
@Data
public static class FontNormalization {
private boolean enabled = false;
}
@Data
public static class CffConverter {
private boolean enabled = true;
private String method = "python";
private String pythonCommand = "/opt/venv/bin/python3";
private String pythonScript = "/scripts/convert_cff_to_ttf.py";
private String fontforgeCommand = "fontforge";
}
@Data
public static class Type3 {
private Library library = new Library();
@Data
public static class Library {
private boolean enabled = true;
private String index = "classpath:/type3/library/index.json";
}
}
}
@Data
public static class Legal {
private String termsAndConditions;
@@ -111,7 +154,7 @@ public class ApplicationProperties {
@Data
public static class Security {
private Boolean enableLogin;
private boolean enableLogin;
private InitialLogin initialLogin = new InitialLogin();
private OAUTH2 oauth2 = new OAUTH2();
private SAML2 saml2 = new SAML2();
@@ -286,7 +329,7 @@ public class ApplicationProperties {
private KeycloakProvider keycloak = new KeycloakProvider();
public Provider get(String registrationId) throws UnsupportedProviderException {
return switch (registrationId.toLowerCase()) {
return switch (registrationId.toLowerCase(Locale.ROOT)) {
case "google" -> getGoogle();
case "github" -> getGithub();
case "keycloak" -> getKeycloak();
@@ -294,8 +337,8 @@ public class ApplicationProperties {
throw new UnsupportedProviderException(
"Logout from the provider "
+ registrationId
+ " is not supported. "
+ "Report it at https://github.com/Stirling-Tools/Stirling-PDF/issues");
+ " is not supported. Report it at"
+ " https://github.com/Stirling-Tools/Stirling-PDF/issues");
};
}
}
@@ -348,30 +391,43 @@ public class ApplicationProperties {
@Data
public static class System {
private String defaultLocale;
private Boolean googlevisibility;
private boolean googlevisibility;
private boolean showUpdate;
private Boolean showUpdateOnlyAdmin;
private boolean showUpdateOnlyAdmin;
private boolean showSettingsWhenNoLogin = true;
private boolean customHTMLFiles;
private String tessdataDir;
private Boolean enableAlphaFunctionality;
private boolean enableAlphaFunctionality;
private Boolean enableAnalytics;
private Boolean enablePosthog;
private Boolean enableScarf;
private Boolean enableDesktopInstallSlide;
private Datasource datasource;
private Boolean disableSanitize;
private boolean disableSanitize;
private int maxDPI;
private Boolean enableUrlToPDF;
private boolean enableUrlToPDF;
private Html html = new Html();
private CustomPaths customPaths = new CustomPaths();
private String fileUploadLimit;
private TempFileManagement tempFileManagement = new TempFileManagement();
private DatabaseBackup databaseBackup = new DatabaseBackup();
private List<String> corsAllowedOrigins = new ArrayList<>();
private String
frontendUrl; // Base URL for frontend (used for invite links, etc.). If not set,
private String backendUrl; // Backend base URL for SAML/OAuth/API callbacks (e.g.
// 'http://localhost:8080', 'https://api.example.com'). Required for
// SSO.
private String frontendUrl; // Frontend URL for invite email links (e.g.
// falls back to backend URL.
// 'https://app.example.com'). If not set, falls back to backendUrl.
private boolean enableMobileScanner = false; // Enable mobile phone QR code upload feature
private MobileScannerSettings mobileScannerSettings = new MobileScannerSettings();
@Data
public static class MobileScannerSettings {
private boolean convertToPdf = true; // Whether to automatically convert images to PDF
private String imageResolution = "full"; // Options: "full", "reduced"
private String pageFormat = "A4"; // Options: "keep", "A4", "letter"
private boolean stretchToFit = false; // Whether to stretch image to fill page
}
public boolean isAnalyticsEnabled() {
return this.getEnableAnalytics() != null && this.getEnableAnalytics();
@@ -411,6 +467,9 @@ public class ApplicationProperties {
public static class Operations {
private String weasyprint;
private String unoconvert;
private String calibre;
private String ocrmypdf;
private String soffice;
}
}
@@ -493,10 +552,10 @@ public class ApplicationProperties {
@Override
public String toString() {
return """
Driver {
driverName='%s'
}
"""
Driver {
driverName='%s'
}
"""
.formatted(driverName);
}
}
@@ -528,7 +587,7 @@ public class ApplicationProperties {
@Data
public static class Metrics {
private Boolean enabled;
private boolean enabled;
}
@Data
@@ -536,6 +595,7 @@ public class ApplicationProperties {
@ToString.Exclude private String key;
private String UUID;
private String appVersion;
private Boolean isNewServer;
}
// TODO: Remove post migration
@@ -548,6 +608,7 @@ public class ApplicationProperties {
private boolean ssoAutoLogin;
private CustomMetadata customMetadata = new CustomMetadata();
@Deprecated
@Data
public static class CustomMetadata {
private boolean autoUpdateMetadata;
@@ -555,16 +616,23 @@ public class ApplicationProperties {
private String creator;
private String producer;
@Deprecated
public String getCreator() {
return creator == null || creator.trim().isEmpty() ? "Stirling-PDF" : creator;
}
@Deprecated
public String getProducer() {
return producer == null || producer.trim().isEmpty() ? "Stirling-PDF" : producer;
}
}
}
/**
* Mail server configuration properties.
*
* @since 0.46.1
*/
@Data
public static class Mail {
private boolean enabled;
@@ -587,6 +655,102 @@ public class ApplicationProperties {
private Boolean sslCheckServerIdentity;
}
/**
* Telegram bot configuration properties.
*
* @since 2.2.x
*/
@Data
public static class Telegram {
private Boolean enabled = false;
@ToString.Exclude private String botToken;
private String botUsername;
private String pipelineInboxFolder = "telegram";
private Boolean customFolderSuffix = false;
private Boolean enableAllowUserIDs = false;
private List<Long> allowUserIDs = new ArrayList<>();
private Boolean enableAllowChannelIDs = false;
private List<Long> allowChannelIDs = new ArrayList<>();
private long processingTimeoutSeconds = 180;
private long pollingIntervalMillis = 2000;
private Feedback feedback = new Feedback();
/**
* Configuration for feedback messages sent by the Telegram bot.
*
* @since 2.2.x
*/
@Data
public static class Feedback {
private Channel channel = new Channel();
private User user = new User();
/**
* Channel-specific feedback settings.
*
* @since 2.2.x
*/
@Data
public static class Channel {
/**
* Set to {@code false} to hide/suppress "no valid document" feedback messages to
* the channel (to avoid spam).
*/
private Boolean noValidDocument = true;
/**
* Set to {@code false} to hide/suppress generic error feedback messages to the
* channel (to avoid spam).
*/
private Boolean errorMessage = true;
/**
* Set to {@code false} to hide/suppress processing error feedback messages to the
* channel (to avoid spam).
*/
private Boolean errorProcessing = true;
/**
* Set to {@code false} to hide/suppress "processing" feedback messages to the
* channel (to avoid spam).
*/
private Boolean processing = true;
}
/**
* User-specific feedback settings.
*
* @since 2.2.x
*/
@Data
public static class User {
/**
* Set to {@code false} to hide/suppress "no valid document" feedback messages to
* users (to avoid spam).
*/
private Boolean noValidDocument = true;
/**
* Set to {@code false} to hide/suppress generic error feedback messages to users
* (to avoid spam).
*/
private Boolean errorMessage = true;
/**
* Set to {@code false} to hide/suppress processing error feedback messages to users
* (to avoid spam).
*/
private Boolean errorProcessing = true;
/**
* Set to {@code false} to hide/suppress "processing" feedback messages to users (to
* avoid spam).
*/
private Boolean processing = true;
}
}
}
@Data
public static class Premium {
private boolean enabled;
@@ -624,6 +788,25 @@ public class ApplicationProperties {
public static class EnterpriseFeatures {
private PersistentMetrics persistentMetrics = new PersistentMetrics();
private Audit audit = new Audit();
private DatabaseNotifications databaseNotifications = new DatabaseNotifications();
@Data
public static class DatabaseNotifications {
private Backup backups = new Backup();
private Imports imports = new Imports();
@Data
public static class Backup {
private boolean successful = false;
private boolean failed = false;
}
@Data
public static class Imports {
private boolean successful = false;
private boolean failed = false;
}
}
@Data
public static class Audit {
@@ -644,6 +827,16 @@ public class ApplicationProperties {
public static class ProcessExecutor {
private SessionLimit sessionLimit = new SessionLimit();
private TimeoutMinutes timeoutMinutes = new TimeoutMinutes();
private boolean autoUnoServer = true;
private List<UnoServerEndpoint> unoServerEndpoints = new ArrayList<>();
@Data
public static class UnoServerEndpoint {
private String host = "127.0.0.1";
private int port = 2003;
private String hostLocation = "auto"; // auto|local|remote
private String protocol = "http"; // http|https
}
@Data
public static class SessionLimit {
@@ -658,6 +851,7 @@ public class ApplicationProperties {
private int tesseractSessionLimit;
private int ghostscriptSessionLimit;
private int ocrMyPdfSessionLimit;
private int ffmpegSessionLimit;
public int getQpdfSessionLimit() {
return qpdfSessionLimit > 0 ? qpdfSessionLimit : 2;
@@ -702,6 +896,10 @@ public class ApplicationProperties {
public int getOcrMyPdfSessionLimit() {
return ocrMyPdfSessionLimit > 0 ? ocrMyPdfSessionLimit : 2;
}
public int getFfmpegSessionLimit() {
return ffmpegSessionLimit > 0 ? ffmpegSessionLimit : 2;
}
}
@Data
@@ -730,6 +928,7 @@ public class ApplicationProperties {
private long qpdfTimeoutMinutes;
private long ghostscriptTimeoutMinutes;
private long ocrMyPdfTimeoutMinutes;
private long ffmpegTimeoutMinutes;
public long getTesseractTimeoutMinutes() {
return tesseractTimeoutMinutes > 0 ? tesseractTimeoutMinutes : 30;
@@ -774,6 +973,10 @@ public class ApplicationProperties {
public long getOcrMyPdfTimeoutMinutes() {
return ocrMyPdfTimeoutMinutes > 0 ? ocrMyPdfTimeoutMinutes : 30;
}
public long getFfmpegTimeoutMinutes() {
return ffmpegTimeoutMinutes > 0 ? ffmpegTimeoutMinutes : 30;
}
}
}
}
@@ -30,11 +30,11 @@ public class FileInfo {
// Formats the file size into a human-readable string.
public String getFormattedFileSize() {
if (fileSize >= 1024 * 1024 * 1024) {
return String.format(Locale.US, "%.2f GB", fileSize / (1024.0 * 1024 * 1024));
return String.format(Locale.ROOT, "%.2f GB", fileSize / (1024.0 * 1024 * 1024));
} else if (fileSize >= 1024 * 1024) {
return String.format(Locale.US, "%.2f MB", fileSize / (1024.0 * 1024));
return String.format(Locale.ROOT, "%.2f MB", fileSize / (1024.0 * 1024));
} else if (fileSize >= 1024) {
return String.format(Locale.US, "%.2f KB", fileSize / 1024.0);
return String.format(Locale.ROOT, "%.2f KB", fileSize / 1024.0);
} else {
return String.format("%d Bytes", fileSize);
}
@@ -7,6 +7,7 @@ import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.pdfbox.Loader;
@@ -249,7 +250,7 @@ public class CustomPDFDocumentFactory {
log.debug(
"Memory status - Free: {}MB ({}%), Used: {}MB, Max: {}MB",
actualFreeMemory / (1024 * 1024),
String.format("%.2f", freeMemoryPercent),
String.format(Locale.ROOT, "%.2f", freeMemoryPercent),
usedMemory / (1024 * 1024),
maxMemory / (1024 * 1024));
@@ -258,7 +259,7 @@ public class CustomPDFDocumentFactory {
|| actualFreeMemory < MIN_FREE_MEMORY_BYTES) {
log.debug(
"Low memory detected ({}%), forcing file-based cache",
String.format("%.2f", freeMemoryPercent));
String.format(Locale.ROOT, "%.2f", freeMemoryPercent));
return createScratchFileCacheFunction(MemoryUsageSetting.setupTempFileOnly());
} else if (contentSize < SMALL_FILE_THRESHOLD) {
log.debug("Using memory-only cache for small document ({}KB)", contentSize / 1024);
@@ -477,11 +478,6 @@ public class CustomPDFDocumentFactory {
return file;
}
/** Create a uniquely named temporary directory */
private Path createTempDirectory(String prefix) throws IOException {
return Files.createTempDirectory(prefix + tempCounter.incrementAndGet() + "-");
}
/** Create new document bytes based on an existing document */
public byte[] createNewBytesBasedOnOldDocument(byte[] oldDocument) throws IOException {
try (PDDocument document = load(oldDocument)) {
@@ -253,6 +253,22 @@ public class JobExecutorService {
log.error("Synchronous job timed out after {} ms", timeoutToUse);
return ResponseEntity.internalServerError()
.body(Map.of("error", "Job timed out after " + timeoutToUse + " ms"));
} catch (RuntimeException e) {
// Check if this is a wrapped typed exception that should be handled by
// GlobalExceptionHandler
Throwable cause = e.getCause();
if (cause instanceof stirling.software.common.util.ExceptionUtils.BaseAppException
|| cause
instanceof
stirling.software.common.util.ExceptionUtils
.BaseValidationException) {
// Rethrow so GlobalExceptionHandler can handle with proper HTTP status codes
throw e;
}
// Handle other RuntimeExceptions as generic errors
log.error("Error executing synchronous job: {}", e.getMessage(), e);
return ResponseEntity.internalServerError()
.body(Map.of("error", "Job failed: " + e.getMessage()));
} catch (Exception e) {
log.error("Error executing synchronous job: {}", e.getMessage(), e);
// Construct a JSON error response
@@ -0,0 +1,438 @@
package stirling.software.common.service;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import lombok.extern.slf4j.Slf4j;
/**
* Service for handling mobile scanner file uploads and temporary storage. Files are stored
* temporarily and automatically cleaned up after 10 minutes or upon retrieval.
*/
@Service
@Slf4j
public class MobileScannerService {
private static final long SESSION_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
private final Map<String, SessionData> activeSessions = new ConcurrentHashMap<>();
private final Path tempDirectory;
public MobileScannerService() throws IOException {
// Create temp directory for mobile scanner uploads
this.tempDirectory =
Paths.get(System.getProperty("java.io.tmpdir"), "stirling-mobile-scanner");
Files.createDirectories(tempDirectory);
log.info("Mobile scanner temp directory: {}", tempDirectory);
}
/**
* Register a new session (called by desktop when QR code is generated)
*
* @param sessionId Unique session identifier
* @return SessionInfo with creation time and expiry
*/
public SessionInfo createSession(String sessionId) {
validateSessionId(sessionId);
SessionData session = new SessionData(sessionId);
activeSessions.put(sessionId, session);
log.info("Created mobile scanner session: {}", sessionId);
return new SessionInfo(
sessionId,
session.createdAt,
session.createdAt + SESSION_TIMEOUT_MS,
SESSION_TIMEOUT_MS);
}
/**
* Validate if a session exists and is not expired
*
* @param sessionId Session identifier to validate
* @return SessionInfo if valid, null if invalid/expired
*/
public SessionInfo validateSession(String sessionId) {
SessionData session = activeSessions.get(sessionId);
if (session == null) {
return null;
}
long now = System.currentTimeMillis();
long expiryTime = session.getLastAccessTime() + SESSION_TIMEOUT_MS;
// Check if expired
if (now > expiryTime) {
deleteSession(sessionId);
return null;
}
session.updateLastAccess();
return new SessionInfo(sessionId, session.createdAt, expiryTime, SESSION_TIMEOUT_MS);
}
/**
* Stores uploaded files for a session
*
* @param sessionId Unique session identifier
* @param files Files to upload
* @throws IOException If file storage fails
*/
public void uploadFiles(String sessionId, List<MultipartFile> files) throws IOException {
validateSessionId(sessionId);
SessionData session =
activeSessions.computeIfAbsent(sessionId, id -> new SessionData(sessionId));
// Create session directory
Path sessionDir = getSafeSessionDirectory(sessionId);
Files.createDirectories(sessionDir);
// Save each file
for (MultipartFile file : files) {
if (file.isEmpty()) {
continue;
}
String originalFilename = file.getOriginalFilename();
if (originalFilename == null || originalFilename.isBlank()) {
originalFilename = "upload-" + System.currentTimeMillis();
}
// Sanitize filename
String safeFilename = sanitizeFilename(originalFilename);
Path filePath = sessionDir.resolve(safeFilename).normalize().toAbsolutePath();
// Ensure resulting path stays within the session directory
if (!filePath.startsWith(sessionDir)) {
throw new IOException("Invalid filename");
}
// Handle duplicate filenames
int counter = 1;
while (Files.exists(filePath)) {
String nameWithoutExt = safeFilename.replaceFirst("[.][^.]+$", "");
String ext =
safeFilename.contains(".")
? safeFilename.substring(safeFilename.lastIndexOf("."))
: "";
safeFilename = nameWithoutExt + "-" + counter + ext;
filePath = sessionDir.resolve(safeFilename).normalize().toAbsolutePath();
if (!filePath.startsWith(sessionDir)) {
throw new IOException("Invalid filename");
}
counter++;
}
file.transferTo(filePath);
session.addFile(new FileMetadata(safeFilename, file.getSize(), file.getContentType()));
log.info(
"Uploaded file for session {}: {} ({} bytes)",
sessionId,
safeFilename,
file.getSize());
}
session.updateLastAccess();
}
/**
* Retrieves file metadata for a session
*
* @param sessionId Session identifier
* @return List of file metadata, or empty list if session doesn't exist
*/
public List<FileMetadata> getSessionFiles(String sessionId) {
SessionData session = activeSessions.get(sessionId);
if (session == null) {
return List.of();
}
session.updateLastAccess();
return new ArrayList<>(session.getFiles());
}
/**
* Retrieves actual file data for download
*
* @param sessionId Session identifier
* @param filename Filename to retrieve
* @return File path
* @throws IOException If file not found or session doesn't exist
*/
public Path getFile(String sessionId, String filename) throws IOException {
SessionData session = activeSessions.get(sessionId);
if (session == null) {
throw new IOException("Session not found: " + sessionId);
}
Path filePath = getSafeFilePath(sessionId, filename);
if (!Files.exists(filePath)) {
throw new IOException("File not found: " + filename);
}
session.updateLastAccess();
session.markFileAsDownloaded(filename);
return filePath;
}
/**
* Deletes a file after it has been served to the client. Should be called after successful
* download.
*
* @param sessionId Session identifier
* @param filename Filename to delete
*/
public void deleteFileAfterDownload(String sessionId, String filename) {
try {
Path filePath = getSafeFilePath(sessionId, filename);
Files.deleteIfExists(filePath);
log.info("Deleted file after download: {}/{}", sessionId, filename);
// Check if all files have been downloaded - if so, delete the entire session
SessionData session = activeSessions.get(sessionId);
if (session != null && session.allFilesDownloaded()) {
deleteSession(sessionId);
log.info("All files downloaded - deleted session: {}", sessionId);
}
} catch (IOException | IllegalArgumentException e) {
log.warn("Failed to delete file after download: {}/{}", sessionId, filename, e);
}
}
/**
* Deletes a session and all its files
*
* @param sessionId Session to delete
*/
public void deleteSession(String sessionId) {
SessionData session = activeSessions.remove(sessionId);
if (session != null) {
try {
Path sessionDir = getSafeSessionDirectory(sessionId);
if (Files.exists(sessionDir)) {
// Delete all files in session directory
Files.walk(sessionDir)
.sorted(
(a, b) ->
-a.compareTo(b)) // Reverse order to delete files before
// directory
.forEach(
path -> {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
log.warn("Failed to delete file: {}", path, e);
}
});
}
log.info("Deleted session: {}", sessionId);
} catch (IllegalArgumentException e) {
log.warn(
"Refused to delete session with invalid sessionId '{}': {}",
sessionId,
e.getMessage());
} catch (IOException e) {
log.error("Error deleting session directory: {}", sessionId, e);
}
}
}
/** Scheduled cleanup of expired sessions (runs every 5 minutes) */
@Scheduled(fixedRate = 5 * 60 * 1000)
public void cleanupExpiredSessions() {
long now = System.currentTimeMillis();
List<String> expiredSessions = new ArrayList<>();
activeSessions.forEach(
(sessionId, session) -> {
if (now - session.getLastAccessTime() > SESSION_TIMEOUT_MS) {
expiredSessions.add(sessionId);
}
});
if (!expiredSessions.isEmpty()) {
log.info("Cleaning up {} expired mobile scanner sessions", expiredSessions.size());
expiredSessions.forEach(this::deleteSession);
}
}
private void validateSessionId(String sessionId) {
if (sessionId == null || sessionId.isBlank()) {
throw new IllegalArgumentException("Session ID cannot be empty");
}
// Basic validation: alphanumeric and hyphens only
if (!sessionId.matches("[a-zA-Z0-9-]+")) {
throw new IllegalArgumentException("Invalid session ID format");
}
}
private String sanitizeFilename(String filename) {
// Remove path traversal attempts and dangerous characters
String sanitized = filename.replaceAll("[^a-zA-Z0-9._-]", "_");
// Ensure we have a non-empty, safe filename
if (sanitized.isBlank()) {
sanitized = "upload-" + System.currentTimeMillis();
}
return sanitized;
}
/**
* Safely resolves and validates a session directory path to prevent directory traversal
* attacks.
*
* @param sessionId Session identifier
* @return Normalized absolute path to the session directory
* @throws IllegalArgumentException if the resolved path escapes the temp directory
*/
private Path getSafeSessionDirectory(String sessionId) {
validateSessionId(sessionId);
Path baseDir = tempDirectory.normalize().toAbsolutePath();
Path sessionDir = baseDir.resolve(sessionId).normalize().toAbsolutePath();
// Verify the resolved path is still within the temp directory
if (!sessionDir.startsWith(baseDir)) {
throw new IllegalArgumentException("Invalid session ID: path traversal detected");
}
return sessionDir;
}
/**
* Safely resolves and validates a file path within a session directory to prevent directory
* traversal attacks.
*
* @param sessionId Session identifier
* @param filename Filename within the session
* @return Normalized absolute path to the file
* @throws IOException if the resolved path escapes the session directory
*/
private Path getSafeFilePath(String sessionId, String filename) throws IOException {
if (filename == null || filename.isBlank()) {
throw new IOException("Filename cannot be empty");
}
// Additional validation: reject filenames with path separators or parent references
if (filename.contains("..") || filename.contains("/") || filename.contains("\\")) {
throw new IOException(
"Invalid filename: contains path separators or parent references");
}
Path sessionDir = getSafeSessionDirectory(sessionId);
Path filePath = sessionDir.resolve(filename).normalize().toAbsolutePath();
// Verify the resolved path is still within the session directory
if (!filePath.startsWith(sessionDir)) {
throw new IOException("Invalid filename: path traversal detected");
}
return filePath;
}
/** Session information for client */
public static class SessionInfo {
private final String sessionId;
private final long createdAt;
private final long expiresAt;
private final long timeoutMs;
public SessionInfo(String sessionId, long createdAt, long expiresAt, long timeoutMs) {
this.sessionId = sessionId;
this.createdAt = createdAt;
this.expiresAt = expiresAt;
this.timeoutMs = timeoutMs;
}
public String getSessionId() {
return sessionId;
}
public long getCreatedAt() {
return createdAt;
}
public long getExpiresAt() {
return expiresAt;
}
public long getTimeoutMs() {
return timeoutMs;
}
}
/** File metadata for client */
public static class FileMetadata {
private final String filename;
private final long size;
private final String contentType;
public FileMetadata(String filename, long size, String contentType) {
this.filename = filename;
this.size = size;
this.contentType = contentType;
}
public String getFilename() {
return filename;
}
public long getSize() {
return size;
}
public String getContentType() {
return contentType;
}
}
/** Session data tracking */
private static class SessionData {
private final String sessionId;
private final List<FileMetadata> files = new ArrayList<>();
private final Map<String, Boolean> downloadedFiles = new HashMap<>();
private final long createdAt;
private long lastAccessTime;
public SessionData(String sessionId) {
this.sessionId = sessionId;
this.createdAt = System.currentTimeMillis();
this.lastAccessTime = createdAt;
}
public void addFile(FileMetadata file) {
files.add(file);
downloadedFiles.put(file.getFilename(), false);
}
public List<FileMetadata> getFiles() {
return files;
}
public void markFileAsDownloaded(String filename) {
downloadedFiles.put(filename, true);
}
public boolean allFilesDownloaded() {
return !downloadedFiles.isEmpty()
&& downloadedFiles.values().stream().allMatch(downloaded -> downloaded);
}
public void updateLastAccess() {
this.lastAccessTime = System.currentTimeMillis();
}
public long getLastAccessTime() {
return lastAccessTime;
}
}
}
@@ -253,7 +253,7 @@ public class PostHogService {
addIfNotEmpty(
properties,
"security_enableLogin",
applicationProperties.getSecurity().getEnableLogin());
applicationProperties.getSecurity().isEnableLogin());
addIfNotEmpty(properties, "security_csrfDisabled", true);
addIfNotEmpty(
properties,
@@ -299,13 +299,13 @@ public class PostHogService {
addIfNotEmpty(
properties,
"system_googlevisibility",
applicationProperties.getSystem().getGooglevisibility());
applicationProperties.getSystem().isGooglevisibility());
addIfNotEmpty(
properties, "system_showUpdate", applicationProperties.getSystem().isShowUpdate());
addIfNotEmpty(
properties,
"system_showUpdateOnlyAdmin",
applicationProperties.getSystem().getShowUpdateOnlyAdmin());
applicationProperties.getSystem().isShowUpdateOnlyAdmin());
addIfNotEmpty(
properties,
"system_customHTMLFiles",
@@ -317,7 +317,7 @@ public class PostHogService {
addIfNotEmpty(
properties,
"system_enableAlphaFunctionality",
applicationProperties.getSystem().getEnableAlphaFunctionality());
applicationProperties.getSystem().isEnableAlphaFunctionality());
addIfNotEmpty(
properties,
"system_enableAnalytics",
@@ -337,7 +337,7 @@ public class PostHogService {
// Capture Metrics properties
addIfNotEmpty(
properties, "metrics_enabled", applicationProperties.getMetrics().getEnabled());
properties, "metrics_enabled", applicationProperties.getMetrics().isEnabled());
// Capture EnterpriseEdition properties
addIfNotEmpty(
@@ -5,6 +5,7 @@ import java.lang.management.MemoryMXBean;
import java.lang.management.OperatingSystemMXBean;
import java.time.Duration;
import java.time.Instant;
import java.util.Locale;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@@ -173,8 +174,8 @@ public class ResourceMonitor {
log.info("System resource status changed from {} to {}", oldStatus, newStatus);
log.info(
"Current metrics - CPU: {}%, Memory: {}%, Free Memory: {} MB",
String.format("%.1f", cpuUsage * 100),
String.format("%.1f", memoryUsage * 100),
String.format(Locale.ROOT, "%.1f", cpuUsage * 100),
String.format(Locale.ROOT, "%.1f", memoryUsage * 100),
freeMemory / (1024 * 1024));
}
} catch (Exception e) {
@@ -5,6 +5,7 @@ import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.Locale;
import java.util.regex.Pattern;
import org.springframework.stereotype.Service;
@@ -83,7 +84,7 @@ public class SsrfProtectionService {
return false;
}
return config.getAllowedDomains().contains(host.toLowerCase());
return config.getAllowedDomains().contains(host.toLowerCase(Locale.ROOT));
} catch (Exception e) {
log.debug("Failed to parse URL for MAX security check: {}", url, e);
@@ -101,7 +102,7 @@ public class SsrfProtectionService {
return false;
}
String hostLower = host.toLowerCase();
String hostLower = host.toLowerCase(Locale.ROOT);
// Check explicit blocked domains
if (config.getBlockedDomains().contains(hostLower)) {
@@ -111,7 +112,7 @@ public class SsrfProtectionService {
// Check internal TLD patterns
for (String tld : config.getInternalTlds()) {
if (hostLower.endsWith(tld.toLowerCase())) {
if (hostLower.endsWith(tld.toLowerCase(Locale.ROOT))) {
log.debug("URL blocked by internal TLD pattern '{}': {}", tld, url);
return false;
}
@@ -123,9 +124,11 @@ public class SsrfProtectionService {
config.getAllowedDomains().stream()
.anyMatch(
domain ->
hostLower.equals(domain.toLowerCase())
hostLower.equals(domain.toLowerCase(Locale.ROOT))
|| hostLower.endsWith(
"." + domain.toLowerCase()));
"."
+ domain.toLowerCase(
Locale.ROOT)));
if (!isAllowed) {
log.debug("URL not in allowed domains list: {}", url);
@@ -7,6 +7,7 @@ import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
@@ -101,14 +102,16 @@ public class TaskManager {
if (!extractedFiles.isEmpty()) {
jobResult.completeWithFiles(extractedFiles);
log.debug(
"Set multiple file results for job ID: {} with {} files extracted from ZIP",
"Set multiple file results for job ID: {} with {} files extracted from"
+ " ZIP",
jobId,
extractedFiles.size());
return;
}
} catch (Exception e) {
log.warn(
"Failed to extract ZIP file for job {}: {}. Falling back to single file result.",
"Failed to extract ZIP file for job {}: {}. Falling back to single file"
+ " result.",
jobId,
e.getMessage());
}
@@ -342,12 +345,12 @@ public class TaskManager {
/** Check if a file is a ZIP file based on content type and filename */
private boolean isZipFile(String contentType, String fileName) {
if (contentType != null
&& (contentType.equals("application/zip")
|| contentType.equals("application/x-zip-compressed"))) {
&& ("application/zip".equals(contentType)
|| "application/x-zip-compressed".equals(contentType))) {
return true;
}
if (fileName != null && fileName.toLowerCase().endsWith(".zip")) {
if (fileName != null && fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) {
return true;
}
@@ -414,7 +417,7 @@ public class TaskManager {
return MediaType.APPLICATION_OCTET_STREAM_VALUE;
}
String lowerName = fileName.toLowerCase();
String lowerName = fileName.toLowerCase(Locale.ROOT);
if (lowerName.endsWith(".pdf")) {
return MediaType.APPLICATION_PDF_VALUE;
} else if (lowerName.endsWith(".txt")) {
@@ -6,6 +6,7 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import org.apache.commons.io.FilenameUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
@@ -58,31 +59,24 @@ public class CbrUtils {
log.warn(
"Failed to open CBR/RAR archive due to corrupt header: {}",
e.getMessage());
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidFormat",
"Invalid or corrupted CBR/RAR archive. "
+ "The file may be corrupted, use an unsupported RAR format (RAR5+), "
+ "or may not be a valid RAR archive. "
+ "Please ensure the file is a valid RAR archive.");
throw ExceptionUtils.createCbrInvalidFormatException(null);
} catch (RarException e) {
log.warn("Failed to open CBR/RAR archive: {}", e.getMessage());
String errorMessage;
String exMessage = e.getMessage() != null ? e.getMessage() : "";
if (exMessage.contains("encrypted")) {
errorMessage = "Encrypted CBR/RAR archives are not supported.";
throw ExceptionUtils.createCbrEncryptedException();
} else if (exMessage.isEmpty()) {
errorMessage =
"Invalid CBR/RAR archive. "
+ "The file may be encrypted, corrupted, or use an unsupported format.";
throw ExceptionUtils.createCbrInvalidFormatException(
"Invalid CBR/RAR archive. The file may be encrypted, corrupted, or"
+ " use an unsupported format.");
} else {
errorMessage =
throw ExceptionUtils.createCbrInvalidFormatException(
"Invalid CBR/RAR archive: "
+ exMessage
+ ". The file may be encrypted, corrupted, or use an unsupported format.";
+ ". The file may be encrypted, corrupted, or use an"
+ " unsupported format.");
}
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidFormat", errorMessage);
} catch (IOException e) {
log.warn("IO error reading CBR/RAR archive: {}", e.getMessage());
throw ExceptionUtils.createFileProcessingException("CBR extraction", e);
@@ -121,7 +115,8 @@ public class CbrUtils {
if (imageEntries.isEmpty()) {
throw ExceptionUtils.createIllegalArgumentException(
"error.fileProcessing",
"No valid images found in the CBR file. The archive may be empty or contain no supported image formats.");
"No valid images found in the CBR file. The archive may be empty or"
+ " contain no supported image formats.");
}
for (ImageEntryData imageEntry : imageEntries) {
@@ -134,7 +129,12 @@ public class CbrUtils {
new PDRectangle(pdImage.getWidth(), pdImage.getHeight()));
document.addPage(page);
try (PDPageContentStream contentStream =
new PDPageContentStream(document, page)) {
new PDPageContentStream(
document,
page,
PDPageContentStream.AppendMode.OVERWRITE,
true,
true)) {
contentStream.drawImage(pdImage, 0, 0);
}
} catch (IOException e) {
@@ -146,7 +146,8 @@ public class CbrUtils {
if (document.getNumberOfPages() == 0) {
throw ExceptionUtils.createIllegalArgumentException(
"error.fileProcessing",
"No images could be processed from the CBR file. All images may be corrupted or in unsupported formats.");
"No images could be processed from the CBR file. All images may be"
+ " corrupted or in unsupported formats.");
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -159,7 +160,6 @@ public class CbrUtils {
return GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
} catch (IOException e) {
log.warn("Ghostscript optimization failed, returning unoptimized PDF", e);
return pdfBytes;
}
}
@@ -170,17 +170,17 @@ public class CbrUtils {
private void validateCbrFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("File cannot be null or empty");
throw ExceptionUtils.createFileNullOrEmptyException();
}
String filename = file.getOriginalFilename();
if (filename == null) {
throw new IllegalArgumentException("File must have a name");
throw ExceptionUtils.createFileNoNameException();
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
if (!"cbr".equals(extension) && !"rar".equals(extension)) {
throw new IllegalArgumentException("File must be a CBR or RAR archive");
throw ExceptionUtils.createNotCbrFileException();
}
}
@@ -190,7 +190,7 @@ public class CbrUtils {
return false;
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
return "cbr".equals(extension) || "rar".equals(extension);
}
@@ -8,6 +8,7 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
@@ -55,10 +56,10 @@ public class CbzUtils {
new java.io.FileInputStream(tempFile.getFile()));
ZipInputStream zis = new ZipInputStream(bis)) {
if (zis.getNextEntry() == null) {
throw new IllegalArgumentException("Archive is empty or invalid ZIP");
throw ExceptionUtils.createCbzEmptyException();
}
} catch (IOException e) {
throw new IllegalArgumentException("Invalid CBZ/ZIP archive", e);
throw ExceptionUtils.createCbzInvalidFormatException(e);
}
try (PDDocument document = pdfDocumentFactory.createNewDocument();
@@ -83,7 +84,7 @@ public class CbzUtils {
Comparator.comparing(ImageEntryData::name, new NaturalOrderComparator()));
if (imageEntries.isEmpty()) {
throw new IllegalArgumentException("No valid images found in the CBZ file");
throw ExceptionUtils.createCbzNoImagesException();
}
for (ImageEntryData imageEntry : imageEntries) {
@@ -96,7 +97,12 @@ public class CbzUtils {
new PDRectangle(pdImage.getWidth(), pdImage.getHeight()));
document.addPage(page);
try (PDPageContentStream contentStream =
new PDPageContentStream(document, page)) {
new PDPageContentStream(
document,
page,
PDPageContentStream.AppendMode.OVERWRITE,
true,
true)) {
contentStream.drawImage(pdImage, 0, 0);
}
} catch (IOException e) {
@@ -106,8 +112,7 @@ public class CbzUtils {
}
if (document.getNumberOfPages() == 0) {
throw new IllegalArgumentException(
"No images could be processed from the CBZ file");
throw ExceptionUtils.createCbzCorruptedImagesException();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
@@ -119,7 +124,6 @@ public class CbzUtils {
return GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
} catch (IOException e) {
log.warn("Ghostscript optimization failed, returning unoptimized PDF", e);
return pdfBytes;
}
}
@@ -130,17 +134,17 @@ public class CbzUtils {
private void validateCbzFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("File cannot be null or empty");
throw ExceptionUtils.createFileNullOrEmptyException();
}
String filename = file.getOriginalFilename();
if (filename == null) {
throw new IllegalArgumentException("File must have a name");
throw ExceptionUtils.createFileNoNameException();
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
if (!"cbz".equals(extension) && !"zip".equals(extension)) {
throw new IllegalArgumentException("File must be a CBZ or ZIP archive");
throw ExceptionUtils.createNotCbzFileException();
}
}
@@ -150,7 +154,7 @@ public class CbzUtils {
return false;
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
return "cbz".equals(extension) || "zip".equals(extension);
}
@@ -160,7 +164,7 @@ public class CbzUtils {
return false;
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
return "cbz".equals(extension)
|| "zip".equals(extension)
|| "cbr".equals(extension)
@@ -11,6 +11,8 @@ public class CheckProgramInstall {
private static final List<String> PYTHON_COMMANDS = Arrays.asList("python3", "python");
private static boolean pythonAvailableChecked = false;
private static String availablePythonCommand = null;
private static boolean ffmpegAvailableChecked = false;
private static boolean ffmpegAvailable = false;
/**
* Checks which Python command is available and returns it.
@@ -56,4 +58,25 @@ public class CheckProgramInstall {
public static boolean isPythonAvailable() {
return getAvailablePythonCommand() != null;
}
/**
* Checks if FFmpeg is available on the system.
*
* @return true if FFmpeg is installed and accessible, false otherwise.
*/
public static boolean isFfmpegAvailable() {
if (!ffmpegAvailableChecked) {
try {
ProcessExecutorResult result =
ProcessExecutor.getInstance(ProcessExecutor.Processes.FFMPEG)
.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version"));
ffmpegAvailable = true;
} catch (IOException | InterruptedException e) {
ffmpegAvailable = false;
} finally {
ffmpegAvailableChecked = true;
}
}
return ffmpegAvailable;
}
}
@@ -58,14 +58,11 @@ public class ChecksumUtils {
* @throws IOException if reading from the stream fails
*/
public static String checksum(InputStream is, String algorithm) throws IOException {
switch (algorithm.toUpperCase(Locale.ROOT)) {
case "CRC32":
return checksumChecksum(is, new CRC32());
case "ADLER32":
return checksumChecksum(is, new Adler32());
default:
return toHex(checksumBytes(is, algorithm));
}
return switch (algorithm.toUpperCase(Locale.ROOT)) {
case "CRC32" -> checksumChecksum(is, new CRC32());
case "ADLER32" -> checksumChecksum(is, new Adler32());
default -> toHex(checksumBytes(is, algorithm));
};
}
/**
@@ -98,14 +95,13 @@ public class ChecksumUtils {
* @throws IOException if reading from the stream fails
*/
public static String checksumBase64(InputStream is, String algorithm) throws IOException {
switch (algorithm.toUpperCase(Locale.ROOT)) {
case "CRC32":
return Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new CRC32()));
case "ADLER32":
return Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new Adler32()));
default:
return Base64.getEncoder().encodeToString(checksumBytes(is, algorithm));
}
return switch (algorithm.toUpperCase(Locale.ROOT)) {
case "CRC32" ->
Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new CRC32()));
case "ADLER32" ->
Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new Adler32()));
default -> Base64.getEncoder().encodeToString(checksumBytes(is, algorithm));
};
}
/**
@@ -179,7 +175,7 @@ public class ChecksumUtils {
for (Map.Entry<String, Checksum> entry : checksums.entrySet()) {
// Keep value as long and mask to ensure unsigned hex formatting.
long unsigned32 = entry.getValue().getValue() & UNSIGNED_32_BIT_MASK;
results.put(entry.getKey(), String.format("%08x", unsigned32));
results.put(entry.getKey(), String.format(Locale.ROOT, "%08x", unsigned32));
}
return results;
}
@@ -258,7 +254,7 @@ public class ChecksumUtils {
}
// Keep as long and mask to ensure correct unsigned representation.
long unsigned32 = checksum.getValue() & UNSIGNED_32_BIT_MASK;
return String.format("%08x", unsigned32);
return String.format(Locale.ROOT, "%08x", unsigned32);
}
/**
@@ -294,7 +290,7 @@ public class ChecksumUtils {
private static String toHex(byte[] hash) {
StringBuilder sb = new StringBuilder(hash.length * 2);
for (byte b : hash) {
sb.append(String.format("%02x", b));
sb.append(String.format(Locale.ROOT, "%02x", b));
}
return sb.toString();
}
@@ -62,8 +62,7 @@ public class CustomHtmlSanitizer {
.and(new HtmlPolicyBuilder().disallowElements("noscript").toFactory());
public String sanitize(String html) {
boolean disableSanitize =
Boolean.TRUE.equals(applicationProperties.getSystem().getDisableSanitize());
boolean disableSanitize = applicationProperties.getSystem().isDisableSanitize();
return disableSanitize ? html : POLICY.sanitize(html);
}
}
@@ -1,646 +1,417 @@
package stirling.software.common.util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.regex.Pattern;
import java.util.Locale;
import java.util.Objects;
import java.util.stream.Collectors;
import org.springframework.http.MediaType;
import org.simplejavamail.api.email.AttachmentResource;
import org.simplejavamail.api.email.Email;
import org.simplejavamail.api.email.Recipient;
import org.simplejavamail.converter.EmailConverter;
import jakarta.activation.DataSource;
import jakarta.mail.Message.RecipientType;
import lombok.Data;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.api.converters.EmlToPdfRequest;
@Slf4j
@UtilityClass
public class EmlParser {
private static volatile Boolean jakartaMailAvailable = null;
private static volatile Method mimeUtilityDecodeTextMethod = null;
private static volatile boolean mimeUtilityChecked = false;
// Configuration constants
private final int DEFAULT_MAX_ATTACHMENT_MB = 10;
private final long MAX_SIZE_ESTIMATION_BYTES = 500L * 1024 * 1024; // 500MB
private static final Pattern MIME_ENCODED_PATTERN =
RegexPatternUtils.getInstance().getMimeEncodedWordPattern();
// Message constants
private final String NO_CONTENT_MESSAGE = "Email content could not be parsed";
private final String ATTACHMENT_PREFIX = "attachment-";
private static final String DISPOSITION_ATTACHMENT = "attachment";
private static final String TEXT_PLAIN = MediaType.TEXT_PLAIN_VALUE;
private static final String TEXT_HTML = MediaType.TEXT_HTML_VALUE;
private static final String MULTIPART_PREFIX = "multipart/";
private static final String HEADER_CONTENT_TYPE = "content-type:";
private static final String HEADER_CONTENT_DISPOSITION = "content-disposition:";
private static final String HEADER_CONTENT_TRANSFER_ENCODING = "content-transfer-encoding:";
private static final String HEADER_CONTENT_ID = "Content-ID";
private static final String HEADER_SUBJECT = "Subject:";
private static final String HEADER_FROM = "From:";
private static final String HEADER_TO = "To:";
private static final String HEADER_CC = "Cc:";
private static final String HEADER_BCC = "Bcc:";
private static final String HEADER_DATE = "Date:";
private static synchronized boolean isJakartaMailAvailable() {
if (jakartaMailAvailable == null) {
try {
Class.forName("jakarta.mail.internet.MimeMessage");
Class.forName("jakarta.mail.Session");
Class.forName("jakarta.mail.internet.MimeUtility");
Class.forName("jakarta.mail.internet.MimePart");
Class.forName("jakarta.mail.internet.MimeMultipart");
Class.forName("jakarta.mail.Multipart");
Class.forName("jakarta.mail.Part");
jakartaMailAvailable = true;
} catch (ClassNotFoundException e) {
jakartaMailAvailable = false;
}
}
return jakartaMailAvailable;
}
public static EmailContent extractEmailContent(
public EmailContent extractEmailContent(
byte[] emlBytes, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer)
throws IOException {
EmlProcessingUtils.validateEmlInput(emlBytes);
if (isJakartaMailAvailable()) {
return extractEmailContentAdvanced(emlBytes, request, customHtmlSanitizer);
} else {
return extractEmailContentBasic(emlBytes, customHtmlSanitizer);
}
Email email = parseEmail(emlBytes);
return buildEmailContent(email, request, customHtmlSanitizer);
}
private static EmailContent extractEmailContentBasic(
byte[] emlBytes, CustomHtmlSanitizer customHtmlSanitizer) {
String emlContent = new String(emlBytes, StandardCharsets.UTF_8);
EmailContent content = new EmailContent();
content.setSubject(extractBasicHeader(emlContent, HEADER_SUBJECT));
content.setFrom(extractBasicHeader(emlContent, HEADER_FROM));
content.setTo(extractBasicHeader(emlContent, HEADER_TO));
content.setCc(extractBasicHeader(emlContent, HEADER_CC));
content.setBcc(extractBasicHeader(emlContent, HEADER_BCC));
String dateStr = extractBasicHeader(emlContent, HEADER_DATE);
if (!dateStr.isEmpty()) {
content.setDateString(dateStr);
}
String htmlBody = extractHtmlBody(emlContent);
if (htmlBody != null) {
content.setHtmlBody(htmlBody);
} else {
String textBody = extractTextBody(emlContent);
content.setTextBody(textBody != null ? textBody : "Email content could not be parsed");
}
content.getAttachments().addAll(extractAttachmentsBasic(emlContent));
return content;
}
private static EmailContent extractEmailContentAdvanced(
byte[] emlBytes, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer) {
try {
Class<?> sessionClass = Class.forName("jakarta.mail.Session");
Class<?> mimeMessageClass = Class.forName("jakarta.mail.internet.MimeMessage");
Method getDefaultInstance =
sessionClass.getMethod("getDefaultInstance", Properties.class);
Object session = getDefaultInstance.invoke(null, new Properties());
Class<?>[] constructorArgs = new Class<?>[] {sessionClass, InputStream.class};
Constructor<?> mimeMessageConstructor =
mimeMessageClass.getConstructor(constructorArgs);
Object message =
mimeMessageConstructor.newInstance(session, new ByteArrayInputStream(emlBytes));
return extractFromMimeMessage(message, request, customHtmlSanitizer);
} catch (ReflectiveOperationException e) {
return extractEmailContentBasic(emlBytes, customHtmlSanitizer);
}
}
private static EmailContent extractFromMimeMessage(
Object message, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer) {
EmailContent content = new EmailContent();
try {
Class<?> messageClass = message.getClass();
Method getSubject = messageClass.getMethod("getSubject");
String subject = (String) getSubject.invoke(message);
content.setSubject(subject != null ? safeMimeDecode(subject) : "No Subject");
Method getFrom = messageClass.getMethod("getFrom");
Object[] fromAddresses = (Object[]) getFrom.invoke(message);
content.setFrom(buildAddressString(fromAddresses));
extractRecipients(message, messageClass, content);
Method getSentDate = messageClass.getMethod("getSentDate");
Date legacyDate = (Date) getSentDate.invoke(message);
if (legacyDate != null) {
content.setDate(
ZonedDateTime.ofInstant(legacyDate.toInstant(), ZoneId.systemDefault()));
}
Method getContent = messageClass.getMethod("getContent");
Object messageContent = getContent.invoke(message);
processMessageContent(message, messageContent, content, request, customHtmlSanitizer);
} catch (ReflectiveOperationException | RuntimeException e) {
content.setSubject("Email Conversion");
content.setFrom("Unknown");
content.setTo("Unknown");
content.setCc("");
content.setBcc("");
content.setTextBody("Email content could not be parsed with advanced processing");
}
return content;
}
private static void extractRecipients(
Object message, Class<?> messageClass, EmailContent content) {
try {
Method getRecipients =
messageClass.getMethod(
"getRecipients", Class.forName("jakarta.mail.Message$RecipientType"));
Class<?> recipientTypeClass = Class.forName("jakarta.mail.Message$RecipientType");
Object toType = recipientTypeClass.getField("TO").get(null);
Object[] toRecipients = (Object[]) getRecipients.invoke(message, toType);
content.setTo(buildAddressString(toRecipients));
Object ccType = recipientTypeClass.getField("CC").get(null);
Object[] ccRecipients = (Object[]) getRecipients.invoke(message, ccType);
content.setCc(buildAddressString(ccRecipients));
Object bccType = recipientTypeClass.getField("BCC").get(null);
Object[] bccRecipients = (Object[]) getRecipients.invoke(message, bccType);
content.setBcc(buildAddressString(bccRecipients));
} catch (ReflectiveOperationException e) {
try {
Method getAllRecipients = messageClass.getMethod("getAllRecipients");
Object[] recipients = (Object[]) getAllRecipients.invoke(message);
content.setTo(buildAddressString(recipients));
content.setCc("");
content.setBcc("");
} catch (ReflectiveOperationException ex) {
content.setTo("");
content.setCc("");
content.setBcc("");
}
}
}
private static String buildAddressString(Object[] addresses) {
if (addresses == null || addresses.length == 0) {
return "";
}
StringBuilder builder = new StringBuilder();
for (int i = 0; i < addresses.length; i++) {
if (i > 0) builder.append(", ");
builder.append(safeMimeDecode(addresses[i].toString()));
}
return builder.toString();
}
private static void processMessageContent(
Object message,
Object messageContent,
EmailContent content,
EmlToPdfRequest request,
CustomHtmlSanitizer customHtmlSanitizer) {
try {
if (messageContent instanceof String stringContent) {
Method getContentType = message.getClass().getMethod("getContentType");
String contentType = (String) getContentType.invoke(message);
if (contentType != null && contentType.toLowerCase().contains(TEXT_HTML)) {
content.setHtmlBody(stringContent);
} else {
content.setTextBody(stringContent);
private Email parseEmail(byte[] emlBytes) throws IOException {
boolean isMsgFile = EmlProcessingUtils.isMsgFile(emlBytes);
try (ByteArrayInputStream input = new ByteArrayInputStream(emlBytes)) {
Email email;
if (isMsgFile) {
try {
email = EmailConverter.outlookMsgToEmail(input);
} catch (Exception e) {
// OLE2 magic bytes match but parsing failed - might be DOC/XLS/other OLE2 file
throw new IOException(
"The file appears to be an OLE2 file (MSG/DOC/XLS) but could not be "
+ "parsed as an Outlook email. Ensure it is a valid .msg file: "
+ e.getMessage(),
e);
}
} else {
Class<?> multipartClass = Class.forName("jakarta.mail.Multipart");
if (multipartClass.isInstance(messageContent)) {
processMultipart(messageContent, content, request, customHtmlSanitizer, 0);
}
}
} catch (ReflectiveOperationException | ClassCastException e) {
content.setTextBody("Email content could not be parsed with advanced processing");
}
}
private static void processMultipart(
Object multipart,
EmailContent content,
EmlToPdfRequest request,
CustomHtmlSanitizer customHtmlSanitizer,
int depth) {
final int MAX_MULTIPART_DEPTH = 10;
if (depth > MAX_MULTIPART_DEPTH) {
content.setHtmlBody("<div class=\"error\">Maximum multipart depth exceeded</div>");
return;
}
try {
Class<?> multipartClass = multipart.getClass();
Method getCount = multipartClass.getMethod("getCount");
int count = (Integer) getCount.invoke(multipart);
Method getBodyPart = multipartClass.getMethod("getBodyPart", int.class);
for (int i = 0; i < count; i++) {
Object part = getBodyPart.invoke(multipart, i);
processPart(part, content, request, customHtmlSanitizer, depth + 1);
email = EmailConverter.emlToEmail(input);
}
} catch (ReflectiveOperationException | ClassCastException e) {
content.setHtmlBody("<div class=\"error\">Error processing multipart content</div>");
}
}
private static void processPart(
Object part,
EmailContent content,
EmlToPdfRequest request,
CustomHtmlSanitizer customHtmlSanitizer,
int depth) {
try {
Class<?> partClass = part.getClass();
Method isMimeType = partClass.getMethod("isMimeType", String.class);
Method getContent = partClass.getMethod("getContent");
Method getDisposition = partClass.getMethod("getDisposition");
Method getFileName = partClass.getMethod("getFileName");
Method getContentType = partClass.getMethod("getContentType");
Method getHeader = partClass.getMethod("getHeader", String.class);
Object disposition = getDisposition.invoke(part);
String filename = (String) getFileName.invoke(part);
String contentType = (String) getContentType.invoke(part);
String normalizedDisposition =
disposition != null ? ((String) disposition).toLowerCase() : null;
if ((Boolean) isMimeType.invoke(part, TEXT_PLAIN) && normalizedDisposition == null) {
Object partContent = getContent.invoke(part);
if (partContent instanceof String stringContent) {
content.setTextBody(stringContent);
}
} else if ((Boolean) isMimeType.invoke(part, TEXT_HTML)
&& normalizedDisposition == null) {
Object partContent = getContent.invoke(part);
if (partContent instanceof String stringContent) {
String htmlBody =
customHtmlSanitizer != null
? customHtmlSanitizer.sanitize(stringContent)
: stringContent;
content.setHtmlBody(htmlBody);
}
} else if ((normalizedDisposition != null
&& normalizedDisposition.contains(DISPOSITION_ATTACHMENT))
|| (filename != null && !filename.trim().isEmpty())) {
processAttachment(
part, content, request, getHeader, getContent, filename, contentType);
} else if ((Boolean) isMimeType.invoke(part, "multipart/*")) {
Object multipartContent = getContent.invoke(part);
if (multipartContent != null) {
Class<?> multipartClass = Class.forName("jakarta.mail.Multipart");
if (multipartClass.isInstance(multipartContent)) {
processMultipart(
multipartContent, content, request, customHtmlSanitizer, depth + 1);
}
}
}
} catch (ReflectiveOperationException | RuntimeException e) {
// Continue processing other parts if one fails
}
}
private static void processAttachment(
Object part,
EmailContent content,
EmlToPdfRequest request,
Method getHeader,
Method getContent,
String filename,
String contentType) {
content.setAttachmentCount(content.getAttachmentCount() + 1);
if (filename != null && !filename.trim().isEmpty()) {
EmailAttachment attachment = new EmailAttachment();
attachment.setFilename(safeMimeDecode(filename));
attachment.setContentType(contentType);
try {
String[] contentIdHeaders = (String[]) getHeader.invoke(part, HEADER_CONTENT_ID);
if (contentIdHeaders != null) {
for (String contentIdHeader : contentIdHeaders) {
if (contentIdHeader != null && !contentIdHeader.trim().isEmpty()) {
attachment.setEmbedded(true);
String contentId =
RegexPatternUtils.getInstance()
.getAngleBracketsPattern()
.matcher(contentIdHeader.trim())
.replaceAll("");
attachment.setContentId(contentId);
break;
}
}
}
} catch (ReflectiveOperationException e) {
}
if ((request != null && request.isIncludeAttachments()) || attachment.isEmbedded()) {
extractAttachmentData(part, attachment, getContent, request);
}
content.getAttachments().add(attachment);
}
}
private static void extractAttachmentData(
Object part, EmailAttachment attachment, Method getContent, EmlToPdfRequest request) {
try {
Object attachmentContent = getContent.invoke(part);
byte[] attachmentData = null;
if (attachmentContent instanceof InputStream inputStream) {
try (InputStream stream = inputStream) {
attachmentData = stream.readAllBytes();
} catch (IOException e) {
if (attachment.isEmbedded()) {
attachmentData = new byte[0];
} else {
throw new RuntimeException(e);
}
}
} else if (attachmentContent instanceof byte[] byteArray) {
attachmentData = byteArray;
} else if (attachmentContent instanceof String stringContent) {
attachmentData = stringContent.getBytes(StandardCharsets.UTF_8);
}
if (attachmentData != null) {
long maxSizeMB = request != null ? request.getMaxAttachmentSizeMB() : 10L;
long maxSizeBytes = maxSizeMB * 1024 * 1024;
if (attachmentData.length <= maxSizeBytes || attachment.isEmbedded()) {
attachment.setData(attachmentData);
attachment.setSizeBytes(attachmentData.length);
} else {
attachment.setSizeBytes(attachmentData.length);
}
}
} catch (ReflectiveOperationException | RuntimeException e) {
// Continue without attachment data
}
}
private static String extractBasicHeader(String emlContent, String headerName) {
try {
String[] lines =
RegexPatternUtils.getInstance().getNewlineSplitPattern().split(emlContent);
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
if (line.toLowerCase().startsWith(headerName.toLowerCase())) {
StringBuilder value =
new StringBuilder(line.substring(headerName.length()).trim());
for (int j = i + 1; j < lines.length; j++) {
if (lines[j].startsWith(" ") || lines[j].startsWith("\t")) {
value.append(" ").append(lines[j].trim());
} else {
break;
}
}
return safeMimeDecode(value.toString());
}
if (line.trim().isEmpty()) break;
}
} catch (RuntimeException e) {
// Ignore errors in header extraction
}
return "";
}
private static String extractHtmlBody(String emlContent) {
try {
String lowerContent = emlContent.toLowerCase();
int htmlStart = lowerContent.indexOf(HEADER_CONTENT_TYPE + " " + TEXT_HTML);
if (htmlStart == -1) return null;
int bodyStart = emlContent.indexOf("\r\n\r\n", htmlStart);
if (bodyStart == -1) bodyStart = emlContent.indexOf("\n\n", htmlStart);
if (bodyStart == -1) return null;
bodyStart += (emlContent.charAt(bodyStart + 1) == '\r') ? 4 : 2;
int bodyEnd = findPartEnd(emlContent, bodyStart);
return emlContent.substring(bodyStart, bodyEnd).trim();
return email;
} catch (IOException e) {
throw e; // Re-throw IOException as-is
} catch (Exception e) {
return null;
throw new IOException(
String.format(
"Failed to parse EML file with Simple Java Mail: %s", e.getMessage()),
e);
}
}
private static String extractTextBody(String emlContent) {
try {
String lowerContent = emlContent.toLowerCase();
int textStart = lowerContent.indexOf(HEADER_CONTENT_TYPE + " " + TEXT_PLAIN);
if (textStart == -1) {
int bodyStart = emlContent.indexOf("\r\n\r\n");
if (bodyStart == -1) bodyStart = emlContent.indexOf("\n\n");
if (bodyStart != -1) {
bodyStart += (emlContent.charAt(bodyStart + 1) == '\r') ? 4 : 2;
int bodyEnd = findPartEnd(emlContent, bodyStart);
return emlContent.substring(bodyStart, bodyEnd).trim();
}
return null;
}
private EmailContent buildEmailContent(
Email email, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer)
throws IOException {
int bodyStart = emlContent.indexOf("\r\n\r\n", textStart);
if (bodyStart == -1) bodyStart = emlContent.indexOf("\n\n", textStart);
if (bodyStart == -1) return null;
EmailContent content = new EmailContent();
content.setSubject(defaultString(email.getSubject()));
content.setFrom(formatRecipient(email.getFromRecipient()));
content.setTo(formatRecipients(email.getRecipients(), RecipientType.TO));
content.setCc(formatRecipients(email.getRecipients(), RecipientType.CC));
content.setBcc(formatRecipients(email.getRecipients(), RecipientType.BCC));
bodyStart += (emlContent.charAt(bodyStart + 1) == '\r') ? 4 : 2;
int bodyEnd = findPartEnd(emlContent, bodyStart);
return emlContent.substring(bodyStart, bodyEnd).trim();
} catch (RuntimeException e) {
return null;
}
}
private static int findPartEnd(String content, int start) {
String[] lines =
RegexPatternUtils.getInstance()
.getNewlineSplitPattern()
.split(content.substring(start));
StringBuilder result = new StringBuilder();
for (String line : lines) {
if (line.startsWith("--") && line.length() > 10) break;
result.append(line).append("\n");
Date sentDate = email.getSentDate();
if (sentDate != null) {
// Use UTC for consistent timezone handling across deployments
content.setDate(ZonedDateTime.ofInstant(sentDate.toInstant(), ZoneOffset.UTC));
}
return start + result.length();
}
String htmlBody = email.getHTMLText();
if (customHtmlSanitizer != null && htmlBody != null) {
htmlBody = customHtmlSanitizer.sanitize(htmlBody);
}
content.setHtmlBody(htmlBody);
String textBody = email.getPlainText();
if (customHtmlSanitizer != null && textBody != null) {
textBody = customHtmlSanitizer.sanitize(textBody);
}
content.setTextBody(textBody);
if (isBlank(content.getHtmlBody()) && isBlank(content.getTextBody())) {
content.setTextBody(NO_CONTENT_MESSAGE);
}
private static List<EmailAttachment> extractAttachmentsBasic(String emlContent) {
List<EmailAttachment> attachments = new ArrayList<>();
try {
String[] lines =
RegexPatternUtils.getInstance().getNewlineSplitPattern().split(emlContent);
boolean inHeaders = true;
String currentContentType = "";
String currentDisposition = "";
String currentFilename = "";
String currentEncoding = "";
attachments.addAll(mapResources(email.getEmbeddedImages(), request, true));
attachments.addAll(mapResources(email.getAttachments(), request, false));
content.setAttachments(attachments);
content.setAttachmentCount(attachments.size());
for (String line : lines) {
String lowerLine = line.toLowerCase().trim();
return content;
}
if (line.trim().isEmpty()) {
inHeaders = false;
if (isAttachment(currentDisposition, currentFilename, currentContentType)) {
EmailAttachment attachment = new EmailAttachment();
attachment.setFilename(currentFilename);
attachment.setContentType(currentContentType);
attachment.setTransferEncoding(currentEncoding);
attachments.add(attachment);
private List<EmailAttachment> mapResources(
List<AttachmentResource> resources, EmlToPdfRequest request, boolean embedded)
throws IOException {
if (resources == null || resources.isEmpty()) {
return List.of();
}
List<EmailAttachment> mapped = new ArrayList<>(resources.size());
int unnamedCounter = 0; // Start at 0, increment before use
for (AttachmentResource resource : resources) {
if (resource == null) {
continue; // Skip null resources early
}
// Pre-determine if this resource needs a generated filename
boolean needsGeneratedName = !embedded && needsGeneratedFilename(resource);
if (needsGeneratedName) {
unnamedCounter++;
}
EmailAttachment attachment =
toEmailAttachment(resource, request, embedded, unnamedCounter);
if (attachment != null) {
mapped.add(attachment);
}
}
return mapped;
}
/** Checks if a resource needs a generated filename (has no usable name). */
private boolean needsGeneratedFilename(AttachmentResource resource) {
if (resource == null) {
return false;
}
String resourceName = resource.getName();
if (!isBlank(resourceName)) {
return false;
}
DataSource dataSource = resource.getDataSource();
return isBlank(dataSource.getName());
}
private EmailAttachment toEmailAttachment(
AttachmentResource resource, EmlToPdfRequest request, boolean embedded, int counter)
throws IOException {
if (resource == null) {
return null;
}
EmailAttachment attachment = new EmailAttachment();
attachment.setEmbedded(embedded);
String resourceName = defaultString(resource.getName());
String filename = resourceName;
DataSource dataSource = resource.getDataSource();
String contentType = dataSource.getContentType();
if (!isBlank(dataSource.getName())) {
filename = dataSource.getName();
}
filename = safeMimeDecode(filename);
// Generate unique filename for unnamed attachments
if (isBlank(filename)) {
String extension = detectExtensionFromMimeType(contentType);
filename = embedded ? resourceName : (ATTACHMENT_PREFIX + counter + extension);
}
attachment.setFilename(filename);
String contentId = embedded ? stripCid(resourceName) : null;
attachment.setContentId(contentId);
String detectedContentType = EmlProcessingUtils.detectMimeType(filename, contentType);
attachment.setContentType(detectedContentType);
// Read data with size limit to prevent OOM
ReadResult readResult = readData(dataSource, embedded, request);
if (readResult != null) {
attachment.setSizeBytes(readResult.totalSize);
if (shouldIncludeAttachmentData(embedded, request, readResult)) {
attachment.setData(readResult.data);
}
}
return attachment;
}
private boolean shouldIncludeAttachmentData(
boolean embedded, EmlToPdfRequest request, ReadResult readResult) {
// Always include embedded images for proper rendering
if (embedded) {
return readResult != null && readResult.data() != null;
}
// Check if attachments are requested and data is available within size limit
if (request == null || !request.isIncludeAttachments()) {
return false;
}
if (readResult == null || readResult.data() == null) {
return false;
}
return readResult.data().length <= getMaxAttachmentSizeBytes(request);
}
private String detectExtensionFromMimeType(String mimeType) {
if (mimeType == null) {
return "";
}
String lower = mimeType.toLowerCase(Locale.ROOT);
// Remove any parameters (e.g., "text/plain; charset=utf-8" -> "text/plain")
int semicolon = lower.indexOf(';');
if (semicolon > 0) {
lower = lower.substring(0, semicolon).trim();
}
// Match exact MIME types first, then fall back to contains() for variants
return switch (lower) {
case "application/pdf" -> ".pdf";
case "image/png" -> ".png";
case "image/jpeg", "image/jpg" -> ".jpg";
case "image/gif" -> ".gif";
case "image/webp" -> ".webp";
case "image/bmp" -> ".bmp";
case "text/plain" -> ".txt";
case "text/html" -> ".html";
case "text/xml", "application/xml" -> ".xml";
case "application/json" -> ".json";
case "application/zip" -> ".zip";
case "application/octet-stream" -> ".bin";
default -> {
if (lower.contains("wordprocessingml") || lower.contains("msword")) yield ".docx";
if (lower.contains("spreadsheetml") || lower.contains("excel")) yield ".xlsx";
if (lower.contains("presentationml") || lower.contains("powerpoint")) yield ".pptx";
if (lower.contains("opendocument.text")) yield ".odt";
if (lower.contains("opendocument.spreadsheet")) yield ".ods";
yield "";
}
};
}
private ReadResult readData(DataSource dataSource, boolean embedded, EmlToPdfRequest request)
throws IOException {
if (dataSource == null) {
return null;
}
long maxBytes = getMaxAttachmentSizeBytes(request);
try (InputStream input = dataSource.getInputStream()) {
// Embedded images are usually needed for display regardless of size,
// but regular attachments should be guarded against OOM
if (!embedded && request != null) {
byte[] buffer = new byte[8192];
ByteArrayOutputStream output = new ByteArrayOutputStream();
int bytesRead;
long totalBytes = 0;
while ((bytesRead = input.read(buffer)) != -1) {
totalBytes += bytesRead;
if (totalBytes > maxBytes) {
// Attachment too large - skip remaining data but estimate total size
long remainingBytes = countRemainingBytes(input, totalBytes);
log.debug(
"Attachment exceeds size limit: {} bytes (max: {} bytes), skipping",
remainingBytes,
maxBytes);
return new ReadResult(null, remainingBytes);
}
currentContentType = "";
currentDisposition = "";
currentFilename = "";
currentEncoding = "";
inHeaders = true;
continue;
}
if (!inHeaders) continue;
if (lowerLine.startsWith(HEADER_CONTENT_TYPE)) {
currentContentType = line.substring(HEADER_CONTENT_TYPE.length()).trim();
} else if (lowerLine.startsWith(HEADER_CONTENT_DISPOSITION)) {
currentDisposition = line.substring(HEADER_CONTENT_DISPOSITION.length()).trim();
currentFilename = extractFilenameFromDisposition(currentDisposition);
} else if (lowerLine.startsWith(HEADER_CONTENT_TRANSFER_ENCODING)) {
currentEncoding =
line.substring(HEADER_CONTENT_TRANSFER_ENCODING.length()).trim();
output.write(buffer, 0, bytesRead);
}
byte[] data = output.toByteArray();
return new ReadResult(data, data.length);
} else {
byte[] data = input.readAllBytes();
return new ReadResult(data, data.length);
}
} catch (RuntimeException e) {
// Continue with empty list
} catch (IOException e) {
if (embedded) {
log.debug(
"Failed to read embedded image, using empty placeholder: {}",
e.getMessage());
return new ReadResult(new byte[0], 0);
}
throw e;
}
return attachments;
}
private static boolean isAttachment(String disposition, String filename, String contentType) {
return (disposition.toLowerCase().contains(DISPOSITION_ATTACHMENT) && !filename.isEmpty())
|| (!filename.isEmpty() && !contentType.toLowerCase().startsWith("text/"))
|| (contentType.toLowerCase().contains("application/") && !filename.isEmpty());
private long countRemainingBytes(InputStream input, long alreadyRead) throws IOException {
long count = alreadyRead;
long skipped;
while (count < MAX_SIZE_ESTIMATION_BYTES
&& (skipped = input.skip(MAX_SIZE_ESTIMATION_BYTES - count)) > 0) {
count += skipped;
}
if (count < MAX_SIZE_ESTIMATION_BYTES && input.available() > 0) {
byte[] buffer = new byte[8192];
int read;
while ((read = input.read(buffer)) != -1 && count < MAX_SIZE_ESTIMATION_BYTES) {
count += read;
}
}
return count;
}
private static String extractFilenameFromDisposition(String disposition) {
if (disposition == null || !disposition.contains("filename=")) {
private String formatRecipients(List<Recipient> recipients, RecipientType type) {
if (recipients == null || type == null) {
return "";
}
// Handle filename*= (RFC 2231 encoded filename)
if (disposition.toLowerCase().contains("filename*=")) {
int filenameStarStart = disposition.toLowerCase().indexOf("filename*=") + 10;
int filenameStarEnd = disposition.indexOf(";", filenameStarStart);
if (filenameStarEnd == -1) filenameStarEnd = disposition.length();
String extendedFilename =
disposition.substring(filenameStarStart, filenameStarEnd).trim();
extendedFilename =
RegexPatternUtils.getInstance()
.getQuotesRemovalPattern()
.matcher(extendedFilename)
.replaceAll("");
if (extendedFilename.contains("'")) {
String[] parts = extendedFilename.split("'", 3);
if (parts.length == 3) {
return EmlProcessingUtils.decodeUrlEncoded(parts[2]);
}
}
}
// Handle regular filename=
int filenameStart = disposition.toLowerCase().indexOf("filename=") + 9;
int filenameEnd = disposition.indexOf(";", filenameStart);
if (filenameEnd == -1) filenameEnd = disposition.length();
String filename = disposition.substring(filenameStart, filenameEnd).trim();
filename =
RegexPatternUtils.getInstance()
.getQuotesRemovalPattern()
.matcher(filename)
.replaceAll("");
return safeMimeDecode(filename);
return recipients.stream()
.filter(Objects::nonNull)
// Use type.equals() for null-safe comparison (recipient.getType() may be null)
.filter(recipient -> type.equals(recipient.getType()))
.map(EmlParser::formatRecipient)
.filter(string -> !isBlank(string))
.collect(Collectors.joining(", "));
}
public static String safeMimeDecode(String headerValue) {
if (headerValue == null || headerValue.trim().isEmpty()) {
private String formatRecipient(Recipient recipient) {
if (recipient == null) {
return "";
}
if (!mimeUtilityChecked) {
synchronized (EmlParser.class) {
if (!mimeUtilityChecked) {
initializeMimeUtilityDecoding();
}
}
}
String name = safeMimeDecode(recipient.getName());
String address = safeMimeDecode(recipient.getAddress());
if (mimeUtilityDecodeTextMethod != null) {
try {
return (String) mimeUtilityDecodeTextMethod.invoke(null, headerValue.trim());
} catch (ReflectiveOperationException | RuntimeException e) {
// Fall through to custom implementation
}
if (!isBlank(name) && !isBlank(address)) {
return name + " <" + address + ">";
}
return !isBlank(name) ? name : address;
}
public String safeMimeDecode(String headerValue) {
if (isBlank(headerValue)) {
return "";
}
return EmlProcessingUtils.decodeMimeHeader(headerValue.trim());
}
private static void initializeMimeUtilityDecoding() {
try {
Class<?> mimeUtilityClass = Class.forName("jakarta.mail.internet.MimeUtility");
mimeUtilityDecodeTextMethod = mimeUtilityClass.getMethod("decodeText", String.class);
} catch (ClassNotFoundException | NoSuchMethodException e) {
mimeUtilityDecodeTextMethod = null;
private String stripCid(String contentId) {
if (contentId == null) {
return null;
}
return RegexPatternUtils.getInstance()
.getAngleBracketsPattern()
.matcher(contentId)
.replaceAll("")
.trim();
}
private long getMaxAttachmentSizeBytes(EmlToPdfRequest request) {
long maxMb = request != null ? request.getMaxAttachmentSizeMB() : DEFAULT_MAX_ATTACHMENT_MB;
return maxMb * 1024L * 1024L;
}
private boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
private String defaultString(String value) {
return value != null ? value : "";
}
private record ReadResult(byte[] data, long totalSize) {
public ReadResult {
if (totalSize < 0) {
throw new IllegalArgumentException("Size cannot be negative: " + totalSize);
}
if (data != null && data.length > totalSize) {
throw new IllegalArgumentException(
"Data length (" + data.length + ") exceeds total size (" + totalSize + ")");
}
}
mimeUtilityChecked = true;
}
@Data
public static class EmailContent {
public class EmailContent {
private String subject;
private String from;
private String to;
private String cc;
private String bcc;
private ZonedDateTime date;
private String dateString; // For basic parsing fallback
private String dateString; // Maintained for compatibility
private String htmlBody;
private String textBody;
private int attachmentCount;
@@ -668,7 +439,7 @@ public class EmlParser {
}
@Data
public static class EmailAttachment {
public class EmailAttachment {
private String filename;
private String contentType;
private byte[] data;
@@ -1,5 +1,7 @@
package stirling.software.common.util;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
@@ -8,32 +10,41 @@ import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import lombok.Synchronized;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.api.converters.EmlToPdfRequest;
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
@Slf4j
@UtilityClass
public class EmlProcessingUtils {
// Style constants
private static final int DEFAULT_FONT_SIZE = 12;
private static final String DEFAULT_FONT_FAMILY = "Helvetica, sans-serif";
private static final float DEFAULT_LINE_HEIGHT = 1.4f;
private static final String DEFAULT_ZOOM = "1.0";
private static final String DEFAULT_TEXT_COLOR = "#202124";
private static final String DEFAULT_BACKGROUND_COLOR = "#ffffff";
private static final String DEFAULT_BORDER_COLOR = "#e8eaed";
private static final String ATTACHMENT_BACKGROUND_COLOR = "#f9f9f9";
private static final String ATTACHMENT_BORDER_COLOR = "#eeeeee";
private final int DEFAULT_FONT_SIZE = 12;
private final String DEFAULT_FONT_FAMILY = "Helvetica, sans-serif";
private final float DEFAULT_LINE_HEIGHT = 1.4f;
private final String DEFAULT_ZOOM = "1.0";
private final String DEFAULT_TEXT_COLOR = "#202124";
private final String DEFAULT_BACKGROUND_COLOR = "#ffffff";
private final String DEFAULT_BORDER_COLOR = "#e8eaed";
private final String ATTACHMENT_BACKGROUND_COLOR = "#f9f9f9";
private final String ATTACHMENT_BORDER_COLOR = "#eeeeee";
private static final int EML_CHECK_LENGTH = 8192;
private static final int MIN_HEADER_COUNT_FOR_VALID_EML = 2;
// MIME type detection
private static final Map<String, String> EXTENSION_TO_MIME_TYPE =
private final String CSS_RESOURCE_PATH = "templates/email-pdf-styles.css";
private final int EML_CHECK_LENGTH = 8192;
private final int MIN_HEADER_COUNT_FOR_VALID_EML = 2;
// MSG file magic bytes (Compound File Binary Format / OLE2)
// D0 CF 11 E0 A1 B1 1A E1
private final byte[] MSG_MAGIC_BYTES = {
(byte) 0xD0, (byte) 0xCF, (byte) 0x11, (byte) 0xE0,
(byte) 0xA1, (byte) 0xB1, (byte) 0x1A, (byte) 0xE1
};
private final Map<String, String> EXTENSION_TO_MIME_TYPE =
Map.of(
".png", MediaType.IMAGE_PNG_VALUE,
".jpg", MediaType.IMAGE_JPEG_VALUE,
@@ -45,18 +56,36 @@ public class EmlProcessingUtils {
".ico", "image/x-icon",
".tiff", "image/tiff",
".tif", "image/tiff");
private volatile String cachedCssContent = null;
public static void validateEmlInput(byte[] emlBytes) {
public void validateEmlInput(byte[] emlBytes) {
if (emlBytes == null || emlBytes.length == 0) {
throw new IllegalArgumentException("EML file is empty or null");
throw ExceptionUtils.createEmlEmptyException();
}
if (isMsgFile(emlBytes)) {
return; // Valid MSG file, no further EML validation needed
}
if (isInvalidEmlFormat(emlBytes)) {
throw new IllegalArgumentException("Invalid EML file format");
throw ExceptionUtils.createEmlInvalidFormatException();
}
}
private static boolean isInvalidEmlFormat(byte[] emlBytes) {
public boolean isMsgFile(byte[] fileBytes) {
if (fileBytes == null || fileBytes.length < MSG_MAGIC_BYTES.length) {
return false;
}
for (int i = 0; i < MSG_MAGIC_BYTES.length; i++) {
if (fileBytes[i] != MSG_MAGIC_BYTES[i]) {
return false;
}
}
return true;
}
private boolean isInvalidEmlFormat(byte[] emlBytes) {
try {
int checkLength = Math.min(emlBytes.length, EML_CHECK_LENGTH);
String content;
@@ -101,7 +130,7 @@ public class EmlProcessingUtils {
}
}
public static String generateEnhancedEmailHtml(
public String generateEnhancedEmailHtml(
EmlParser.EmailContent content,
EmlToPdfRequest request,
CustomHtmlSanitizer customHtmlSanitizer) {
@@ -109,12 +138,13 @@ public class EmlProcessingUtils {
html.append(
String.format(
Locale.ROOT,
"""
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8">
<title>%s</title>
<style>
""",
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8">
<title>%s</title>
<style>
""",
sanitizeText(content.getSubject(), customHtmlSanitizer)));
appendEnhancedStyles(html);
@@ -127,14 +157,15 @@ public class EmlProcessingUtils {
html.append(
String.format(
Locale.ROOT,
"""
<div class="email-container">
<div class="email-header">
<h1>%s</h1>
<div class="email-meta">
<div><strong>From:</strong> %s</div>
<div><strong>To:</strong> %s</div>
""",
<div class="email-container">
<div class="email-header">
<h1>%s</h1>
<div class="email-meta">
<div><strong>From:</strong> %s</div>
<div><strong>To:</strong> %s</div>
""",
sanitizeText(content.getSubject(), customHtmlSanitizer),
sanitizeText(content.getFrom(), customHtmlSanitizer),
sanitizeText(content.getTo(), customHtmlSanitizer)));
@@ -142,32 +173,36 @@ public class EmlProcessingUtils {
if (content.getCc() != null && !content.getCc().trim().isEmpty()) {
html.append(
String.format(
"<div><strong>CC:</strong> %s</div>\n",
Locale.ROOT,
"<div><strong>CC:</strong> %s</div>%n",
sanitizeText(content.getCc(), customHtmlSanitizer)));
}
if (content.getBcc() != null && !content.getBcc().trim().isEmpty()) {
html.append(
String.format(
"<div><strong>BCC:</strong> %s</div>\n",
Locale.ROOT,
"<div><strong>BCC:</strong> %s</div>%n",
sanitizeText(content.getBcc(), customHtmlSanitizer)));
}
if (content.getDate() != null) {
html.append(
String.format(
"<div><strong>Date:</strong> %s</div>\n",
Locale.ROOT,
"<div><strong>Date:</strong> %s</div>%n",
PdfAttachmentHandler.formatEmailDate(content.getDate())));
} else if (content.getDateString() != null && !content.getDateString().trim().isEmpty()) {
html.append(
String.format(
"<div><strong>Date:</strong> %s</div>\n",
Locale.ROOT,
"<div><strong>Date:</strong> %s</div>%n",
sanitizeText(content.getDateString(), customHtmlSanitizer)));
}
html.append("</div></div>\n");
html.append(String.format(Locale.ROOT, "</div></div>%n"));
html.append("<div class=\"email-body\">\n");
html.append(String.format(Locale.ROOT, "<div class=\"email-body\">%n"));
if (content.getHtmlBody() != null && !content.getHtmlBody().trim().isEmpty()) {
String processedHtml =
processEmailHtmlBody(content.getHtmlBody(), content, customHtmlSanitizer);
@@ -175,22 +210,23 @@ public class EmlProcessingUtils {
} else if (content.getTextBody() != null && !content.getTextBody().trim().isEmpty()) {
html.append(
String.format(
Locale.ROOT,
"<div class=\"text-body\">%s</div>",
convertTextToHtml(content.getTextBody(), customHtmlSanitizer)));
} else {
html.append("<div class=\"no-content\"><p><em>No content available</em></p></div>");
}
html.append("</div>\n");
html.append(String.format(Locale.ROOT, "</div>%n"));
if (content.getAttachmentCount() > 0 || !content.getAttachments().isEmpty()) {
appendAttachmentsSection(html, content, request, customHtmlSanitizer);
appendAttachmentsSection(html, content, request);
}
html.append("</div>\n</body></html>");
html.append(String.format(Locale.ROOT, "</div>%n</body></html>"));
return html.toString();
}
public static String processEmailHtmlBody(
public String processEmailHtmlBody(
String htmlBody,
EmlParser.EmailContent emailContent,
CustomHtmlSanitizer customHtmlSanitizer) {
@@ -217,8 +253,7 @@ public class EmlProcessingUtils {
return processed;
}
public static String convertTextToHtml(
String textBody, CustomHtmlSanitizer customHtmlSanitizer) {
public String convertTextToHtml(String textBody, CustomHtmlSanitizer customHtmlSanitizer) {
if (textBody == null) return "";
String html =
@@ -234,171 +269,111 @@ public class EmlProcessingUtils {
.getUrlLinkPattern()
.matcher(html)
.replaceAll(
"<a href=\"$1\" style=\"color: #1a73e8; text-decoration: underline;\">$1</a>");
"<a href=\"$1\" style=\"color: #1a73e8; text-decoration:"
+ " underline;\">$1</a>");
html =
RegexPatternUtils.getInstance()
.getEmailLinkPattern()
.matcher(html)
.replaceAll(
"<a href=\"mailto:$1\" style=\"color: #1a73e8; text-decoration: underline;\">$1</a>");
"<a href=\"mailto:$1\" style=\"color: #1a73e8; text-decoration:"
+ " underline;\">$1</a>");
return html;
}
private static void appendEnhancedStyles(StringBuilder html) {
String css =
private void appendEnhancedStyles(StringBuilder html) {
html.append(
String.format(
Locale.ROOT,
"""
body {
font-family: %s;
font-size: %dpx;
line-height: %s;
color: %s;
margin: 0;
padding: 16px;
background-color: %s;
}
.email-container {
width: 100%%;
max-width: 100%%;
margin: 0 auto;
}
.email-header {
padding-bottom: 10px;
border-bottom: 1px solid %s;
margin-bottom: 10px;
}
.email-header h1 {
margin: 0 0 10px 0;
font-size: %dpx;
font-weight: bold;
}
.email-meta div {
margin-bottom: 2px;
font-size: %dpx;
}
.email-body {
word-wrap: break-word;
}
.attachment-section {
margin-top: 15px;
padding: 10px;
background-color: %s;
border: 1px solid %s;
border-radius: 3px;
}
.attachment-section h3 {
margin: 0 0 8px 0;
font-size: %dpx;
}
.attachment-item {
padding: 5px 0;
}
.attachment-icon {
margin-right: 5px;
}
.attachment-details, .attachment-type {
font-size: %dpx;
color: #555555;
}
.attachment-inclusion-note, .attachment-info-note {
margin-top: 8px;
padding: 6px;
font-size: %dpx;
border-radius: 3px;
}
.attachment-inclusion-note {
background-color: #e6ffed;
border: 1px solid #d4f7dc;
color: #006420;
}
.attachment-info-note {
background-color: #fff9e6;
border: 1px solid #fff0c2;
color: #664d00;
}
.attachment-link-container {
display: flex;
align-items: center;
padding: 8px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
margin: 4px 0;
}
.attachment-link-container:hover {
background-color: #e9ecef;
}
.attachment-note {
font-size: %dpx;
color: #6c757d;
font-style: italic;
margin-left: 8px;
}
.no-content {
padding: 20px;
text-align: center;
color: #666;
font-style: italic;
}
.text-body {
white-space: pre-wrap;
}
img {
max-width: 100%%;
height: auto;
display: block;
}
""",
:root {
--font-family: %s;
--font-size: %dpx;
--line-height: %s;
--text-color: %s;
--bg-color: %s;
--border-color: %s;
--header-font-size: %dpx;
--meta-font-size: %dpx;
--attachment-bg: %s;
--attachment-border: %s;
--attachment-header-size: %dpx;
--attachment-detail-size: %dpx;
--note-font-size: %dpx;
}
""",
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
DEFAULT_LINE_HEIGHT,
DEFAULT_TEXT_COLOR,
DEFAULT_BACKGROUND_COLOR,
DEFAULT_BORDER_COLOR,
DEFAULT_FONT_SIZE + 4,
DEFAULT_FONT_SIZE - 1,
DEFAULT_FONT_SIZE + 6,
DEFAULT_FONT_SIZE,
ATTACHMENT_BACKGROUND_COLOR,
ATTACHMENT_BORDER_COLOR,
DEFAULT_FONT_SIZE + 1,
DEFAULT_FONT_SIZE - 2,
DEFAULT_FONT_SIZE - 2,
DEFAULT_FONT_SIZE - 3);
DEFAULT_FONT_SIZE + 2,
DEFAULT_FONT_SIZE - 1,
DEFAULT_FONT_SIZE - 1));
html.append(css);
html.append(loadEmailStyles());
}
private static void appendAttachmentsSection(
StringBuilder html,
EmlParser.EmailContent content,
EmlToPdfRequest request,
CustomHtmlSanitizer customHtmlSanitizer) {
html.append("<div class=\"attachment-section\">\n");
@Synchronized
private String loadEmailStyles() {
if (cachedCssContent != null) {
return cachedCssContent;
}
try {
ClassPathResource resource = new ClassPathResource(CSS_RESOURCE_PATH);
try (InputStream inputStream = resource.getInputStream()) {
cachedCssContent = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
return cachedCssContent;
}
} catch (IOException e) {
log.warn("Failed to load email CSS from resource, using fallback: {}", e.getMessage());
cachedCssContent = getFallbackStyles(); // Cache fallback to avoid repeated attempts
return cachedCssContent;
}
}
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; }
""";
}
private void appendAttachmentsSection(
StringBuilder html, EmlParser.EmailContent content, EmlToPdfRequest request) {
html.append(String.format(Locale.ROOT, "<div class=\"attachment-section\">%n"));
int displayedAttachmentCount =
content.getAttachmentCount() > 0
? content.getAttachmentCount()
: content.getAttachments().size();
html.append("<h3>Attachments (").append(displayedAttachmentCount).append(")</h3>\n");
html.append(
String.format(
Locale.ROOT, "<h3>Attachments (%d)</h3>%n", displayedAttachmentCount));
if (!content.getAttachments().isEmpty()) {
for (int i = 0; i < content.getAttachments().size(); i++) {
@@ -420,13 +395,14 @@ public class EmlProcessingUtils {
String attachmentId = "attachment_" + i;
html.append(
String.format(
Locale.ROOT,
"""
<div class="attachment-item" id="%s">
<span class="attachment-icon" data-filename="%s">@</span>
<span class="attachment-name">%s</span>
<span class="attachment-details">(%s%s)</span>
</div>
""",
<div class="attachment-item" id="%s">
<span class="attachment-icon" data-filename="%s">@</span>
<span class="attachment-name">%s</span>
<span class="attachment-details">(%s%s)</span>
</div>
""",
attachmentId,
escapeHtml(embeddedFilename),
escapeHtml(EmlParser.safeMimeDecode(attachment.getFilename())),
@@ -450,10 +426,10 @@ public class EmlProcessingUtils {
</div>
""");
}
html.append("</div>\n");
html.append(String.format(Locale.ROOT, "</div>%n"));
}
public static HTMLToPdfRequest createHtmlRequest(EmlToPdfRequest request) {
public HTMLToPdfRequest createHtmlRequest(EmlToPdfRequest request) {
HTMLToPdfRequest htmlRequest = new HTMLToPdfRequest();
if (request != null) {
@@ -464,13 +440,13 @@ public class EmlProcessingUtils {
return htmlRequest;
}
public static String detectMimeType(String filename, String existingMimeType) {
public String detectMimeType(String filename, String existingMimeType) {
if (existingMimeType != null && !existingMimeType.isEmpty()) {
return existingMimeType;
}
if (filename != null) {
String lowerFilename = filename.toLowerCase();
String lowerFilename = filename.toLowerCase(Locale.ROOT);
for (Map.Entry<String, String> entry : EXTENSION_TO_MIME_TYPE.entrySet()) {
if (lowerFilename.endsWith(entry.getKey())) {
return entry.getValue();
@@ -481,7 +457,7 @@ public class EmlProcessingUtils {
return MediaType.IMAGE_PNG_VALUE; // Default MIME type
}
public static String decodeUrlEncoded(String encoded) {
public String decodeUrlEncoded(String encoded) {
try {
return java.net.URLDecoder.decode(encoded, StandardCharsets.UTF_8);
} catch (Exception e) {
@@ -489,7 +465,7 @@ public class EmlProcessingUtils {
}
}
public static String decodeMimeHeader(String encodedText) {
public String decodeMimeHeader(String encodedText) {
if (encodedText == null || encodedText.trim().isEmpty()) {
return encodedText;
}
@@ -516,7 +492,7 @@ public class EmlProcessingUtils {
result.append(processedText, lastEnd, matcher.start());
String charset = matcher.group(1);
String encoding = matcher.group(2).toUpperCase();
String encoding = matcher.group(2).toUpperCase(Locale.ROOT);
String encodedValue = matcher.group(3);
try {
@@ -555,7 +531,7 @@ public class EmlProcessingUtils {
}
}
private static String decodeQuotedPrintable(String encodedText, String charset) {
private String decodeQuotedPrintable(String encodedText, String charset) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < encodedText.length(); i++) {
char c = encodedText.charAt(i);
@@ -598,7 +574,7 @@ public class EmlProcessingUtils {
}
}
public static String escapeHtml(String text) {
public String escapeHtml(String text) {
if (text == null) return "";
return text.replace("&", "&amp;")
.replace("<", "&lt;")
@@ -607,7 +583,7 @@ public class EmlProcessingUtils {
.replace("'", "&#39;");
}
public static String sanitizeText(String text, CustomHtmlSanitizer customHtmlSanitizer) {
public String sanitizeText(String text, CustomHtmlSanitizer customHtmlSanitizer) {
if (customHtmlSanitizer != null) {
return customHtmlSanitizer.sanitize(text);
} else {
@@ -615,7 +591,7 @@ public class EmlProcessingUtils {
}
}
public static String simplifyHtmlContent(String htmlContent) {
public String simplifyHtmlContent(String htmlContent) {
String simplified =
RegexPatternUtils.getInstance()
.getScriptTagPattern()
File diff suppressed because it is too large Load Diff
@@ -5,13 +5,11 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@@ -37,9 +35,11 @@ public class FileToPdf {
try (TempFile tempInputFile =
new TempFile(
tempFileManager,
fileName.toLowerCase().endsWith(".html") ? ".html" : ".zip")) {
fileName.toLowerCase(Locale.ROOT).endsWith(".html")
? ".html"
: ".zip")) {
if (fileName.toLowerCase().endsWith(".html")) {
if (fileName.toLowerCase(Locale.ROOT).endsWith(".html")) {
String sanitizedHtml =
sanitizeHtmlContent(
new String(fileBytes, StandardCharsets.UTF_8),
@@ -47,7 +47,7 @@ public class FileToPdf {
Files.write(
tempInputFile.getPath(),
sanitizedHtml.getBytes(StandardCharsets.UTF_8));
} else if (fileName.toLowerCase().endsWith(".zip")) {
} else if (fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) {
Files.write(tempInputFile.getPath(), fileBytes);
sanitizeHtmlFilesInZip(
tempInputFile.getPath(), tempFileManager, customHtmlSanitizer);
@@ -102,8 +102,8 @@ public class FileToPdf {
tempUnzippedDir.getPath().resolve(sanitizeZipFilename(entry.getName()));
if (!entry.isDirectory()) {
Files.createDirectories(filePath.getParent());
if (entry.getName().toLowerCase().endsWith(".html")
|| entry.getName().toLowerCase().endsWith(".htm")) {
if (entry.getName().toLowerCase(Locale.ROOT).endsWith(".html")
|| entry.getName().toLowerCase(Locale.ROOT).endsWith(".htm")) {
String content =
new String(zipIn.readAllBytes(), StandardCharsets.UTF_8);
String sanitizedContent =
@@ -145,64 +145,6 @@ public class FileToPdf {
}
}
private static void deleteDirectory(Path dir) throws IOException {
Files.walkFileTree(
dir,
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}
private static Path unzipAndGetMainHtml(byte[] fileBytes) throws IOException {
Path tempDirectory = Files.createTempDirectory("unzipped_");
try (ZipInputStream zipIn =
ZipSecurity.createHardenedInputStream(new ByteArrayInputStream(fileBytes))) {
ZipEntry entry = zipIn.getNextEntry();
while (entry != null) {
Path filePath = tempDirectory.resolve(sanitizeZipFilename(entry.getName()));
if (entry.isDirectory()) {
Files.createDirectories(filePath); // Explicitly create the directory structure
} else {
Files.createDirectories(
filePath.getParent()); // Create parent directories if they don't exist
Files.copy(zipIn, filePath);
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
}
}
// Search for the main HTML file.
try (Stream<Path> walk = Files.walk(tempDirectory)) {
List<Path> htmlFiles = walk.filter(file -> file.toString().endsWith(".html")).toList();
if (htmlFiles.isEmpty()) {
throw new IOException("No HTML files found in the unzipped directory.");
}
// Prioritize 'index.html' if it exists, otherwise use the first .html file
for (Path htmlFile : htmlFiles) {
if ("index.html".equals(htmlFile.getFileName().toString())) {
return htmlFile;
}
}
return htmlFiles.get(0);
}
}
static String sanitizeZipFilename(String entryName) {
if (entryName == null || entryName.trim().isEmpty()) {
return "";
@@ -1,4 +1,4 @@
package stirling.software.proprietary.util;
package stirling.software.common.util;
import java.io.IOException;
import java.util.HashMap;
@@ -23,9 +23,13 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDTerminalField;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
/**
* Utility class for copying and transforming PDF form fields during page operations. Used by
* multi-page layout and other page manipulation operations that need to preserve form fields.
*/
@Slf4j
@UtilityClass
public class FormCopyUtils {
public class GeneralFormCopyUtils {
public boolean hasAnyRotatedPage(PDDocument document) {
try {
@@ -188,7 +192,8 @@ public class FormCopyUtils {
continue;
}
FormFieldTypeSupport handler = FormFieldTypeSupport.forField(terminalField);
GeneralFormFieldTypeSupport handler =
GeneralFormFieldTypeSupport.forField(terminalField);
if (handler == null) {
log.debug(
"Skipping unsupported field type '{}' for widget '{}'",
@@ -222,7 +227,7 @@ public class FormCopyUtils {
}
private void copyFieldUsingHandler(
FormFieldTypeSupport handler,
GeneralFormFieldTypeSupport handler,
PDTerminalField sourceField,
PDAcroForm newAcroForm,
PDPage destinationPage,
@@ -0,0 +1,165 @@
package stirling.software.common.util;
import java.io.IOException;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDCheckBox;
import org.apache.pdfbox.pdmodel.interactive.form.PDComboBox;
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.apache.pdfbox.pdmodel.interactive.form.PDListBox;
import org.apache.pdfbox.pdmodel.interactive.form.PDPushButton;
import org.apache.pdfbox.pdmodel.interactive.form.PDRadioButton;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
import org.apache.pdfbox.pdmodel.interactive.form.PDTerminalField;
import org.apache.pdfbox.pdmodel.interactive.form.PDTextField;
import lombok.extern.slf4j.Slf4j;
/**
* Simplified form field type support for general PDF operations. This is a subset of the full
* proprietary FormFieldTypeSupport, containing only what's needed for basic form field copying
* during page operations.
*/
@Slf4j
public enum GeneralFormFieldTypeSupport {
TEXT("text", "textField", PDTextField.class) {
@Override
PDTerminalField createField(PDAcroForm acroForm) {
PDTextField textField = new PDTextField(acroForm);
textField.setDefaultAppearance("/Helv 12 Tf 0 g");
return textField;
}
@Override
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
PDTextField src = (PDTextField) source;
PDTextField dst = (PDTextField) target;
String value = src.getValueAsString();
if (value != null) {
dst.setValue(value);
}
}
},
CHECKBOX("checkbox", "checkBox", PDCheckBox.class) {
@Override
PDTerminalField createField(PDAcroForm acroForm) {
return new PDCheckBox(acroForm);
}
@Override
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
PDCheckBox src = (PDCheckBox) source;
PDCheckBox dst = (PDCheckBox) target;
if (src.isChecked()) {
dst.check();
} else {
dst.unCheck();
}
}
},
RADIO("radio", "radioButton", PDRadioButton.class) {
@Override
PDTerminalField createField(PDAcroForm acroForm) {
return new PDRadioButton(acroForm);
}
@Override
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
PDRadioButton src = (PDRadioButton) source;
PDRadioButton dst = (PDRadioButton) target;
if (src.getExportValues() != null) {
dst.setExportValues(src.getExportValues());
}
if (src.getValue() != null) {
dst.setValue(src.getValue());
}
}
},
COMBOBOX("combobox", "comboBox", PDComboBox.class) {
@Override
PDTerminalField createField(PDAcroForm acroForm) {
return new PDComboBox(acroForm);
}
@Override
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
PDComboBox src = (PDComboBox) source;
PDComboBox dst = (PDComboBox) target;
if (src.getOptions() != null) {
dst.setOptions(src.getOptions());
}
if (src.getValue() != null && !src.getValue().isEmpty()) {
dst.setValue(src.getValue());
}
}
},
LISTBOX("listbox", "listBox", PDListBox.class) {
@Override
PDTerminalField createField(PDAcroForm acroForm) {
return new PDListBox(acroForm);
}
@Override
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
PDListBox src = (PDListBox) source;
PDListBox dst = (PDListBox) target;
if (src.getOptions() != null) {
dst.setOptions(src.getOptions());
}
if (src.getValue() != null && !src.getValue().isEmpty()) {
dst.setValue(src.getValue());
}
}
},
SIGNATURE("signature", "signature", PDSignatureField.class) {
@Override
PDTerminalField createField(PDAcroForm acroForm) {
return new PDSignatureField(acroForm);
}
},
BUTTON("button", "pushButton", PDPushButton.class) {
@Override
PDTerminalField createField(PDAcroForm acroForm) {
return new PDPushButton(acroForm);
}
};
private final String typeName;
private final String fallbackWidgetName;
private final Class<? extends PDTerminalField> fieldClass;
GeneralFormFieldTypeSupport(
String typeName,
String fallbackWidgetName,
Class<? extends PDTerminalField> fieldClass) {
this.typeName = typeName;
this.fallbackWidgetName = fallbackWidgetName;
this.fieldClass = fieldClass;
}
public static GeneralFormFieldTypeSupport forField(PDField field) {
if (field == null) {
return null;
}
for (GeneralFormFieldTypeSupport handler : values()) {
if (handler.fieldClass.isInstance(field)) {
return handler;
}
}
return null;
}
String typeName() {
return typeName;
}
String fallbackWidgetName() {
return fallbackWidgetName;
}
abstract PDTerminalField createField(PDAcroForm acroForm);
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
// default no-op
}
}
@@ -33,6 +33,11 @@ import stirling.software.common.configuration.InstallationPathConfig;
@UtilityClass
public class GeneralUtils {
/**
* Maximum number of resolved DNS addresses allowed for a host before it is considered unsafe.
*/
private static final int MAX_DNS_ADDRESSES = 20;
private final Set<String> DEFAULT_VALID_SCRIPTS = Set.of("png_to_webp.py", "split_photos.py");
private final Set<String> DEFAULT_VALID_PIPELINE =
Set.of(
@@ -86,36 +91,18 @@ public class GeneralUtils {
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
if (tempFile.exists()) {
try {
Files.delete(tempFile.toPath());
} catch (IOException ignored) {
}
}
throw e;
}
return tempFile;
}
/*
* Gets the configured temporary directory, creating it if necessary.
*
* @return Path to the temporary directory
* @throws IOException if directory creation fails
*/
private Path getTempDirectory() throws IOException {
String customTempDir = System.getenv("STIRLING_TEMPFILES_DIRECTORY");
if (customTempDir == null || customTempDir.isEmpty()) {
customTempDir = System.getProperty("stirling.tempfiles.directory");
}
Path tempDir;
if (customTempDir != null && !customTempDir.isEmpty()) {
tempDir = Path.of(customTempDir);
} else {
tempDir = Path.of(System.getProperty("java.io.tmpdir"), "stirling-pdf");
}
if (!Files.exists(tempDir)) {
Files.createDirectories(tempDir);
}
return tempDir;
}
/*
* Remove file extension
*
@@ -154,7 +141,7 @@ public class GeneralUtils {
return matcher.find() ? matcher.replaceFirst("") : filename;
}
/*
/**
* Append suffix to base name with null safety.
*
* @param baseName the base filename, null becomes "default"
@@ -165,7 +152,7 @@ public class GeneralUtils {
return (baseName == null ? "default" : baseName) + (suffix != null ? suffix : "");
}
/*
/**
* Generate a PDF filename by removing extension from first file and adding suffix.
*
* <p>High-level utility method for common PDF naming scenarios. Handles null safety and uses
@@ -180,7 +167,7 @@ public class GeneralUtils {
return appendSuffix(baseName, suffix);
}
/*
/**
* Process a list of filenames by removing extensions and adding suffix.
*
* <p>Efficiently processes multiple filenames using streaming operations and bulk operations
@@ -201,7 +188,7 @@ public class GeneralUtils {
.forEach(processor);
}
/*
/**
* Extract title from filename by removing extension, with fallback handling.
*
* <p>Returns "Untitled" for null or empty filenames, otherwise removes the extension using the
@@ -269,6 +256,12 @@ public class GeneralUtils {
.getResources(pattern);
}
/**
* Validates URL syntax and disallows common-infrastructure targets to reduce SSRF risk.
*
* @param urlStr a URL string to validate
* @return {@code true} if the URL is syntactically valid and allowed; {@code false} otherwise
*/
public boolean isValidURL(String urlStr) {
try {
Urls.create(
@@ -279,7 +272,7 @@ public class GeneralUtils {
}
}
/*
/**
* Checks if a URL is reachable with proper timeout configuration and error handling.
*
* @param urlStr the URL string to check
@@ -289,15 +282,17 @@ public class GeneralUtils {
return isURLReachable(urlStr, 5000, 5000);
}
/*
* Checks if a URL is reachable with configurable timeouts.
/**
* Checks whether a URL is reachable using configurable timeouts. Only {@code http} and {@code
* https} protocols are permitted, and local/private/multicast ranges are blocked.
*
* @param urlStr the URL string to check
* @param urlStr the URL to probe
* @param connectTimeout connection timeout in milliseconds
* @param readTimeout read timeout in milliseconds
* @return true if URL is reachable, false otherwise
* @return {@code true} if a HEAD request returns a 2xx or 3xx status; {@code false} otherwise
*/
public boolean isURLReachable(String urlStr, int connectTimeout, int readTimeout) {
HttpURLConnection connection = null;
try {
// Parse the URL
URL url = URI.create(urlStr).toURL();
@@ -308,14 +303,17 @@ public class GeneralUtils {
return false; // Disallow other protocols
}
// Check if the host is a local address
String host = url.getHost();
if (isLocalAddress(host)) {
return false; // Exclude local addresses
if (host == null || host.isBlank()) {
return false;
}
if (isDisallowedNetworkLocation(host)) {
return false; // Exclude local, private or otherwise sensitive addresses
}
// Check if the URL is reachable
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("HEAD");
connection.setConnectTimeout(connectTimeout);
connection.setReadTimeout(readTimeout);
@@ -326,29 +324,173 @@ public class GeneralUtils {
} catch (Exception e) {
log.debug("URL {} is not reachable: {}", urlStr, e.getMessage());
return false; // Return false in case of any exception
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
private boolean isLocalAddress(String host) {
/**
* Determines whether the specified host resolves to a disallowed network location, such as
* local, private, multicast, or reserved ranges. Excessive DNS results are also blocked.
*
* @param host the hostname to resolve
* @return {@code true} if the host should be considered unsafe
*/
private boolean isDisallowedNetworkLocation(String host) {
// Resolution is delegated to the JVM/OS resolver which already applies system
// configured query limits and timeouts. We only need the resolved addresses here so
// that we can enforce the MAX_DNS_ADDRESSES limit and perform the sensitive range
// checks below.
try {
// Resolve DNS to IP address
InetAddress address = InetAddress.getByName(host);
// Check for local addresses
return address.isAnyLocalAddress()
|| // Matches 0.0.0.0 or similar
address.isLoopbackAddress()
|| // Matches 127.0.0.1 or ::1
address.isSiteLocalAddress()
|| // Matches private IPv4 ranges: 192.168.x.x, 10.x.x.x, 172.16.x.x to
// 172.31.x.x
address.getHostAddress()
.startsWith("fe80:"); // Matches link-local IPv6 addresses
InetAddress[] addresses = InetAddress.getAllByName(host);
if (addresses.length > MAX_DNS_ADDRESSES) {
log.debug(
"Blocking URL to host {} due to excessive DNS records (>{})",
host,
MAX_DNS_ADDRESSES);
return true;
}
for (InetAddress address : addresses) {
if (address == null || isSensitiveAddress(address)) {
log.debug("Blocking URL to host {} resolved to {}", host, address);
return true;
}
}
return false;
} catch (Exception e) {
return false; // Return false for invalid or unresolved addresses
log.debug("Unable to resolve host {}: {}", host, e.getMessage());
return true; // Treat resolution issues as unsafe to avoid SSRF
}
}
/**
* Returns whether the given IP address lies within ranges that should not be contacted by the
* server (loopback, link-local, private, multicast, etc.). IPv6 ULA and IPv4-mapped addresses
* are handled.
*
* @param address the resolved address
* @return {@code true} if the address is considered sensitive
*/
private boolean isSensitiveAddress(InetAddress address) {
if (address.isAnyLocalAddress()
|| address.isLoopbackAddress()
|| address.isLinkLocalAddress()
|| address.isSiteLocalAddress()
|| address.isMulticastAddress()) {
return true;
}
byte[] rawAddress = address.getAddress();
if (address instanceof Inet4Address) {
return isPrivateOrReservedIPv4(rawAddress);
}
if (address instanceof Inet6Address inet6Address) {
if (isUniqueLocalIPv6(rawAddress)) {
return true;
}
if (isIPv4MappedAddress(rawAddress) || inet6Address.isIPv4CompatibleAddress()) {
byte[] ipv4 =
Arrays.copyOfRange(rawAddress, rawAddress.length - 4, rawAddress.length);
return isPrivateOrReservedIPv4(ipv4);
}
}
return false;
}
/**
* Checks whether an IPv4 address is private or reserved. Any malformed input defaults to {@code
* true} (conservative) to avoid misuse.
*
* @param address 4-byte IPv4 address
* @return {@code true} if private/reserved
*/
private boolean isPrivateOrReservedIPv4(byte[] address) {
// IPv4 addresses must be exactly 4 bytes. Treat null or unexpected lengths as
// sensitive to avoid processing malformed input.
if (address == null || address.length != 4) {
return true;
}
int first = Byte.toUnsignedInt(address[0]);
int second = Byte.toUnsignedInt(address[1]);
if (first == 0 || first == 127) {
return true; // 0.0.0.0/8 and 127.0.0.0/8
}
if (first == 100 && second >= 64 && second <= 127) {
return true; // 100.64.0.0/10 Carrier-grade NAT
}
if (first == 169 && second == 254) {
return true; // 169.254.0.0/16 Link-local
}
if (first == 172 && second >= 16 && second <= 31) {
return true; // 172.16.0.0/12 Private
}
if (first == 192 && second == 0 && Byte.toUnsignedInt(address[2]) == 0) {
return true; // 192.0.0.0/24 IETF Protocol Assignments
}
if (first == 192 && second == 0 && Byte.toUnsignedInt(address[2]) == 2) {
return true; // 192.0.2.0/24 TEST-NET-1
}
if (first == 192 && second == 168) {
return true; // 192.168.0.0/16 Private
}
if (first == 198 && (second == 18 || second == 19)) {
return true; // 198.18.0.0/15 Benchmark tests
}
if (first == 198 && second == 51 && Byte.toUnsignedInt(address[2]) == 100) {
return true; // 198.51.100.0/24 TEST-NET-2
}
if (first == 203 && second == 0 && Byte.toUnsignedInt(address[2]) == 113) {
return true; // 203.0.113.0/24 TEST-NET-3
}
if (first == 10) {
return true; // 10.0.0.0/8 Private
}
if (first >= 224) {
return true; // 224.0.0.0/4 Multicast and 240.0.0.0/4 Reserved for future use
}
return false;
}
/**
* Checks whether an IPv6 address is a Unique Local Address (ULA, fc00::/7). Any malformed input
* defaults to {@code true} (conservative) to avoid misuse.
*
* @param address 16-byte IPv6 address
* @return {@code true} if ULA
*/
private boolean isUniqueLocalIPv6(byte[] address) {
if (address == null || address.length != 16) {
return true;
}
int first = Byte.toUnsignedInt(address[0]);
return (first & 0xFE) == 0xFC; // fc00::/7 Unique local addresses
}
/**
* Checks whether an IPv6 address is an IPv4-mapped address (::ffff:0:0/96). Any malformed input
* defaults to {@code false} (conservative) to avoid misuse.
*
* @param address 16-byte IPv6 address
* @return {@code true} if IPv4-mapped
*/
private boolean isIPv4MappedAddress(byte[] address) {
if (address == null || address.length != 16) {
return false;
}
for (int i = 0; i < 10; i++) {
if (address[i] != 0) {
return false;
}
}
return address[10] == (byte) 0xFF && address[11] == (byte) 0xFF;
}
/*
* Improved multipart file conversion using the shared helper method.
*
@@ -365,6 +507,12 @@ public class GeneralUtils {
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
try {
Files.deleteIfExists(tempFile);
} catch (IOException ignored) {
}
throw e;
}
return tempFile.toFile();
}
@@ -386,7 +534,7 @@ public class GeneralUtils {
throw new IllegalArgumentException("Invalid default unit: " + defaultUnit);
}
sizeStr = sizeStr.trim().toUpperCase();
sizeStr = sizeStr.trim().toUpperCase(Locale.ROOT);
sizeStr = sizeStr.replace(",", ".").replace(" ", "");
try {
@@ -415,7 +563,7 @@ public class GeneralUtils {
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
} else {
// Use provided default unit or fall back to MB
String unit = defaultUnit != null ? defaultUnit.toUpperCase() : "MB";
String unit = defaultUnit != null ? defaultUnit.toUpperCase(Locale.ROOT) : "MB";
double value = Double.parseDouble(sizeStr);
return switch (unit) {
case "TB" -> (long) (value * 1024L * 1024L * 1024L * 1024L);
@@ -457,13 +605,14 @@ public class GeneralUtils {
if (bytes < 1024) {
return bytes + " B";
} else if (bytes < 1024L * 1024L) {
return String.format(Locale.US, "%.2f KB", bytes / 1024.0);
return String.format(Locale.ROOT, "%.2f KB", bytes / 1024.0);
} else if (bytes < 1024L * 1024L * 1024L) {
return String.format(Locale.US, "%.2f MB", bytes / (1024.0 * 1024.0));
return String.format(Locale.ROOT, "%.2f MB", bytes / (1024.0 * 1024.0));
} else if (bytes < 1024L * 1024L * 1024L * 1024L) {
return String.format(Locale.US, "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
return String.format(Locale.ROOT, "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
} else {
return String.format(Locale.US, "%.2f TB", bytes / (1024.0 * 1024.0 * 1024.0 * 1024.0));
return String.format(
Locale.ROOT, "%.2f TB", bytes / (1024.0 * 1024.0 * 1024.0 * 1024.0));
}
}
@@ -723,7 +872,7 @@ public class GeneralUtils {
byte[] mac = net.getHardwareAddress();
if (mac != null && mac.length > 0) {
for (byte b : mac) {
sb.append(String.format("%02X", b));
sb.append(String.format(Locale.ROOT, "%02X", b));
}
break; // Use the first valid network interface
}
@@ -733,7 +882,7 @@ public class GeneralUtils {
byte[] mac = network.getHardwareAddress();
if (mac != null) {
for (byte b : mac) {
sb.append(String.format("%02X", b));
sb.append(String.format(Locale.ROOT, "%02X", b));
}
}
}
@@ -940,17 +1089,29 @@ public class GeneralUtils {
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
.runCommandWithOutputHandling(command);
ExceptionUtils.GhostscriptException detectedError =
ExceptionUtils.detectGhostscriptCriticalError(result.getMessages());
if (detectedError != null) {
log.warn(
"Ghostscript ebook optimization reported a critical error: {}",
detectedError.getMessage());
throw detectedError;
}
if (result.getRc() != 0) {
log.warn(
"Ghostscript ebook optimization failed with return code: {}",
result.getRc());
throw ExceptionUtils.createGhostscriptCompressionException();
throw ExceptionUtils.createGhostscriptCompressionException(result.getMessages());
}
return Files.readAllBytes(tempOutput);
} catch (Exception e) {
log.warn("Ghostscript ebook optimization failed", e);
if (e instanceof ExceptionUtils.GhostscriptException ghostscriptException) {
throw ghostscriptException;
}
throw ExceptionUtils.createGhostscriptCompressionException(e);
} finally {
if (tempInput != null) {
@@ -6,6 +6,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.Locale;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
@@ -62,15 +63,16 @@ public class ImageProcessingUtils {
} else {
int width = image.getWidth();
int height = image.getHeight();
int[] pixels = new int[width * height];
image.getRGB(0, 0, width, height, pixels, 0, width);
byte[] data = new byte[width * height * 3];
int index = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int rgb = image.getRGB(x, y);
data[index++] = (byte) ((rgb >> 16) & 0xFF); // Red
data[index++] = (byte) ((rgb >> 8) & 0xFF); // Green
data[index++] = (byte) (rgb & 0xFF); // Blue
}
for (int rgb : pixels) {
data[index++] = (byte) ((rgb >> 16) & 0xFF); // Red
data[index++] = (byte) ((rgb >> 8) & 0xFF); // Green
data[index++] = (byte) (rgb & 0xFF); // Blue
}
return data;
}
@@ -118,7 +120,7 @@ public class ImageProcessingUtils {
BufferedImage image = null;
String filename = file.getOriginalFilename();
if (filename != null && filename.toLowerCase().endsWith(".psd")) {
if (filename != null && filename.toLowerCase(Locale.ROOT).endsWith(".psd")) {
// For PSD files, try explicit ImageReader
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("PSD");
if (readers.hasNext()) {
@@ -134,7 +136,8 @@ public class ImageProcessingUtils {
throw new IOException(
"Unable to read image from file: "
+ filename
+ ". Supported PSD formats: RGB/CMYK/Gray 8-32 bit, RLE/ZIP compression");
+ ". Supported PSD formats: RGB/CMYK/Gray 8-32 bit, RLE/ZIP"
+ " compression");
}
} else {
// For non-PSD files, use standard ImageIO
@@ -142,7 +145,7 @@ public class ImageProcessingUtils {
}
if (image == null) {
throw new IOException("Unable to read image from file: " + filename);
throw ExceptionUtils.createImageReadException(filename);
}
double orientation = extractImageOrientation(file.getInputStream());
@@ -27,15 +27,22 @@ import io.github.pixee.security.Filenames;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
@Slf4j
public class PDFToFile {
private final TempFileManager tempFileManager;
private final RuntimePathConfig runtimePathConfig;
public PDFToFile(TempFileManager tempFileManager) {
this(tempFileManager, null);
}
public PDFToFile(TempFileManager tempFileManager, RuntimePathConfig runtimePathConfig) {
this.tempFileManager = tempFileManager;
this.runtimePathConfig = runtimePathConfig;
}
public ResponseEntity<byte[]> processPdfToMarkdown(MultipartFile inputFile)
@@ -241,31 +248,65 @@ public class PDFToFile {
byte[] fileBytes;
String fileName;
Path libreOfficeProfile = null;
try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf");
TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) {
Path tempInputFile = inputFileTemp.getPath();
Path tempOutputDir = outputDirTemp.getPath();
Path unoOutputFile =
tempOutputDir.resolve(
pdfBaseName + "." + resolvePrimaryExtension(outputFormat));
// Save the uploaded file to a temporary location
inputFile.transferTo(tempInputFile);
// Run the LibreOffice command
List<String> command =
new ArrayList<>(
Arrays.asList(
"soffice",
"--headless",
"--nologo",
"--infilter=" + libreOfficeFilter,
"--convert-to",
outputFormat,
"--outdir",
tempOutputDir.toString(),
tempInputFile.toString()));
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
.runCommandWithOutputHandling(command);
ProcessExecutorResult returnCode = null;
IOException unoconvertException = null;
if (isUnoConvertEnabled()) {
try {
List<String> unoCommand =
buildUnoConvertCommand(
tempInputFile, unoOutputFile, outputFormat, libreOfficeFilter);
returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
.runCommandWithOutputHandling(unoCommand);
} catch (IOException e) {
unoconvertException = e;
log.warn(
"Unoconvert command failed ({}). Falling back to soffice command.",
e.getMessage());
}
}
if (returnCode == null) {
// Run the LibreOffice command as a fallback
libreOfficeProfile = Files.createTempDirectory("libreoffice_profile_");
List<String> command = new ArrayList<>();
command.add(runtimePathConfig.getSOfficePath());
command.add("-env:UserInstallation=" + libreOfficeProfile.toUri().toString());
command.add("--headless");
command.add("--nologo");
command.add("--infilter=" + libreOfficeFilter);
command.add("--convert-to");
command.add(outputFormat);
command.add("--outdir");
command.add(tempOutputDir.toString());
command.add(tempInputFile.toString());
try {
returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
.runCommandWithOutputHandling(command);
} catch (IOException e) {
if (unoconvertException != null) {
e.addSuppressed(unoconvertException);
}
throw e;
}
}
// Get output files
List<File> outputFiles = Arrays.asList(tempOutputDir.toFile().listFiles());
@@ -300,8 +341,40 @@ public class PDFToFile {
fileBytes = byteArrayOutputStream.toByteArray();
}
} finally {
if (libreOfficeProfile != null) {
FileUtils.deleteQuietly(libreOfficeProfile.toFile());
}
}
return WebResponseUtils.bytesToWebResponse(
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
}
private boolean isUnoConvertEnabled() {
return runtimePathConfig != null
&& runtimePathConfig.getUnoConvertPath() != null
&& !runtimePathConfig.getUnoConvertPath().isBlank();
}
private List<String> buildUnoConvertCommand(
Path inputFile, Path outputFile, String outputFormat, String libreOfficeFilter) {
List<String> command = new ArrayList<>();
command.add(runtimePathConfig.getUnoConvertPath());
command.add("--convert-to");
command.add(outputFormat);
if (libreOfficeFilter != null && !libreOfficeFilter.isBlank()) {
command.add("--input-filter=" + libreOfficeFilter);
}
command.add(inputFile.toString());
command.add(outputFile.toString());
return command;
}
private String resolvePrimaryExtension(String outputFormat) {
if (outputFormat == null) {
return "";
}
int colonIndex = outputFormat.indexOf(':');
return colonIndex > 0 ? outputFormat.substring(0, colonIndex) : outputFormat;
}
}
@@ -318,7 +318,7 @@ public class PdfAttachmentHandler {
private static String normalizeFilename(String filename) {
if (filename == null) return "";
String normalized = filename.toLowerCase().trim();
String normalized = filename.toLowerCase(Locale.ROOT).trim();
normalized =
RegexPatternUtils.getInstance()
.getWhitespacePattern()
@@ -560,7 +560,7 @@ public class PdfAttachmentHandler {
@Override
protected void writeString(String string, List<TextPosition> textPositions)
throws IOException {
String lowerString = string.toLowerCase();
String lowerString = string.toLowerCase(Locale.ROOT);
if (ATTACHMENT_SECTION_PATTERN.matcher(lowerString).find()) {
isInAttachmentSection = true;
@@ -9,6 +9,7 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import javax.imageio.ImageIO;
@@ -34,7 +35,7 @@ public class PdfToCbrUtils {
try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
if (document.getNumberOfPages() == 0) {
throw new IllegalArgumentException("PDF file contains no pages");
throw ExceptionUtils.createPdfNoPages();
}
return createCbrFromPdf(document, dpi);
@@ -43,22 +44,23 @@ public class PdfToCbrUtils {
private static void validatePdfFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("File cannot be null or empty");
throw ExceptionUtils.createFileNullOrEmptyException();
}
String filename = file.getOriginalFilename();
if (filename == null) {
throw new IllegalArgumentException("File must have a name");
throw ExceptionUtils.createFileNoNameException();
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
if (!"pdf".equals(extension)) {
throw new IllegalArgumentException("File must be a PDF");
throw ExceptionUtils.createPdfFileRequiredException();
}
}
private static byte[] createCbrFromPdf(PDDocument document, int dpi) throws IOException {
PDFRenderer pdfRenderer = new PDFRenderer(document);
pdfRenderer.setSubsamplingAllowed(true); // Enable subsampling to reduce memory usage
Path tempDir = Files.createTempDirectory("stirling-pdf-cbr-");
List<Path> generatedImages = new ArrayList<>();
@@ -66,27 +68,36 @@ public class PdfToCbrUtils {
int totalPages = document.getNumberOfPages();
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
final int currentPage = pageIndex;
try {
BufferedImage image =
pdfRenderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB);
ExceptionUtils.handleOomRendering(
currentPage + 1,
dpi,
() ->
pdfRenderer.renderImageWithDPI(
currentPage, dpi, ImageType.RGB));
String imageFilename = String.format("page_%03d.png", pageIndex + 1);
String imageFilename =
String.format(Locale.ROOT, "page_%03d.png", currentPage + 1);
Path imagePath = tempDir.resolve(imageFilename);
ImageIO.write(image, "PNG", imagePath.toFile());
generatedImages.add(imagePath);
} catch (ExceptionUtils.OutOfMemoryDpiException e) {
// Re-throw OOM exceptions without wrapping
throw e;
} catch (IOException e) {
log.warn("Error processing page {}: {}", pageIndex + 1, e.getMessage());
} catch (OutOfMemoryError e) {
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
} catch (NegativeArraySizeException e) {
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
// Wrap other IOExceptions with context
throw ExceptionUtils.createFileProcessingException(
"CBR creation for page " + (currentPage + 1), e);
}
}
if (generatedImages.isEmpty()) {
throw new IOException("Failed to render any pages to images for CBR conversion");
throw ExceptionUtils.createFileProcessingException(
"CBR conversion", new IOException("No pages were successfully rendered"));
}
return createRarArchive(tempDir, generatedImages);
@@ -115,15 +126,18 @@ public class PdfToCbrUtils {
ProcessExecutorResult result =
executor.runCommandWithOutputHandling(command, tempDir.toFile());
if (result.getRc() != 0) {
throw new IOException("RAR command failed: " + result.getMessages());
throw ExceptionUtils.createFileProcessingException(
"RAR archive creation",
new IOException("RAR command failed with code " + result.getRc()));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("RAR command interrupted", e);
throw ExceptionUtils.createProcessingInterruptedException("RAR creation", e);
}
if (!Files.exists(rarFile)) {
throw new IOException("RAR file was not created");
throw ExceptionUtils.createFileProcessingException(
"RAR archive creation", new IOException("RAR file was not created"));
}
try (FileInputStream fis = new FileInputStream(rarFile.toFile());
@@ -167,7 +181,7 @@ public class PdfToCbrUtils {
return false;
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
return "pdf".equals(extension);
}
}
@@ -3,6 +3,7 @@ package stirling.software.common.util;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -29,7 +30,7 @@ public class PdfToCbzUtils {
try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
if (document.getNumberOfPages() == 0) {
throw new IllegalArgumentException("PDF file contains no pages");
throw ExceptionUtils.createPdfNoPages();
}
return createCbzFromPdf(document, dpi);
@@ -38,22 +39,23 @@ public class PdfToCbzUtils {
private static void validatePdfFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("File cannot be null or empty");
throw ExceptionUtils.createFileNullOrEmptyException();
}
String filename = file.getOriginalFilename();
if (filename == null) {
throw new IllegalArgumentException("File must have a name");
throw ExceptionUtils.createFileNoNameException();
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
if (!"pdf".equals(extension)) {
throw new IllegalArgumentException("File must be a PDF");
throw ExceptionUtils.createPdfFileRequiredException();
}
}
private static byte[] createCbzFromPdf(PDDocument document, int dpi) throws IOException {
PDFRenderer pdfRenderer = new PDFRenderer(document);
pdfRenderer.setSubsamplingAllowed(true); // Enable subsampling to reduce memory usage
try (ByteArrayOutputStream cbzOutputStream = new ByteArrayOutputStream();
ZipOutputStream zipOut = new ZipOutputStream(cbzOutputStream)) {
@@ -61,24 +63,31 @@ public class PdfToCbzUtils {
int totalPages = document.getNumberOfPages();
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
final int currentPage = pageIndex;
try {
BufferedImage image =
pdfRenderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB);
String imageFilename = String.format("page_%03d.png", pageIndex + 1);
ExceptionUtils.handleOomRendering(
currentPage + 1,
dpi,
() ->
pdfRenderer.renderImageWithDPI(
currentPage, dpi, ImageType.RGB));
String imageFilename =
String.format(Locale.ROOT, "page_%03d.png", currentPage + 1);
ZipEntry zipEntry = new ZipEntry(imageFilename);
zipOut.putNextEntry(zipEntry);
ImageIO.write(image, "PNG", zipOut);
zipOut.closeEntry();
} catch (ExceptionUtils.OutOfMemoryDpiException e) {
// Re-throw OOM exceptions without wrapping
throw e;
} catch (IOException e) {
log.warn("Error processing page {}: {}", pageIndex + 1, e.getMessage());
} catch (OutOfMemoryError e) {
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
} catch (NegativeArraySizeException e) {
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
// Wrap other IOExceptions with context
throw ExceptionUtils.createFileProcessingException(
"CBZ creation for page " + (currentPage + 1), e);
}
}
@@ -93,7 +102,7 @@ public class PdfToCbzUtils {
return false;
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
return "pdf".equals(extension);
}
}
@@ -8,6 +8,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
@@ -50,36 +51,18 @@ public class PdfUtils {
public PDRectangle textToPageSize(String size) {
switch (size.toUpperCase()) {
case "A0" -> {
return PDRectangle.A0;
}
case "A1" -> {
return PDRectangle.A1;
}
case "A2" -> {
return PDRectangle.A2;
}
case "A3" -> {
return PDRectangle.A3;
}
case "A4" -> {
return PDRectangle.A4;
}
case "A5" -> {
return PDRectangle.A5;
}
case "A6" -> {
return PDRectangle.A6;
}
case "LETTER" -> {
return PDRectangle.LETTER;
}
case "LEGAL" -> {
return PDRectangle.LEGAL;
}
return switch (size.toUpperCase(Locale.ROOT)) {
case "A0" -> PDRectangle.A0;
case "A1" -> PDRectangle.A1;
case "A2" -> PDRectangle.A2;
case "A3" -> PDRectangle.A3;
case "A4" -> PDRectangle.A4;
case "A5" -> PDRectangle.A5;
case "A6" -> PDRectangle.A6;
case "LETTER" -> PDRectangle.LETTER;
case "LEGAL" -> PDRectangle.LEGAL;
default -> throw ExceptionUtils.createInvalidPageSizeException(size);
}
};
}
public List<RenderedImage> getAllImages(PDResources resources) throws IOException {
@@ -136,11 +119,11 @@ public class PdfUtils {
public boolean hasTextOnPage(PDPage page, String phrase) throws IOException {
PDFTextStripper textStripper = new PDFTextStripper();
PDDocument tempDoc = new PDDocument();
tempDoc.addPage(page);
String pageText = textStripper.getText(tempDoc);
tempDoc.close();
return pageText.contains(phrase);
try (PDDocument tempDoc = new PDDocument()) {
tempDoc.addPage(page);
String pageText = textStripper.getText(tempDoc);
return pageText.contains(phrase);
}
}
public byte[] convertFromPdf(
@@ -170,7 +153,8 @@ public class PdfUtils {
maxSafeDpi);
}
try (PDDocument document = pdfDocumentFactory.load(inputStream)) {
try (PDDocument document = pdfDocumentFactory.load(inputStream);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
PDFRenderer pdfRenderer = new PDFRenderer(document);
pdfRenderer.setSubsamplingAllowed(true);
if (!includeAnnotations) {
@@ -178,12 +162,9 @@ public class PdfUtils {
}
int pageCount = document.getNumberOfPages();
// Create a ByteArrayOutputStream to save the image(s) to
ByteArrayOutputStream baos = new ByteArrayOutputStream();
if (singleImage) {
if ("tiff".equals(imageType.toLowerCase())
|| "tif".equals(imageType.toLowerCase())) {
if ("tiff".equals(imageType.toLowerCase(Locale.ROOT))
|| "tif".equals(imageType.toLowerCase(Locale.ROOT))) {
// Write the images to the output stream as a TIFF with multiple frames
ImageWriter writer = ImageIO.getImageWritersByFormatName("tiff").next();
ImageWriteParam param = writer.getDefaultWriteParam();
@@ -196,9 +177,20 @@ public class PdfUtils {
writer.prepareWriteSequence(null);
for (int i = 0; i < pageCount; ++i) {
final int pageIndex = i;
BufferedImage image;
try {
image = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
// Validate dimensions before rendering
ExceptionUtils.validateRenderingDimensions(
document.getPage(pageIndex), pageIndex + 1, DPI);
image =
ExceptionUtils.handleOomRendering(
pageIndex + 1,
DPI,
() ->
pdfRenderer.renderImageWithDPI(
pageIndex, DPI, colorType));
} catch (IllegalArgumentException e) {
if (e.getMessage() != null
&& e.getMessage()
@@ -212,10 +204,6 @@ public class PdfUtils {
DPI);
}
throw e;
} catch (OutOfMemoryError e) {
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
} catch (NegativeArraySizeException e) {
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
}
writer.writeToSequence(new IIOImage(image, null, null), param);
}
@@ -239,6 +227,7 @@ public class PdfUtils {
HashMap<PdfRenderSettingsKey, PdfImageDimensionValue> pageSizes =
new HashMap<>();
for (int i = 0; i < pageCount; ++i) {
final int pageIndex = i;
PDPage page = document.getPage(i);
PDRectangle mediaBox = page.getMediaBox();
int rotation = page.getRotation();
@@ -249,7 +238,17 @@ public class PdfUtils {
if (dimension == null) {
// Render the image to get the dimensions
try {
pdfSizeImage = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
// Validate dimensions before rendering
ExceptionUtils.validateRenderingDimensions(
page, pageIndex + 1, DPI);
pdfSizeImage =
ExceptionUtils.handleOomRendering(
pageIndex + 1,
DPI,
() ->
pdfRenderer.renderImageWithDPI(
pageIndex, DPI, colorType));
} catch (IllegalArgumentException e) {
if (e.getMessage() != null
&& e.getMessage()
@@ -264,10 +263,6 @@ public class PdfUtils {
DPI);
}
throw e;
} catch (OutOfMemoryError e) {
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
} catch (NegativeArraySizeException e) {
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
}
pdfSizeImageIndex = i;
dimension =
@@ -293,11 +288,22 @@ public class PdfUtils {
boolean firstImageAlreadyRendered = pdfSizeImageIndex == 0;
for (int i = 0; i < pageCount; ++i) {
final int pageIndex = i;
if (firstImageAlreadyRendered && i == 0) {
pageImage = pdfSizeImage;
} else {
try {
pageImage = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
// Validate dimensions before rendering
ExceptionUtils.validateRenderingDimensions(
document.getPage(pageIndex), pageIndex + 1, DPI);
pageImage =
ExceptionUtils.handleOomRendering(
pageIndex + 1,
DPI,
() ->
pdfRenderer.renderImageWithDPI(
pageIndex, DPI, colorType));
} catch (IllegalArgumentException e) {
if (e.getMessage() != null
&& e.getMessage()
@@ -311,10 +317,6 @@ public class PdfUtils {
DPI);
}
throw e;
} catch (OutOfMemoryError e) {
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
} catch (NegativeArraySizeException e) {
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
}
}
@@ -335,9 +337,20 @@ public class PdfUtils {
// Zip the images and return as byte array
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
for (int i = 0; i < pageCount; ++i) {
final int pageIndex = i;
BufferedImage image;
try {
image = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
// Validate dimensions before rendering
ExceptionUtils.validateRenderingDimensions(
document.getPage(pageIndex), pageIndex + 1, DPI);
image =
ExceptionUtils.handleOomRendering(
pageIndex + 1,
DPI,
() ->
pdfRenderer.renderImageWithDPI(
pageIndex, DPI, colorType));
} catch (IllegalArgumentException e) {
if (e.getMessage() != null
&& e.getMessage().contains("Maximum size of image exceeded")) {
@@ -349,10 +362,6 @@ public class PdfUtils {
DPI);
}
throw e;
} catch (OutOfMemoryError e) {
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
} catch (NegativeArraySizeException e) {
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
}
try (ByteArrayOutputStream baosImage = new ByteArrayOutputStream()) {
ImageIO.write(image, imageType, baosImage);
@@ -361,9 +370,10 @@ public class PdfUtils {
zos.putNextEntry(
new ZipEntry(
String.format(
Locale.ROOT,
filename + "_%d.%s",
i + 1,
imageType.toLowerCase())));
imageType.toLowerCase(Locale.ROOT))));
zos.write(baosImage.toByteArray());
}
}
@@ -388,51 +398,61 @@ public class PdfUtils {
*/
public PDDocument convertPdfToPdfImage(PDDocument document) throws IOException {
PDDocument imageDocument = new PDDocument();
PDFRenderer pdfRenderer = new PDFRenderer(document);
pdfRenderer.setSubsamplingAllowed(true);
for (int page = 0; page < document.getNumberOfPages(); ++page) {
BufferedImage bim;
try {
PDFRenderer pdfRenderer = new PDFRenderer(document);
pdfRenderer.setSubsamplingAllowed(true);
for (int page = 0; page < document.getNumberOfPages(); ++page) {
final int pageIndex = page;
BufferedImage bim;
// Use global maximum DPI setting, fallback to 300 if not set
int renderDpi = 300; // Default fallback
ApplicationProperties properties =
ApplicationContextProvider.getBean(ApplicationProperties.class);
if (properties != null && properties.getSystem() != null) {
renderDpi = properties.getSystem().getMaxDPI();
}
try {
bim = pdfRenderer.renderImageWithDPI(page, renderDpi, ImageType.RGB);
} catch (IllegalArgumentException e) {
if (e.getMessage() != null
&& 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.",
page + 1);
// Use global maximum DPI setting, fallback to 300 if not set
int renderDpi = 300; // Default fallback
ApplicationProperties properties =
ApplicationContextProvider.getBean(ApplicationProperties.class);
if (properties != null && properties.getSystem() != null) {
renderDpi = properties.getSystem().getMaxDPI();
}
throw e;
} catch (OutOfMemoryError e) {
throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, 300, e);
} catch (NegativeArraySizeException e) {
throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, 300, e);
final int dpi = renderDpi;
try {
bim =
ExceptionUtils.handleOomRendering(
pageIndex + 1,
dpi,
() ->
pdfRenderer.renderImageWithDPI(
pageIndex, dpi, ImageType.RGB));
} catch (IllegalArgumentException e) {
if (e.getMessage() != null
&& 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.",
pageIndex + 1);
}
throw e;
}
PDPage originalPage = document.getPage(page);
float width = originalPage.getMediaBox().getWidth();
float height = originalPage.getMediaBox().getHeight();
PDPage newPage = new PDPage(new PDRectangle(width, height));
imageDocument.addPage(newPage);
PDImageXObject pdImage = LosslessFactory.createFromImage(imageDocument, bim);
try (PDPageContentStream contentStream =
new PDPageContentStream(
imageDocument, newPage, AppendMode.APPEND, true, true)) {
contentStream.drawImage(pdImage, 0, 0, width, height);
}
bim.flush();
}
PDPage originalPage = document.getPage(page);
float width = originalPage.getMediaBox().getWidth();
float height = originalPage.getMediaBox().getHeight();
PDPage newPage = new PDPage(new PDRectangle(width, height));
imageDocument.addPage(newPage);
PDImageXObject pdImage = LosslessFactory.createFromImage(imageDocument, bim);
PDPageContentStream contentStream =
new PDPageContentStream(imageDocument, newPage, AppendMode.APPEND, true, true);
contentStream.drawImage(pdImage, 0, 0, width, height);
contentStream.close();
return imageDocument;
} catch (Exception e) {
throw e;
}
return imageDocument;
}
private BufferedImage prepareImageForPdfToImage(int maxWidth, int height, String imageType) {
@@ -463,8 +483,8 @@ public class PdfUtils {
String contentType = file.getContentType();
String originalFilename = Filenames.toSimpleFileName(file.getOriginalFilename());
if (originalFilename != null
&& (originalFilename.toLowerCase().endsWith(".tiff")
|| originalFilename.toLowerCase().endsWith(".tif"))) {
&& (originalFilename.toLowerCase(Locale.ROOT).endsWith(".tiff")
|| originalFilename.toLowerCase(Locale.ROOT).endsWith(".tif"))) {
ImageReader reader = ImageIO.getImageReadersByFormatName("tiff").next();
reader.setInput(ImageIO.createImageInputStream(file.getInputStream()));
int numPages = reader.getNumImages(true);
@@ -567,7 +587,7 @@ public class PdfUtils {
PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, "");
// Draw the image onto the page at the specified x and y coordinates
contentStream.drawImage(image, x, y);
log.info("Image successfully overlayed onto PDF");
log.info("Image successfully overlaid onto PDF");
if (!everyPage && i == 0) {
break;
}
@@ -631,7 +651,7 @@ public class PdfUtils {
int actualPageCount = pdfDocument.getNumberOfPages();
pdfDocument.close();
return switch (comparator.toLowerCase()) {
return switch (comparator.toLowerCase(Locale.ROOT)) {
case "greater" -> actualPageCount > pageCount;
case "equal" -> actualPageCount == pageCount;
case "less" -> actualPageCount < pageCount;
@@ -6,9 +6,12 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
@@ -26,11 +29,15 @@ public class ProcessExecutor {
private static final Map<Processes, ProcessExecutor> instances = new ConcurrentHashMap<>();
private static ApplicationProperties applicationProperties = new ApplicationProperties();
private static volatile UnoServerPool unoServerPool;
private final Semaphore semaphore;
private final boolean liveUpdates;
private long timeoutDuration;
private final Processes processType;
private ProcessExecutor(int semaphoreLimit, boolean liveUpdates, long timeout) {
private ProcessExecutor(
Processes processType, int semaphoreLimit, boolean liveUpdates, long timeout) {
this.processType = processType;
this.semaphore = new Semaphore(semaphoreLimit);
this.liveUpdates = liveUpdates;
this.timeoutDuration = timeout;
@@ -102,6 +109,11 @@ public class ProcessExecutor {
.getSessionLimit()
.getOcrMyPdfSessionLimit();
case CFF_CONVERTER -> 1;
case FFMPEG ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getFfmpegSessionLimit();
};
long timeoutMinutes =
@@ -162,11 +174,21 @@ public class ProcessExecutor {
.getTimeoutMinutes()
.getOcrMyPdfTimeoutMinutes();
case CFF_CONVERTER -> 5L;
case FFMPEG ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getFfmpegTimeoutMinutes();
};
return new ProcessExecutor(semaphoreLimit, liveUpdates, timeoutMinutes);
return new ProcessExecutor(
processType, semaphoreLimit, liveUpdates, timeoutMinutes);
});
}
public static void setUnoServerPool(UnoServerPool pool) {
unoServerPool = pool;
}
public ProcessExecutorResult runCommandWithOutputHandling(List<String> command)
throws IOException, InterruptedException {
return runCommandWithOutputHandling(command, null);
@@ -176,11 +198,22 @@ public class ProcessExecutor {
List<String> command, File workingDirectory) throws IOException, InterruptedException {
String messages = "";
int exitCode = 1;
semaphore.acquire();
UnoServerPool.UnoServerLease unoLease = null;
boolean useSemaphore = true;
List<String> commandToRun = command;
if (shouldUseUnoServerPool(command)) {
unoLease = unoServerPool.acquireEndpoint();
commandToRun = applyUnoServerEndpoint(command, unoLease.getEndpoint());
useSemaphore = false;
}
if (useSemaphore) {
semaphore.acquire();
}
try {
log.info("Running command: {}", String.join(" ", command));
ProcessBuilder processBuilder = new ProcessBuilder(command);
validateCommand(commandToRun);
log.info("Running command: {}", String.join(" ", commandToRun));
ProcessBuilder processBuilder = new ProcessBuilder(commandToRun);
// Use the working directory if it's set
if (workingDirectory != null) {
@@ -258,7 +291,9 @@ public class ProcessExecutor {
outputReaderThread.join();
boolean isQpdf =
command != null && !command.isEmpty() && command.get(0).contains("qpdf");
commandToRun != null
&& !commandToRun.isEmpty()
&& commandToRun.get(0).contains("qpdf");
if (!outputLines.isEmpty()) {
String outputMessage = String.join("\n", outputLines);
@@ -299,11 +334,195 @@ public class ProcessExecutor {
}
}
} finally {
semaphore.release();
if (useSemaphore) {
semaphore.release();
}
if (unoLease != null) {
unoLease.close();
}
}
return new ProcessExecutorResult(exitCode, messages);
}
private boolean shouldUseUnoServerPool(List<String> command) {
if (processType != Processes.LIBRE_OFFICE || unoServerPool == null) {
return false;
}
if (unoServerPool.isEmpty()) {
return false;
}
if (command == null || command.isEmpty()) {
return false;
}
// Check if this is a UNO conversion by looking for unoconvert executable
String executable = command.get(0);
if (executable != null) {
// Extract basename from path for matching
String basename = executable;
int lastSlash = Math.max(executable.lastIndexOf('/'), executable.lastIndexOf('\\'));
if (lastSlash >= 0) {
basename = executable.substring(lastSlash + 1);
}
// Strip .exe extension on Windows
if (basename.toLowerCase(java.util.Locale.ROOT).endsWith(".exe")) {
basename = basename.substring(0, basename.length() - 4);
}
// Match common unoconvert variants (but NOT soffice)
String lowerBasename = basename.toLowerCase(java.util.Locale.ROOT);
if (lowerBasename.contains("unoconvert") || lowerBasename.equals("unoconv")) {
return true;
}
}
return false;
}
private List<String> applyUnoServerEndpoint(
List<String> command,
ApplicationProperties.ProcessExecutor.UnoServerEndpoint endpoint) {
if (endpoint == null || command == null || command.isEmpty()) {
return command;
}
List<String> updated = stripUnoEndpointArgs(command);
String host = endpoint.getHost();
int port = endpoint.getPort();
String hostLocation = endpoint.getHostLocation();
String protocol = endpoint.getProtocol();
// Normalize and validate host
if (host == null || host.isBlank()) {
host = "127.0.0.1";
}
// Normalize and validate port
if (port <= 0) {
port = 2003;
}
// Normalize and validate hostLocation (only auto|local|remote allowed)
if (hostLocation == null) {
hostLocation = "auto";
} else {
hostLocation = hostLocation.trim().toLowerCase(java.util.Locale.ROOT);
if (!Set.of("auto", "local", "remote").contains(hostLocation)) {
log.warn(
"Invalid hostLocation '{}' for endpoint {}:{}, defaulting to 'auto'",
hostLocation,
host,
port);
hostLocation = "auto";
}
}
// Normalize and validate protocol (only http|https allowed)
if (protocol == null) {
protocol = "http";
} else {
protocol = protocol.trim().toLowerCase(java.util.Locale.ROOT);
if (!Set.of("http", "https").contains(protocol)) {
log.warn(
"Invalid protocol '{}' for endpoint {}:{}, defaulting to 'http'",
protocol,
host,
port);
protocol = "http";
}
}
int insertIndex = Math.min(1, updated.size());
updated.add(insertIndex++, "--host");
updated.add(insertIndex++, host);
updated.add(insertIndex++, "--port");
updated.add(insertIndex++, String.valueOf(port));
// Only inject --host-location if non-default (for compatibility with older unoconvert)
if (!"auto".equals(hostLocation)) {
updated.add(insertIndex++, "--host-location");
updated.add(insertIndex++, hostLocation);
}
// Only inject --protocol if non-default (for compatibility with older unoconvert)
if (!"http".equals(protocol)) {
updated.add(insertIndex++, "--protocol");
updated.add(insertIndex, protocol);
}
return updated;
}
private List<String> stripUnoEndpointArgs(List<String> command) {
List<String> stripped = new ArrayList<>(command.size());
for (int i = 0; i < command.size(); i++) {
String arg = command.get(i);
if ("--host".equals(arg)
|| "--port".equals(arg)
|| "--host-location".equals(arg)
|| "--protocol".equals(arg)) {
i++;
continue;
}
if (arg != null
&& (arg.startsWith("--host=")
|| arg.startsWith("--port=")
|| arg.startsWith("--host-location=")
|| arg.startsWith("--protocol="))) {
continue;
}
stripped.add(arg);
}
return stripped;
}
private void validateCommand(List<String> command) {
if (command == null || command.isEmpty()) {
throw new IllegalArgumentException("Command must not be empty");
}
// Validate all arguments for null bytes and newlines (actual security concerns)
for (String arg : command) {
if (arg == null) {
throw new IllegalArgumentException("Command contains null argument");
}
if (arg.indexOf('\0') >= 0 || arg.indexOf('\n') >= 0 || arg.indexOf('\r') >= 0) {
throw new IllegalArgumentException("Command contains invalid characters");
}
}
// Validate executable (first argument)
String executable = command.get(0);
if (executable == null || executable.isBlank()) {
throw new IllegalArgumentException("Command executable must not be empty");
}
// Check for path traversal in executable
if (executable.contains("..")) {
throw new IllegalArgumentException(
"Command executable contains path traversal: " + executable);
}
// For absolute paths, verify the file exists and is executable
if (executable.contains("/") || executable.contains("\\")) {
Path execPath;
try {
execPath = Path.of(executable);
} catch (Exception e) {
throw new IllegalArgumentException("Invalid executable path: " + executable, e);
}
if (!Files.exists(execPath)) {
throw new IllegalArgumentException(
"Command executable does not exist: " + executable);
}
if (!Files.isRegularFile(execPath)) {
throw new IllegalArgumentException(
"Command executable is not a regular file: " + executable);
}
}
// For relative paths, trust that PATH resolution will work or fail appropriately
}
public enum Processes {
LIBRE_OFFICE,
PDFTOHTML,
@@ -316,7 +535,8 @@ public class ProcessExecutor {
QPDF,
GHOSTSCRIPT,
OCR_MY_PDF,
CFF_CONVERTER
CFF_CONVERTER,
FFMPEG
}
@Setter
@@ -325,6 +325,11 @@ public final class RegexPatternUtils {
return getPattern("\\+");
}
/** Pattern for splitting on pipe delimiter (used for hint lists in i18n messages) */
public Pattern getPipeDelimiterPattern() {
return getPattern("\\|");
}
/** Pattern for username validation */
public Pattern getUsernameValidationPattern() {
return getPattern("^[a-zA-Z0-9](?!.*[-@._+]{2,})[a-zA-Z0-9@._+-]{1,48}[a-zA-Z0-9]$");
@@ -567,6 +572,16 @@ public final class RegexPatternUtils {
return getPattern("^[a-zA-Z0-9]{2,4}$", Pattern.CASE_INSENSITIVE);
}
/** Pattern for splitting on line breaks (Unicode line separator) */
public Pattern getLineSeparatorPattern() {
return getPattern("\\R");
}
/** Pattern for removing leading asterisks and whitespace */
public Pattern getLeadingAsterisksWhitespacePattern() {
return getPattern("^[*\\s]+");
}
private record PatternKey(String regex, int flags) {
// Record automatically provides equals, hashCode, and toString
}
@@ -52,6 +52,11 @@ public class RequestUriUtils {
return true;
}
// Mobile scanner page for QR code-based file uploads (peer-to-peer, no backend auth needed)
if (normalizedUri.startsWith("/mobile-scanner")) {
return true;
}
// Treat common static file extensions as static resources
return normalizedUri.endsWith(".svg")
|| normalizedUri.endsWith(".png")
@@ -168,6 +173,8 @@ public class RequestUriUtils {
"/api/v1/ui-data/footer-info") // Public footer configuration
|| trimmedUri.startsWith("/api/v1/invite/validate")
|| trimmedUri.startsWith("/api/v1/invite/accept")
|| trimmedUri.startsWith(
"/api/v1/mobile-scanner/") // Mobile scanner endpoints (no auth)
|| trimmedUri.startsWith("/v1/api-docs");
}
@@ -28,6 +28,17 @@ public class TempFileManager {
private final TempFileRegistry registry;
private final ApplicationProperties applicationProperties;
/**
* Create a managed temporary file that will be tracked by the TempFileManager.
*
* @param suffix The suffix for the temporary file
* @return The created temporary file wrapper
* @throws IOException If an I/O error occurs
*/
public TempFile createManagedTempFile(String suffix) throws IOException {
return new TempFile(this, suffix);
}
/**
* Create a temporary file with the Stirling-PDF prefix. The file is automatically registered
* with the registry.
@@ -130,7 +141,6 @@ public class TempFileManager {
return deleted;
} catch (IOException e) {
log.warn("Failed to delete temp file: {}", path.toString(), e);
return false;
}
}
return false;
@@ -0,0 +1,85 @@
package stirling.software.common.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import stirling.software.common.model.ApplicationProperties;
public class UnoServerPool {
private final List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints;
private final BlockingQueue<Integer> availableIndices;
public UnoServerPool(List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints) {
if (endpoints == null || endpoints.isEmpty()) {
this.endpoints = Collections.emptyList();
this.availableIndices = new LinkedBlockingQueue<>();
} else {
this.endpoints = new ArrayList<>(endpoints);
this.availableIndices = new LinkedBlockingQueue<>();
// Initialize queue with all endpoint indices
for (int i = 0; i < this.endpoints.size(); i++) {
this.availableIndices.offer(i);
}
}
}
public boolean isEmpty() {
return endpoints.isEmpty();
}
public UnoServerLease acquireEndpoint() throws InterruptedException {
if (endpoints.isEmpty()) {
return new UnoServerLease(defaultEndpoint(), null, this);
}
// Block until an endpoint index becomes available
Integer index = availableIndices.take();
return new UnoServerLease(endpoints.get(index), index, this);
}
private void releaseEndpoint(Integer index) {
if (index != null) {
availableIndices.offer(index);
}
}
private static ApplicationProperties.ProcessExecutor.UnoServerEndpoint defaultEndpoint() {
return new ApplicationProperties.ProcessExecutor.UnoServerEndpoint();
}
public static class UnoServerLease implements AutoCloseable {
private final ApplicationProperties.ProcessExecutor.UnoServerEndpoint endpoint;
private final Integer index;
private final UnoServerPool pool;
private final AtomicBoolean closed = new AtomicBoolean(false);
public UnoServerLease(
ApplicationProperties.ProcessExecutor.UnoServerEndpoint endpoint,
Integer index,
UnoServerPool pool) {
this.endpoint = endpoint;
this.index = index;
this.pool = pool;
}
public ApplicationProperties.ProcessExecutor.UnoServerEndpoint getEndpoint() {
return endpoint;
}
@Override
public void close() {
// Idempotent close: only release once even if close() called multiple times
if (!closed.compareAndSet(false, true)) {
return;
}
if (pool != null && index != null) {
pool.releaseEndpoint(index);
}
}
}
}
@@ -1,14 +0,0 @@
package stirling.software.common.util;
import java.util.Collection;
public class ValidationUtil {
public static boolean isStringEmpty(String input) {
return input == null || input.isBlank();
}
public static boolean isCollectionEmpty(Collection<String> input) {
return input == null || input.isEmpty();
}
}
@@ -69,7 +69,6 @@ public class WebResponseUtils {
// Open Byte Array and save document to it
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
document.close();
return baosToWebResponse(baos, docName);
}
@@ -146,13 +146,18 @@ public class CustomColorReplaceStrategy extends ReplaceAndInvertColorStrategy {
// Save the modified PDF to a ByteArrayOutputStream
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
document.save(byteArrayOutputStream);
document.close();
// Prepare the modified PDF for download
ByteArrayInputStream inputStream =
new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
InputStreamResource resource = new InputStreamResource(inputStream);
return resource;
} finally {
try {
Files.deleteIfExists(file.toPath());
} catch (IOException e) {
log.warn("Failed to delete temporary file: {}", file.getAbsolutePath(), e);
}
}
}
@@ -7,6 +7,7 @@ import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.imageio.ImageIO;
@@ -19,11 +20,14 @@ import org.apache.pdfbox.rendering.PDFRenderer;
import org.springframework.core.io.InputStreamResource;
import org.springframework.web.multipart.MultipartFile;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.api.misc.ReplaceAndInvert;
import stirling.software.common.util.ApplicationContextProvider;
import stirling.software.common.util.ExceptionUtils;
@Slf4j
public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
public InvertFullColorStrategy(MultipartFile file, ReplaceAndInvert replaceAndInvert) {
@@ -32,85 +36,101 @@ public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
@Override
public InputStreamResource replace() throws IOException {
File file = null;
try {
// Create a temporary file, with the original filename from the multipart file
file = Files.createTempFile("temp", getFileInput().getOriginalFilename()).toFile();
try (TempFile tempFile =
new TempFile(
Files.createTempFile("temp", getFileInput().getOriginalFilename())
.toFile())) {
// Transfer the content of the multipart file to the file
getFileInput().transferTo(file);
getFileInput().transferTo(tempFile.getFile());
// Load the uploaded PDF
PDDocument document = Loader.loadPDF(file);
try (PDDocument document = Loader.loadPDF(tempFile.getFile())) {
// Render each page and invert colors
PDFRenderer pdfRenderer = new PDFRenderer(document);
pdfRenderer.setSubsamplingAllowed(
true); // Enable subsampling to reduce memory usage
for (int page = 0; page < document.getNumberOfPages(); page++) {
BufferedImage image;
// Render each page and invert colors
PDFRenderer pdfRenderer = new PDFRenderer(document);
for (int page = 0; page < document.getNumberOfPages(); page++) {
BufferedImage image;
// Use global maximum DPI setting, fallback to 300 if not set
int renderDpi = 300; // Default fallback
ApplicationProperties properties =
ApplicationContextProvider.getBean(ApplicationProperties.class);
if (properties != null && properties.getSystem() != null) {
renderDpi = properties.getSystem().getMaxDPI();
}
final int dpi = renderDpi;
final int pageNum = page;
// Use global maximum DPI setting, fallback to 300 if not set
int renderDpi = 300; // Default fallback
ApplicationProperties properties =
ApplicationContextProvider.getBean(ApplicationProperties.class);
if (properties != null && properties.getSystem() != null) {
renderDpi = properties.getSystem().getMaxDPI();
}
try {
image =
pdfRenderer.renderImageWithDPI(
page, renderDpi); // Render page with global DPI setting
} catch (OutOfMemoryError e) {
throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, renderDpi, e);
} catch (NegativeArraySizeException e) {
throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, renderDpi, e);
}
ExceptionUtils.handleOomRendering(
pageNum + 1,
dpi,
() -> pdfRenderer.renderImageWithDPI(pageNum, dpi));
// Invert the colors
invertImageColors(image);
// Invert the colors
invertImageColors(image);
// Create a new PDPage from the inverted image
PDPage pdPage = document.getPage(page);
File tempImageFile = null;
try {
tempImageFile = convertToBufferedImageTpFile(image);
PDImageXObject pdImage =
PDImageXObject.createFromFileByContent(tempImageFile, document);
// Create a new PDPage from the inverted image
PDPage pdPage = document.getPage(page);
File tempImageFile = null;
try {
tempImageFile = convertToBufferedImageTpFile(image);
PDImageXObject pdImage =
PDImageXObject.createFromFileByContent(tempImageFile, document);
PDPageContentStream contentStream =
new PDPageContentStream(
document,
pdPage,
PDPageContentStream.AppendMode.OVERWRITE,
true);
contentStream.drawImage(
pdImage,
0,
0,
pdPage.getMediaBox().getWidth(),
pdPage.getMediaBox().getHeight());
contentStream.close();
} finally {
if (tempImageFile != null && tempImageFile.exists()) {
Files.delete(tempImageFile.toPath());
// Delete temp file immediately after loading into memory to prevent disk
// exhaustion
// The file content is now in the PDImageXObject, so the file is no longer
// needed
try {
Files.deleteIfExists(tempImageFile.toPath());
tempImageFile = null; // Mark as deleted to avoid double deletion
} catch (IOException e) {
log.warn(
"Failed to delete temporary image file: {}",
tempImageFile.getAbsolutePath(),
e);
}
try (PDPageContentStream contentStream =
new PDPageContentStream(
document,
pdPage,
PDPageContentStream.AppendMode.OVERWRITE,
true,
true)) { // resetContext=true ensures clean graphics state
contentStream.drawImage(
pdImage,
0,
0,
pdPage.getMediaBox().getWidth(),
pdPage.getMediaBox().getHeight());
}
} finally {
// Safety net: ensure temp file is deleted even if an exception occurred
if (tempImageFile != null && tempImageFile.exists()) {
try {
Files.deleteIfExists(tempImageFile.toPath());
} catch (IOException e) {
log.warn(
"Failed to delete temporary image file: {}",
tempImageFile.getAbsolutePath(),
e);
}
}
}
}
}
// Save the modified PDF to a ByteArrayOutputStream
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
document.save(byteArrayOutputStream);
document.close();
// Save the modified PDF to a ByteArrayOutputStream
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
document.save(byteArrayOutputStream);
// Prepare the modified PDF for download
ByteArrayInputStream inputStream =
new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
InputStreamResource resource = new InputStreamResource(inputStream);
return resource;
} finally {
if (file != null && file.exists()) {
Files.delete(file.toPath());
// Prepare the modified PDF for download
ByteArrayInputStream inputStream =
new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
InputStreamResource resource = new InputStreamResource(inputStream);
return resource;
}
}
}
@@ -119,24 +139,48 @@ public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
private void invertImageColors(BufferedImage image) {
int width = image.getWidth();
int height = image.getHeight();
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
int rgba = image.getRGB(x, y);
Color color = new Color(rgba, true);
Color invertedColor =
new Color(
255 - color.getRed(),
255 - color.getGreen(),
255 - color.getBlue());
image.setRGB(x, y, invertedColor.getRGB());
}
int[] pixels = new int[width * height];
image.getRGB(0, 0, width, height, pixels, 0, width);
for (int i = 0; i < pixels.length; i++) {
int pixel = pixels[i];
int a = 0xff;
int r = (pixel >> 16) & 0xff;
int g = (pixel >> 8) & 0xff;
int b = pixel & 0xff;
pixels[i] = (a << 24) | ((255 - r) << 16) | ((255 - g) << 8) | (255 - b);
}
image.setRGB(0, 0, width, height, pixels, 0, width);
}
// Helper method to convert BufferedImage to InputStream
private File convertToBufferedImageTpFile(BufferedImage image) throws IOException {
File file = File.createTempFile("image", ".png");
// Use Files.createTempFile instead of File.createTempFile for better security and modern
// Java practices
Path tempPath = Files.createTempFile("image", ".png");
File file = tempPath.toFile();
ImageIO.write(image, "png", file);
return file;
}
private static class TempFile implements AutoCloseable {
private final File file;
public TempFile(File file) {
this.file = file;
}
public File getFile() {
return file;
}
@Override
public void close() throws IOException {
if (file != null && file.exists()) {
Files.delete(file.toPath());
}
}
}
}
@@ -0,0 +1,233 @@
*, *::before, *::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
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 24px;
background-color: var(--bg-color, #ffffff);
word-wrap: break-word;
overflow-wrap: break-word;
hyphens: auto;
}
.email-container {
width: 100%;
max-width: 100%;
margin: 0 auto;
}
.email-header {
padding-bottom: 16px;
border-bottom: 2px solid var(--border-color, #e8eaed);
margin-bottom: 20px;
}
.email-header h1 {
margin: 0 0 12px 0;
font-size: var(--header-font-size, 18px);
font-weight: 600;
color: #1a1a1a;
line-height: 1.3;
word-break: break-word;
}
.email-meta {
font-size: var(--meta-font-size, 12px);
color: #5f6368;
}
.email-meta div {
margin-bottom: 4px;
line-height: 1.5;
}
.email-meta strong {
color: #3c4043;
font-weight: 600;
min-width: 50px;
display: inline-block;
}
.email-body {
word-wrap: break-word;
overflow-wrap: break-word;
line-height: 1.6;
}
.email-body p {
margin: 0 0 1em 0;
}
.email-body a {
color: #1a73e8;
text-decoration: underline;
}
.email-body table {
border-collapse: collapse;
width: auto;
max-width: 100%;
margin: 8px 0;
}
.email-body td,
.email-body th {
padding: 8px 12px;
vertical-align: top;
border: 1px solid #e0e0e0;
}
.email-body ul,
.email-body ol {
margin: 0.5em 0;
padding-left: 2em;
}
.email-body li {
margin-bottom: 0.25em;
}
.email-body blockquote {
margin: 1em 0;
padding: 0 0 0 16px;
border-left: 3px solid #dadce0;
color: #5f6368;
}
.email-body pre,
.email-body code {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.9em;
background-color: #f8f9fa;
border-radius: 3px;
}
.email-body pre {
padding: 12px;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.email-body code {
padding: 2px 6px;
}
.email-body hr {
border: none;
border-top: 1px solid #e0e0e0;
margin: 1.5em 0;
}
.attachment-section {
margin-top: 24px;
padding: 16px;
background-color: var(--attachment-bg, #f9f9f9);
border: 1px solid var(--attachment-border, #eeeeee);
border-radius: 6px;
page-break-inside: avoid;
}
.attachment-section h3 {
margin: 0 0 12px 0;
font-size: var(--attachment-header-size, 14px);
font-weight: 600;
color: #3c4043;
}
.attachment-item {
padding: 8px 0;
border-bottom: 1px solid #eeeeee;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.attachment-item:last-child {
border-bottom: none;
}
.attachment-icon {
margin-right: 8px;
font-weight: bold;
color: #5f6368;
}
.attachment-name {
font-weight: 500;
color: #1a1a1a;
word-break: break-all;
}
.attachment-details,
.attachment-type {
font-size: var(--attachment-detail-size, 11px);
color: #5f6368;
margin-left: 8px;
}
.attachment-info-note {
margin-top: 12px;
padding: 10px 12px;
font-size: var(--note-font-size, 11px);
border-radius: 4px;
background-color: #e8f0fe;
border: 1px solid #d2e3fc;
color: #1967d2;
}
.attachment-info-note p {
margin: 0;
}
.no-content {
padding: 32px 20px;
text-align: center;
color: #80868b;
font-style: italic;
background-color: #f8f9fa;
border-radius: 6px;
}
.text-body {
white-space: pre-wrap;
word-wrap: break-word;
font-family: inherit;
line-height: 1.6;
}
img {
max-width: 100%;
height: auto;
display: block;
margin: 8px 0;
}
@media print {
body {
padding: 0;
font-size: 11pt;
}
.email-header {
page-break-after: avoid;
}
.attachment-section {
page-break-inside: avoid;
}
a {
text-decoration: none;
color: inherit;
}
}
.email-body div[class*="signature"],
.email-body table[class*="signature"] {
margin-top: 1.5em;
padding-top: 1em;
border-top: 1px solid #e0e0e0;
font-size: 0.95em;
color: #5f6368;
}
@@ -1,8 +1,6 @@
package stirling.software.common.annotations;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -31,8 +29,6 @@ import stirling.software.common.aop.AutoJobAspect;
import stirling.software.common.model.api.PDFFile;
import stirling.software.common.service.FileStorage;
import stirling.software.common.service.JobExecutorService;
import stirling.software.common.service.JobQueue;
import stirling.software.common.service.ResourceMonitor;
@ExtendWith(MockitoExtension.class)
class AutoJobPostMappingIntegrationTest {
@@ -45,10 +41,6 @@ class AutoJobPostMappingIntegrationTest {
@Mock private FileStorage fileStorage;
@Mock private ResourceMonitor resourceMonitor;
@Mock private JobQueue jobQueue;
@BeforeEach
void setUp() {
autoJobAspect = new AutoJobAspect(jobExecutorService, request, fileStorage);
@@ -73,7 +65,7 @@ class AutoJobPostMappingIntegrationTest {
// Given
PDFFile pdfFile = new PDFFile();
pdfFile.setFileId("test-file-id");
Object[] args = new Object[] {pdfFile};
Object[] args = {pdfFile};
when(joinPoint.getArgs()).thenReturn(args);
when(request.getParameter("async")).thenReturn("true");
@@ -153,7 +145,7 @@ class AutoJobPostMappingIntegrationTest {
// Given
PDFFile pdfFile = new PDFFile();
pdfFile.setFileInput(mock(MultipartFile.class));
Object[] args = new Object[] {pdfFile};
Object[] args = {pdfFile};
when(joinPoint.getArgs()).thenReturn(args);
when(request.getParameter("async")).thenReturn("true");
@@ -0,0 +1,17 @@
package stirling.software.common.configuration.interfaces;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
class ShowAdminInterfaceTest {
// Create a simple implementation for testing
static class TestImpl implements ShowAdminInterface {}
@Test
void getShowUpdateOnlyAdmins_returnsTrueByDefault() {
ShowAdminInterface instance = new TestImpl();
assertTrue(instance.getShowUpdateOnlyAdmins(), "Default should return true");
}
}
@@ -18,17 +18,17 @@ class ApplicationPropertiesDynamicYamlPropertySourceTest {
@Test
void loads_yaml_into_environment() throws Exception {
// YAML-Config in Temp-Datei schreiben
String yaml =
""
+ "ui:\n"
+ " appName: \"My App\"\n"
+ "system:\n"
+ " enableAnalytics: true\n";
"""
\
ui:
appName: "My App"
system:
enableAnalytics: true
""";
Path tmp = Files.createTempFile("spdf-settings-", ".yml");
Files.writeString(tmp, yaml);
// Pfad per statischem Mock liefern
try (MockedStatic<InstallationPathConfig> mocked =
Mockito.mockStatic(InstallationPathConfig.class)) {
mocked.when(InstallationPathConfig::getSettingsPath).thenReturn(tmp.toString());
@@ -36,7 +36,7 @@ class ApplicationPropertiesDynamicYamlPropertySourceTest {
ConfigurableEnvironment env = new StandardEnvironment();
ApplicationProperties props = new ApplicationProperties();
props.dynamicYamlPropertySource(env); // fügt PropertySource an erster Stelle ein
props.dynamicYamlPropertySource(env);
assertEquals("My App", env.getProperty("ui.appName"));
assertEquals("true", env.getProperty("system.enableAnalytics"));
@@ -44,7 +44,7 @@ class ApplicationPropertiesDynamicYamlPropertySourceTest {
}
@Test
void throws_when_settings_file_missing() throws Exception {
void throws_when_settings_file_missing() {
String missing = "/path/does/not/exist/spdf.yml";
try (MockedStatic<InstallationPathConfig> mocked =
Mockito.mockStatic(InstallationPathConfig.class)) {
@@ -61,7 +61,7 @@ class ApplicationPropertiesSaml2HttpTest {
Resource r = s.getSpCert();
assertNotNull(r);
assertTrue(r instanceof FileSystemResource, "Expected FileSystemResource for FS path");
assertInstanceOf(FileSystemResource.class, r, "Expected FileSystemResource for FS path");
assertTrue(r.exists(), "Temp file should exist");
}
@@ -75,7 +75,7 @@ class ApplicationPropertiesSaml2HttpTest {
Resource r = s.getIdpCert();
assertNotNull(r);
assertTrue(r instanceof FileSystemResource, "Expected FileSystemResource for FS path");
assertInstanceOf(FileSystemResource.class, r, "Expected FileSystemResource for FS path");
assertFalse(r.exists(), "Resource should not exist for missing file");
}
}
@@ -1,16 +1,20 @@
package stirling.software.common.model;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.*;
import java.io.File;
import java.nio.file.Path;
import java.time.LocalDateTime;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
public class FileInfoTest {
private static final LocalDateTime FIXED_NOW = LocalDateTime.of(2025, 11, 1, 12, 0, 0);
@ParameterizedTest(name = "{index}: fileSize={0}")
@CsvSource({
"0, '0 Bytes'",
@@ -25,87 +29,146 @@ public class FileInfoTest {
FileInfo fileInfo =
new FileInfo(
"example.txt",
File.separator
+ "path"
+ File.separator
+ "to"
+ File.separator
+ "example.txt",
LocalDateTime.now(),
"/path/to/example.txt",
FIXED_NOW,
fileSize,
LocalDateTime.now().minusDays(1));
FIXED_NOW.minusDays(1));
assertEquals(expectedFormattedSize, fileInfo.getFormattedFileSize());
}
@Test
void testGetFilePathAsPath() {
FileInfo fileInfo =
new FileInfo(
"test.pdf",
File.separator + "tmp" + File.separator + "test.pdf",
LocalDateTime.now(),
1234,
LocalDateTime.now().minusDays(2));
assertEquals(
File.separator + "tmp" + File.separator + "test.pdf",
fileInfo.getFilePathAsPath().toString());
@Nested
@DisplayName("getFilePathAsPath")
class GetFilePathAsPathTests {
@Test
@DisplayName("Should convert filePath string into a Path instance")
void shouldConvertStringToPath() {
FileInfo fi =
new FileInfo(
"example.txt",
"/path/to/example.txt",
FIXED_NOW,
123,
FIXED_NOW.minusDays(1));
Path path = fi.getFilePathAsPath();
// Basic sanity checks
assertNotNull(path, "Path should not be null");
assertEquals(
Path.of("/path/to/example.txt"),
path,
"Converted Path should match input string");
}
}
@Test
void testGetFormattedModificationDate() {
LocalDateTime modDate = LocalDateTime.of(2024, 6, 1, 15, 30, 45);
FileInfo fileInfo =
new FileInfo(
"file.txt",
File.separator + "file.txt",
modDate,
100,
LocalDateTime.of(2024, 5, 31, 10, 0, 0));
assertEquals("2024-06-01 15:30:45", fileInfo.getFormattedModificationDate());
@Nested
@DisplayName("Date formatting")
class DateFormattingTests {
@Test
@DisplayName("Should format modificationDate as 'yyyy-MM-dd HH:mm:ss'")
void shouldFormatModificationDate() {
LocalDateTime mod = LocalDateTime.of(2025, 8, 10, 15, 30, 45);
FileInfo fi =
new FileInfo(
"example.txt",
"/path/to/example.txt",
mod,
1,
LocalDateTime.of(2024, 1, 1, 0, 0, 0));
assertEquals("2025-08-10 15:30:45", fi.getFormattedModificationDate());
}
@Test
@DisplayName("Should format creationDate as 'yyyy-MM-dd HH:mm:ss'")
void shouldFormatCreationDate() {
LocalDateTime created = LocalDateTime.of(2024, 12, 31, 23, 59, 59);
FileInfo fi =
new FileInfo(
"example.txt",
"/path/to/example.txt",
LocalDateTime.of(2025, 1, 1, 0, 0, 0),
1,
created);
assertEquals("2024-12-31 23:59:59", fi.getFormattedCreationDate());
}
@Test
@DisplayName("Should throw NPE when modificationDate is null (current behavior)")
void shouldThrowWhenModificationDateNull() {
// Assumption: Current implementation does not guard null -> NPE is expected.
FileInfo fi =
new FileInfo(
"example.txt",
"/path/to/example.txt",
null, // modificationDate null
1,
FIXED_NOW);
assertThrows(
NullPointerException.class,
fi::getFormattedModificationDate,
"Formatting a null modificationDate should throw NPE with current"
+ " implementation");
}
@Test
@DisplayName("Should throw NPE when creationDate is null (current behavior)")
void shouldThrowWhenCreationDateNull() {
// Assumption: Current implementation does not guard null -> NPE is expected.
FileInfo fi =
new FileInfo(
"example.txt",
"/path/to/example.txt",
FIXED_NOW,
1,
null); // creationDate null
assertThrows(
NullPointerException.class,
fi::getFormattedCreationDate,
"Formatting a null creationDate should throw NPE with current implementation");
}
}
@Test
void testGetFormattedCreationDate() {
LocalDateTime creationDate = LocalDateTime.of(2023, 12, 25, 8, 15, 0);
FileInfo fileInfo =
new FileInfo(
"holiday.txt",
File.separator + "holiday.txt",
LocalDateTime.of(2024, 1, 1, 0, 0, 0),
500,
creationDate);
assertEquals("2023-12-25 08:15:00", fileInfo.getFormattedCreationDate());
}
@Nested
@DisplayName("Additional size formatting cases")
class AdditionalSizeFormattingTests {
@Test
void testGettersAndSetters() {
LocalDateTime now = LocalDateTime.now();
FileInfo fileInfo =
new FileInfo(
"doc.pdf",
File.separator + "docs" + File.separator + "doc.pdf",
now,
2048,
now.minusDays(1));
// Test getters
assertEquals("doc.pdf", fileInfo.getFileName());
assertEquals(File.separator + "docs" + File.separator + "doc.pdf", fileInfo.getFilePath());
assertEquals(now, fileInfo.getModificationDate());
assertEquals(2048, fileInfo.getFileSize());
assertEquals(now.minusDays(1), fileInfo.getCreationDate());
@Test
@DisplayName("Should round to two decimals for KB (e.g., 1536 B -> 1.50 KB)")
void shouldRoundKbToTwoDecimals() {
FileInfo fi =
new FileInfo(
"example.txt",
"/path/to/example.txt",
FIXED_NOW,
1536, // 1.5 KB
FIXED_NOW.minusDays(1));
// Test setters
fileInfo.setFileName("new.pdf");
fileInfo.setFilePath(File.separator + "new" + File.separator + "new.pdf");
fileInfo.setModificationDate(now.plusDays(1));
fileInfo.setFileSize(4096);
fileInfo.setCreationDate(now.minusDays(2));
assertEquals("1.50 KB", fi.getFormattedFileSize());
}
assertEquals("new.pdf", fileInfo.getFileName());
assertEquals(File.separator + "new" + File.separator + "new.pdf", fileInfo.getFilePath());
assertEquals(now.plusDays(1), fileInfo.getModificationDate());
assertEquals(4096, fileInfo.getFileSize());
assertEquals(now.minusDays(2), fileInfo.getCreationDate());
@Test
@DisplayName("Values above 1 TB are still represented in GB (design choice)")
void shouldRepresentTerabytesInGb() {
// 2 TB = 2 * 1024 GB -> 2 * 1024 * 1024^3 bytes
long twoTB = 2L * 1024 * 1024 * 1024 * 1024; // 2 * 2^40
FileInfo fi =
new FileInfo(
"example.txt",
"/path/to/example.txt",
FIXED_NOW,
twoTB,
FIXED_NOW.minusDays(1));
// 2 TB equals 2048.00 GB with current implementation
assertEquals(
"2048.00 GB",
fi.getFormattedFileSize(),
"Current implementation caps at GB and shows TB in GB units");
}
}
}
@@ -0,0 +1,34 @@
package stirling.software.common.model.exception;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@DisplayName("Tests for UnsupportedClaimException")
class UnsupportedClaimExceptionTest {
@Test
@DisplayName("should store message passed to constructor")
void shouldStoreMessageFromConstructor() {
String expectedMessage = "This claim is not supported";
UnsupportedClaimException exception = new UnsupportedClaimException(expectedMessage);
// Verify the stored message
assertEquals(
expectedMessage,
exception.getMessage(),
"Constructor should correctly store the provided message");
}
@Test
@DisplayName("should allow null message without throwing exception")
void shouldAllowNullMessage() {
UnsupportedClaimException exception = new UnsupportedClaimException(null);
// Null message should be stored as null
assertNull(
exception.getMessage(),
"Constructor should accept null message and store it as null");
}
}
@@ -1,7 +1,6 @@
package stirling.software.common.service;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static stirling.software.common.service.SpyPDFDocumentFactory.*;
@@ -22,7 +21,6 @@ import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import stirling.software.common.model.api.PDFFile;
import stirling.software.common.service.SpyPDFDocumentFactory.StrategyType;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@@ -43,58 +41,7 @@ class CustomPDFDocumentFactoryTest {
}
}
@ParameterizedTest
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
void testStrategy_FileInput(int sizeMB, StrategyType expected) throws IOException {
File file = writeTempFile(inflatePdf(basePdfBytes, sizeMB));
try (PDDocument doc = factory.load(file)) {
Assertions.assertEquals(expected, factory.lastStrategyUsed);
}
}
@ParameterizedTest
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
void testStrategy_ByteArray(int sizeMB, StrategyType expected) throws IOException {
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
try (PDDocument doc = factory.load(inflated)) {
Assertions.assertEquals(expected, factory.lastStrategyUsed);
}
}
@ParameterizedTest
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
void testStrategy_InputStream(int sizeMB, StrategyType expected) throws IOException {
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
try (PDDocument doc = factory.load(new ByteArrayInputStream(inflated))) {
Assertions.assertEquals(expected, factory.lastStrategyUsed);
}
}
@ParameterizedTest
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
void testStrategy_MultipartFile(int sizeMB, StrategyType expected) throws IOException {
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
MockMultipartFile multipart =
new MockMultipartFile("file", "doc.pdf", MediaType.APPLICATION_PDF_VALUE, inflated);
try (PDDocument doc = factory.load(multipart)) {
Assertions.assertEquals(expected, factory.lastStrategyUsed);
}
}
@ParameterizedTest
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
void testStrategy_PDFFile(int sizeMB, StrategyType expected) throws IOException {
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
MockMultipartFile multipart =
new MockMultipartFile("file", "doc.pdf", MediaType.APPLICATION_PDF_VALUE, inflated);
PDFFile pdfFile = new PDFFile();
pdfFile.setFileInput(multipart);
try (PDDocument doc = factory.load(pdfFile)) {
Assertions.assertEquals(expected, factory.lastStrategyUsed);
}
}
private byte[] inflatePdf(byte[] input, int sizeInMB) throws IOException {
private static byte[] inflatePdf(byte[] input, int sizeInMB) throws IOException {
try (PDDocument doc = Loader.loadPDF(input)) {
byte[] largeData = new byte[sizeInMB * 1024 * 1024];
Arrays.fill(largeData, (byte) 'A');
@@ -113,6 +60,46 @@ class CustomPDFDocumentFactoryTest {
}
}
private static File writeTempFile(byte[] content) throws IOException {
File file = Files.createTempFile("pdf-test-", ".pdf").toFile();
Files.write(file.toPath(), content);
return file;
}
@ParameterizedTest
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
void testStrategy_FileInput(int sizeMB, StrategyType expected) throws IOException {
File file = writeTempFile(inflatePdf(basePdfBytes, sizeMB));
factory.load(file);
Assertions.assertEquals(expected, factory.lastStrategyUsed);
}
@ParameterizedTest
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
void testStrategy_ByteArray(int sizeMB, StrategyType expected) throws IOException {
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
factory.load(inflated);
Assertions.assertEquals(expected, factory.lastStrategyUsed);
}
@ParameterizedTest
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
void testStrategy_InputStream(int sizeMB, StrategyType expected) throws IOException {
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
factory.load(new ByteArrayInputStream(inflated));
Assertions.assertEquals(expected, factory.lastStrategyUsed);
}
@ParameterizedTest
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
void testStrategy_MultipartFile(int sizeMB, StrategyType expected) throws IOException {
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
MockMultipartFile multipart =
new MockMultipartFile("file", "doc.pdf", MediaType.APPLICATION_PDF_VALUE, inflated);
factory.load(multipart);
Assertions.assertEquals(expected, factory.lastStrategyUsed);
}
@Test
void testLoadFromPath() throws IOException {
File file = writeTempFile(inflatePdf(basePdfBytes, 5));
@@ -130,7 +117,7 @@ class CustomPDFDocumentFactoryTest {
}
}
// neeed to add password pdf
// need to add password pdf
// @Test
// void testLoadPasswordProtectedPdfFromInputStream() throws IOException {
// try (InputStream is = getClass().getResourceAsStream("/protected.pdf")) {
@@ -212,10 +199,16 @@ class CustomPDFDocumentFactoryTest {
assertTrue(newBytes.length > 0);
}
private File writeTempFile(byte[] content) throws IOException {
File file = Files.createTempFile("pdf-test-", ".pdf").toFile();
Files.write(file.toPath(), content);
return file;
@ParameterizedTest
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
void testStrategy_PDFFile(int sizeMB, StrategyType expected) throws IOException {
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
MockMultipartFile multipart =
new MockMultipartFile("file", "doc.pdf", MediaType.APPLICATION_PDF_VALUE, inflated);
PDFFile pdfFile = new PDFFile();
pdfFile.setFileInput(multipart);
factory.load(pdfFile);
Assertions.assertEquals(expected, factory.lastStrategyUsed);
}
@BeforeEach
@@ -1,15 +1,11 @@
package stirling.software.common.service;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.AdditionalAnswers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -86,7 +82,7 @@ class FileStorageTest {
void testRetrieveFile() throws IOException {
// Arrange
byte[] fileContent = "Test PDF content".getBytes();
String fileId = UUID.randomUUID().toString();
String fileId = "test-file-1";
Path filePath = tempDir.resolve(fileId);
Files.write(filePath, fileContent);
@@ -106,7 +102,7 @@ class FileStorageTest {
void testRetrieveBytes() throws IOException {
// Arrange
byte[] fileContent = "Test PDF content".getBytes();
String fileId = UUID.randomUUID().toString();
String fileId = "test-file-2";
Path filePath = tempDir.resolve(fileId);
Files.write(filePath, fileContent);
@@ -139,7 +135,7 @@ class FileStorageTest {
void testDeleteFile() throws IOException {
// Arrange
byte[] fileContent = "Test PDF content".getBytes();
String fileId = UUID.randomUUID().toString();
String fileId = "test-file-3";
Path filePath = tempDir.resolve(fileId);
Files.write(filePath, fileContent);
@@ -167,7 +163,7 @@ class FileStorageTest {
void testFileExists() throws IOException {
// Arrange
byte[] fileContent = "Test PDF content".getBytes();
String fileId = UUID.randomUUID().toString();
String fileId = "test-file-4";
Path filePath = tempDir.resolve(fileId);
Files.write(filePath, fileContent);
@@ -1,6 +1,7 @@
package stirling.software.common.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -105,7 +106,7 @@ class JobExecutorServiceTest {
// Then
assertEquals(HttpStatus.OK, response.getStatusCode());
assertTrue(response.getBody() instanceof JobResponse);
assertInstanceOf(JobResponse.class, response.getBody());
JobResponse<?> jobResponse = (JobResponse<?>) response.getBody();
assertTrue(jobResponse.isAsync());
assertNotNull(jobResponse.getJobId());
@@ -134,7 +135,7 @@ class JobExecutorServiceTest {
}
@Test
void shouldQueueJobWhenResourcesLimited() {
void shouldQueueJobWhenResourcesLimited() throws Exception {
// Given
Supplier<Object> work = () -> "test-result";
CompletableFuture<ResponseEntity<?>> future = new CompletableFuture<>();
@@ -150,7 +151,7 @@ class JobExecutorServiceTest {
// Then
assertEquals(HttpStatus.OK, response.getStatusCode());
assertTrue(response.getBody() instanceof JobResponse);
assertInstanceOf(JobResponse.class, response.getBody());
// Verify job was queued
verify(jobQueue).queueJob(anyString(), eq(80), any(), eq(5000L));
@@ -184,13 +185,13 @@ class JobExecutorServiceTest {
// Given
Supplier<Object> work =
() -> {
try {
Thread.sleep(100); // Simulate long-running job
return "test-result";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
// Simulate long-running job without actual sleep
// Use a loop to consume time instead of Thread.sleep
long startTime = System.nanoTime();
while (System.nanoTime() - startTime < 100_000_000) { // 100ms in nanoseconds
// Busy wait to simulate work without Thread.sleep
}
return "test-result";
};
// Use reflection to access the private executeWithTimeout method
@@ -203,7 +204,7 @@ class JobExecutorServiceTest {
try {
executeMethod.invoke(jobExecutorService, work, 1L); // Very short timeout
} catch (Exception e) {
assertTrue(e.getCause() instanceof TimeoutException);
assertInstanceOf(TimeoutException.class, e.getCause());
}
}
}
@@ -1,12 +1,11 @@
package stirling.software.common.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;
import java.lang.management.MemoryMXBean;
import java.lang.management.OperatingSystemMXBean;
import java.time.Instant;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.BeforeEach;
@@ -33,11 +32,11 @@ class ResourceMonitorTest {
@Mock private MemoryMXBean memoryMXBean;
@Spy
private AtomicReference<ResourceStatus> currentStatus =
private final AtomicReference<ResourceStatus> currentStatus =
new AtomicReference<>(ResourceStatus.OK);
@Spy
private AtomicReference<ResourceMetrics> latestMetrics =
private final AtomicReference<ResourceMetrics> latestMetrics =
new AtomicReference<>(new ResourceMetrics());
@BeforeEach
@@ -118,18 +117,24 @@ class ResourceMonitorTest {
shouldQueue,
result,
String.format(
Locale.ROOT,
"For weight %d and status %s, shouldQueue should be %s",
weight, status, shouldQueue));
weight,
status,
shouldQueue));
}
@Test
void resourceMetricsShouldDetectStaleState() {
// Capture test time at the beginning for deterministic calculations
final Instant testTime = Instant.now();
// Given
Instant now = Instant.now();
Instant pastInstant = now.minusMillis(6000);
Instant pastInstant =
testTime.minusMillis(6000); // 6 seconds ago (relative to test start time)
ResourceMetrics staleMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, pastInstant);
ResourceMetrics freshMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, now);
ResourceMetrics freshMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, testTime);
// When/Then
assertTrue(
@@ -5,7 +5,6 @@ import static org.mockito.Mockito.*;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.UUID;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -42,7 +41,7 @@ class TaskManagerTest {
@Test
void testCreateTask() {
// Act
String jobId = UUID.randomUUID().toString();
String jobId = "test-job-1";
taskManager.createTask(jobId);
// Assert
@@ -56,7 +55,7 @@ class TaskManagerTest {
@Test
void testSetResult() {
// Arrange
String jobId = UUID.randomUUID().toString();
String jobId = "test-job-2";
taskManager.createTask(jobId);
Object resultObject = "Test result";
@@ -74,7 +73,7 @@ class TaskManagerTest {
@Test
void testSetFileResult() throws Exception {
// Arrange
String jobId = UUID.randomUUID().toString();
String jobId = "test-job-3";
taskManager.createTask(jobId);
String fileId = "file-id";
String originalFileName = "test.pdf";
@@ -108,7 +107,7 @@ class TaskManagerTest {
@Test
void testSetError() {
// Arrange
String jobId = UUID.randomUUID().toString();
String jobId = "test-job-4";
taskManager.createTask(jobId);
String errorMessage = "Test error";
@@ -126,7 +125,7 @@ class TaskManagerTest {
@Test
void testSetComplete_WithExistingResult() {
// Arrange
String jobId = UUID.randomUUID().toString();
String jobId = "test-job-5";
taskManager.createTask(jobId);
Object resultObject = "Test result";
taskManager.setResult(jobId, resultObject);
@@ -144,7 +143,7 @@ class TaskManagerTest {
@Test
void testSetComplete_WithoutExistingResult() {
// Arrange
String jobId = UUID.randomUUID().toString();
String jobId = "test-job-6";
taskManager.createTask(jobId);
// Act
@@ -160,7 +159,7 @@ class TaskManagerTest {
@Test
void testIsComplete() {
// Arrange
String jobId = UUID.randomUUID().toString();
String jobId = "test-job-7";
taskManager.createTask(jobId);
// Assert - not complete initially
@@ -215,7 +214,9 @@ class TaskManagerTest {
}
@Test
void testCleanupOldJobs() throws Exception {
void testCleanupOldJobs() {
// Capture test time at the beginning for deterministic calculations
final LocalDateTime testTime = LocalDateTime.now();
// Arrange
// 1. Create a recent completed job
String recentJobId = "recent-job";
@@ -227,8 +228,9 @@ class TaskManagerTest {
taskManager.createTask(oldJobId);
JobResult oldJob = taskManager.getJobResult(oldJobId);
// Manually set the completion time to be older than the expiry
LocalDateTime oldTime = LocalDateTime.now().minusHours(1);
// Manually set the completion time to be older than the expiry (relative to test start
// time)
LocalDateTime oldTime = testTime.minusHours(1);
ReflectionTestUtils.setField(oldJob, "completedAt", oldTime);
ReflectionTestUtils.setField(oldJob, "complete", true);
@@ -253,6 +255,7 @@ class TaskManagerTest {
taskManager.createTask(activeJobId);
// Verify all jobs are in the map
assertNotNull(jobResultsMap);
assertTrue(jobResultsMap.containsKey(recentJobId));
assertTrue(jobResultsMap.containsKey(oldJobId));
assertTrue(jobResultsMap.containsKey(activeJobId));
@@ -268,7 +271,7 @@ class TaskManagerTest {
}
@Test
void testShutdown() throws Exception {
void testShutdown() {
// This mainly tests that the shutdown method doesn't throw exceptions
taskManager.shutdown();
@@ -279,7 +282,7 @@ class TaskManagerTest {
@Test
void testAddNote() {
// Arrange
String jobId = UUID.randomUUID().toString();
String jobId = "test-job-8";
taskManager.createTask(jobId);
String note = "Test note";
@@ -1,8 +1,6 @@
package stirling.software.common.service;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.*;
import java.io.File;
@@ -133,6 +131,9 @@ public class TempFileCleanupServiceTest {
// Use MockedStatic to mock Files operations
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
// Capture test time at the beginning for deterministic calculations
final long testTime = System.currentTimeMillis();
// Mock Files.list for each directory we'll process
mockedFiles
.when(() -> Files.list(eq(systemTempDir)))
@@ -177,18 +178,17 @@ public class TempFileCleanupServiceTest {
// maxAgeMillis
if (fileName.contains("old")) {
return FileTime.fromMillis(
System.currentTimeMillis() - 5000000);
testTime - 5000000); // ~1.4 hours ago
}
// For empty.tmp file, return a timestamp older than 5 minutes (for
// empty file test)
else if (fileName.equals("empty.tmp")) {
else if ("empty.tmp".equals(fileName)) {
return FileTime.fromMillis(
System.currentTimeMillis() - 6 * 60 * 1000);
testTime - 6 * 60 * 1000); // 6 minutes ago
}
// For all other files, return a recent timestamp
else {
return FileTime.fromMillis(
System.currentTimeMillis() - 60000); // 1 minute ago
return FileTime.fromMillis(testTime - 60000); // 1 minute ago
}
});
@@ -201,7 +201,7 @@ public class TempFileCleanupServiceTest {
String fileName = path.getFileName().toString();
// Return 0 bytes for the empty file
if (fileName.equals("empty.tmp")) {
if ("empty.tmp".equals(fileName)) {
return 0L;
}
// Return normal size for all other files
@@ -221,9 +221,9 @@ public class TempFileCleanupServiceTest {
});
// Act - set containerMode to false for this test
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
invokeCleanupDirectoryStreaming(customTempDir, false, 0, 3600000);
invokeCleanupDirectoryStreaming(libreOfficeTempDir, false, 0, 3600000);
invokeCleanupDirectoryStreaming(systemTempDir, false, 3600000);
invokeCleanupDirectoryStreaming(customTempDir, false, 3600000);
invokeCleanupDirectoryStreaming(libreOfficeTempDir, false, 3600000);
// Assert - Only old temp files and empty files should be deleted
assertTrue(deletedFiles.contains(oldTempFile), "Old temp file should be deleted");
@@ -276,6 +276,9 @@ public class TempFileCleanupServiceTest {
// Use MockedStatic to mock Files operations
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
// Capture test time at the beginning for deterministic calculations
final long testTime = System.currentTimeMillis();
// Mock Files.list for systemTempDir
mockedFiles
.when(() -> Files.list(eq(systemTempDir)))
@@ -290,9 +293,7 @@ public class TempFileCleanupServiceTest {
// Configure Files.getLastModifiedTime to return recent timestamps
mockedFiles
.when(() -> Files.getLastModifiedTime(any(Path.class)))
.thenReturn(
FileTime.fromMillis(
System.currentTimeMillis() - 60000)); // 1 minute ago
.thenReturn(FileTime.fromMillis(testTime - 60000)); // 1 minute ago
// Configure Files.size to return normal size
mockedFiles.when(() -> Files.size(any(Path.class))).thenReturn(1024L); // 1 KB
@@ -308,7 +309,7 @@ public class TempFileCleanupServiceTest {
});
// Act - set containerMode to true and maxAgeMillis to 0 for container startup cleanup
invokeCleanupDirectoryStreaming(systemTempDir, true, 0, 0);
invokeCleanupDirectoryStreaming(systemTempDir, true, 0);
// Assert - In container mode, both our temp files and system temp files should be
// deleted
@@ -337,6 +338,9 @@ public class TempFileCleanupServiceTest {
// Use MockedStatic to mock Files operations
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
// Capture test time at the beginning for deterministic calculations
final long testTime = System.currentTimeMillis();
// Mock Files.list for systemTempDir
mockedFiles
.when(() -> Files.list(eq(systemTempDir)))
@@ -356,14 +360,14 @@ public class TempFileCleanupServiceTest {
Path path = invocation.getArgument(0);
String fileName = path.getFileName().toString();
if (fileName.equals("empty.tmp")) {
if ("empty.tmp".equals(fileName)) {
// More than 5 minutes old
return FileTime.fromMillis(
System.currentTimeMillis() - 6 * 60 * 1000);
testTime - 6 * 60 * 1000); // 6 minutes ago
} else {
// Less than 5 minutes old
return FileTime.fromMillis(
System.currentTimeMillis() - 2 * 60 * 1000);
testTime - 2 * 60 * 1000); // 2 minutes ago
}
});
@@ -381,7 +385,7 @@ public class TempFileCleanupServiceTest {
});
// Act
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
invokeCleanupDirectoryStreaming(systemTempDir, false, 3600000);
// Assert
assertTrue(
@@ -412,14 +416,25 @@ public class TempFileCleanupServiceTest {
// Use MockedStatic to mock Files operations
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
// Capture test time at the beginning for deterministic calculations
final long testTime = System.currentTimeMillis();
// Mock Files.list for each directory
mockedFiles.when(() -> Files.list(eq(systemTempDir))).thenReturn(Stream.of(dir1));
mockedFiles
.when(() -> Files.list(eq(systemTempDir)))
.thenAnswer(invocation -> Stream.of(dir1));
mockedFiles.when(() -> Files.list(eq(dir1))).thenReturn(Stream.of(tempFile1, dir2));
mockedFiles
.when(() -> Files.list(eq(dir1)))
.thenAnswer(invocation -> Stream.of(tempFile1, dir2));
mockedFiles.when(() -> Files.list(eq(dir2))).thenReturn(Stream.of(tempFile2, dir3));
mockedFiles
.when(() -> Files.list(eq(dir2)))
.thenAnswer(invocation -> Stream.of(tempFile2, dir3));
mockedFiles.when(() -> Files.list(eq(dir3))).thenReturn(Stream.of(tempFile3));
mockedFiles
.when(() -> Files.list(eq(dir3)))
.thenAnswer(invocation -> Stream.of(tempFile3));
// Configure Files.isDirectory for each path
mockedFiles.when(() -> Files.isDirectory(eq(dir1))).thenReturn(true);
@@ -432,6 +447,9 @@ public class TempFileCleanupServiceTest {
// Configure Files.exists to return true for all paths
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
// Configure Files.size to return 0 for all files (ensure they're not empty)
mockedFiles.when(() -> Files.size(any(Path.class))).thenReturn(1024L);
// Configure Files.getLastModifiedTime to return different times based on file names
mockedFiles
.when(() -> Files.getLastModifiedTime(any(Path.class)))
@@ -441,19 +459,14 @@ public class TempFileCleanupServiceTest {
String fileName = path.getFileName().toString();
if (fileName.contains("old")) {
// Old file
return FileTime.fromMillis(
System.currentTimeMillis() - 5000000);
// Old file - very old timestamp (older than 1 hour)
return FileTime.fromMillis(testTime - 7200000); // 2 hours ago
} else {
// Recent file
return FileTime.fromMillis(System.currentTimeMillis() - 60000);
// Recent file - very recent timestamp (less than 1 hour)
return FileTime.fromMillis(testTime - 60000); // 1 minute ago
}
});
// Configure Files.size to return normal size
mockedFiles.when(() -> Files.size(any(Path.class))).thenReturn(1024L);
// For deleteIfExists, track which files would be deleted
mockedFiles
.when(() -> Files.deleteIfExists(any(Path.class)))
.thenAnswer(
@@ -463,12 +476,8 @@ public class TempFileCleanupServiceTest {
return true;
});
// Act
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
// Debug - print what was deleted
System.out.println("Deleted files: " + deletedFiles);
System.out.println("Looking for: " + tempFile3);
// Act - pass maxAgeMillis = 3600000 (1 hour)
invokeCleanupDirectoryStreaming(systemTempDir, false, 3600000);
// Assert
assertFalse(deletedFiles.contains(tempFile1), "Recent temp file should be preserved");
@@ -481,8 +490,7 @@ public class TempFileCleanupServiceTest {
/** Helper method to invoke the private cleanupDirectoryStreaming method using reflection */
private void invokeCleanupDirectoryStreaming(
Path directory, boolean containerMode, int depth, long maxAgeMillis)
throws IOException {
Path directory, boolean containerMode, long maxAgeMillis) {
try {
// Create a consumer that tracks deleted files
AtomicInteger deleteCount = new AtomicInteger(0);
@@ -505,7 +513,7 @@ public class TempFileCleanupServiceTest {
cleanupService,
directory,
containerMode,
depth,
0,
maxAgeMillis,
false,
deleteCallback);
@@ -1,9 +1,6 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
@@ -26,6 +23,7 @@ class CheckProgramInstallTest {
private MockedStatic<ProcessExecutor> mockProcessExecutor;
private ProcessExecutor mockExecutor;
private ProcessExecutor mockFfmpegExecutor;
@BeforeEach
void setUp() throws Exception {
@@ -34,10 +32,14 @@ class CheckProgramInstallTest {
// Set up mock for ProcessExecutor
mockExecutor = Mockito.mock(ProcessExecutor.class);
mockFfmpegExecutor = Mockito.mock(ProcessExecutor.class);
mockProcessExecutor = mockStatic(ProcessExecutor.class);
mockProcessExecutor
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV))
.thenReturn(mockExecutor);
mockProcessExecutor
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.FFMPEG))
.thenReturn(mockFfmpegExecutor);
}
@AfterEach
@@ -49,7 +51,7 @@ class CheckProgramInstallTest {
}
/** Reset static fields in the CheckProgramInstall class using reflection */
private void resetStaticFields() throws Exception {
private static void resetStaticFields() throws Exception {
Field pythonAvailableCheckedField =
CheckProgramInstall.class.getDeclaredField("pythonAvailableChecked");
pythonAvailableCheckedField.setAccessible(true);
@@ -59,6 +61,15 @@ class CheckProgramInstallTest {
CheckProgramInstall.class.getDeclaredField("availablePythonCommand");
availablePythonCommandField.setAccessible(true);
availablePythonCommandField.set(null, null);
Field ffmpegAvailableCheckedField =
CheckProgramInstall.class.getDeclaredField("ffmpegAvailableChecked");
ffmpegAvailableCheckedField.setAccessible(true);
ffmpegAvailableCheckedField.set(null, false);
Field ffmpegAvailableField = CheckProgramInstall.class.getDeclaredField("ffmpegAvailable");
ffmpegAvailableField.setAccessible(true);
ffmpegAvailableField.set(null, false);
}
@Test
@@ -108,8 +119,7 @@ class CheckProgramInstallTest {
}
@Test
void testGetAvailablePythonCommand_WhenPythonReturnsNonZeroExitCode()
throws IOException, InterruptedException, Exception {
void testGetAvailablePythonCommand_WhenPythonReturnsNonZeroExitCode() throws Exception {
// Arrange
// Reset the static fields again to ensure clean state
resetStaticFields();
@@ -205,4 +215,45 @@ class CheckProgramInstallTest {
// Verify getAvailablePythonCommand was called internally
verify(mockExecutor).runCommandWithOutputHandling(Arrays.asList("python3", "--version"));
}
@Test
void testIsFfmpegAvailable_WhenInstalled() throws Exception {
resetStaticFields();
ProcessExecutorResult result = Mockito.mock(ProcessExecutorResult.class);
when(mockFfmpegExecutor.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version")))
.thenReturn(result);
assertTrue(CheckProgramInstall.isFfmpegAvailable());
verify(mockFfmpegExecutor)
.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version"));
}
@Test
void testIsFfmpegAvailable_WhenNotInstalled() throws Exception {
resetStaticFields();
when(mockFfmpegExecutor.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version")))
.thenThrow(new IOException("Command not found"));
assertFalse(CheckProgramInstall.isFfmpegAvailable());
verify(mockFfmpegExecutor)
.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version"));
}
@Test
void testIsFfmpegAvailable_CachesResult() throws Exception {
resetStaticFields();
ProcessExecutorResult result = Mockito.mock(ProcessExecutorResult.class);
when(mockFfmpegExecutor.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version")))
.thenReturn(result);
assertTrue(CheckProgramInstall.isFfmpegAvailable());
when(mockFfmpegExecutor.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version")))
.thenThrow(new IOException("Command not found"));
assertTrue(CheckProgramInstall.isFfmpegAvailable());
verify(mockFfmpegExecutor, times(1))
.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version"));
}
}

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