Compare commits

...

126 Commits

Author SHA1 Message Date
pixeebot[bot]
b094634799 Hardening suggestions for Stirling-PDF / fix-sig-logo (#2144)
Modernize and secure temp file creation

Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>
2024-10-31 16:17:23 -04:00
sbplat
9e597a4390 fix signature logo not loading and add option to disable it 2024-10-31 16:06:36 -04:00
Anthony Stirling
febc3cf48b Update pull_request_template.md 2024-10-31 17:46:30 +00:00
Philip H.
c5abb47403 navbar.css: prevent overlapping of elements (#2140)
go-pro-link is overlapping the settings button
2024-10-31 17:45:44 +00:00
Anthony Stirling
0e3c9bcc10 Update README.md 2024-10-31 14:52:41 +00:00
github-actions[bot]
384c3ee88f 💾 Update Version (#2139)
💾 Sync Versions
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-31 13:06:55 +00:00
Anthony Stirling
5f7a0537f9 Update build.gradle 2024-10-31 13:06:12 +00:00
Anthony Stirling
5aa5628465 [bug fix] Update compress-pdf.html (#2138)
Update compress-pdf.html
2024-10-31 10:59:51 +00:00
albanobattistella
0d91bca932 Update messages_it_IT.properties (#2135) 2024-10-30 19:55:54 +00:00
Ludovic Ortega
8e88591499 chore(helm): bump chart version according to semver (#2109)
Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>
2024-10-30 13:49:28 +00:00
github-actions[bot]
3e051d0105 Update 3rd Party Licenses (#2134)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-10-30 13:00:51 +00:00
dependabot[bot]
4a9b16ff8f Bump org.springframework.security:spring-security-saml2-service-provider from 6.3.3 to 6.3.4 (#2052)
Bump org.springframework.security:spring-security-saml2-service-provider

Bumps [org.springframework.security:spring-security-saml2-service-provider](https://github.com/spring-projects/spring-security) from 6.3.3 to 6.3.4.
- [Release notes](https://github.com/spring-projects/spring-security/releases)
- [Changelog](https://github.com/spring-projects/spring-security/blob/main/RELEASE.adoc)
- [Commits](https://github.com/spring-projects/spring-security/compare/6.3.3...6.3.4)

---
updated-dependencies:
- dependency-name: org.springframework.security:spring-security-saml2-service-provider
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-30 12:58:47 +00:00
github-actions[bot]
a7082ecd85 💾 Update Version (#2132)
💾 Sync Versions
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-30 12:48:44 +00:00
github-actions[bot]
966e6a4923 📝 Update README: Translation Progress Table (#2133)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-30 12:48:30 +00:00
Anthony Stirling
27d2681a97 Feature/save signs (#2127)
* apply fix

* Fixes empty th:action

* Update build.gradle

* fix

* formatting

* Save signatures

* Fix code scanning alert no. 42: Uncontrolled data used in path expression

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* fix UserServiceInterface

* Merge branch 'feature/saveSigns' of
git@github.com:Stirling-Tools/Stirling-PDF.git into feature/saveSigns

* 0.31.0 bump and further csrf

* formatting

* preview name

* add

* sign doc

* Update translation files (#2128)

Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>

---------

Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: Dimitrios Kaitantzidis <james_k23@hotmail.gr>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: a <a>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-10-30 12:46:44 +00:00
github-actions[bot]
ed75fa4e1b 📝 Update README: Translation Progress Table (#2129)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-29 19:05:01 +00:00
Rania Amina
9b9752bd7a Update id_ID Translation and fix some grammars (#2108)
* Update id_ID Translation and fix some grammars

* sync lines to fix build warning

* get back new line at end of file

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-10-29 19:03:00 +00:00
Anthony Stirling
903dc7638c Fix csrf (#2126)
* apply fix

* Fixes empty th:action

* Update build.gradle

* fix

* formatting

---------

Co-authored-by: Dimitrios Kaitantzidis <james_k23@hotmail.gr>
2024-10-29 17:56:29 +00:00
github-actions[bot]
c39b111edc 📝 Update README: Translation Progress Table (#2121)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-29 16:31:25 +00:00
github-actions[bot]
d910929aa6 Update translation files (#2125)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-10-29 16:30:44 +00:00
reecebrowne
a9ce0e80ee Feature/298 improve compare performance (#2124)
* Implement Diff.js

* Compare feature - add service worker and improve efficiency for large files

* Compare - messages updated to be compatable with language packs

* Compare - Acknowledge Diff.js usage

* Add message warning there is  no text in uploaded pdf to messages file

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-10-29 15:56:45 +00:00
Florian Fish
4922ab700e Add new french translations (#2120)
Add new french translations and improve simple quote
2024-10-29 10:12:49 +00:00
github-actions[bot]
01f3c138a6 Update 3rd Party Licenses (#2119)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-10-28 23:22:15 +00:00
github-actions[bot]
4e21f76979 📝 Update README: Translation Progress Table (#2103)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-28 23:22:02 +00:00
dependabot[bot]
a9ccd85e75 Bump org.springframework.boot from 3.3.4 to 3.3.5 (#2118)
Bumps [org.springframework.boot](https://github.com/spring-projects/spring-boot) from 3.3.4 to 3.3.5.
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.4...v3.3.5)

---
updated-dependencies:
- dependency-name: org.springframework.boot
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-28 23:19:59 +00:00
dependabot[bot]
6f407f1d2f Bump springBootVersion from 3.3.4 to 3.3.5 (#2117)
Bumps `springBootVersion` from 3.3.4 to 3.3.5.

Updates `org.springframework.boot:spring-boot-starter-web` from 3.3.4 to 3.3.5
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.4...v3.3.5)

Updates `org.springframework.boot:spring-boot-starter-jetty` from 3.3.4 to 3.3.5
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.4...v3.3.5)

Updates `org.springframework.boot:spring-boot-starter-thymeleaf` from 3.3.4 to 3.3.5
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.4...v3.3.5)

Updates `org.springframework.boot:spring-boot-starter-security` from 3.3.4 to 3.3.5
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.4...v3.3.5)

Updates `org.springframework.boot:spring-boot-starter-data-jpa` from 3.3.4 to 3.3.5
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.4...v3.3.5)

Updates `org.springframework.boot:spring-boot-starter-oauth2-client` from 3.3.4 to 3.3.5
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.4...v3.3.5)

Updates `org.springframework.boot:spring-boot-starter-test` from 3.3.4 to 3.3.5
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.4...v3.3.5)

Updates `org.springframework.boot:spring-boot-starter-actuator` from 3.3.4 to 3.3.5
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.4...v3.3.5)

Updates `org.springframework.boot:spring-boot-devtools` from 3.3.4 to 3.3.5
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.4...v3.3.5)

---
updated-dependencies:
- dependency-name: org.springframework.boot:spring-boot-starter-web
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-jetty
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-thymeleaf
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-security
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-data-jpa
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-oauth2-client
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-test
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-actuator
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-devtools
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-28 23:19:36 +00:00
pixeebot[bot]
af5e2b6895 Modernize and secure temp file creation (#2106)
Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>
2024-10-28 23:19:12 +00:00
Ludy
d2046c64d8 Optimierung der SAML2-Integration und Verbesserung der Zertifikats- und Fehlerbehandlung (#2105)
* certificate processing

* Hides dialog when provider list is empty

* removed: unused
2024-10-27 22:17:36 +00:00
Manuel Mora Gordillo
1b88d89191 Spanish translate (#2102)
* Spanish translate

* Added blank line

---------

Co-authored-by: Manu <manuel@fusiontelecom.co>
2024-10-25 13:20:13 +01:00
github-actions[bot]
03bf98265b 📝 Update README: Translation Progress Table (#2072)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-24 14:21:17 +01:00
Anthony Stirling
89da2a5c01 Auto detect presence of external dependencies (LibreOffice etc) and disable/enable features dynamically (#2082)
* Create ExternalAppDepConfig.java

* Update EndpointConfiguration.java

* Hardening suggestions for Stirling-PDF / ExternalAppDepConfig (#2083)

Switch order of literals to prevent NullPointerException

Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>

---------

Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>
2024-10-24 13:59:17 +01:00
swanemar
a10d06b693 added some missing translations (#2085) 2024-10-24 10:35:32 +00:00
Eric
a7ed99084f visual certificate signing (#2084)
add visual digital signature
2024-10-24 07:08:09 +01:00
github-actions[bot]
88f3594d80 Update 3rd Party Licenses (#2080)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-10-23 12:28:58 +01:00
Eric
e0b77ca274 extract and apply the image orientation from exif data in imageToPdf (#2073) 2024-10-23 12:17:40 +01:00
albanobattistella
bac81c930d Update messages_it_IT.properties (#2077) 2024-10-23 12:16:49 +01:00
Corbinian Grimm
2f49626a4c Update messages_de_DE.properties (#2070)
* Update messages_de_DE.properties

Completed translations for German language.

* Update messages_de_DE.properties
2024-10-22 21:53:13 +01:00
Anthony Stirling
83ef003505 Update PostHogService.java 2024-10-22 15:36:54 +01:00
Anthony Stirling
949b87005c Fix metricCollection 2024-10-22 15:36:22 +01:00
a
532f7cdbbf Merge branch 'main' of git@github.com:Stirling-Tools/Stirling-PDF.git into main 2024-10-22 12:22:20 +01:00
Anthony Stirling
51c4a60313 Remove pro badge if enabled 2024-10-22 12:22:08 +01:00
reecebrowne
aa00808219 Removed horizontal scroll logic from multi-tool template (#2065)
* Removed horizontal scroll logic from multi-tool template

* Remove unused horizontalScroll.js
2024-10-22 12:02:00 +01:00
github-actions[bot]
5d40175e18 💾 Update Version (#2064)
💾 Sync Versions
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-22 11:30:59 +01:00
Anthony Stirling
a40fdd5a0b Fixes for analyticsPrompt 2024-10-22 11:10:09 +01:00
github-actions[bot]
6ea7ffc36c 📝 Update README: Translation Progress Table (#2062)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-22 09:54:46 +01:00
Thomas BERNARD
39e0fd8eef French translation improvements (#2061)
* fix a spelling mistake in a french message

n'importe quel fichier au singulier

* Translate "Remove Certificate Sign" to French

* french translation for pdfToPDFA.pdfWithDigitalSignature

* fix french translation for BootToPDF and PDFToBook

* Translate "Remove image" to French

* Translate "Split PDF by Chapters" to French

* fr translation : Popular => Populaire

* french translation for adminUserSettings.* messages

* french translation for session.expired
2024-10-22 08:11:48 +01:00
Anthony Stirling
cae8cd0aa9 Add on hover color to sign (#2059)
* Fixed layering issue with z-index, and added smoother transitions for… (#1996)

Fixed layering issue with z-index, and added smoother transitions for signing

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>

* Delete package-lock.json

---------

Co-authored-by: Surya Karthikeyan Vijayalakshmi <108506548+SuryaKV101@users.noreply.github.com>
2024-10-22 00:44:22 +01:00
Anthony Stirling
04d5ae1912 Default terms and conditions to stirlingpdf.com (#2058) 2024-10-22 00:42:17 +01:00
github-actions[bot]
e01ba93cf8 Update 3rd Party Licenses (#2057)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-10-22 00:41:44 +01:00
github-actions[bot]
edd0ec9d23 Update 3rd Party Licenses (#2056)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-10-22 00:22:01 +01:00
dependabot[bot]
899f3d267b Bump org.commonmark:commonmark from 0.23.0 to 0.24.0 (#2054)
Bumps [org.commonmark:commonmark](https://github.com/commonmark/commonmark-java) from 0.23.0 to 0.24.0.
- [Release notes](https://github.com/commonmark/commonmark-java/releases)
- [Changelog](https://github.com/commonmark/commonmark-java/blob/main/CHANGELOG.md)
- [Commits](https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.23.0...commonmark-parent-0.24.0)

---
updated-dependencies:
- dependency-name: org.commonmark:commonmark
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-22 00:21:14 +01:00
dependabot[bot]
88c0a9e26b Bump org.springframework:spring-webmvc from 6.1.13 to 6.1.14 (#2053)
Bumps [org.springframework:spring-webmvc](https://github.com/spring-projects/spring-framework) from 6.1.13 to 6.1.14.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.13...v6.1.14)

---
updated-dependencies:
- dependency-name: org.springframework:spring-webmvc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-22 00:19:52 +01:00
dependabot[bot]
dc6cec9daf Bump org.commonmark:commonmark-ext-gfm-tables from 0.23.0 to 0.24.0 (#2055)
Bumps [org.commonmark:commonmark-ext-gfm-tables](https://github.com/commonmark/commonmark-java) from 0.23.0 to 0.24.0.
- [Release notes](https://github.com/commonmark/commonmark-java/releases)
- [Changelog](https://github.com/commonmark/commonmark-java/blob/main/CHANGELOG.md)
- [Commits](https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.23.0...commonmark-parent-0.24.0)

---
updated-dependencies:
- dependency-name: org.commonmark:commonmark-ext-gfm-tables
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-22 00:19:14 +01:00
github-actions[bot]
a64dd2e282 📝 Update README: Translation Progress Table (#2047)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-20 21:02:09 +01:00
github-actions[bot]
c9b7d848b4 Update translation files (#2048)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-10-20 21:01:50 +01:00
Anthony Stirling
89a9ba6ebc remove unused translation 2024-10-20 21:00:16 +01:00
Patryk Marszelewski
22249ef9bf Update messages_pl_PL.properties (#2042) 2024-10-20 20:34:39 +01:00
github-actions[bot]
619a863b99 Update 3rd Party Licenses (#2044)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-10-20 20:32:57 +01:00
Peter Dave Hello
e098b2999c Update and improve zh_TW Traditional Chinese locale (#2046)
This is a small follow-up to #2030, but it will significantly improve the user experience for Traditional Chinese users.
2024-10-20 20:32:40 +01:00
IT Creativity + Art Team
1149f2a30d Update messages_bg_BG.properties (#2045)
Update messages_bg_BG.properties
2024-10-20 20:32:18 +01:00
Ludy
eff1843061 Major Enhancements to SAML2 and OAuth2 Integration with Simplified Security Configurations (#2040)
* implement Saml2 login/logout

* changed: deprecation code

* relyingPartyRegistrations only enabled samle
2024-10-20 12:30:58 +01:00
Anthony Stirling
227d18a469 bug Update remove image to show on api docs 2024-10-18 22:22:44 +01:00
Anthony Stirling
84abd60c4f Update PdfImageRemovalController.java 2024-10-18 21:34:25 +01:00
pixeebot[bot]
09c9944fc3 Switch order of literals to prevent NullPointerException (#2035)
Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>
2024-10-18 07:15:10 +01:00
pixeebot[bot]
b31564968c Introduced protections against system command injection (#2011)
* Introduced protections against system command injection

* Update translation files (#2034)

Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>

---------

Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-10-18 00:10:42 +01:00
github-actions[bot]
ca535b0abe Update 3rd Party Licenses (#2033)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-10-17 23:57:01 +01:00
github-actions[bot]
376ec865b8 Update 3rd Party Licenses (#2032)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-10-17 23:50:06 +01:00
dependabot[bot]
094ed12b85 Bump io.micrometer:micrometer-core from 1.13.4 to 1.13.6 (#2019)
Bumps [io.micrometer:micrometer-core](https://github.com/micrometer-metrics/micrometer) from 1.13.4 to 1.13.6.
- [Release notes](https://github.com/micrometer-metrics/micrometer/releases)
- [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.13.4...v1.13.6)

---
updated-dependencies:
- dependency-name: io.micrometer:micrometer-core
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-17 23:48:53 +01:00
dependabot[bot]
fe92f99093 Bump org.apache.xmlgraphics:batik-all from 1.17 to 1.18 (#2018)
Bumps org.apache.xmlgraphics:batik-all from 1.17 to 1.18.

---
updated-dependencies:
- dependency-name: org.apache.xmlgraphics:batik-all
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-17 23:48:41 +01:00
dependabot[bot]
46a6a585a9 Bump imageioVersion from 3.11.0 to 3.12.0 (#1998)
Bumps `imageioVersion` from 3.11.0 to 3.12.0.

Updates `com.twelvemonkeys.imageio:imageio-batik` from 3.11.0 to 3.12.0

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

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

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

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

---
updated-dependencies:
- dependency-name: com.twelvemonkeys.imageio:imageio-batik
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.twelvemonkeys.imageio:imageio-bmp
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.twelvemonkeys.imageio:imageio-jpeg
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.twelvemonkeys.imageio:imageio-tiff
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.twelvemonkeys.imageio:imageio-webp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-17 23:48:06 +01:00
github-actions[bot]
8e9acdd053 📝 Update README: Translation Progress Table (#2031)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-17 23:47:11 +01:00
albanobattistella
8aadef1412 Update messages_it_IT.properties (#2022) 2024-10-17 23:42:58 +01:00
thiagoor-cpu
80d80f7d8f Update messages_pt_BR.properties (#2029)
Several changes to pt_BR version
2024-10-17 23:42:47 +01:00
Peter Dave Hello
4132e5b78b Update and improve zh_TW Traditional Chinese locale (#2030) 2024-10-17 23:22:14 +01:00
Anthony Stirling
bd36841094 Update DeveloperGuide.md 2024-10-16 18:56:51 +01:00
Anthony Stirling
22b727df17 Update DeveloperGuide.md 2024-10-15 18:12:25 +01:00
Anthony Stirling
6bb2910b2d Update DeveloperGuide.md 2024-10-15 18:11:28 +01:00
Anthony Stirling
c2236349ac Create DeveloperGuide.md 2024-10-15 18:10:53 +01:00
Anthony Stirling
320bd14d1e Update CONTRIBUTING.md 2024-10-15 17:22:15 +01:00
Anthony Stirling
9ee5dc3486 Update CONTRIBUTING.md 2024-10-15 17:20:57 +01:00
github-actions[bot]
dfad952612 📝 Update README: Translation Progress Table (#2021)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-15 13:43:38 +01:00
albanobattistella
e023b13505 Update messages_it_IT.properties (#2020) 2024-10-15 13:35:38 +01:00
github-actions[bot]
2078b75790 📝 Update README: Translation Progress Table (#2015)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-14 22:36:38 +01:00
github-actions[bot]
23bda46653 Update 3rd Party Licenses (#2016)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-10-14 22:36:19 +01:00
github-actions[bot]
73b87c15cc 💾 Update Version (#2014)
💾 Sync Versions
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-14 22:35:33 +01:00
Anthony Stirling
c85463bc18 Frooodle/license (#1994) 2024-10-14 22:34:41 +01:00
github-actions[bot]
ceeecc37ab 📝 Update README: Translation Progress Table (#1991)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-05 09:25:26 +01:00
albanobattistella
fec717484f Update messages_it_IT.properties (#1990) 2024-10-05 09:22:12 +01:00
NorthOuterTowner
85e1716aa2 Update messages_zh_CN.properties (#1989) 2024-10-05 09:21:57 +01:00
github-actions[bot]
2a6b4ca87f 📝 Update README: Translation Progress Table (#1988)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-04 22:15:30 +01:00
github-actions[bot]
e325943f16 Update translation files (#1987)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-10-04 22:14:58 +01:00
Charan19001A0231
48aae48f0e Added page counts to merge pdf tool (#1986)
* Added page counts to merge pdf tool

* used page and pages in en_GB and hindi properties file
2024-10-04 22:14:15 +01:00
Hashim
494bc2c09f commit for feature developing invert-replace color of a pdf for stirl… (#1982)
commit for feature developing invert-replace color of a pdf for stirling PDF
2024-10-04 16:53:00 +01:00
bxjyj
45e4c15d2d Searchbar Dynamically Resizes (#1985)
Fixed Searchbar sizing
2024-10-04 16:22:35 +01:00
github-actions[bot]
22a58ad0c3 📝 Update README: Translation Progress Table (#1981)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-03 09:48:29 +01:00
dogancandemir
bb37ba1f30 Turkish translation (#1980)
* up to Settings translation completed

* up to HomePage translation completed

* up to WebPages translation completed

* Whole translation done!
2024-10-02 22:20:25 +01:00
FiratUsta
092b4cc5cb [Bug Fix] New Home Page Bug Fixes (#1973)
* Fix favorites section being cut off if it has too many items.

* Fix the group collapse transition animation playing on page load.
2024-09-30 12:00:30 +01:00
github-actions[bot]
86bb37aa7a Update 3rd Party Licenses (#1956)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-09-25 23:14:22 +01:00
dependabot[bot]
da988e8127 Bump org.springframework.boot from 3.3.3 to 3.3.4 (#1954)
Bumps [org.springframework.boot](https://github.com/spring-projects/spring-boot) from 3.3.3 to 3.3.4.
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.3...v3.3.4)

---
updated-dependencies:
- dependency-name: org.springframework.boot
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-25 13:46:46 +01:00
HardikaZalavadia
b8115531e2 fix Show Javascript card layout (#1959) 2024-09-24 20:33:13 +01:00
dependabot[bot]
9b96367496 Bump commons-io:commons-io from 2.16.1 to 2.17.0 (#1955)
Bumps commons-io:commons-io from 2.16.1 to 2.17.0.

---
updated-dependencies:
- dependency-name: commons-io:commons-io
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-24 13:27:46 +01:00
dependabot[bot]
3ded6de576 Bump springBootVersion from 3.3.3 to 3.3.4 (#1953)
Bumps `springBootVersion` from 3.3.3 to 3.3.4.

Updates `org.springframework.boot:spring-boot-starter-web` from 3.3.3 to 3.3.4
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.3...v3.3.4)

Updates `org.springframework.boot:spring-boot-starter-jetty` from 3.3.3 to 3.3.4
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.3...v3.3.4)

Updates `org.springframework.boot:spring-boot-starter-thymeleaf` from 3.3.3 to 3.3.4
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.3...v3.3.4)

Updates `org.springframework.boot:spring-boot-starter-security` from 3.3.3 to 3.3.4
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.3...v3.3.4)

Updates `org.springframework.boot:spring-boot-starter-data-jpa` from 3.3.3 to 3.3.4
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.3...v3.3.4)

Updates `org.springframework.boot:spring-boot-starter-oauth2-client` from 3.3.3 to 3.3.4
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.3...v3.3.4)

Updates `org.springframework.boot:spring-boot-starter-test` from 3.3.3 to 3.3.4
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.3...v3.3.4)

Updates `org.springframework.boot:spring-boot-starter-actuator` from 3.3.3 to 3.3.4
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.3...v3.3.4)

Updates `org.springframework.boot:spring-boot-devtools` from 3.3.3 to 3.3.4
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.3...v3.3.4)

---
updated-dependencies:
- dependency-name: org.springframework.boot:spring-boot-starter-web
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-jetty
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-thymeleaf
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-security
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-data-jpa
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-oauth2-client
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-test
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-actuator
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-devtools
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-24 13:27:37 +01:00
maxi322
1c6e5df77d [fix]: check for encryption in PageNumbers (#1949)
[fix]: check for empty password encryption on load

Co-authored-by: maxi322 <maxi322@users.noreply.github.com>
2024-09-23 20:52:57 +01:00
Aman Khan
df901db1f8 [Bug fix] Tooltip support added for Theme & Settings in the Navigation bar (#1947)
* length of card which was getting displayed on hovering is reduced

* issue #1818 solved

* issue #1818 fixed

* theme.css changed to previous code

* issue #1801 fixed

* navbar.html updated

* multi language fixed
2024-09-23 11:49:50 +01:00
Akhil Sharma
fde1f626eb Added functionality to use the next available port (#1913)
* Added [Feature Request]: command flag to use the next available port #1882

* Added [Feature Request]: command flag to use the next available port #1882

* minor changes - build successful

* Update: port finding starts from 0 instead of default 8080 port

* Update: port finding starts from 0 instead of default 8080 port

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-09-22 23:47:11 +01:00
yubiuser
f47ed3b42e Fix startup errors on ultra-lite image (#1950)
* Add installFonts.sh to ultra-lite image

Signed-off-by: yubiuser <github@yubiuser.dev>

* Create /usr/share/fonts/opentype/noto on ultra-lite images

Signed-off-by: yubiuser <github@yubiuser.dev>

---------

Signed-off-by: yubiuser <github@yubiuser.dev>
2024-09-22 21:25:38 +01:00
Diallo
a81856d83b remove style color (#1948) 2024-09-22 11:23:16 +01:00
Aman Khan
d6e9e8b20b [Bug fix] Favorite Icon highlighted with yellow color when selected (#1934)
* length of card which was getting displayed on hovering is reduced

* issue #1818 solved

* issue #1818 fixed

* theme.css changed to previous code
2024-09-21 10:53:01 +01:00
github-actions[bot]
0f43062cc1 Update 3rd Party Licenses (#1944)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-09-20 23:28:38 +01:00
dependabot[bot]
f72e5c8bca Bump org.commonmark:commonmark from 0.22.0 to 0.23.0 (#1922)
Bumps [org.commonmark:commonmark](https://github.com/commonmark/commonmark-java) from 0.22.0 to 0.23.0.
- [Release notes](https://github.com/commonmark/commonmark-java/releases)
- [Changelog](https://github.com/commonmark/commonmark-java/blob/main/CHANGELOG.md)
- [Commits](https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.22.0...commonmark-parent-0.23.0)

---
updated-dependencies:
- dependency-name: org.commonmark:commonmark
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-20 23:25:56 +01:00
dependabot[bot]
936f36f171 Bump org.springframework:spring-webmvc from 6.1.9 to 6.1.13 (#1921)
Bumps [org.springframework:spring-webmvc](https://github.com/spring-projects/spring-framework) from 6.1.9 to 6.1.13.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.9...v6.1.13)

---
updated-dependencies:
- dependency-name: org.springframework:spring-webmvc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-20 23:25:39 +01:00
MrErne
dab230e3f8 Update LocalRunGuide.md (#1885)
Update documentation about listen IP
2024-09-20 23:24:20 +01:00
FabioL
0d3ac8bebe Smaller italian optimizations (#1943)
* Smaller italian optimizations

* Minor fixes

---------

Co-authored-by: loviuz <loviuz@mailbox.org>
2024-09-20 23:11:01 +01:00
FiratUsta
6e1a5d2ea0 Home page improvements (#1940)
* Add feautre group header fragment for homepage.

* Add feature group headers to feature groups.

* Style feature groups.

* Add collapsing/expanding functionality as well as a favorites section.

* Cards are now sorted in the order of update link > favorite > alphabetical on the homepage.

* Decrease space between section title and cards.

* Add filtering buttons and view options to homepage.

* Hide list view button in preparation for release.

---------

Co-authored-by: FiratUsta <ahmetfiratusta@gmail.com>
2024-09-20 11:29:00 +00:00
HardikaZalavadia
35490f6ff7 [fix]: home button on view PDF page (#1933)
home button visible in view PDF page
2024-09-19 13:00:54 +01:00
HardikaZalavadia
9f63b0b115 [FIX]: Reduce card size in "Get info on PDF" page (#1932)
Reduce card size in get info on pdf page
2024-09-18 19:40:40 +01:00
github-actions[bot]
c2a8771c66 📝 Update README: Translation Progress Table (#1928)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-17 22:40:04 +01:00
Aman Khan
f87801323c length of card which was getting displayed on hovering is reduced (#1915) 2024-09-17 22:37:10 +01:00
tkymmm
bed6227bbe Update messages_ja_JP.properties (#1912) 2024-09-17 22:36:39 +01:00
P1LH4
24f99fce31 Updating pt-BR translation file. (#1920) 2024-09-17 22:36:16 +01:00
ipod86
ba2311b3e5 Update messages_de_DE.properties (#1927)
Translated Lines 80-84
2024-09-17 22:35:22 +01:00
Saidul Arefin
688e01d70d fixed colorspace array exception (#1925)
* fixed colorspace array exception

* used lsf4j logger instead of prntln

* removed unnecessary comment
2024-09-17 12:29:11 +01:00
albanobattistella
0014560a96 Update messages_it_IT.properties (#1910) 2024-09-15 20:34:18 +01:00
Eric
cbf1c3a59b feat: rotate preview in multitool (#1909) 2024-09-15 18:31:19 +01:00
Eric
b13b925bf0 Fix pdfa conversion (#1907)
* fix: use gs to convert to pdfa and return output by reading file as bytes

* feat: update translation files for pdfToPDFA.credit

* Hardening suggestions for Stirling-PDF / fix_pdfa_conversion (#1908)

Switch order of literals to prevent NullPointerException

Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>

---------

Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>
2024-09-15 18:01:33 +01:00
Eric
c6c33d611a Load pdf libs when needed (#1902)
* feat: only load pdf-lib when its used

* feat: only load pdfjs when its used
2024-09-15 08:24:04 +01:00
206 changed files with 12823 additions and 2560 deletions

View File

@@ -8,6 +8,7 @@ Closes #(issue_number)
- [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have performed a self-review of my own code
- [ ] I have attached images of the change if it is UI based
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] My changes generate no new warnings
- [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only)

View File

@@ -8,17 +8,20 @@ gradle_path = "build.gradle"
def get_chart_version(path):
"""
Reads the appVersion from Chart.yaml.
Reads the version and the appVersion from Chart.yaml.
Args:
path (str): The file path to the Chart.yaml.
Returns:
str: The appVersion if found, otherwise an empty string.
dict: The version under "chart" key and the appVersion under "app" key.
"""
with open(path, encoding="utf-8") as file:
chart_yaml = yaml.safe_load(file)
return chart_yaml.get("appVersion", "")
return {
"chart": chart_yaml["version"],
"app": chart_yaml["appVersion"]
}
def get_gradle_version(path):
@@ -39,17 +42,46 @@ def get_gradle_version(path):
return ""
def update_chart_version(path, new_version):
def get_new_chart_version(chart_version, old_app_version, new_app_version):
"""
Get the new chart version from
Args:
str: The current chart version.
str: The current app version.
str: The new app version.
Returns:
str: The new chart version to update to.
"""
chart_major, chart_minor, chart_patch = chart_version.split(".")
old_major, old_minor, old_patch = old_app_version.split(".")
new_major, new_minor, new_patch = new_app_version.split(".")
if old_major != new_major:
new_chart_version = f"{int(chart_major)+1}.0.0"
elif old_minor != new_minor:
new_chart_version = f"{chart_major}.{int(chart_minor)+1}.0"
elif old_patch != new_patch:
new_chart_version = f"{chart_major}.{chart_minor}.{int(chart_patch)+1}"
return new_chart_version
def update_chart_version(path, new_chart_version, new_app_version):
"""
Updates the appVersion in Chart.yaml with a new version.
Updates the version and the appVersion in Chart.yaml with a new version.
Args:
path (str): The file path to the Chart.yaml.
new_version (str): The new version to update to.
new_chart_version (str): The new chart version to update to.
new_app_version (str): The new app version to update to.
"""
with open(path, encoding="utf-8") as file:
chart_yaml = yaml.safe_load(file)
chart_yaml["appVersion"] = new_version
chart_yaml["version"] = new_chart_version
chart_yaml["appVersion"] = new_app_version
with open(path, "w", encoding="utf-8") as file:
yaml.safe_dump(chart_yaml, file)
@@ -58,10 +90,11 @@ def update_chart_version(path, new_version):
chart_version = get_chart_version(chart_yaml_path)
gradle_version = get_gradle_version(gradle_path)
if chart_version != gradle_version:
if chart_version["app"] != gradle_version:
new_chart_version = get_new_chart_version(chart_version["chart"], chart_version["app"], gradle_version, )
print(
f"Versions do not match. Updating Chart.yaml from {chart_version} to {gradle_version}."
f"Versions do not match. Updating Chart.yaml from {chart_version['chart']} to {new_chart_version}."
)
update_chart_version(chart_yaml_path, gradle_version)
update_chart_version(chart_yaml_path, new_chart_version, gradle_version)
else:
print("Versions match. No update required.")

2
.gitignore vendored
View File

@@ -4,6 +4,7 @@ bin/
tmp/
*.tmp
*.bak
*.exe
*.swp
*~.nib
local.properties
@@ -110,7 +111,6 @@ watchedFolders/
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
*.db

View File

@@ -29,7 +29,7 @@ If you would like to add or modify a translation, please see [How to add new lan
## Docs
Documentation for Stirling-PDF is handled in a separate repository. Please see [Docs repository](https://github.com/Stirling-Tools/Stirling-Tools.github.io) or use "edit this page"-button at the bottom of each page at [https://stirlingtools.com/docs/](https://stirlingtools.com/docs/).
Documentation for Stirling-PDF is handled in a separate repository. Please see [Docs repository](https://github.com/Stirling-Tools/Stirling-Tools.github.io) or use "edit this page"-button at the bottom of each page at [https://docs.stirlingpdf.com/](https://docs.stirlingpdf.com/).
## Fixing Bugs or Adding a New Feature
@@ -41,4 +41,4 @@ If, at any point of time, you have a question, please feel free to ask in the sa
## License
By contributing to this project, you agree that your contributions will be licensed under the [GPL 3 License](LICENSE). You also acknowledge and agree that your contributions will be included in Stirling-PDF and that they can be relicensed in the future under the MPL 2.0 (Mozilla Public License Version 2.0) license.
By contributing to this project, you agree that your contributions will be licensed under the [MIT License](LICENSE).

557
DeveloperGuide.md Normal file
View File

@@ -0,0 +1,557 @@
# Stirling-PDF Developer Guide
## 1. Introduction
Stirling-PDF is a robust, locally hosted web-based PDF manipulation tool. This guide focuses on Docker-based development and testing, which is the recommended approach for working with the full version of Stirling-PDF.
## 2. Project Overview
Stirling-PDF is built using:
- Spring Boot + Thymeleaf
- PDFBox
- LibreOffice
- OcrMyPdf
- HTML, CSS, JavaScript
- Docker
- PDF.js
- PDF-LIB.js
- Lombok
## 3. Development Environment Setup
### Prerequisites
- Docker
- Git
- Java JDK 17 or later
- Gradle 7.0 or later (Included within repo)
### Setup Steps
1. Clone the repository:
```
git clone https://github.com/Stirling-Tools/Stirling-PDF.git
cd Stirling-PDF
```
2. Install Docker and JDK17 if not already installed.
3. Install a recommended Java IDE such as Eclipse, IntelliJ or VSCode
4. Lombok Setup
Stirling-PDF uses Lombok to reduce boilerplate code. Some IDEs, like Eclipse, don't support Lombok out of the box. To set up Lombok in your development environment:
Visit the [Lombok website](https://projectlombok.org/setup/) for installation instructions specific to your IDE.
5. Add environment variable
For local testing you should generally be testing the full 'Security' version of Stirling-PDF to do this you must add the environment flag DOCKER_ENABLE_SECURITY=true to your system and/or IDE build/run step
## 4. Project Structure
```
Stirling-PDF/
├── .github/ # GitHub-specific files (workflows, issue templates)
├── configs/ # Configuration files used by stirling at runtime (generated at runtime)
├── cucumber/ # Cucumber test files
│ ├── features/
├── customFiles/ # Custom static files and templates (generated at runtime used to replace existing files)
├── docs/ # Documentation files
├── exampleYmlFiles/ # Example YAML configuration files
├── images/ # Image assets
├── pipeline/ # Pipeline-related files (generated at runtime)
├── scripts/ # Utility scripts
├── src/ # Source code
│ ├── main/
│ │ ├── java/
│ │ │ └── stirling/
│ │ │ └── software/
│ │ │ └── SPDF/
│ │ │ ├── config/
│ │ │ ├── controller/
│ │ │ ├── model/
│ │ │ ├── repository/
│ │ │ ├── service/
│ │ │ └── utils/
│ │ └── resources/
│ │ ├── static/
│ │ │ ├── css/
│ │ │ ├── js/
│ │ │ └── pdfjs/
│ │ └── templates/
│ └── test/
│ └── java/
│ └── stirling/
│ └── software/
│ └── SPDF/
├── build.gradle # Gradle build configuration
├── Dockerfile # Main Dockerfile
├── Dockerfile-ultra-lite # Dockerfile for ultra-lite version
├── Dockerfile-fat # Dockerfile for fat version
├── docker-compose.yml # Docker Compose configuration
└── test.sh # Test script to deploy all docker versions and run cuke tests
```
## 5. Docker-based Development
Stirling-PDF offers several Docker versions:
- Full: All features included
- Ultra-Lite: Basic PDF operations only
- Fat: Includes additional libraries and fonts predownloaded
### Example Docker Compose Files
Stirling-PDF provides several example Docker Compose files in the `exampleYmlFiles` directory such as :
- `docker-compose-latest.yml`: Latest version without security features
- `docker-compose-latest-security.yml`: Latest version with security features enabled
- `docker-compose-latest-fat-security.yml`: Fat version with security features enabled
These files provide pre-configured setups for different scenarios. For example, here's a snippet from `docker-compose-latest-security.yml`:
```yaml
services:
stirling-pdf:
container_name: Stirling-PDF-Security
image: frooodle/s-pdf:latest
deploy:
resources:
limits:
memory: 4G
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
interval: 5s
timeout: 10s
retries: 16
ports:
- "8080:8080"
volumes:
- /stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true"
PUID: 1002
PGID: 1002
UMASK: "022"
SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest with Security
UI_APPNAMENAVBAR: Stirling-PDF Latest
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
restart: on-failure:5
```
To use these example files, copy the desired file to your project root and rename it to `docker-compose.yml`, or specify the file explicitly when running Docker Compose:
```bash
docker-compose -f exampleYmlFiles/docker-compose-latest-security.yml up
```
### Building Docker Images
Stirling-PDF uses different Docker images for various configurations. The build process is controlled by environment variables and uses specific Dockerfile variants. Here's how to build the Docker images:
1. Set the security environment variable:
```bash
export DOCKER_ENABLE_SECURITY=false # or true for security-enabled builds
```
2. Build the project with Gradle:
```bash
./gradlew clean build
```
3. Build the Docker images:
For the latest version:
```bash
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest -f ./Dockerfile .
```
For the ultra-lite version:
```bash
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-ultra-lite -f ./Dockerfile-ultra-lite .
```
For the fat version (with security enabled):
```bash
export DOCKER_ENABLE_SECURITY=true
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-fat -f ./Dockerfile-fat .
```
Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase
## 6. Testing
### Comprehensive Testing Script
Stirling-PDF provides a `test.sh` script in the root directory. This script builds all versions of Stirling-PDF, checks that each version works, and runs Cucumber tests. It's recommended to run this script before submitting a final pull request.
To run the test script:
```bash
./test.sh
```
This script performs the following actions:
1. Builds all Docker images (full, ultra-lite, fat)
2. Runs each version to ensure it starts correctly
3. Executes Cucumber tests against main version and ensures feature compatibility, in the event these tests fail your PR will not be merged
Note: The `test.sh` script will run automatically when you raise a PR. However, it's recommended to run it locally first to save resources and catch any issues early.
### Full Testing with Docker
1. Build and run the Docker container per the above instructions:
2. Access the application at `http://localhost:8080` and manually test all features developed.
### Local Testing (Java and UI Components)
For quick iterations and development of Java backend, JavaScript, and UI components, you can run and test Stirling-PDF locally without Docker. This approach allows you to work on and verify changes to:
- Java backend logic
- RESTful API endpoints
- JavaScript functionality
- User interface components and styling
- Thymeleaf templates
To run Stirling-PDF locally:
1. Compile and run the project using built in IDE methods or by running:
```
./gradlew bootRun
```
2. Access the application at `http://localhost:8080` in your web browser.
3. Manually test the features you're working on through the UI.
4. For API changes, use tools like Postman or curl to test endpoints directly.
Important notes:
- Local testing doesn't include features that depend on external tools like OCRmyPDF, LibreOffice, or Python scripts.
- There are currently no automated unit tests. All testing is done manually through the UI or API calls. (You are welcome to add JUnits!)
- Always verify your changes in the full Docker environment before submitting pull requests, as some integrations and features will only work in the complete setup.
## 7. Contributing
1. Fork the repository on GitHub.
2. Create a new branch for your feature or bug fix.
3. Make your changes and commit them with clear, descriptive messages and ensure any documentation is updated related to your changes.
4. Test your changes thoroughly in the Docker environment.
5. Run the `test.sh` script to ensure all versions build correctly and pass the Cucumber tests:
```bash
./test.sh
```
6. Push your changes to your fork.
7. Submit a pull request to the main repository.
8. See additional [contributing guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
When you raise a PR:
- The `test.sh` script will run automatically against your PR.
- The PR checks will verify versioning and dependency updates.
- Documentation will be automatically updated for dependency changes.
- Security issues will be checked using Snyk and PixeeBot.
Address any issues that arise from these checks before finalizing your pull request.
## 8. API Documentation
API documentation is available at `/swagger-ui/index.html` when running the application. You can also view the latest API documentation [here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/).
## 9. Customization
Stirling-PDF can be customized through environment variables or a `settings.yml` file. Key customization options include:
- Application name and branding
- Security settings
- UI customization
- Endpoint management
When using Docker, pass environment variables using the `-e` flag or in your `docker-compose.yml` file.
Example:
```
docker run -p 8080:8080 -e APP_NAME="My PDF Tool" stirling-pdf:full
```
Refer to the main README for a full list of customization options.
## 10. Language Translations
For managing language translations that affect multiple files, Stirling-PDF provides a helper script:
```bash
/scripts/replace_translation_line.sh
```
This script helps you make consistent replacements across language files.
When contributing translations:
1. Use the helper script for multi-file changes.
2. Ensure all language files are updated consistently.
3. The PR checks will verify consistency in language file updates.
Remember to test your changes thoroughly to ensure they don't break any existing functionality.
# Code examples
### Overview of Thymeleaf
Thymeleaf is a server-side Java HTML template engine. It is used in Stirling-PDF to render dynamic web pages. Thymeleaf integrates heavily with Spring Boot
### Thymeleaf overview
In Stirling-PDF, Thymeleaf is used to create HTML templates that are rendered on the server side. These templates are located in the `src/main/resources/templates` directory. Thymeleaf templates use a combination of HTML and special Thymeleaf attributes to dynamically generate content.
Some examples of this are
```html
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
or
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
```
Where it uses the th:block, th: indicating its a special thymeleaf element to be used serverside in generating the html, and block being the actual element type.
In this case we are inserting the ``navbar`` entry within the ``fragments/navbar.html`` fragment into the ``th:block`` element.
They can be more complex such as
```html
<th:block th:insert="~{fragments/common :: head(title=#{pageExtracter.title}, header=#{pageExtracter.header})}"></th:block>
```
Which is the same as above but passes the parameters title and header into the fragment common.html to be used in its HTML generation
Thymeleaf can also be used to loop through objects or pass things from java side into html side.
```java
@GetMapping
public String newFeaturePage(Model model) {
model.addAttribute("exampleData", exampleData);
return "new-feature";
}
```
in above example if exampleData is a list of plain java objects of class Person and within it you had id, name, age etc. You can reference it like so
```html
<tbody>
<!-- Use th:each to iterate over the list -->
<tr th:each="person : ${exampleData}">
<td th:text="${person.id}"></td>
<td th:text="${person.name}"></td>
<td th:text="${person.age}"></td>
<td th:text="${person.email}"></td>
</tr>
</tbody>
```
This would generate n entries of tr for each person in exampleData
### Adding a New Feature to the Backend (API)
1. **Create a New Controller:**
- Create a new Java class in the `src/main/java/stirling/software/SPDF/controller/api` directory.
- Annotate the class with `@RestController` and `@RequestMapping` to define the API endpoint.
- Ensure to add API documentation annotations like `@Tag(name = "General", description = "General APIs")` and `@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")`.
```java
package stirling.software.SPDF.controller.api;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@RestController
@RequestMapping("/api/v1/new-feature")
@Tag(name = "General", description = "General APIs")
public class NewFeatureController {
@GetMapping
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
public String newFeature() {
return "NewFeatureResponse"; // This refers to the NewFeatureResponse.html template presenting the user with the generated html from that file when they navigate to /api/v1/new-feature
}
}
```
2. **Define the Service Layer:** (Not required but often useful)
- Create a new service class in the `src/main/java/stirling/software/SPDF/service` directory.
- Implement the business logic for the new feature.
```java
package stirling.software.SPDF.service;
import org.springframework.stereotype.Service;
@Service
public class NewFeatureService {
public String getNewFeatureData() {
// Implement business logic here
return "New Feature Data";
}
}
```
2b. **Integrate the Service with the Controller:**
- Autowire the service class in the controller and use it to handle the API request.
```java
package stirling.software.SPDF.controller.api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import stirling.software.SPDF.service.NewFeatureService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@RestController
@RequestMapping("/api/v1/new-feature")
@Tag(name = "General", description = "General APIs")
public class NewFeatureController {
@Autowired
private NewFeatureService newFeatureService;
@GetMapping
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
public String newFeature() {
return newFeatureService.getNewFeatureData();
}
}
```
### Adding a New Feature to the Frontend (UI)
1. **Create a New Thymeleaf Template:**
- Create a new HTML file in the `src/main/resources/templates` directory.
- Use Thymeleaf attributes to dynamically generate content.
- Use `extract-page.html` as a base example for the HTML template, useful to ensure importing of the general layout, navbar and footer.
```html
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{newFeature.title}, header=#{newFeature.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon organize">upload</span>
<span class="tool-header-text" th:text="#{newFeature.header}"></span>
</div>
<form th:action="@{'/api/v1/new-feature'}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<input type="hidden" id="customMode" name="customMode" value="">
<div class="mb-3">
<label for="featureInput" th:text="#{newFeature.prompt}"></label>
<input type="text" class="form-control" id="featureInput" name="featureInput" th:placeholder="#{newFeature.placeholder}" required>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{newFeature.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
```
2. **Create a New Controller for the UI:**
- Create a new Java class in the `src/main/java/stirling/software/SPDF/controller/ui` directory.
- Annotate the class with `@Controller` and `@RequestMapping` to define the UI endpoint.
```java
package stirling.software.SPDF.controller.ui;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import stirling.software.SPDF.service.NewFeatureService;
@Controller
@RequestMapping("/new-feature")
public class NewFeatureUIController {
@Autowired
private NewFeatureService newFeatureService;
@GetMapping
public String newFeaturePage(Model model) {
model.addAttribute("newFeatureData", newFeatureService.getNewFeatureData());
return "new-feature";
}
}
```
3. **Update the Navigation Bar:**
- Add a link to the new feature page in the navigation bar.
- Update the `src/main/resources/templates/fragments/navbar.html` file.
```html
<li class="nav-item">
<a class="nav-link" th:href="@{/new-feature}">New Feature</a>
</li>
```
## Adding New Translations to Existing Language Files in Stirling-PDF
When adding a new feature or modifying existing ones in Stirling-PDF, you'll need to add new translation entries to the existing language files. Here's a step-by-step guide:
### 1. Locate Existing Language Files
Find the existing `messages.properties` files in the `src/main/resources` directory. You'll see files like:
- `messages.properties` (default, usually English)
- `messages_en_GB.properties`
- `messages_fr.properties`
- `messages_de.properties`
- etc.
### 2. Add New Translation Entries
Open each of these files and add your new translation entries. For example, if you're adding a new feature called "PDF Splitter",
Use descriptive, hierarchical keys (e.g., `feature.element.description`)
you might add:
```properties
pdfSplitter.title=PDF Splitter
pdfSplitter.description=Split your PDF into multiple documents
pdfSplitter.button.split=Split PDF
pdfSplitter.input.pages=Enter page numbers to split
```
Add these entries to the default GB language file and any others you wish, translating the values as appropriate for each language.
### 3. Use Translations in Thymeleaf Templates
In your Thymeleaf templates, use the `#{key}` syntax to reference the new translations:
```html
<h1 th:text="#{pdfSplitter.title}">PDF Splitter</h1>
<p th:text="#{pdfSplitter.description}">Split your PDF into multiple documents</p>
<input type="text" th:placeholder="#{pdfSplitter.input.pages}">
<button th:text="#{pdfSplitter.button.split}">Split PDF</button>
```
Remember, never hard-code text in your templates or Java code. Always use translation keys to ensure proper localization.

View File

@@ -15,6 +15,7 @@ ENV DOCKER_ENABLE_SECURITY=false \
# Copy necessary files
COPY scripts/download-security-jar.sh /scripts/download-security-jar.sh
COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
COPY scripts/installFonts.sh /scripts/installFonts.sh
COPY pipeline /pipeline
COPY build/libs/*.jar app.jar
@@ -33,11 +34,11 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
su-exec \
openjdk21-jre && \
# User permissions
mkdir /configs /logs /customFiles && \
mkdir -p /configs /logs /customFiles /usr/share/fonts/opentype/noto && \
chmod +x /scripts/*.sh && \
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline && \
chown stirlingpdfuser:stirlingpdfgroup /app.jar
chown stirlingpdfuser:stirlingpdfgroup /app.jar
# Set environment variables
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI

View File

@@ -257,9 +257,11 @@ To override the default configuration, you can add the following to `/.git/Stirl
```bash
server:
host: 0.0.0.0
host: 0.0.0.0 # Not working - use instead address
address: 0.0.0.0
port: 3000
```
'-Djava.net.preferIPv4Stack=true' --> To force ipv4 only in the java starting command
**Note:** This file is created after the first application launch. To have it before that, you can create the directory and add the file yourself.

112
README.md
View File

@@ -166,48 +166,55 @@ Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "pod
Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md
## Reuse stored files
Certain functionality like ``Sign`` Supports pre-saved files stored at ``/customFiles/signatures/``, image files placed within here will be accesable to be used via webUI
Currently this supports two folder types
- ``/customFiles/signatures/ALL_USERS`` accessible to all users, useful for orginasations were many users use same files or for users not using authentication
- ``/customFiles/signatures/{username}`` such as ``/customFiles/signatures/froodle`` accessible to only the ``froodle`` username, private for all others
## Supported Languages
Stirling PDF currently supports 38!
| Language | Progress |
| ------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![99%](https://geps.dev/progress/99) |
| Basque (Euskara) (eu_ES) | ![60%](https://geps.dev/progress/60) |
| Bulgarian (Български) (bg_BG) | ![91%](https://geps.dev/progress/91) |
| Catalan (Català) (ca_CA) | ![47%](https://geps.dev/progress/47) |
| Croatian (Hrvatski) (hr_HR) | ![91%](https://geps.dev/progress/91) |
| Czech (Česky) (cs_CZ) | ![87%](https://geps.dev/progress/87) |
| Danish (Dansk) (da_DK) | ![96%](https://geps.dev/progress/96) |
| Dutch (Nederlands) (nl_NL) | ![93%](https://geps.dev/progress/93) |
| Arabic (العربية) (ar_AR) | ![93%](https://geps.dev/progress/93) |
| Basque (Euskara) (eu_ES) | ![56%](https://geps.dev/progress/56) |
| Bulgarian (Български) (bg_BG) | ![98%](https://geps.dev/progress/98) |
| Catalan (Català) (ca_CA) | ![44%](https://geps.dev/progress/44) |
| Croatian (Hrvatski) (hr_HR) | ![86%](https://geps.dev/progress/86) |
| Czech (Česky) (cs_CZ) | ![82%](https://geps.dev/progress/82) |
| Danish (Dansk) (da_DK) | ![90%](https://geps.dev/progress/90) |
| Dutch (Nederlands) (nl_NL) | ![87%](https://geps.dev/progress/87) |
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| French (Français) (fr_FR) | ![90%](https://geps.dev/progress/90) |
| German (Deutsch) (de_DE) | ![98%](https://geps.dev/progress/98) |
| Greek (Ελληνικά) (el_GR) | ![79%](https://geps.dev/progress/79) |
| Hindi (हिंदी) (hi_IN) | ![76%](https://geps.dev/progress/76) |
| Hungarian (Magyar) (hu_HU) | ![73%](https://geps.dev/progress/73) |
| Indonesia (Bahasa Indonesia) (id_ID) | ![74%](https://geps.dev/progress/74) |
| Irish (Gaeilge) (ga_IE) | ![95%](https://geps.dev/progress/95) |
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) |
| Japanese (日本語) (ja_JP) | ![89%](https://geps.dev/progress/89) |
| Korean (한국어) (ko_KR) | ![81%](https://geps.dev/progress/81) |
| Norwegian (Norsk) (no_NB) | ![95%](https://geps.dev/progress/95) |
| Polish (Polski) (pl_PL) | ![89%](https://geps.dev/progress/89) |
| Portuguese (Português) (pt_PT) | ![76%](https://geps.dev/progress/76) |
| French (Français) (fr_FR) | ![94%](https://geps.dev/progress/94) |
| German (Deutsch) (de_DE) | ![97%](https://geps.dev/progress/97) |
| Greek (Ελληνικά) (el_GR) | ![75%](https://geps.dev/progress/75) |
| Hindi (हिंदी) (hi_IN) | ![71%](https://geps.dev/progress/71) |
| Hungarian (Magyar) (hu_HU) | ![69%](https://geps.dev/progress/69) |
| Indonesia (Bahasa Indonesia) (id_ID) | ![95%](https://geps.dev/progress/95) |
| Irish (Gaeilge) (ga_IE) | ![89%](https://geps.dev/progress/89) |
| Italian (Italiano) (it_IT) | ![98%](https://geps.dev/progress/98) |
| Japanese (日本語) (ja_JP) | ![86%](https://geps.dev/progress/86) |
| Korean (한국어) (ko_KR) | ![76%](https://geps.dev/progress/76) |
| Norwegian (Norsk) (no_NB) | ![89%](https://geps.dev/progress/89) |
| Polish (Polski) (pl_PL) | ![98%](https://geps.dev/progress/98) |
| Portuguese (Português) (pt_PT) | ![71%](https://geps.dev/progress/71) |
| Portuguese Brazilian (Português) (pt_BR) | ![98%](https://geps.dev/progress/98) |
| Romanian (Română) (ro_RO) | ![97%](https://geps.dev/progress/97) |
| Russian (Русский) (ru_RU) | ![81%](https://geps.dev/progress/81) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![76%](https://geps.dev/progress/76) |
| Simplified Chinese (简体中文) (zh_CN) | ![98%](https://geps.dev/progress/98) |
| Slovakian (Slovensky) (sk_SK) | ![89%](https://geps.dev/progress/89) |
| Spanish (Español) (es_ES) | ![98%](https://geps.dev/progress/98) |
| Swedish (Svenska) (sv_SE) | ![97%](https://geps.dev/progress/97) |
| Thai (ไทย) (th_TH) | ![96%](https://geps.dev/progress/96) |
| Traditional Chinese (繁體中文) (zh_TW) | ![95%](https://geps.dev/progress/95) |
| Turkish (Türkçe) (tr_TR) | ![96%](https://geps.dev/progress/96) |
| Ukrainian (Українська) (uk_UA) | ![87%](https://geps.dev/progress/87) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![96%](https://geps.dev/progress/96) |
| Romanian (Română) (ro_RO) | ![91%](https://geps.dev/progress/91) |
| Russian (Русский) (ru_RU) | ![76%](https://geps.dev/progress/76) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![71%](https://geps.dev/progress/71) |
| Simplified Chinese (简体中文) (zh_CN) | ![92%](https://geps.dev/progress/92) |
| Slovakian (Slovensky) (sk_SK) | ![83%](https://geps.dev/progress/83) |
| Spanish (Español) (es_ES) | ![97%](https://geps.dev/progress/97) |
| Swedish (Svenska) (sv_SE) | ![93%](https://geps.dev/progress/93) |
| Thai (ไทย) (th_TH) | ![90%](https://geps.dev/progress/90) |
| Traditional Chinese (繁體中文) (zh_TW) | ![98%](https://geps.dev/progress/98) |
| Turkish (Türkçe) (tr_TR) | ![93%](https://geps.dev/progress/93) |
| Ukrainian (Українська) (uk_UA) | ![81%](https://geps.dev/progress/81) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![90%](https://geps.dev/progress/90) |
## Contributing (creating issues, translations, fixing bugs, etc.)
@@ -273,14 +280,44 @@ security:
useAsUsername: email # Default is 'email'; custom fields can be used as the username
scopes: openid, profile, email # Specify the scopes for which the application will request permissions
provider: google # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
saml2:
enabled: false # Currently in alpha, not recommended for use yet, enableAlphaFunctionality must be set to true
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
registrationId: stirling
idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata
idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml
idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml
idpIssuer: http://www.okta.com/externalKey
idpCert: classpath:octa.crt
privateKey: classpath:saml-private-key.key
spCert: classpath:saml-public-cert.crt
enterpriseEdition:
enabled: false # set to 'true' to enable enterprise edition
key: 00000000-0000-0000-0000-000000000000
CustomMetadata:
autoUpdateMetadata: false # set to 'true' to automatically update metadata with below values
author: username # Supports text such as 'John Doe' or types such as username to autopopulate with users username
creator: Stirling-PDF # Supports text such as 'Company-PDF'
producer: Stirling-PDF # Supports text such as 'Company-PDF'
legal:
termsAndConditions: https://www.stirlingpdf.com/terms-and-conditions # URL to the terms and conditions of your application (e.g. https://example.com/terms) Empty string to disable or filename to load from local file in static folder
privacyPolicy: https://www.stirlingpdf.com/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy) Empty string to disable or filename to load from local file in static folder
accessibilityStatement: '' # URL to the accessibility statement of your application (e.g. https://example.com/accessibility) Empty string to disable or filename to load from local file in static folder
cookiePolicy: '' # URL to the cookie policy of your application (e.g. https://example.com/cookie) Empty string to disable or filename to load from local file in static folder
impressum: '' # URL to the impressum of your application (e.g. https://example.com/impressum) Empty string to disable or filename to load from local file in static folder
system:
defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
defaultLocale: en-US # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
enableAlphaFunctionality: false # Set to enable functionality which might need more testing before it fully goes live (This feature might make no changes)
showUpdate: true # see when a new update is available
showUpdate: false # see when a new update is available
showUpdateOnlyAdmin: false # Only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template html files
tessdataDir: /usr/share/tessdata # Path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
enableAnalytics: undefined # Set to 'true' to enable analytics, set to 'false' to disable analytics, for enterprise users this is set to true
ui:
appName: '' # Application's visible name
@@ -293,6 +330,11 @@ endpoints:
metrics:
enabled: true # 'true' to enable Info APIs (`/api/*`) endpoints, 'false' to disable
# Automatically Generated Settings (Do Not Edit Directly)
AutomaticallyGenerated:
key: example
UUID: example
```
There is an additional config file ``/configs/custom_settings.yml`` were users familiar with java and spring application.properties can input their own settings on-top of Stirling-PDFs existing ones

View File

@@ -1,6 +1,6 @@
plugins {
id "java"
id "org.springframework.boot" version "3.3.3"
id "org.springframework.boot" version "3.3.5"
id "io.spring.dependency-management" version "1.1.6"
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
id "io.swagger.swaggerhub" version "1.3.2"
@@ -13,16 +13,16 @@ plugins {
import com.github.jk1.license.render.*
ext {
springBootVersion = "3.3.3"
springBootVersion = "3.3.5"
pdfboxVersion = "3.0.3"
logbackVersion = "1.5.7"
imageioVersion = "3.11.0"
imageioVersion = "3.12.0"
lombokVersion = "1.18.34"
bouncycastleVersion = "1.78.1"
}
group = "stirling.software"
version = "0.29.0"
version = "0.31.1"
java {
// 17 is lowest but we support and recommend 21
@@ -32,6 +32,10 @@ java {
repositories {
mavenCentral()
maven { url "https://jitpack.io" }
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
maven {
url 'https://build.shibboleth.net/maven/releases'
}
}
licenseReport {
@@ -115,7 +119,7 @@ configurations.all {
}
dependencies {
//security updates
implementation "org.springframework:spring-webmvc:6.1.9"
implementation "org.springframework:spring-webmvc:6.1.14"
implementation("io.github.pixee:java-security-toolkit:1.2.0")
@@ -127,22 +131,37 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-jetty:$springBootVersion"
implementation "org.springframework.boot:spring-boot-starter-thymeleaf:$springBootVersion"
implementation 'com.posthog.java:posthog:1.1.1'
implementation 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
runtimeOnly "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE"
implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE"
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
implementation 'org.springframework.security:spring-security-saml2-service-provider:6.3.4'
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
//2.2.x requires rebuild of DB file.. need migration path
runtimeOnly "com.h2database:h2:2.1.214"
// implementation "com.h2database:h2:2.2.224"
constraints {
implementation "org.opensaml:opensaml-core"
implementation "org.opensaml:opensaml-saml-api"
implementation "org.opensaml:opensaml-saml-impl"
}
implementation "org.springframework.security:spring-security-saml2-service-provider"
implementation 'com.coveo:saml-client:5.0.0'
}
testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
// Batik
implementation "org.apache.xmlgraphics:batik-all:1.17"
implementation "org.apache.xmlgraphics:batik-all:1.18"
// TwelveMonkeys
runtimeOnly "com.twelvemonkeys.imageio:imageio-batik:$imageioVersion"
@@ -162,7 +181,10 @@ dependencies {
runtimeOnly "com.twelvemonkeys.imageio:imageio-webp:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-xwd:$imageioVersion"
implementation "commons-io:commons-io:2.16.1"
// Image metadata extractor
implementation "com.drewnoakes:metadata-extractor:2.19.0"
implementation "commons-io:commons-io:2.17.0"
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0"
//general PDF
@@ -184,11 +206,11 @@ dependencies {
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
implementation "io.micrometer:micrometer-core:1.13.4"
implementation "io.micrometer:micrometer-core:1.13.6"
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
// https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation "org.commonmark:commonmark:0.22.0"
implementation "org.commonmark:commonmark-ext-gfm-tables:0.22.0"
implementation "org.commonmark:commonmark:0.24.0"
implementation "org.commonmark:commonmark-ext-gfm-tables:0.24.0"
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
implementation "com.bucket4j:bucket4j_jdk17-core:8.14.0"
implementation "com.fathzer:javaluator:3.0.5"

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 0.29.0
appVersion: 0.31.1
description: locally hosted web application that allows you to perform various operations
on PDF files
home: https://github.com/Stirling-Tools/Stirling-PDF
@@ -13,4 +13,4 @@ maintainers:
name: stirling-pdf-chart
sources:
- https://github.com/Stirling-Tools/Stirling-PDF
version: 1.0.0
version: 1.0.1

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<body>
<h1>My First Heading</h1>
<p>My first paragraph.</p>
</body>
</html>

View File

@@ -0,0 +1,16 @@
header
============
Header2
------------
text
text2
## **PDF Features**
### **Page Operations**
- View and modify PDFs - View multi page PDFs with custom viewing sorting and searching. Plus on page edit features like annotate, draw and adding text and images. (Using PDF.js with Joxit and Liberation.Liberation fonts)
- Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages.
- Merge multiple PDFs together into a single resultant file.

Binary file not shown.

View File

@@ -123,7 +123,7 @@ Feature: API Validation
| odt | .odt |
| doc | .doc |
@ocr
@ocr @pdfa1
Scenario: PDFA
Given I use an example file at "exampleFiles/pdfa2.pdf" as parameter "fileInput"
And the request data includes
@@ -134,7 +134,7 @@ Feature: API Validation
And the response file should have extension ".pdf"
And the response file should have size greater than 100
@ocr
@ocr @pdfa2
Scenario: PDFA1
Given I use an example file at "exampleFiles/pdfa1.pdf" as parameter "fileInput"
And the request data includes
@@ -218,6 +218,28 @@ Feature: API Validation
| .odt |
| .pptx |
| .rtf |
@calibre @positive @htmltopdf
Scenario: Convert HTML to PDF
Given I use an example file at "exampleFiles/example.html" as parameter "fileInput"
When I send the API request to the endpoint "/api/v1/convert/html/pdf"
Then the response status code should be 200
And the response file should have size greater than 100
And the response file should have extension ".pdf"
@calibre @positive @zippedhtmltopdf
Scenario: Convert zipped HTML to PDF
Given I use an example file at "exampleFiles/example_html.zip" as parameter "fileInput"
When I send the API request to the endpoint "/api/v1/convert/html/pdf"
Then the response status code should be 200
And the response file should have size greater than 100
And the response file should have extension ".pdf"
@calibre @positive @markdowntopdf
Scenario: Convert Markdown to PDF
Given I use an example file at "exampleFiles/example.md" as parameter "fileInput"
When I send the API request to the endpoint "/api/v1/convert/markdown/pdf"
Then the response status code should be 200
And the response file should have size greater than 100
And the response file should have extension ".pdf"

View File

@@ -7,7 +7,7 @@ services:
limits:
memory: 4G
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"]
interval: 5s
timeout: 10s
retries: 16
@@ -19,7 +19,7 @@ services:
- /stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true"
SECURITY_ENABLELOGIN: "false"
PUID: 1002
PGID: 1002
UMASK: "022"

View File

@@ -0,0 +1,20 @@
#!/bin/bash
# Check if a key was provided
if [ $# -eq 0 ]; then
echo "Please provide a key to remove."
exit 1
fi
key_to_remove="$1"
for file in ../src/main/resources/messages_*.properties; do
# If the key ends with a dot, remove all keys starting with it
if [[ "$key_to_remove" == *. ]]; then
sed -i "/^${key_to_remove//./\\.}/d" "$file"
else
# Otherwise, remove only the exact key match
sed -i "/^${key_to_remove//./\\.}=/d" "$file"
fi
echo "Updated $file"
done

View File

@@ -0,0 +1,10 @@
#!/bin/bash
translation_key="pdfToPDFA.credit"
old_value="OCRmyPDF"
new_value="ghostscript"
for file in ../src/main/resources/messages_*.properties; do
sed -i "/^$translation_key=/s/$old_value/$new_value/" "$file"
echo "Updated $file"
done

View File

@@ -1,25 +1,23 @@
package stirling.software.SPDF.EE;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
@Configuration
@Lazy
@Slf4j
public class EEAppConfig {
private static final Logger logger = LoggerFactory.getLogger(EEAppConfig.class);
@Autowired ApplicationProperties applicationProperties;
@Autowired private LicenseKeyChecker licenseKeyChecker;
@Bean(name = "RunningEE")
@Bean(name = "runningEE")
public boolean runningEnterpriseEdition() {
// TODO: Implement EE detection
return false;
return licenseKeyChecker.getEnterpriseEnabledResult();
}
}

View File

@@ -0,0 +1,204 @@
package stirling.software.SPDF.EE;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.posthog.java.shaded.org.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.utils.GeneralUtils;
@Service
@Slf4j
public class KeygenLicenseVerifier {
private static final String ACCOUNT_ID = "e5430f69-e834-4ae4-befd-b602aae5f372";
private static final String BASE_URL = "https://api.keygen.sh/v1/accounts";
private static final ObjectMapper objectMapper = new ObjectMapper();
private final ApplicationProperties applicationProperties;
@Autowired
public KeygenLicenseVerifier(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
public boolean verifyLicense(String licenseKey) {
try {
log.info("Checking license key");
String machineFingerprint = generateMachineFingerprint();
// First, try to validate the license
JsonNode validationResponse = validateLicense(licenseKey, machineFingerprint);
if (validationResponse != null) {
boolean isValid = validationResponse.path("meta").path("valid").asBoolean();
String licenseId = validationResponse.path("data").path("id").asText();
if (!isValid) {
String code = validationResponse.path("meta").path("code").asText();
log.debug(code);
if ("NO_MACHINE".equals(code)
|| "NO_MACHINES".equals(code)
|| "FINGERPRINT_SCOPE_MISMATCH".equals(code)) {
log.info(
"License not activated for this machine. Attempting to activate...");
boolean activated =
activateMachine(licenseKey, licenseId, machineFingerprint);
if (activated) {
// Revalidate after activation
validationResponse = validateLicense(licenseKey, machineFingerprint);
isValid =
validationResponse != null
&& validationResponse
.path("meta")
.path("valid")
.asBoolean();
}
}
}
return isValid;
}
return false;
} catch (Exception e) {
log.error("Error verifying license: " + e.getMessage());
return false;
}
}
private JsonNode validateLicense(String licenseKey, String machineFingerprint)
throws Exception {
HttpClient client = HttpClient.newHttpClient();
String requestBody =
String.format(
"{\"meta\":{\"key\":\"%s\",\"scope\":{\"fingerprint\":\"%s\"}}}",
licenseKey, machineFingerprint);
HttpRequest request =
HttpRequest.newBuilder()
.uri(
URI.create(
BASE_URL
+ "/"
+ ACCOUNT_ID
+ "/licenses/actions/validate-key"))
.header("Content-Type", "application/vnd.api+json")
.header("Accept", "application/vnd.api+json")
// .header("Authorization", "License " + licenseKey)
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
log.info(" validateLicenseResponse body: " + response.body());
JsonNode jsonResponse = objectMapper.readTree(response.body());
if (response.statusCode() == 200) {
JsonNode metaNode = jsonResponse.path("meta");
boolean isValid = metaNode.path("valid").asBoolean();
String detail = metaNode.path("detail").asText();
String code = metaNode.path("code").asText();
log.debug("License validity: " + isValid);
log.debug("Validation detail: " + detail);
log.debug("Validation code: " + code);
int users =
jsonResponse
.path("data")
.path("attributes")
.path("metadata")
.path("users")
.asInt(0);
applicationProperties.getEnterpriseEdition().setMaxUsers(users);
log.info(applicationProperties.toString());
} else {
log.error("Error validating license. Status code: " + response.statusCode());
}
return jsonResponse;
}
private boolean activateMachine(String licenseKey, String licenseId, String machineFingerprint)
throws Exception {
HttpClient client = HttpClient.newHttpClient();
String hostname;
try {
hostname = java.net.InetAddress.getLocalHost().getHostName();
} catch (Exception e) {
hostname = "Unknown";
}
JSONObject body =
new JSONObject()
.put(
"data",
new JSONObject()
.put("type", "machines")
.put(
"attributes",
new JSONObject()
.put("fingerprint", machineFingerprint)
.put(
"platform",
System.getProperty(
"os.name")) // Added
// platform
// parameter
.put(
"name",
hostname)) // Added name parameter
.put(
"relationships",
new JSONObject()
.put(
"license",
new JSONObject()
.put(
"data",
new JSONObject()
.put(
"type",
"licenses")
.put(
"id",
licenseId)))));
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/machines"))
.header("Content-Type", "application/vnd.api+json")
.header("Accept", "application/vnd.api+json")
.header(
"Authorization",
"License " + licenseKey) // Keep the license key authentication
.POST(
HttpRequest.BodyPublishers.ofString(
body.toString())) // Send the JSON body
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
log.debug("activateMachine Response body: " + response.body());
if (response.statusCode() == 201) {
log.info("Machine activated successfully");
return true;
} else {
log.error(
"Error activating machine. Status code: {}, error: {}",
response.statusCode(),
response.body());
return false;
}
}
private String generateMachineFingerprint() {
return GeneralUtils.generateMachineFingerprint();
}
}

View File

@@ -0,0 +1,59 @@
package stirling.software.SPDF.EE;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.utils.GeneralUtils;
@Component
@Slf4j
public class LicenseKeyChecker {
private final KeygenLicenseVerifier licenseService;
private final ApplicationProperties applicationProperties;
private boolean enterpriseEnbaledResult = false;
@Autowired
public LicenseKeyChecker(
KeygenLicenseVerifier licenseService, ApplicationProperties applicationProperties) {
this.licenseService = licenseService;
this.applicationProperties = applicationProperties;
}
@Scheduled(fixedRate = 604800000, initialDelay = 1000) // 7 days in milliseconds
public void checkLicensePeriodically() {
checkLicense();
}
private void checkLicense() {
if (!applicationProperties.getEnterpriseEdition().isEnabled()) {
enterpriseEnbaledResult = false;
} else {
enterpriseEnbaledResult =
licenseService.verifyLicense(
applicationProperties.getEnterpriseEdition().getKey());
if (enterpriseEnbaledResult) {
log.info("License key is valid.");
} else {
log.info("License key is invalid.");
}
}
}
public void updateLicenseKey(String newKey) throws IOException {
applicationProperties.getEnterpriseEdition().setKey(newKey);
GeneralUtils.saveKeyToConfig("EnterpriseEdition.key", newKey, false);
checkLicense();
}
public boolean getEnterpriseEnabledResult() {
return enterpriseEnbaledResult;
}
}

View File

@@ -0,0 +1,39 @@
package stirling.software.SPDF.Factories;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.model.api.misc.HighContrastColorCombination;
import stirling.software.SPDF.model.api.misc.ReplaceAndInvert;
import stirling.software.SPDF.utils.misc.CustomColorReplaceStrategy;
import stirling.software.SPDF.utils.misc.InvertFullColorStrategy;
import stirling.software.SPDF.utils.misc.ReplaceAndInvertColorStrategy;
@Component
public class ReplaceAndInvertColorFactory {
public ReplaceAndInvertColorStrategy replaceAndInvert(
MultipartFile file,
ReplaceAndInvert replaceAndInvertOption,
HighContrastColorCombination highContrastColorCombination,
String backGroundColor,
String textColor) {
if (replaceAndInvertOption == ReplaceAndInvert.CUSTOM_COLOR
|| replaceAndInvertOption == ReplaceAndInvert.HIGH_CONTRAST_COLOR) {
return new CustomColorReplaceStrategy(
file,
replaceAndInvertOption,
textColor,
backGroundColor,
highContrastColorCombination);
} else if (replaceAndInvertOption == ReplaceAndInvert.FULL_INVERSION) {
return new InvertFullColorStrategy(file, replaceAndInvertOption);
}
return null;
}
}

View File

@@ -11,6 +11,9 @@ import org.slf4j.LoggerFactory;
import io.github.pixee.security.SystemCommand;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LibreOfficeListener {
private static final Logger logger = LoggerFactory.getLogger(LibreOfficeListener.class);
@@ -31,7 +34,7 @@ public class LibreOfficeListener {
private LibreOfficeListener() {}
private boolean isListenerRunning() {
System.out.println("waiting for listener to start");
log.info("waiting for listener to start");
try (Socket socket = new Socket()) {
socket.connect(
new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second

View File

@@ -1,6 +1,7 @@
package stirling.software.SPDF;
import java.io.IOException;
import java.net.ServerSocket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -30,30 +31,61 @@ public class SPdfApplication {
private static final Logger logger = LoggerFactory.getLogger(SPdfApplication.class);
@Autowired private Environment env;
@Autowired ApplicationProperties applicationProperties;
private static String baseUrlStatic;
private static String serverPortStatic;
@Value("${baseUrl:http://localhost}")
private String baseUrl;
@Value("${server.port:8080}")
public void setServerPortStatic(String port) {
SPdfApplication.serverPortStatic = port;
if ("auto".equalsIgnoreCase(port)) {
// Use Spring Boot's automatic port assignment (server.port=0)
SPdfApplication.serverPortStatic =
"0"; // This will let Spring Boot assign an available port
} else {
SPdfApplication.serverPortStatic = port;
}
}
// Optionally keep this method if you want to provide a manual port-incrementation fallback.
private static String findAvailablePort(int startPort) {
int port = startPort;
while (!isPortAvailable(port)) {
port++;
}
return String.valueOf(port);
}
private static boolean isPortAvailable(int port) {
try (ServerSocket socket = new ServerSocket(port)) {
return true;
} catch (IOException e) {
return false;
}
}
@PostConstruct
public void init() {
baseUrlStatic = this.baseUrl;
// Check if the BROWSER_OPEN environment variable is set to true
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
if (browserOpen) {
try {
String url = "http://localhost:" + getNonStaticPort();
String url = baseUrl + ":" + getStaticPort();
String os = System.getProperty("os.name").toLowerCase();
Runtime rt = Runtime.getRuntime();
if (os.contains("win")) {
// For Windows
SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url);
} else if (os.contains("mac")) {
SystemCommand.runCommand(rt, "open " + url);
} else if (os.contains("nix") || os.contains("nux")) {
SystemCommand.runCommand(rt, "xdg-open " + url);
}
} catch (Exception e) {
logger.error("Error opening browser: {}", e.getMessage());
@@ -69,15 +101,13 @@ public class SPdfApplication {
app.addInitializers(new ConfigInitializer());
Map<String, String> propertyFiles = new HashMap<>();
// stirling pdf settings file
// External config files
if (Files.exists(Paths.get("configs/settings.yml"))) {
propertyFiles.put("spring.config.additional-location", "file:configs/settings.yml");
} else {
logger.warn(
"External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
logger.warn("External configuration file 'configs/settings.yml' does not exist.");
}
// custom javs settings file
if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
String existingLocation =
propertyFiles.getOrDefault("spring.config.additional-location", "");
@@ -100,28 +130,31 @@ public class SPdfApplication {
app.run(args);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Thread interrupted while sleeping", e);
}
// Ensure directories are created
try {
Files.createDirectories(Path.of("customFiles/static/"));
Files.createDirectories(Path.of("customFiles/templates/"));
} catch (Exception e) {
logger.error("Error creating directories: {}", e.getMessage());
}
printStartupLogs();
}
private static void printStartupLogs() {
logger.info("Stirling-PDF Started.");
String url = "http://localhost:" + getStaticPort();
String url = baseUrlStatic + ":" + getStaticPort();
logger.info("Navigate to {}", url);
}
public static String getStaticBaseUrl() {
return baseUrlStatic;
}
public String getNonStaticBaseUrl() {
return baseUrlStatic;
}
public static String getStaticPort() {
return serverPortStatic;
}

View File

@@ -15,6 +15,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
@@ -160,4 +161,29 @@ public class AppConfig {
public String accessibilityStatement() {
return applicationProperties.getLegal().getAccessibilityStatement();
}
@Bean(name = "analyticsPrompt")
@Scope("request")
public boolean analyticsPrompt() {
return applicationProperties.getSystem().getEnableAnalytics() == null
|| "undefined".equals(applicationProperties.getSystem().getEnableAnalytics());
}
@Bean(name = "analyticsEnabled")
@Scope("request")
public boolean analyticsEnabled() {
if (applicationProperties.getEnterpriseEdition().isEnabled()) return true;
return applicationProperties.getSystem().getEnableAnalytics() != null
&& Boolean.parseBoolean(applicationProperties.getSystem().getEnableAnalytics());
}
@Bean(name = "StirlingPDFLabel")
public String stirlingPDFLabel() {
return "Stirling-PDF" + " v" + appVersion();
}
@Bean(name = "UUID")
public String uuid() {
return applicationProperties.getAutomaticallyGenerated().getUUID();
}
}

View File

@@ -5,6 +5,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import stirling.software.SPDF.config.interfaces.ShowAdminInterface;
import stirling.software.SPDF.model.ApplicationProperties;
@Service

View File

@@ -5,6 +5,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -42,7 +43,7 @@ public class EndpointConfiguration {
public void disableEndpoint(String endpoint) {
if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
logger.info("Disabling {}", endpoint);
logger.debug("Disabling {}", endpoint);
endpointStatuses.put(endpoint, false);
}
}
@@ -76,6 +77,23 @@ public class EndpointConfiguration {
}
}
public void logDisabledEndpointsSummary() {
List<String> disabledList =
endpointStatuses.entrySet().stream()
.filter(entry -> !entry.getValue()) // only get disabled endpoints (value
// is false)
.map(Map.Entry::getKey)
.sorted()
.collect(Collectors.toList());
if (!disabledList.isEmpty()) {
logger.info(
"Total disabled endpoints: {}. Disabled endpoints: {}",
disabledList.size(),
String.join(", ", disabledList));
}
}
public void init() {
// Adding endpoints to "PageOps" group
addEndpointToGroup("PageOps", "remove-pages");
@@ -163,14 +181,12 @@ public class EndpointConfiguration {
// python
addEndpointToGroup("Python", "extract-image-scans");
addEndpointToGroup("Python", REMOVE_BLANKS);
addEndpointToGroup("Python", "html-to-pdf");
addEndpointToGroup("Python", "url-to-pdf");
addEndpointToGroup("Python", "pdf-to-img");
// openCV
addEndpointToGroup("OpenCV", "extract-image-scans");
addEndpointToGroup("OpenCV", REMOVE_BLANKS);
// LibreOffice
addEndpointToGroup("LibreOffice", "repair");
@@ -230,6 +246,17 @@ public class EndpointConfiguration {
addEndpointToGroup("Javascript", "sign");
addEndpointToGroup("Javascript", "compare");
addEndpointToGroup("Javascript", "adjust-contrast");
// Ghostscript dependent endpoints
addEndpointToGroup("Ghostscript", "compress-pdf");
addEndpointToGroup("Ghostscript", "pdf-to-pdfa");
// Weasyprint dependent endpoints
addEndpointToGroup("Weasyprint", "html-to-pdf");
addEndpointToGroup("Weasyprint", "url-to-pdf");
// Pdftohtml dependent endpoints
addEndpointToGroup("Pdftohtml", "pdf-to-html");
}
private void processEnvironmentConfigs() {
@@ -251,5 +278,9 @@ public class EndpointConfiguration {
}
}
public Set<String> getEndpointsForGroup(String group) {
return endpointGroups.getOrDefault(group, new HashSet<>());
}
private static final String REMOVE_BLANKS = "remove-blanks";
}

View File

@@ -0,0 +1,146 @@
package stirling.software.SPDF.config;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
@Configuration
@Slf4j
public class ExternalAppDepConfig {
@Autowired private EndpointConfiguration endpointConfiguration;
private boolean isCommandAvailable(String command) {
try {
ProcessBuilder processBuilder = new ProcessBuilder();
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
processBuilder.command("where", command);
} else {
processBuilder.command("which", command);
}
Process process = processBuilder.start();
int exitCode = process.waitFor();
return exitCode == 0;
} catch (Exception e) {
log.debug("Error checking for command {}: {}", command, e.getMessage());
return false;
}
}
private final Map<String, List<String>> commandToGroupMapping =
new HashMap<>() {
{
put("gs", List.of("Ghostscript"));
put("soffice", List.of("LibreOffice"));
put("ocrmypdf", List.of("OCRmyPDF"));
put("weasyprint", List.of("Weasyprint"));
put("pdftohtml", List.of("Pdftohtml"));
}
};
private List<String> getAffectedFeatures(String group) {
return endpointConfiguration.getEndpointsForGroup(group).stream()
.map(endpoint -> formatEndpointAsFeature(endpoint))
.collect(Collectors.toList());
}
private String formatEndpointAsFeature(String endpoint) {
// First replace common terms
String feature = endpoint.replace("-", " ").replace("pdf", "PDF").replace("img", "image");
// Split into words and capitalize each word
return Arrays.stream(feature.split("\\s+"))
.map(word -> capitalizeWord(word))
.collect(Collectors.joining(" "));
}
private String capitalizeWord(String word) {
if (word.isEmpty()) {
return word;
}
if ("pdf".equalsIgnoreCase(word)) {
return "PDF";
}
return word.substring(0, 1).toUpperCase() + word.substring(1).toLowerCase();
}
private void checkDependencyAndDisableGroup(String command) {
boolean isAvailable = isCommandAvailable(command);
if (!isAvailable) {
List<String> affectedGroups = commandToGroupMapping.get(command);
if (affectedGroups != null) {
for (String group : affectedGroups) {
List<String> affectedFeatures = getAffectedFeatures(group);
endpointConfiguration.disableGroup(group);
log.warn(
"Missing dependency: {} - Disabling group: {} (Affected features: {})",
command,
group,
affectedFeatures != null && !affectedFeatures.isEmpty()
? String.join(", ", affectedFeatures)
: "unknown");
}
}
}
}
@PostConstruct
public void checkDependencies() {
// Check core dependencies
checkDependencyAndDisableGroup("gs");
checkDependencyAndDisableGroup("soffice");
checkDependencyAndDisableGroup("ocrmypdf");
checkDependencyAndDisableGroup("weasyprint");
checkDependencyAndDisableGroup("pdftohtml");
// Special handling for Python/OpenCV dependencies
boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python");
if (!pythonAvailable) {
List<String> pythonFeatures = getAffectedFeatures("Python");
List<String> openCVFeatures = getAffectedFeatures("OpenCV");
endpointConfiguration.disableGroup("Python");
endpointConfiguration.disableGroup("OpenCV");
log.warn(
"Missing dependency: Python - Disabling Python features: {} and OpenCV features: {}",
String.join(", ", pythonFeatures),
String.join(", ", openCVFeatures));
} else {
// If Python is available, check for OpenCV
try {
ProcessBuilder processBuilder = new ProcessBuilder();
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
processBuilder.command("python", "-c", "import cv2");
} else {
processBuilder.command("python3", "-c", "import cv2");
}
Process process = processBuilder.start();
int exitCode = process.waitFor();
if (exitCode != 0) {
List<String> openCVFeatures = getAffectedFeatures("OpenCV");
endpointConfiguration.disableGroup("OpenCV");
log.warn(
"OpenCV not available in Python - Disabling OpenCV features: {}",
String.join(", ", openCVFeatures));
}
} catch (Exception e) {
List<String> openCVFeatures = getAffectedFeatures("OpenCV");
endpointConfiguration.disableGroup("OpenCV");
log.warn(
"Error checking OpenCV: {} - Disabling OpenCV features: {}",
e.getMessage(),
String.join(", ", openCVFeatures));
}
}
endpointConfiguration.logDisabledEndpointsSummary();
}
}

View File

@@ -0,0 +1,63 @@
package stirling.software.SPDF.config;
import java.io.IOException;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import io.micrometer.common.util.StringUtils;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.utils.GeneralUtils;
@Component
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
public class InitialSetup {
@Autowired private ApplicationProperties applicationProperties;
@PostConstruct
public void initUUIDKey() throws IOException {
String uuid = applicationProperties.getAutomaticallyGenerated().getUUID();
if (!GeneralUtils.isValidUUID(uuid)) {
uuid = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.UUID", uuid);
applicationProperties.getAutomaticallyGenerated().setUUID(uuid);
}
}
@PostConstruct
public void initSecretKey() throws IOException {
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
if (!GeneralUtils.isValidUUID(secretKey)) {
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.key", secretKey);
applicationProperties.getAutomaticallyGenerated().setKey(secretKey);
}
}
@PostConstruct
public void initLegalUrls() throws IOException {
// Initialize Terms and Conditions
String termsUrl = applicationProperties.getLegal().getTermsAndConditions();
if (StringUtils.isEmpty(termsUrl)) {
String defaultTermsUrl = "https://www.stirlingpdf.com/terms-and-conditions";
GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl);
applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl);
}
// Initialize Privacy Policy
String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy();
if (StringUtils.isEmpty(privacyUrl)) {
String defaultPrivacyUrl = "https://www.stirlingpdf.com/privacy-policy";
GeneralUtils.saveKeyToConfig("legal.privacyPolicy", defaultPrivacyUrl);
applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl);
}
}
}

View File

@@ -14,7 +14,7 @@ import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import stirling.software.SPDF.model.ApplicationProperties;
@Configuration
public class Beans implements WebMvcConfigurer {
public class LocaleConfiguration implements WebMvcConfigurer {
@Autowired ApplicationProperties applicationProperties;

View File

@@ -13,6 +13,7 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import stirling.software.SPDF.utils.RequestUriUtils;
@Component
@@ -32,10 +33,11 @@ public class MetricsFilter extends OncePerRequestFilter {
String uri = request.getRequestURI();
if (RequestUriUtils.isTrackableResource(request.getContextPath(), uri)) {
HttpSession session = request.getSession(false);
String sessionId = (session != null) ? session.getId() : "no-session";
Counter counter =
Counter.builder("http.requests")
.tag("session", request.getSession().getId())
.tag("session", sessionId)
.tag("method", request.getMethod())
.tag("uri", uri)
.register(meterRegistry);

View File

@@ -0,0 +1,34 @@
package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.posthog.java.PostHog;
import jakarta.annotation.PreDestroy;
@Configuration
public class PostHogConfig {
@Value("${posthog.api.key}")
private String posthogApiKey;
@Value("${posthog.host}")
private String posthogHost;
private PostHog postHogClient;
@Bean
public PostHog postHogClient() {
postHogClient = new PostHog.Builder(posthogApiKey).host(posthogHost).build();
return postHogClient;
}
@PreDestroy
public void shutdownPostHog() {
if (postHogClient != null) {
postHogClient.shutdown();
}
}
}

View File

@@ -0,0 +1,68 @@
// package stirling.software.SPDF.config.fingerprint;
//
// import java.io.IOException;
//
// import org.springframework.beans.factory.annotation.Autowired;
// import org.springframework.stereotype.Component;
// import org.springframework.web.filter.OncePerRequestFilter;
//
// import jakarta.servlet.FilterChain;
// import jakarta.servlet.ServletException;
// import jakarta.servlet.http.HttpServletRequest;
// import jakarta.servlet.http.HttpServletResponse;
// import jakarta.servlet.http.HttpSession;
// import lombok.extern.slf4j.Slf4j;
// import stirling.software.SPDF.utils.RequestUriUtils;
//
//// @Component
// @Slf4j
// public class FingerprintBasedSessionFilter extends OncePerRequestFilter {
// private final FingerprintGenerator fingerprintGenerator;
// private final FingerprintBasedSessionManager sessionManager;
//
// @Autowired
// public FingerprintBasedSessionFilter(
// FingerprintGenerator fingerprintGenerator,
// FingerprintBasedSessionManager sessionManager) {
// this.fingerprintGenerator = fingerprintGenerator;
// this.sessionManager = sessionManager;
// }
//
// @Override
// protected void doFilterInternal(
// HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
// throws ServletException, IOException {
//
// if (RequestUriUtils.isStaticResource(request.getContextPath(), request.getRequestURI())) {
// filterChain.doFilter(request, response);
// return;
// }
//
// String fingerprint = fingerprintGenerator.generateFingerprint(request);
// log.debug("Generated fingerprint for request: {}", fingerprint);
//
// HttpSession session = request.getSession();
// boolean isNewSession = session.isNew();
// String sessionId = session.getId();
//
// if (isNewSession) {
// log.info("New session created: {}", sessionId);
// }
//
// if (!sessionManager.isFingerPrintAllowed(fingerprint)) {
// log.info("Blocked fingerprint detected, redirecting: {}", fingerprint);
// response.sendRedirect(request.getContextPath() + "/too-many-requests");
// return;
// }
//
// session.setAttribute("userFingerprint", fingerprint);
// session.setAttribute(
// FingerprintBasedSessionManager.STARTUP_TIMESTAMP,
// FingerprintBasedSessionManager.APP_STARTUP_TIME);
//
// sessionManager.registerFingerprint(fingerprint, sessionId);
//
// log.debug("Proceeding with request: {}", request.getRequestURI());
// filterChain.doFilter(request, response);
// }
// }

View File

@@ -0,0 +1,134 @@
// package stirling.software.SPDF.config.fingerprint;
//
// import java.util.Iterator;
// import java.util.Map;
// import java.util.concurrent.ConcurrentHashMap;
// import java.util.concurrent.TimeUnit;
//
// import org.springframework.scheduling.annotation.Scheduled;
// import org.springframework.stereotype.Component;
//
// import jakarta.servlet.http.HttpSession;
// import jakarta.servlet.http.HttpSessionAttributeListener;
// import jakarta.servlet.http.HttpSessionEvent;
// import jakarta.servlet.http.HttpSessionListener;
// import lombok.AllArgsConstructor;
// import lombok.Data;
// import lombok.extern.slf4j.Slf4j;
//
// @Slf4j
// @Component
// public class FingerprintBasedSessionManager
// implements HttpSessionListener, HttpSessionAttributeListener {
// private static final ConcurrentHashMap<String, FingerprintInfo> activeFingerprints =
// new ConcurrentHashMap<>();
//
// // To be reduced in later version to 8~
// private static final int MAX_ACTIVE_FINGERPRINTS = 30;
//
// static final String STARTUP_TIMESTAMP = "appStartupTimestamp";
// static final long APP_STARTUP_TIME = System.currentTimeMillis();
// private static final long FINGERPRINT_EXPIRATION = TimeUnit.MINUTES.toMillis(30);
//
// @Override
// public void sessionCreated(HttpSessionEvent se) {
// HttpSession session = se.getSession();
// String sessionId = session.getId();
// String fingerprint = (String) session.getAttribute("userFingerprint");
//
// if (fingerprint == null) {
// log.warn("Session created without fingerprint: {}", sessionId);
// return;
// }
//
// synchronized (activeFingerprints) {
// if (activeFingerprints.size() >= MAX_ACTIVE_FINGERPRINTS
// && !activeFingerprints.containsKey(fingerprint)) {
// log.info("Max fingerprints reached. Marking session as blocked: {}", sessionId);
// session.setAttribute("blocked", true);
// } else {
// activeFingerprints.put(
// fingerprint, new FingerprintInfo(sessionId, System.currentTimeMillis()));
// log.info(
// "New fingerprint registered: {}. Total active fingerprints: {}",
// fingerprint,
// activeFingerprints.size());
// }
// session.setAttribute(STARTUP_TIMESTAMP, APP_STARTUP_TIME);
// }
// }
//
// @Override
// public void sessionDestroyed(HttpSessionEvent se) {
// HttpSession session = se.getSession();
// String fingerprint = (String) session.getAttribute("userFingerprint");
//
// if (fingerprint != null) {
// synchronized (activeFingerprints) {
// activeFingerprints.remove(fingerprint);
// log.info(
// "Fingerprint removed: {}. Total active fingerprints: {}",
// fingerprint,
// activeFingerprints.size());
// }
// }
// }
//
// public boolean isFingerPrintAllowed(String fingerprint) {
// synchronized (activeFingerprints) {
// return activeFingerprints.size() < MAX_ACTIVE_FINGERPRINTS
// || activeFingerprints.containsKey(fingerprint);
// }
// }
//
// public void registerFingerprint(String fingerprint, String sessionId) {
// synchronized (activeFingerprints) {
// activeFingerprints.put(
// fingerprint, new FingerprintInfo(sessionId, System.currentTimeMillis()));
// }
// }
//
// public void unregisterFingerprint(String fingerprint) {
// synchronized (activeFingerprints) {
// activeFingerprints.remove(fingerprint);
// }
// }
//
// @Scheduled(fixedRate = 1800000) // Run every 30 mins
// public void cleanupStaleFingerprints() {
// log.info("Starting cleanup of stale fingerprints");
// long now = System.currentTimeMillis();
// int removedCount = 0;
//
// synchronized (activeFingerprints) {
// Iterator<Map.Entry<String, FingerprintInfo>> iterator =
// activeFingerprints.entrySet().iterator();
// while (iterator.hasNext()) {
// Map.Entry<String, FingerprintInfo> entry = iterator.next();
// FingerprintInfo info = entry.getValue();
//
// if (now - info.getLastAccessTime() > FINGERPRINT_EXPIRATION) {
// iterator.remove();
// removedCount++;
// log.info("Removed stale fingerprint: {}", entry.getKey());
// }
// }
// }
//
// log.info("Cleanup complete. Removed {} stale fingerprints", removedCount);
// }
//
// public void updateLastAccessTime(String fingerprint) {
// FingerprintInfo info = activeFingerprints.get(fingerprint);
// if (info != null) {
// info.setLastAccessTime(System.currentTimeMillis());
// }
// }
//
// @Data
// @AllArgsConstructor
// private static class FingerprintInfo {
// private String sessionId;
// private long lastAccessTime;
// }
// }

View File

@@ -0,0 +1,77 @@
// package stirling.software.SPDF.config.fingerprint;
//
// import java.security.MessageDigest;
// import java.security.NoSuchAlgorithmException;
//
// import org.springframework.stereotype.Component;
//
// import jakarta.servlet.http.HttpServletRequest;
//
// @Component
// public class FingerprintGenerator {
//
// public String generateFingerprint(HttpServletRequest request) {
// if (request == null) {
// return "";
// }
// StringBuilder fingerprintBuilder = new StringBuilder();
//
// // Add IP address
// fingerprintBuilder.append(request.getRemoteAddr());
//
// // Add X-Forwarded-For header if present (for clients behind proxies)
// String forwardedFor = request.getHeader("X-Forwarded-For");
// if (forwardedFor != null) {
// fingerprintBuilder.append(forwardedFor);
// }
//
// // Add User-Agent
// String userAgent = request.getHeader("User-Agent");
// if (userAgent != null) {
// fingerprintBuilder.append(userAgent);
// }
//
// // Add Accept-Language header
// String acceptLanguage = request.getHeader("Accept-Language");
// if (acceptLanguage != null) {
// fingerprintBuilder.append(acceptLanguage);
// }
//
// // Add Accept header
// String accept = request.getHeader("Accept");
// if (accept != null) {
// fingerprintBuilder.append(accept);
// }
//
// // Add Connection header
// String connection = request.getHeader("Connection");
// if (connection != null) {
// fingerprintBuilder.append(connection);
// }
//
// // Add server port
// fingerprintBuilder.append(request.getServerPort());
//
// // Add secure flag
// fingerprintBuilder.append(request.isSecure());
//
// // Generate a hash of the fingerprint
// return generateHash(fingerprintBuilder.toString());
// }
//
// private String generateHash(String input) {
// try {
// MessageDigest digest = MessageDigest.getInstance("SHA-256");
// byte[] hash = digest.digest(input.getBytes());
// StringBuilder hexString = new StringBuilder();
// for (byte b : hash) {
// String hex = Integer.toHexString(0xff & b);
// if (hex.length() == 1) hexString.append('0');
// hexString.append(hex);
// }
// return hexString.toString();
// } catch (NoSuchAlgorithmException e) {
// throw new RuntimeException("Failed to generate fingerprint hash", e);
// }
// }
// }

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.config;
package stirling.software.SPDF.config.interfaces;
import java.io.IOException;
import java.util.List;

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.config;
package stirling.software.SPDF.config.interfaces;
public interface ShowAdminInterface {
default boolean getShowUpdateOnlyAdmins() {

View File

@@ -7,7 +7,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import stirling.software.SPDF.config.ShowAdminInterface;
import stirling.software.SPDF.config.interfaces.ShowAdminInterface;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository;

View File

@@ -1,27 +1,237 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.io.Resource;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import com.coveo.saml.SamlClient;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.config.security.saml2.CertificateUtils;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.Provider;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.UrlUtils;
@Slf4j
@AllArgsConstructor
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private final ApplicationProperties applicationProperties;
@Override
public void onLogoutSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
if (request.getParameter("userIsDisabled") != null) {
getRedirectStrategy()
.sendRedirect(request, response, "/login?erroroauth=userIsDisabled");
return;
if (!response.isCommitted()) {
// Handle user logout due to disabled account
if (request.getParameter("userIsDisabled") != null) {
response.sendRedirect(
request.getContextPath() + "/login?erroroauth=userIsDisabled");
return;
}
// Handle OAuth2 authentication error
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
response.sendRedirect(
request.getContextPath() + "/login?erroroauth=userAlreadyExistsWeb");
return;
}
if (authentication != null) {
// Handle SAML2 logout redirection
if (authentication instanceof Saml2Authentication) {
getRedirect_saml2(request, response, authentication);
return;
}
// Handle OAuth2 logout redirection
else if (authentication instanceof OAuth2AuthenticationToken) {
getRedirect_oauth2(request, response, authentication);
return;
}
// Handle Username/Password logout
else if (authentication instanceof UsernamePasswordAuthenticationToken) {
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
return;
}
// Handle unknown authentication types
else {
log.error(
"authentication class unknown: "
+ authentication.getClass().getSimpleName());
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
return;
}
} else {
// Redirect to login page after logout
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
return;
}
}
}
// Redirect for SAML2 authentication logout
private void getRedirect_saml2(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
String registrationId = samlConf.getRegistrationId();
Saml2Authentication samlAuthentication = (Saml2Authentication) authentication;
CustomSaml2AuthenticatedPrincipal principal =
(CustomSaml2AuthenticatedPrincipal) samlAuthentication.getPrincipal();
String nameIdValue = principal.getName();
try {
// Read certificate from the resource
Resource certificateResource = samlConf.getSpCert();
X509Certificate certificate = CertificateUtils.readCertificate(certificateResource);
List<X509Certificate> certificates = new ArrayList<>();
certificates.add(certificate);
// Construct URLs required for SAML configuration
String serverUrl =
SPdfApplication.getStaticBaseUrl() + ":" + SPdfApplication.getStaticPort();
String relyingPartyIdentifier =
serverUrl + "/saml2/service-provider-metadata/" + registrationId;
String assertionConsumerServiceUrl = serverUrl + "/login/saml2/sso/" + registrationId;
String idpUrl = samlConf.getIdpSingleLogoutUrl();
String idpIssuer = samlConf.getIdpIssuer();
// Create SamlClient instance for SAML logout
SamlClient samlClient =
new SamlClient(
relyingPartyIdentifier,
assertionConsumerServiceUrl,
idpUrl,
idpIssuer,
certificates,
SamlClient.SamlIdpBinding.POST);
// Read private key for service provider
Resource privateKeyResource = samlConf.getPrivateKey();
RSAPrivateKey privateKey = CertificateUtils.readPrivateKey(privateKeyResource);
// Set service provider keys for the SamlClient
samlClient.setSPKeys(certificate, privateKey);
// Redirect to identity provider for logout
samlClient.redirectToIdentityProvider(response, null, nameIdValue);
} catch (Exception e) {
log.error(nameIdValue, e);
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
}
}
// Redirect for OAuth2 authentication logout
private void getRedirect_oauth2(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
String param = "logout=true";
String registrationId = null;
String issuer = null;
String clientId = null;
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (authentication instanceof OAuth2AuthenticationToken) {
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
registrationId = oauthToken.getAuthorizedClientRegistrationId();
try {
// Get OAuth2 provider details from configuration
Provider provider = oauth.getClient().get(registrationId);
issuer = provider.getIssuer();
clientId = provider.getClientId();
} catch (UnsupportedProviderException e) {
log.error(e.getMessage());
}
} else {
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
issuer = oauth.getIssuer();
clientId = oauth.getClientId();
}
String errorMessage = "";
// Handle different error scenarios during logout
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
param = "erroroauth=oauth2AuthenticationErrorWeb";
} else if ((errorMessage = request.getParameter("error")) != null) {
param = "error=" + sanitizeInput(errorMessage);
} else if ((errorMessage = request.getParameter("erroroauth")) != null) {
param = "erroroauth=" + sanitizeInput(errorMessage);
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
param = "error=oauth2AutoCreateDisabled";
} else if (request.getParameter("oauth2_admin_blocked_user") != null) {
param = "erroroauth=oauth2_admin_blocked_user";
} else if (request.getParameter("userIsDisabled") != null) {
param = "erroroauth=userIsDisabled";
} else if (request.getParameter("badcredentials") != null) {
param = "error=badcredentials";
}
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
// Redirect based on OAuth2 provider
switch (registrationId.toLowerCase()) {
case "keycloak":
// Add Keycloak specific logout URL if needed
String logoutUrl =
issuer
+ "/protocol/openid-connect/logout"
+ "?client_id="
+ clientId
+ "&post_logout_redirect_uri="
+ response.encodeRedirectURL(redirect_url);
log.info("Redirecting to Keycloak logout URL: " + logoutUrl);
response.sendRedirect(logoutUrl);
break;
case "github":
// Add GitHub specific logout URL if needed
String githubLogoutUrl = "https://github.com/logout";
log.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
response.sendRedirect(githubLogoutUrl);
break;
case "google":
// Add Google specific logout URL if needed
// String googleLogoutUrl =
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
// + response.encodeRedirectURL(redirect_url);
log.info("Google does not have a specific logout URL");
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
// response.sendRedirect(googleLogoutUrl);
// break;
default:
String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
log.info("Redirecting to default logout URL: " + defaultRedirectUrl);
response.sendRedirect(defaultRedirectUrl);
break;
}
}
// Sanitize input to avoid potential security vulnerabilities
private String sanitizeInput(String input) {
return input.replaceAll("[^a-zA-Z0-9 ]", "");
}
}

View File

@@ -1,6 +1,8 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
@@ -14,9 +16,12 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.utils.RequestUriUtils;
@Slf4j
@Component
public class FirstLoginFilter extends OncePerRequestFilter {
@@ -50,6 +55,22 @@ public class FirstLoginFilter extends OncePerRequestFilter {
return;
}
}
if (log.isDebugEnabled()) {
HttpSession session = request.getSession(true);
SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss");
String creationTime = timeFormat.format(new Date(session.getCreationTime()));
log.debug(
"Request Info - New: {}, creationTimeSession {}, ID: {}, IP: {}, User-Agent: {}, Referer: {}, Request URL: {}",
session.isNew(),
creationTime,
session.getId(),
request.getRemoteAddr(),
request.getHeader("User-Agent"),
request.getHeader("Referer"),
request.getRequestURL().toString());
}
filterChain.doFilter(request, response);
}
}

View File

@@ -1,19 +1,14 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
import org.simpleyaml.configuration.file.YamlFile;
import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.DatabaseBackupInterface;
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.Role;
@@ -39,15 +34,6 @@ public class InitialSecuritySetup {
initializeInternalApiUser();
}
@PostConstruct
public void initSecretKey() throws IOException {
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
if (!isValidUUID(secretKey)) {
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
saveKeyToConfig(secretKey);
}
}
private void initializeAdminUser() throws IOException {
String initialUsername =
applicationProperties.getSecurity().getInitialLogin().getUsername();
@@ -89,33 +75,4 @@ public class InitialSecuritySetup {
log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId());
}
}
private void saveKeyToConfig(String key) throws IOException {
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
final YamlFile settingsYml = new YamlFile(path.toFile());
DumperOptions yamlOptionssettingsYml =
((SimpleYamlImplementation) settingsYml.getImplementation()).getDumperOptions();
yamlOptionssettingsYml.setSplitLines(false);
settingsYml.loadWithComments();
settingsYml
.path("AutomaticallyGenerated.key")
.set(key)
.comment("# Automatically Generated Settings (Do Not Edit Directly)");
settingsYml.save();
}
private boolean isValidUUID(String uuid) {
if (uuid == null) {
return false;
}
try {
UUID.fromString(uuid);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
}

View File

@@ -1,15 +1,17 @@
package stirling.software.SPDF.config.security;
import java.security.cert.X509Certificate;
import java.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.Resource;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -24,20 +26,34 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
import stirling.software.SPDF.config.security.saml2.CertificateUtils;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationFailureHandler;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationSuccessHandler;
import stirling.software.SPDF.config.security.saml2.CustomSaml2ResponseAuthenticationConverter;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.model.provider.GithubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider;
@@ -47,12 +63,11 @@ import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@Slf4j
public class SecurityConfiguration {
@Autowired private CustomUserDetailsService userDetailsService;
private static final Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
@@ -75,11 +90,48 @@ public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
if (loginEnabledValue) {
http.addFilterBefore(
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
if (applicationProperties.getSecurity().getCsrfDisabled()) {
http.csrf(csrf -> csrf.disable());
} else {
CookieCsrfTokenRepository cookieRepo =
CookieCsrfTokenRepository.withHttpOnlyFalse();
CsrfTokenRequestAttributeHandler requestHandler =
new CsrfTokenRequestAttributeHandler();
requestHandler.setCsrfRequestAttributeName(null);
http.csrf(
csrf ->
csrf.ignoringRequestMatchers(
request -> {
String apiKey = request.getHeader("X-API-Key");
http.csrf(csrf -> csrf.disable());
// If there's no API key, don't ignore CSRF
// (return false)
if (apiKey == null || apiKey.trim().isEmpty()) {
return false;
}
// Validate API key using existing UserService
try {
Optional<User> user =
userService.getUserByApiKey(apiKey);
// If API key is valid, ignore CSRF (return
// true)
// If API key is invalid, don't ignore CSRF
// (return false)
return user.isPresent();
} catch (Exception e) {
// If there's any error validating the API
// key, don't ignore CSRF
return false;
}
})
.csrfTokenRepository(cookieRepo)
.csrfTokenRequestHandler(requestHandler));
}
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
http.sessionManagement(
@@ -91,114 +143,154 @@ public class SecurityConfiguration {
.sessionRegistry(sessionRegistry)
.expiredUrl("/login?logout=true"));
http.formLogin(
formLogin ->
formLogin
.loginPage("/login")
.successHandler(
new CustomAuthenticationSuccessHandler(
loginAttemptService, userService))
.defaultSuccessUrl("/")
.failureHandler(
new CustomAuthenticationFailureHandler(
loginAttemptService, userService))
.permitAll())
.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()))
.logout(
logout ->
logout.logoutRequestMatcher(
new AntPathRequestMatcher("/logout"))
.logoutSuccessHandler(new CustomLogoutSuccessHandler())
.invalidateHttpSession(true) // Invalidate session
.deleteCookies("JSESSIONID", "remember-me"))
.rememberMe(
rememberMeConfigurer ->
rememberMeConfigurer // Use the configurator directly
.key("uniqueAndSecret")
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(1209600) // 2 weeks
)
.authorizeHttpRequests(
authz ->
authz.requestMatchers(
req -> {
String uri = req.getRequestURI();
String contextPath = req.getContextPath();
http.authenticationProvider(daoAuthenticationProvider());
http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()));
http.logout(
logout ->
logout.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessHandler(
new CustomLogoutSuccessHandler(applicationProperties))
.clearAuthentication(true)
.invalidateHttpSession(true) // Invalidate session
.deleteCookies("JSESSIONID", "remember-me"));
http.rememberMe(
rememberMeConfigurer ->
rememberMeConfigurer // Use the configurator directly
.key("uniqueAndSecret")
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(1209600) // 2 weeks
);
http.authorizeHttpRequests(
authz ->
authz.requestMatchers(
req -> {
String uri = req.getRequestURI();
String contextPath = req.getContextPath();
// Remove the context path from the URI
String trimmedUri =
uri.startsWith(contextPath)
? uri.substring(
contextPath
.length())
: uri;
// Remove the context path from the URI
String trimmedUri =
uri.startsWith(contextPath)
? uri.substring(
contextPath.length())
: uri;
return trimmedUri.startsWith("/login")
|| trimmedUri.startsWith("/oauth")
|| trimmedUri.endsWith(".svg")
|| trimmedUri.startsWith(
"/register")
|| trimmedUri.startsWith("/error")
|| trimmedUri.startsWith("/images/")
|| trimmedUri.startsWith("/public/")
|| trimmedUri.startsWith("/css/")
|| trimmedUri.startsWith("/fonts/")
|| trimmedUri.startsWith("/js/")
|| trimmedUri.startsWith(
"/api/v1/info/status");
})
.permitAll()
.anyRequest()
.authenticated());
return trimmedUri.startsWith("/login")
|| trimmedUri.startsWith("/oauth")
|| trimmedUri.startsWith("/saml2")
|| trimmedUri.endsWith(".svg")
|| trimmedUri.startsWith("/register")
|| trimmedUri.startsWith("/error")
|| trimmedUri.startsWith("/images/")
|| trimmedUri.startsWith("/public/")
|| trimmedUri.startsWith("/css/")
|| trimmedUri.startsWith("/fonts/")
|| trimmedUri.startsWith("/js/")
|| trimmedUri.startsWith(
"/api/v1/info/status");
})
.permitAll()
.anyRequest()
.authenticated());
// Handle User/Password Logins
if (applicationProperties.getSecurity().isUserPass()) {
http.formLogin(
formLogin ->
formLogin
.loginPage("/login")
.successHandler(
new CustomAuthenticationSuccessHandler(
loginAttemptService, userService))
.failureHandler(
new CustomAuthenticationFailureHandler(
loginAttemptService, userService))
.defaultSuccessUrl("/")
.permitAll());
}
// Handle OAUTH2 Logins
if (applicationProperties.getSecurity().getOauth2() != null
&& applicationProperties.getSecurity().getOauth2().getEnabled()
&& !applicationProperties
.getSecurity()
.getLoginMethod()
.equalsIgnoreCase("normal")) {
if (applicationProperties.getSecurity().isOauth2Activ()) {
http.oauth2Login(
oauth2 ->
oauth2.loginPage("/oauth2")
/*
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser'
is set as true, else login fails with an error message advising the same.
*/
oauth2 ->
oauth2.loginPage("/oauth2")
/*
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser'
is set as true, else login fails with an error message advising the same.
*/
.successHandler(
new CustomOAuth2AuthenticationSuccessHandler(
loginAttemptService,
applicationProperties,
userService))
.failureHandler(
new CustomOAuth2AuthenticationFailureHandler())
// Add existing Authorities from the database
.userInfoEndpoint(
userInfoEndpoint ->
userInfoEndpoint
.oidcUserService(
new CustomOAuth2UserService(
applicationProperties,
userService,
loginAttemptService))
.userAuthoritiesMapper(
userAuthoritiesMapper()))
.permitAll());
}
// Handle SAML
if (applicationProperties.getSecurity().isSaml2Activ()
&& applicationProperties.getSystem().getEnableAlphaFunctionality()) {
http.authenticationProvider(samlAuthenticationProvider());
http.saml2Login(
saml2 ->
saml2.loginPage("/saml2")
.successHandler(
new CustomOAuth2AuthenticationSuccessHandler(
new CustomSaml2AuthenticationSuccessHandler(
loginAttemptService,
applicationProperties,
userService))
.failureHandler(
new CustomOAuth2AuthenticationFailureHandler())
// Add existing Authorities from the database
.userInfoEndpoint(
userInfoEndpoint ->
userInfoEndpoint
.oidcUserService(
new CustomOAuth2UserService(
applicationProperties,
userService,
loginAttemptService))
.userAuthoritiesMapper(
userAuthoritiesMapper())))
.logout(
logout ->
logout.logoutSuccessHandler(
new CustomOAuth2LogoutSuccessHandler(
applicationProperties)));
new CustomSaml2AuthenticationFailureHandler())
.permitAll())
.addFilterBefore(
userAuthenticationFilter, Saml2WebSsoAuthenticationFilter.class);
}
} else {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
if (applicationProperties.getSecurity().getCsrfDisabled()) {
http.csrf(csrf -> csrf.disable());
} else {
CookieCsrfTokenRepository cookieRepo =
CookieCsrfTokenRepository.withHttpOnlyFalse();
CsrfTokenRequestAttributeHandler requestHandler =
new CsrfTokenRequestAttributeHandler();
requestHandler.setCsrfRequestAttributeName(null);
http.csrf(
csrf ->
csrf.csrfTokenRepository(cookieRepo)
.csrfTokenRequestHandler(requestHandler));
}
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
}
return http.build();
}
@Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public AuthenticationProvider samlAuthenticationProvider() {
OpenSaml4AuthenticationProvider authenticationProvider =
new OpenSaml4AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter(
new CustomSaml2ResponseAuthenticationConverter(userService));
return authenticationProvider;
}
// Client Registration Repository for OAUTH2 OIDC Login
@Bean
@ConditionalOnProperty(
@@ -214,7 +306,7 @@ public class SecurityConfiguration {
keycloakClientRegistration().ifPresent(registrations::add);
if (registrations.isEmpty()) {
logger.error("At least one OAuth2 provider must be configured");
log.error("At least one OAuth2 provider must be configured");
System.exit(1);
}
@@ -275,6 +367,7 @@ public class SecurityConfiguration {
}
private Optional<ClientRegistration> githubClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
@@ -329,6 +422,52 @@ public class SecurityConfiguration {
.build());
}
@Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
Resource privateKeyResource = samlConf.getPrivateKey();
Resource certificateResource = samlConf.getSpCert();
Saml2X509Credential signingCredential =
new Saml2X509Credential(
CertificateUtils.readPrivateKey(privateKeyResource),
CertificateUtils.readCertificate(certificateResource),
Saml2X509CredentialType.SIGNING);
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
RelyingPartyRegistration rp =
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
.signingX509Credentials((c) -> c.add(signingCredential))
.assertingPartyDetails(
(details) ->
details.entityId(samlConf.getIdpIssuer())
.singleSignOnServiceLocation(
samlConf.getIdpSingleLoginUrl())
.verificationX509Credentials(
(c) -> c.add(verificationCredential))
.wantAuthnRequestsSigned(true))
.build();
return new InMemoryRelyingPartyRegistrationRepository(rp);
}
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
/*
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
This is required for the internal; 'hasRole()' function to give out the correct role.
@@ -386,4 +525,14 @@ public class SecurityConfiguration {
public boolean activSecurity() {
return true;
}
// // Only Dev test
// @Bean
// public WebSecurityCustomizer webSecurityCustomizer() {
// return (web) ->
// web.ignoring()
// .requestMatchers(
// "/css/**", "/images/**", "/js/**", "/**.svg",
// "/pdfjs-legacy/**");
// }
}

View File

@@ -5,7 +5,6 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
@@ -23,6 +22,7 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
import stirling.software.SPDF.model.User;
@@ -30,13 +30,18 @@ import stirling.software.SPDF.model.User;
@Component
public class UserAuthenticationFilter extends OncePerRequestFilter {
@Autowired @Lazy private UserService userService;
private final UserService userService;
private final SessionPersistentRegistry sessionPersistentRegistry;
private final boolean loginEnabledValue;
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
@Autowired
@Qualifier("loginEnabled")
public boolean loginEnabledValue;
public UserAuthenticationFilter(
@Lazy UserService userService,
SessionPersistentRegistry sessionPersistentRegistry,
@Qualifier("loginEnabled") boolean loginEnabledValue) {
this.userService = userService;
this.sessionPersistentRegistry = sessionPersistentRegistry;
this.loginEnabledValue = loginEnabledValue;
}
@Override
protected void doFilterInternal(
@@ -51,6 +56,19 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
String requestURI = request.getRequestURI();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// Check for session expiration (unsure if needed)
// if (authentication != null && authentication.isAuthenticated()) {
// String sessionId = request.getSession().getId();
// SessionInformation sessionInfo =
// sessionPersistentRegistry.getSessionInformation(sessionId);
//
// if (sessionInfo != null && sessionInfo.isExpired()) {
// SecurityContextHolder.clearContext();
// response.sendRedirect(request.getContextPath() + "/login?expired=true");
// return;
// }
// }
// Check for API key in the request headers if no authentication exists
if (authentication == null || !authentication.isAuthenticated()) {
String apiKey = request.getHeader("X-API-Key");
@@ -94,7 +112,9 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter()
.write(
"Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternatively you can disable authentication if this is unexpected");
"Authentication required. Please provide a X-API-KEY in request header.\n"
+ "This is found in Settings -> Account Settings -> API Key\n"
+ "Alternatively you can disable authentication if this is unexpected");
return;
}
}
@@ -107,6 +127,8 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
username = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) {
username = ((OAuth2User) principal).getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
} else if (principal instanceof String) {
username = (String) principal;
}

View File

@@ -19,7 +19,8 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import stirling.software.SPDF.config.DatabaseBackupInterface;
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.AuthenticationType;
@@ -334,6 +335,10 @@ public class UserService implements UserServiceInterface {
} else if (principal instanceof OAuth2User) {
OAuth2User oAuth2User = (OAuth2User) principal;
usernameP = oAuth2User.getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
CustomSaml2AuthenticatedPrincipal saml2User =
(CustomSaml2AuthenticatedPrincipal) principal;
usernameP = saml2User.getName();
} else if (principal instanceof String) {
usernameP = (String) principal;
}
@@ -353,4 +358,9 @@ public class UserService implements UserServiceInterface {
return principal.toString();
}
}
@Override
public long getTotalUsersCount() {
return userRepository.count();
}
}

View File

@@ -24,7 +24,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.DatabaseBackupInterface;
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
import stirling.software.SPDF.utils.FileInfo;
@Slf4j

View File

@@ -51,8 +51,7 @@ public class CustomOAuth2AuthenticationFailureHandler
}
log.error("OAuth2 Authentication error: " + errorCode);
log.error("OAuth2AuthenticationException", exception);
getRedirectStrategy()
.sendRedirect(request, response, "/logout?erroroauth=" + errorCode);
getRedirectStrategy().sendRedirect(request, response, "/login?erroroauth=" + errorCode);
return;
}
log.error("Unhandled authentication exception", exception);

View File

@@ -75,6 +75,11 @@ public class CustomOAuth2AuthenticationSuccessHandler
throw new LockedException(
"Your account has been locked due to too many failed login attempts.");
}
if (userService.isUserDisabled(username)) {
getRedirectStrategy()
.sendRedirect(request, response, "/logout?userIsDisabled=true");
return;
}
if (userService.usernameExistsIgnoreCase(username)
&& userService.hasPassword(username)
&& !userService.isAuthenticationTypeByUsername(

View File

@@ -1,122 +0,0 @@
package stirling.software.SPDF.config.security.oauth2;
import java.io.IOException;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.Provider;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.UrlUtils;
@Slf4j
public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private final ApplicationProperties applicationProperties;
public CustomOAuth2LogoutSuccessHandler(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@Override
public void onLogoutSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
String param = "logout=true";
String registrationId = null;
String issuer = null;
String clientId = null;
if (authentication == null) {
if (request.getParameter("userIsDisabled") != null) {
response.sendRedirect(
request.getContextPath() + "/login?erroroauth=userIsDisabled");
} else {
super.onLogoutSuccess(request, response, authentication);
}
return;
}
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (authentication instanceof OAuth2AuthenticationToken) {
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
registrationId = oauthToken.getAuthorizedClientRegistrationId();
try {
Provider provider = oauth.getClient().get(registrationId);
issuer = provider.getIssuer();
clientId = provider.getClientId();
} catch (UnsupportedProviderException e) {
log.error(e.getMessage());
}
} else {
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
issuer = oauth.getIssuer();
clientId = oauth.getClientId();
}
String errorMessage = "";
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
param = "erroroauth=oauth2AuthenticationErrorWeb";
} else if ((errorMessage = request.getParameter("error")) != null) {
param = "error=" + sanitizeInput(errorMessage);
} else if ((errorMessage = request.getParameter("erroroauth")) != null) {
param = "erroroauth=" + sanitizeInput(errorMessage);
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
param = "error=oauth2AutoCreateDisabled";
} else if (request.getParameter("oauth2_admin_blocked_user") != null) {
param = "erroroauth=oauth2_admin_blocked_user";
} else if (request.getParameter("userIsDisabled") != null) {
param = "erroroauth=userIsDisabled";
} else if (request.getParameter("badcredentials") != null) {
param = "error=badcredentials";
}
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
switch (registrationId.toLowerCase()) {
case "keycloak":
// Add Keycloak specific logout URL if needed
String logoutUrl =
issuer
+ "/protocol/openid-connect/logout"
+ "?client_id="
+ clientId
+ "&post_logout_redirect_uri="
+ response.encodeRedirectURL(redirect_url);
log.info("Redirecting to Keycloak logout URL: " + logoutUrl);
response.sendRedirect(logoutUrl);
break;
case "github":
// Add GitHub specific logout URL if needed
String githubLogoutUrl = "https://github.com/logout";
log.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
response.sendRedirect(githubLogoutUrl);
break;
case "google":
// Add Google specific logout URL if needed
// String googleLogoutUrl =
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
// + response.encodeRedirectURL(redirect_url);
log.info("Google does not have a specific logout URL");
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
// response.sendRedirect(googleLogoutUrl);
// break;
default:
String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
log.info("Redirecting to default logout URL: " + defaultRedirectUrl);
response.sendRedirect(defaultRedirectUrl);
break;
}
}
private String sanitizeInput(String input) {
return input.replaceAll("[^a-zA-Z0-9 ]", "");
}
}

View File

@@ -0,0 +1,42 @@
package stirling.software.SPDF.config.security.saml2;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import org.springframework.core.io.Resource;
public class CertificateUtils {
public static X509Certificate readCertificate(Resource certificateResource) throws Exception {
try (PemReader pemReader =
new PemReader(
new InputStreamReader(
certificateResource.getInputStream(), StandardCharsets.UTF_8))) {
PemObject pemObject = pemReader.readPemObject();
byte[] decodedCert = pemObject.getContent();
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(decodedCert));
}
}
public static RSAPrivateKey readPrivateKey(Resource privateKeyResource) throws Exception {
try (PemReader pemReader =
new PemReader(
new InputStreamReader(
privateKeyResource.getInputStream(), StandardCharsets.UTF_8))) {
PemObject pemObject = pemReader.readPemObject();
byte[] decodedKey = pemObject.getContent();
return (RSAPrivateKey)
KeyFactory.getInstance("RSA")
.generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
}
}
}

View File

@@ -0,0 +1,45 @@
package stirling.software.SPDF.config.security.saml2;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
public class CustomSaml2AuthenticatedPrincipal
implements Saml2AuthenticatedPrincipal, Serializable {
private final String name;
private final Map<String, List<Object>> attributes;
private final String nameId;
private final List<String> sessionIndexes;
public CustomSaml2AuthenticatedPrincipal(
String name,
Map<String, List<Object>> attributes,
String nameId,
List<String> sessionIndexes) {
this.name = name;
this.attributes = attributes;
this.nameId = nameId;
this.sessionIndexes = sessionIndexes;
}
@Override
public String getName() {
return this.name;
}
@Override
public Map<String, List<Object>> getAttributes() {
return this.attributes;
}
public String getNameId() {
return this.nameId;
}
public List<String> getSessionIndexes() {
return this.sessionIndexes;
}
}

View File

@@ -0,0 +1,38 @@
package stirling.software.SPDF.config.security.saml2;
import java.io.IOException;
import org.springframework.security.authentication.ProviderNotFoundException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
if (exception instanceof Saml2AuthenticationException) {
Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error();
getRedirectStrategy()
.sendRedirect(request, response, "/login?erroroauth=" + error.getErrorCode());
} else if (exception instanceof ProviderNotFoundException) {
getRedirectStrategy()
.sendRedirect(
request,
response,
"/login?erroroauth=not_authentication_provider_found");
}
log.error("AuthenticationException: " + exception);
}
}

View File

@@ -0,0 +1,91 @@
package stirling.software.SPDF.config.security.saml2;
import java.io.IOException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.SavedRequest;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.AllArgsConstructor;
import stirling.software.SPDF.config.security.LoginAttemptService;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.utils.RequestUriUtils;
@AllArgsConstructor
public class CustomSaml2AuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler {
private LoginAttemptService loginAttemptService;
private ApplicationProperties applicationProperties;
private UserService userService;
@Override
public void onAuthenticationSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
Object principal = authentication.getPrincipal();
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
String username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
// Get the saved request
HttpSession session = request.getSession(false);
String contextPath = request.getContextPath();
SavedRequest savedRequest =
(session != null)
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
: null;
if (savedRequest != null
&& !RequestUriUtils.isStaticResource(
contextPath, savedRequest.getRedirectUrl())) {
// Redirect to the original destination
super.onAuthenticationSuccess(request, response, authentication);
} else {
SAML2 saml2 = applicationProperties.getSecurity().getSaml2();
if (loginAttemptService.isBlocked(username)) {
if (session != null) {
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
}
throw new LockedException(
"Your account has been locked due to too many failed login attempts.");
}
if (userService.usernameExistsIgnoreCase(username)
&& userService.hasPassword(username)
&& !userService.isAuthenticationTypeByUsername(
username, AuthenticationType.OAUTH2)
&& saml2.getAutoCreateUser()) {
response.sendRedirect(
contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
return;
}
try {
if (saml2.getBlockRegistration()
&& !userService.usernameExistsIgnoreCase(username)) {
response.sendRedirect(
contextPath + "/login?erroroauth=oauth2_admin_blocked_user");
return;
}
userService.processOAuth2PostLogin(username, saml2.getAutoCreateUser());
response.sendRedirect(contextPath + "/");
return;
} catch (IllegalArgumentException e) {
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
return;
}
}
} else {
super.onAuthenticationSuccess(request, response, authentication);
}
}
}

View File

@@ -0,0 +1,86 @@
package stirling.software.SPDF.config.security.saml2;
import java.util.*;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.schema.XSBoolean;
import org.opensaml.core.xml.schema.XSString;
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AttributeStatement;
import org.opensaml.saml.saml2.core.AuthnStatement;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.User;
@Component
@Slf4j
public class CustomSaml2ResponseAuthenticationConverter
implements Converter<ResponseToken, Saml2Authentication> {
private UserService userService;
public CustomSaml2ResponseAuthenticationConverter(UserService userService) {
this.userService = userService;
}
@Override
public Saml2Authentication convert(ResponseToken responseToken) {
// Extract the assertion from the response
Assertion assertion = responseToken.getResponse().getAssertions().get(0);
// Extract the NameID
String nameId = assertion.getSubject().getNameID().getValue();
Optional<User> userOpt = userService.findByUsernameIgnoreCase(nameId);
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
if (userOpt.isPresent()) {
User user = userOpt.get();
if (user != null) {
simpleGrantedAuthority =
new SimpleGrantedAuthority(userService.findRole(user).getAuthority());
}
}
// Extract the SessionIndexes
List<String> sessionIndexes = new ArrayList<>();
for (AuthnStatement authnStatement : assertion.getAuthnStatements()) {
sessionIndexes.add(authnStatement.getSessionIndex());
}
// Extract the Attributes
Map<String, List<Object>> attributes = extractAttributes(assertion);
// Create the custom principal
CustomSaml2AuthenticatedPrincipal principal =
new CustomSaml2AuthenticatedPrincipal(nameId, attributes, nameId, sessionIndexes);
// Create the Saml2Authentication
return new Saml2Authentication(
principal,
responseToken.getToken().getSaml2Response(),
Collections.singletonList(simpleGrantedAuthority));
}
private Map<String, List<Object>> extractAttributes(Assertion assertion) {
Map<String, List<Object>> attributes = new HashMap<>();
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
for (Attribute attribute : attributeStatement.getAttributes()) {
String attributeName = attribute.getName();
List<Object> values = new ArrayList<>();
for (XMLObject xmlObject : attribute.getAttributeValues()) {
log.info("BOOL: " + ((XSBoolean) xmlObject).getValue());
values.add(((XSString) xmlObject).getValue());
}
attributes.put(attributeName, values);
}
}
return attributes;
}
}

View File

@@ -11,16 +11,19 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CustomHttpSessionListener implements HttpSessionListener {
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
private SessionPersistentRegistry sessionPersistentRegistry;
@Autowired
public CustomHttpSessionListener(SessionPersistentRegistry sessionPersistentRegistry) {
super();
this.sessionPersistentRegistry = sessionPersistentRegistry;
}
@Override
public void sessionCreated(HttpSessionEvent se) {
log.info("Session created: " + se.getSession().getId());
}
public void sessionCreated(HttpSessionEvent se) {}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
log.info("Session destroyed: " + se.getSession().getId());
sessionPersistentRegistry.expireSession(se.getSession().getId());
}
}

View File

@@ -16,6 +16,7 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Component;
import jakarta.transaction.Transactional;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.model.SessionEntity;
@Component
@@ -50,6 +51,8 @@ public class SessionPersistentRegistry implements SessionRegistry {
principalName = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) {
principalName = ((OAuth2User) principal).getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
principalName = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
} else if (principal instanceof String) {
principalName = (String) principal;
}
@@ -79,11 +82,21 @@ public class SessionPersistentRegistry implements SessionRegistry {
principalName = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) {
principalName = ((OAuth2User) principal).getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
principalName = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
} else if (principal instanceof String) {
principalName = (String) principal;
}
if (principalName != null) {
// Clear old sessions for the principal (unsure if needed)
// List<SessionEntity> existingSessions =
// sessionRepository.findByPrincipalName(principalName);
// for (SessionEntity session : existingSessions) {
// session.setExpired(true);
// sessionRepository.save(session);
// }
SessionEntity sessionEntity = new SessionEntity();
sessionEntity.setSessionId(sessionId);
sessionEntity.setPrincipalName(principalName);

View File

@@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.CropPdfForm;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.service.PostHogService;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@@ -36,9 +37,13 @@ public class CropController {
private final CustomPDDocumentFactory pdfDocumentFactory;
private final PostHogService postHogService;
@Autowired
public CropController(CustomPDDocumentFactory pdfDocumentFactory) {
public CropController(
CustomPDDocumentFactory pdfDocumentFactory, PostHogService postHogService) {
this.pdfDocumentFactory = pdfDocumentFactory;
this.postHogService = postHogService;
}
@PostMapping(value = "/crop", consumes = "multipart/form-data")

View File

@@ -9,6 +9,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
@@ -21,6 +22,7 @@ import stirling.software.SPDF.utils.WebResponseUtils;
*/
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
public class PdfImageRemovalController {
// Service for removing images from PDFs

View File

@@ -109,7 +109,7 @@ public class ScalePagesController {
}
private PDRectangle getTargetSize(String targetPDRectangle, PDDocument sourceDocument) {
if (targetPDRectangle.equals("KEEP")) {
if ("KEEP".equals(targetPDRectangle)) {
if (sourceDocument.getNumberOfPages() == 0) {
return null;
}

View File

@@ -0,0 +1,38 @@
package stirling.software.SPDF.controller.api;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.utils.GeneralUtils;
@Controller
@Tag(name = "Settings", description = "Settings APIs")
@RequestMapping("/api/v1/settings")
@Hidden
public class SettingsController {
@Autowired ApplicationProperties applicationProperties;
@PostMapping("/update-enable-analytics")
@Hidden
public ResponseEntity<String> updateApiKey(@RequestBody Boolean enabled) throws IOException {
if (!"undefined".equals(applicationProperties.getSystem().getEnableAnalytics())) {
return ResponseEntity.status(HttpStatus.ALREADY_REPORTED)
.body(
"Setting has already been set, To adjust please edit /config/settings.yml");
}
GeneralUtils.saveKeyToConfig("system.enableAnalytics", String.valueOf(enabled), false);
applicationProperties.getSystem().setEnableAnalytics(String.valueOf(enabled));
return ResponseEntity.ok("Updated");
}
}

View File

@@ -60,8 +60,6 @@ public class SplitPDFController {
// PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document);
int totalPages = document.getNumberOfPages();
List<Integer> pageNumbers = request.getPageNumbersList(document, false);
System.out.println(
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
if (!pageNumbers.contains(totalPages - 1)) {
// Create a mutable ArrayList so we can add to it
pageNumbers = new ArrayList<>(pageNumbers);

View File

@@ -32,9 +32,9 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import stirling.software.SPDF.config.PdfMetadataService;
import stirling.software.SPDF.model.PdfMetadata;
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
import stirling.software.SPDF.service.PdfMetadataService;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@@ -67,15 +67,6 @@ public class SplitPdfByChaptersController {
}
PDDocument sourceDocument = Loader.loadPDF(file.getBytes());
// checks if the document is encrypted by an empty user password
if (sourceDocument.isEncrypted()) {
try {
sourceDocument.setAllSecurityToBeRemoved(true);
logger.info("Removing security from the source document ");
} catch (Exception e) {
logger.warn("Cannot decrypt the pdf");
}
}
PDDocumentOutline outline = sourceDocument.getDocumentCatalog().getDocumentOutline();
if (outline == null) {

View File

@@ -30,7 +30,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.Role;
@@ -40,6 +42,7 @@ import stirling.software.SPDF.model.api.user.UsernameAndPass;
@Controller
@Tag(name = "User", description = "User APIs")
@RequestMapping("/api/v1/user")
@Slf4j
public class UserController {
@Autowired private UserService userService;
@@ -191,13 +194,11 @@ public class UserController {
Map<String, String[]> paramMap = request.getParameterMap();
Map<String, String> updates = new HashMap<>();
System.out.println("Received parameter map: " + paramMap);
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
updates.put(entry.getKey(), entry.getValue()[0]);
}
System.out.println("Processed updates: " + updates);
log.debug("Processed updates: " + updates);
// Assuming you have a method in userService to update the settings for a user
userService.updateUserSettings(principal.getName(), updates);
@@ -209,7 +210,7 @@ public class UserController {
@PostMapping("/admin/saveUser")
public RedirectView saveUser(
@RequestParam(name = "username", required = true) String username,
@RequestParam(name = "password", required = true) String password,
@RequestParam(name = "password", required = false) String password,
@RequestParam(name = "role") String role,
@RequestParam(name = "authType") String authType,
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
@@ -336,6 +337,8 @@ public class UserController {
userNameP = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) {
userNameP = ((OAuth2User) principal).getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
userNameP = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
} else if (principal instanceof String) {
userNameP = (String) principal;
}

View File

@@ -1,5 +1,6 @@
package stirling.software.SPDF.controller.api.converters;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
@@ -14,7 +15,6 @@ import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils;
// Disabled for now
// @RestController
// @Tag(name = "Convert", description = "Convert APIs")
// @RequestMapping("/api/v1/convert")
@@ -24,7 +24,7 @@ public class ConvertBookToPDFController {
private final CustomPDDocumentFactory pdfDocumentFactory;
// @Autowired
@Autowired
public ConvertBookToPDFController(
CustomPDDocumentFactory pdfDocumentFactory,
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled) {
@@ -66,6 +66,8 @@ public class ConvertBookToPDFController {
}
byte[] pdfBytes = FileToPdf.convertBookTypeToPdf(fileInput.getBytes(), originalFilename);
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
String outputFilename =
originalFilename.replaceFirst("[.][^.]+$", "")
+ ".pdf"; // Remove file extension and append .pdf

View File

@@ -1,27 +1,39 @@
package stirling.software.SPDF.controller.api.converters;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.converters.HTMLToPdfRequest;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils;
// Disabled for now
// @RestController
// @Tag(name = "Convert", description = "Convert APIs")
// @RequestMapping("/api/v1/convert")
@RestController
@Tag(name = "Convert", description = "Convert APIs")
@RequestMapping("/api/v1/convert")
public class ConvertHtmlToPDF {
// @Autowired
@Qualifier("bookAndHtmlFormatsInstalled")
private boolean bookAndHtmlFormatsInstalled;
private final boolean bookAndHtmlFormatsInstalled;
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public ConvertHtmlToPDF(
CustomPDDocumentFactory pdfDocumentFactory,
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled) {
this.pdfDocumentFactory = pdfDocumentFactory;
this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled;
}
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
@Operation(
@@ -49,6 +61,8 @@ public class ConvertHtmlToPDF {
originalFilename,
bookAndHtmlFormatsInstalled);
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
String outputFilename =
originalFilename.replaceFirst("[.][^.]+$", "")
+ ".pdf"; // Remove file extension and append .pdf

View File

@@ -82,7 +82,7 @@ public class ConvertImgPDFController {
result =
PdfUtils.convertFromPdf(
pdfBytes,
imageFormat.equalsIgnoreCase("webp") ? "png" : imageFormat.toUpperCase(),
"webp".equalsIgnoreCase(imageFormat) ? "png" : imageFormat.toUpperCase(),
colorTypeResult,
singleImage,
Integer.valueOf(dpi),
@@ -90,9 +90,9 @@ public class ConvertImgPDFController {
if (result == null || result.length == 0) {
logger.error("resultant bytes for {} is null, error converting ", filename);
}
if (imageFormat.equalsIgnoreCase("webp") && !CheckProgramInstall.isPythonAvailable()) {
if ("webp".equalsIgnoreCase(imageFormat) && !CheckProgramInstall.isPythonAvailable()) {
throw new IOException("Python is not installed. Required for WebP conversion.");
} else if (imageFormat.equalsIgnoreCase("webp")
} else if ("webp".equalsIgnoreCase(imageFormat)
&& CheckProgramInstall.isPythonAvailable()) {
// Write the output stream to a temp file
Path tempFile = Files.createTempFile("temp_png", ".png");

View File

@@ -10,28 +10,40 @@ import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.AttributeProvider;
import org.commonmark.renderer.html.HtmlRenderer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.GeneralFile;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils;
// Disabled for now
// @RestController
// @Tag(name = "Convert", description = "Convert APIs")
// @RequestMapping("/api/v1/convert")
@RestController
@Tag(name = "Convert", description = "Convert APIs")
@RequestMapping("/api/v1/convert")
public class ConvertMarkdownToPdf {
// @Autowired
@Qualifier("bookAndHtmlFormatsInstalled")
private boolean bookAndHtmlFormatsInstalled;
private final boolean bookAndHtmlFormatsInstalled;
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public ConvertMarkdownToPdf(
CustomPDDocumentFactory pdfDocumentFactory,
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled) {
this.pdfDocumentFactory = pdfDocumentFactory;
this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled;
}
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
@Operation(
@@ -70,7 +82,7 @@ public class ConvertMarkdownToPdf {
htmlContent.getBytes(),
"converted.html",
bookAndHtmlFormatsInstalled);
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
String outputFilename =
originalFilename.replaceFirst("[.][^.]+$", "")
+ ".pdf"; // Remove file extension and append .pdf

View File

@@ -6,6 +6,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
@@ -20,13 +21,12 @@ import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
// Disabled for now
// @RestController
// @Tag(name = "Convert", description = "Convert APIs")
// @RequestMapping("/api/v1/convert")
public class ConvertPDFToBookController {
// @Autowired
@Autowired
@Qualifier("bookAndHtmlFormatsInstalled")
private boolean bookAndHtmlFormatsInstalled;

View File

@@ -1,22 +1,15 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@@ -29,7 +22,6 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.converters.PdfToPdfARequest;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@@ -41,13 +33,6 @@ public class ConvertPDFToPDFA {
private static final Logger logger = LoggerFactory.getLogger(ConvertPDFToPDFA.class);
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public ConvertPDFToPDFA(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa")
@Operation(
summary = "Convert a PDF to a PDF/A",
@@ -61,32 +46,7 @@ public class ConvertPDFToPDFA {
// Convert MultipartFile to byte[]
byte[] pdfBytes = inputFile.getBytes();
// Load the PDF document
PDDocument document = pdfDocumentFactory.load(pdfBytes);
// Get the document catalog
PDDocumentCatalog catalog = document.getDocumentCatalog();
// Get the AcroForm
PDAcroForm acroForm = catalog.getAcroForm();
if (acroForm != null) {
// Remove signature fields safely
List<PDField> fieldsToRemove =
acroForm.getFields().stream()
.filter(field -> field instanceof PDSignatureField)
.collect(Collectors.toList());
if (!fieldsToRemove.isEmpty()) {
acroForm.flatten(fieldsToRemove, false);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
pdfBytes = baos.toByteArray();
}
}
document.close();
// Save the uploaded (and possibly modified) file to a temporary location
// Save the uploaded file to a temporary location
Path tempInputFile = Files.createTempFile("input_", ".pdf");
try (OutputStream outputStream = new FileOutputStream(tempInputFile.toFile())) {
outputStream.write(pdfBytes);
@@ -95,28 +55,37 @@ public class ConvertPDFToPDFA {
// Prepare the output file path
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
// Prepare the OCRmyPDF command
// Prepare the ghostscript command
List<String> command = new ArrayList<>();
command.add("ocrmypdf");
command.add("--skip-text");
command.add("--tesseract-timeout=0");
command.add("--output-type");
command.add(outputFormat.toString());
command.add(tempInputFile.toString());
command.add("gs");
command.add("-dPDFA=" + ("pdfa".equals(outputFormat) ? "2" : "1"));
command.add("-dNOPAUSE");
command.add("-dBATCH");
command.add("-sColorConversionStrategy=sRGB");
command.add("-sDEVICE=pdfwrite");
command.add("-dPDFACompatibilityPolicy=2");
command.add("-o");
command.add(tempOutputFile.toString());
command.add(tempInputFile.toString());
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
.runCommandWithOutputHandling(command);
if (returnCode.getRc() != 0) {
logger.info(
outputFormat + " conversion failed with return code: " + returnCode.getRc());
}
try {
PDDocument doc = pdfDocumentFactory.load(tempOutputFile.toFile());
byte[] pdfBytesOutput = Files.readAllBytes(tempOutputFile);
// Return the optimized PDF as a response
String outputFilename =
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "")
+ "_PDFA.pdf";
return WebResponseUtils.pdfDocToWebResponse(doc, outputFilename);
return WebResponseUtils.bytesToWebResponse(
pdfBytesOutput, outputFilename, MediaType.APPLICATION_PDF);
} finally {
// Clean up the temporary files
Files.deleteIfExists(tempInputFile);

View File

@@ -60,8 +60,6 @@ public class ExtractImagesController {
MultipartFile file = request.getFileInput();
String format = request.getFormat();
boolean allowDuplicates = request.isAllowDuplicates();
System.out.println(
System.currentTimeMillis() + " file=" + file.getName() + ", format=" + format);
PDDocument document = Loader.loadPDF(file.getBytes());
// Determine if multithreading should be used based on PDF size or number of pages
@@ -90,22 +88,35 @@ public class ExtractImagesController {
// Iterate over each page
for (int pgNum = 0; pgNum < document.getPages().getCount(); pgNum++) {
PDPage page = document.getPage(pgNum);
int pageNum = document.getPages().indexOf(page) + 1;
// Submit a task for processing each page
Future<Void> future =
executor.submit(
() -> {
extractImagesFromPage(
page,
format,
filename,
pageNum,
processedImages,
zos,
allowDuplicates);
return null;
// Use the page number directly from the iterator, so no need to
// calculate manually
int pageNum = document.getPages().indexOf(page) + 1;
try {
// Call the image extraction method for each page
extractImagesFromPage(
page,
format,
filename,
pageNum,
processedImages,
zos,
allowDuplicates);
} catch (IOException e) {
// Log the error and continue processing other pages
logger.error(
"Error extracting images from page {}: {}",
pageNum,
e.getMessage());
}
return null; // Callable requires a return type
});
// Add the Future object to the list to track completion
futures.add(future);
}

View File

@@ -26,11 +26,13 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.PrintFileRequest;
@RestController
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
@Slf4j
public class PrintFileController {
// TODO
@@ -59,7 +61,7 @@ public class PrintFileController {
new IllegalArgumentException(
"No matching printer found"));
System.out.println("Selected Printer: " + selectedService.getName());
log.info("Selected Printer: " + selectedService.getName());
if ("application/pdf".equals(contentType)) {
PDDocument document = Loader.loadPDF(file.getBytes());

View File

@@ -0,0 +1,55 @@
package stirling.software.SPDF.controller.api.misc;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import stirling.software.SPDF.model.api.misc.ReplaceAndInvertColorRequest;
import stirling.software.SPDF.service.misc.ReplaceAndInvertColorService;
@RestController
@RequestMapping("/api/v1/misc")
public class ReplaceAndInvertColorController {
private ReplaceAndInvertColorService replaceAndInvertColorService;
@Autowired
public ReplaceAndInvertColorController(
ReplaceAndInvertColorService replaceAndInvertColorService) {
this.replaceAndInvertColorService = replaceAndInvertColorService;
}
@PostMapping(consumes = "multipart/form-data", value = "/replace-invert-pdf")
@Operation(
summary = "Replace-Invert Color PDF",
description =
"This endpoint accepts a PDF file and option of invert all colors or replace text and background colors. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<InputStreamResource> replaceAndInvertColor(
@ModelAttribute ReplaceAndInvertColorRequest replaceAndInvertColorRequest)
throws IOException {
InputStreamResource resource =
replaceAndInvertColorService.replaceAndInvertColor(
replaceAndInvertColorRequest.getFileInput(),
replaceAndInvertColorRequest.getReplaceAndInvertOption(),
replaceAndInvertColorRequest.getHighContrastColorCombination(),
replaceAndInvertColorRequest.getBackGroundColor(),
replaceAndInvertColorRequest.getTextColor());
// Return the modified PDF as a downloadable file
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=inverted.pdf")
.contentType(MediaType.APPLICATION_PDF)
.body(resource);
}
}

View File

@@ -4,4 +4,6 @@ public interface UserServiceInterface {
String getApiKeyForUser(String username);
String getCurrentUsername();
long getTotalUsersCount();
}

View File

@@ -1,10 +1,14 @@
package stirling.software.SPDF.controller.api.security;
import java.awt.Color;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.file.Files;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
@@ -14,12 +18,39 @@ import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Calendar;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.apache.pdfbox.examples.signature.CreateSignatureBase;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts.FontName;
import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
import org.apache.pdfbox.util.Matrix;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x500.style.IETFUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
@@ -35,6 +66,7 @@ import org.bouncycastle.pkcs.PKCSException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@@ -62,6 +94,8 @@ public class CertSignController {
}
class CreateSignature extends CreateSignatureBase {
File logoFile;
public CreateSignature(KeyStore keystore, char[] pin)
throws KeyStoreException,
UnrecoverableKeyException,
@@ -69,6 +103,101 @@ public class CertSignController {
IOException,
CertificateException {
super(keystore, pin);
ClassPathResource resource = new ClassPathResource("static/images/signature.png");
try (InputStream is = resource.getInputStream()) {
logoFile = Files.createTempFile("signature", ".png").toFile();
FileUtils.copyInputStreamToFile(is, logoFile);
} catch (IOException e) {
logger.error("Failed to load image signature file");
throw e;
}
}
public InputStream createVisibleSignature(
PDDocument srcDoc, PDSignature signature, Integer pageNumber, Boolean showLogo)
throws IOException {
// modified from org.apache.pdfbox.examples.signature.CreateVisibleSignature2
try (PDDocument doc = new PDDocument()) {
PDPage page = new PDPage(srcDoc.getPage(pageNumber).getMediaBox());
doc.addPage(page);
PDAcroForm acroForm = new PDAcroForm(doc);
doc.getDocumentCatalog().setAcroForm(acroForm);
PDSignatureField signatureField = new PDSignatureField(acroForm);
PDAnnotationWidget widget = signatureField.getWidgets().get(0);
List<PDField> acroFormFields = acroForm.getFields();
acroForm.setSignaturesExist(true);
acroForm.setAppendOnly(true);
acroForm.getCOSObject().setDirect(true);
acroFormFields.add(signatureField);
PDRectangle rect = new PDRectangle(0, 0, 200, 50);
widget.setRectangle(rect);
// from PDVisualSigBuilder.createHolderForm()
PDStream stream = new PDStream(doc);
PDFormXObject form = new PDFormXObject(stream);
PDResources res = new PDResources();
form.setResources(res);
form.setFormType(1);
PDRectangle bbox = new PDRectangle(rect.getWidth(), rect.getHeight());
float height = bbox.getHeight();
form.setBBox(bbox);
PDFont font = new PDType1Font(FontName.TIMES_BOLD);
// from PDVisualSigBuilder.createAppearanceDictionary()
PDAppearanceDictionary appearance = new PDAppearanceDictionary();
appearance.getCOSObject().setDirect(true);
PDAppearanceStream appearanceStream = new PDAppearanceStream(form.getCOSObject());
appearance.setNormalAppearance(appearanceStream);
widget.setAppearance(appearance);
try (PDPageContentStream cs = new PDPageContentStream(doc, appearanceStream)) {
if (showLogo) {
cs.saveGraphicsState();
PDExtendedGraphicsState extState = new PDExtendedGraphicsState();
extState.setBlendMode(BlendMode.MULTIPLY);
extState.setNonStrokingAlphaConstant(0.5f);
cs.setGraphicsStateParameters(extState);
cs.transform(Matrix.getScaleInstance(0.08f, 0.08f));
PDImageXObject img =
PDImageXObject.createFromFileByExtension(logoFile, doc);
cs.drawImage(img, 100, 0);
cs.restoreGraphicsState();
}
// show text
float fontSize = 10;
float leading = fontSize * 1.5f;
cs.beginText();
cs.setFont(font, fontSize);
cs.setNonStrokingColor(Color.black);
cs.newLineAtOffset(fontSize, height - leading);
cs.setLeading(leading);
X509Certificate cert = (X509Certificate) getCertificateChain()[0];
// https://stackoverflow.com/questions/2914521/
X500Name x500Name = new X500Name(cert.getSubjectX500Principal().getName());
RDN cn = x500Name.getRDNs(BCStyle.CN)[0];
String name = IETFUtils.valueToString(cn.getFirst().getValue());
String date = signature.getSignDate().getTime().toString();
String reason = signature.getReason();
cs.showText("Signed by " + name);
cs.newLine();
cs.showText(date);
cs.newLine();
cs.showText(reason);
cs.endText();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
doc.save(baos);
return new ByteArrayInputStream(baos.toByteArray());
}
}
}
@@ -97,7 +226,8 @@ public class CertSignController {
String reason = request.getReason();
String location = request.getLocation();
String name = request.getName();
Integer pageNumber = request.getPageNumber();
Integer pageNumber = request.getPageNumber() - 1;
Boolean showLogo = request.isShowLogo();
if (certType == null) {
throw new IllegalArgumentException("Cert type must be provided");
@@ -126,11 +256,19 @@ public class CertSignController {
throw new IllegalArgumentException("Invalid cert type: " + certType);
}
// TODO: page number
CreateSignature createSignature = new CreateSignature(ks, password.toCharArray());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
sign(pdfDocumentFactory, pdf.getBytes(), baos, createSignature, name, location, reason);
sign(
pdfDocumentFactory,
pdf.getBytes(),
baos,
createSignature,
showSignature,
pageNumber,
name,
location,
reason,
showLogo);
return WebResponseUtils.boasToWebResponse(
baos,
Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
@@ -142,9 +280,12 @@ public class CertSignController {
byte[] input,
OutputStream output,
CreateSignature instance,
Boolean showSignature,
Integer pageNumber,
String name,
String location,
String reason) {
String reason,
Boolean showLogo) {
try (PDDocument doc = pdfDocumentFactory.load(input)) {
PDSignature signature = new PDSignature();
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
@@ -154,7 +295,17 @@ public class CertSignController {
signature.setReason(reason);
signature.setSignDate(Calendar.getInstance());
doc.addSignature(signature, instance);
if (showSignature) {
SignatureOptions signatureOptions = new SignatureOptions();
signatureOptions.setVisualSignature(
instance.createVisibleSignature(doc, signature, pageNumber, showLogo));
signatureOptions.setPage(pageNumber);
doc.addSignature(signature, instance, signatureOptions);
} else {
doc.addSignature(signature, instance);
}
doc.saveIncremental(output);
} catch (Exception e) {
logger.error("exception", e);

View File

@@ -58,7 +58,6 @@ public class RedactController {
float customPadding = request.getCustomPadding();
boolean convertPDFToImage = request.isConvertPDFToImage();
System.out.println(listOfTextString);
String[] listOfText = listOfTextString.split("\n");
PDDocument document = pdfDocumentFactory.load(file);
@@ -75,7 +74,6 @@ public class RedactController {
for (String text : listOfText) {
text = text.trim();
System.out.println(text);
TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool);
List<PDFText> foundTexts = textFinder.getTextLocations(document);
redactFoundText(document, foundTexts, customPadding, redactColor);

View File

@@ -21,10 +21,13 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.*;
import stirling.software.SPDF.model.ApplicationProperties.Security;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.provider.GithubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider;
@@ -51,38 +54,54 @@ public class AccountWebController {
Map<String, String> providerList = new HashMap<>();
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
Security securityProps = applicationProperties.getSecurity();
OAUTH2 oauth = securityProps.getOauth2();
if (oauth != null) {
if (oauth.isSettingsValid()) {
providerList.put("oidc", oauth.getProvider());
}
Client client = oauth.getClient();
if (client != null) {
GoogleProvider google = client.getGoogle();
if (google.isSettingsValid()) {
providerList.put(google.getName(), google.getClientName());
if (oauth.getEnabled()) {
if (oauth.isSettingsValid()) {
providerList.put("/oauth2/authorization/oidc", oauth.getProvider());
}
Client client = oauth.getClient();
if (client != null) {
GoogleProvider google = client.getGoogle();
if (google.isSettingsValid()) {
providerList.put(
"/oauth2/authorization/" + google.getName(),
google.getClientName());
}
GithubProvider github = client.getGithub();
if (github.isSettingsValid()) {
providerList.put(github.getName(), github.getClientName());
}
GithubProvider github = client.getGithub();
if (github.isSettingsValid()) {
providerList.put(
"/oauth2/authorization/" + github.getName(),
github.getClientName());
}
KeycloakProvider keycloak = client.getKeycloak();
if (keycloak.isSettingsValid()) {
providerList.put(keycloak.getName(), keycloak.getClientName());
KeycloakProvider keycloak = client.getKeycloak();
if (keycloak.isSettingsValid()) {
providerList.put(
"/oauth2/authorization/" + keycloak.getName(),
keycloak.getClientName());
}
}
}
}
SAML2 saml2 = securityProps.getSaml2();
if (securityProps.isSaml2Activ()
&& applicationProperties.getSystem().getEnableAlphaFunctionality()) {
providerList.put("/saml2/authenticate/" + saml2.getRegistrationId(), "SAML 2");
}
// Remove any null keys/values from the providerList
providerList
.entrySet()
.removeIf(entry -> entry.getKey() == null || entry.getValue() == null);
model.addAttribute("providerlist", providerList);
model.addAttribute("loginMethod", applicationProperties.getSecurity().getLoginMethod());
model.addAttribute(
"oAuth2Enabled", applicationProperties.getSecurity().getOauth2().getEnabled());
model.addAttribute("loginMethod", securityProps.getLoginMethod());
boolean altLogin = providerList.size() > 0 ? securityProps.isAltLogin() : false;
model.addAttribute("altLogin", altLogin);
model.addAttribute("currentPage", "login");
@@ -145,6 +164,17 @@ public class AccountWebController {
case "userIsDisabled":
erroroauth = "login.userIsDisabled";
break;
case "invalid_destination":
erroroauth = "login.invalid_destination";
break;
// Valid InResponseTo was not available from the validation context, unable to
// evaluate
case "invalid_in_response_to":
erroroauth = "login.invalid_in_response_to";
break;
case "not_authentication_provider_found":
erroroauth = "login.not_authentication_provider_found";
break;
default:
break;
}
@@ -349,6 +379,17 @@ public class AccountWebController {
// Add oAuth2 Login attributes to the model
model.addAttribute("oAuth2Login", true);
}
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
// Cast the principal object to OAuth2User
CustomSaml2AuthenticatedPrincipal userDetails =
(CustomSaml2AuthenticatedPrincipal) principal;
// Retrieve username and other attributes
username = userDetails.getName();
// Add oAuth2 Login attributes to the model
model.addAttribute("oAuth2Login", true);
}
if (username != null) {
// Fetch user details from the database
Optional<User> user =

View File

@@ -15,7 +15,7 @@ import stirling.software.SPDF.utils.CheckProgramInstall;
@Tag(name = "Convert", description = "Convert APIs")
public class ConverterWebController {
@ConditionalOnExpression("#{bookAndHtmlFormatsInstalled}")
@ConditionalOnExpression("${bookAndHtmlFormatsInstalled}")
@GetMapping("/book-to-pdf")
@Hidden
public String convertBookToPdfForm(Model model) {
@@ -60,7 +60,7 @@ public class ConverterWebController {
// PDF TO......
@ConditionalOnExpression("#{bookAndHtmlFormatsInstalled}")
@ConditionalOnExpression("${bookAndHtmlFormatsInstalled}")
@GetMapping("/pdf-to-book")
@Hidden
public String convertPdfToBookForm(Model model) {

View File

@@ -31,6 +31,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.SignatureFile;
import stirling.software.SPDF.service.SignatureService;
@Controller
@Tag(name = "General", description = "General APIs")
public class GeneralWebController {
@@ -108,6 +112,13 @@ public class GeneralWebController {
return "split-pdf-by-sections";
}
@GetMapping("/split-pdf-by-chapters")
@Hidden
public String splitPdfByChapters(Model model) {
model.addAttribute("currentPage", "split-pdf-by-chapters");
return "split-pdf-by-chapters";
}
@GetMapping("/view-pdf")
@Hidden
public String ViewPdfForm2(Model model) {
@@ -164,11 +175,28 @@ public class GeneralWebController {
return "split-pdfs";
}
private static final String SIGNATURE_BASE_PATH = "customFiles/static/signatures/";
private static final String ALL_USERS_FOLDER = "ALL_USERS";
@Autowired private SignatureService signatureService;
@Autowired(required = false)
private UserServiceInterface userService;
@GetMapping("/sign")
@Hidden
public String signForm(Model model) {
String username = "";
if (userService != null) {
username = userService.getCurrentUsername();
}
// Get signatures from both personal and ALL_USERS folders
List<SignatureFile> signatures = signatureService.getAvailableSignatures(username);
model.addAttribute("currentPage", "sign");
model.addAttribute("fonts", getFontNames());
model.addAttribute("signatures", signatures);
return "sign";
}

View File

@@ -31,6 +31,13 @@ public class OtherWebController {
return "misc/compress-pdf";
}
@GetMapping("/replace-and-invert-color-pdf")
@Hidden
public String replaceAndInvertColorPdfForm(Model model) {
model.addAttribute("currentPage", "replace-invert-color-pdf");
return "misc/replace-color";
}
@GetMapping("/extract-image-scans")
@Hidden
public ModelAndView extractImageScansForm() {

View File

@@ -0,0 +1,44 @@
package stirling.software.SPDF.controller.web;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.service.SignatureService;
@Controller
@RequestMapping("/api/v1/general/")
public class SignatureController {
@Autowired private SignatureService signatureService;
@Autowired(required = false)
private UserServiceInterface userService;
@GetMapping("/sign/{fileName}")
public ResponseEntity<byte[]> getSignature(@PathVariable(name = "fileName") String fileName)
throws IOException {
String username = "NON_SECURITY_USER";
if (userService != null) {
username = userService.getCurrentUsername();
}
// Verify access permission
if (!signatureService.hasAccessToFile(username, fileName)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
byte[] imageBytes = signatureService.getSignatureBytes(username, fileName);
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_JPEG) // Adjust based on file type
.body(imageBytes);
}
}

View File

@@ -1,18 +1,29 @@
package stirling.software.SPDF.model;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import stirling.software.SPDF.config.YamlPropertySourceFactory;
import stirling.software.SPDF.model.provider.GithubProvider;
@@ -24,6 +35,7 @@ import stirling.software.SPDF.model.provider.UnsupportedProviderException;
@ConfigurationProperties(prefix = "")
@PropertySource(value = "file:./configs/settings.yml", factory = YamlPropertySourceFactory.class)
@Data
@Order(Ordered.HIGHEST_PRECEDENCE)
public class ApplicationProperties {
private Legal legal = new Legal();
@@ -35,7 +47,6 @@ public class ApplicationProperties {
private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated();
private EnterpriseEdition enterpriseEdition = new EnterpriseEdition();
private AutoPipeline autoPipeline = new AutoPipeline();
private static final Logger logger = LoggerFactory.getLogger(ApplicationProperties.class);
@Data
public static class AutoPipeline {
@@ -57,16 +68,112 @@ public class ApplicationProperties {
private Boolean csrfDisabled;
private InitialLogin initialLogin = new InitialLogin();
private OAUTH2 oauth2 = new OAUTH2();
private SAML2 saml2 = new SAML2();
private int loginAttemptCount;
private long loginResetTimeMinutes;
private String loginMethod = "all";
public Boolean isAltLogin() {
return saml2.getEnabled() || oauth2.getEnabled();
}
public enum LoginMethods {
ALL("all"),
NORMAL("normal"),
OAUTH2("oauth2"),
SAML2("saml2");
private String method;
LoginMethods(String method) {
this.method = method;
}
@Override
public String toString() {
return method;
}
}
public boolean isUserPass() {
return (loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())
|| loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString()));
}
public boolean isOauth2Activ() {
return (oauth2 != null
&& oauth2.getEnabled()
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
}
public boolean isSaml2Activ() {
return (saml2 != null
&& saml2.getEnabled()
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
}
@Data
public static class InitialLogin {
private String username;
@ToString.Exclude private String password;
}
@Getter
@Setter
public static class SAML2 {
private Boolean enabled = false;
private Boolean autoCreateUser = false;
private Boolean blockRegistration = false;
private String registrationId = "stirling";
private String idpMetadataUri;
private String idpSingleLogoutUrl;
private String idpSingleLoginUrl;
private String idpIssuer;
private String idpCert;
private String privateKey;
private String spCert;
public InputStream getIdpMetadataUri() throws IOException {
if (idpMetadataUri.startsWith("classpath:")) {
return new ClassPathResource(idpMetadataUri.substring("classpath".length()))
.getInputStream();
}
try {
URI uri = new URI(idpMetadataUri);
URL url = uri.toURL();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
return connection.getInputStream();
} catch (URISyntaxException e) {
throw new IOException("Invalid URI format: " + idpMetadataUri, e);
}
}
public Resource getSpCert() {
if (spCert.startsWith("classpath:")) {
return new ClassPathResource(spCert.substring("classpath:".length()));
} else {
return new FileSystemResource(spCert);
}
}
public Resource getidpCert() {
if (idpCert.startsWith("classpath:")) {
return new ClassPathResource(idpCert.substring("classpath:".length()));
} else {
return new FileSystemResource(idpCert);
}
}
public Resource getPrivateKey() {
if (privateKey.startsWith("classpath:")) {
return new ClassPathResource(privateKey.substring("classpath:".length()));
} else {
return new FileSystemResource(privateKey);
}
}
}
@Data
public static class OAUTH2 {
private Boolean enabled = false;
@@ -136,6 +243,7 @@ public class ApplicationProperties {
private boolean customHTMLFiles;
private String tessdataDir;
private Boolean enableAlphaFunctionality;
private String enableAnalytics;
}
@Data
@@ -175,11 +283,14 @@ public class ApplicationProperties {
@Data
public static class AutomaticallyGenerated {
@ToString.Exclude private String key;
private String UUID;
}
@Data
public static class EnterpriseEdition {
private boolean enabled;
@ToString.Exclude private String key;
private int maxUsers;
private CustomMetadata customMetadata = new CustomMetadata();
@Data

View File

@@ -19,7 +19,6 @@ public class Provider implements ProviderInterface {
return true;
}
return false;
// throw new IllegalArgumentException(getName() + ": " + name + " is required!");
}
protected boolean isValid(Collection<String> value, String name) {
@@ -27,66 +26,55 @@ public class Provider implements ProviderInterface {
return true;
}
return false;
// throw new IllegalArgumentException(getName() + ": " + name + " is required!");
}
@Override
public Collection<String> getScopes() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getScope'");
}
@Override
public void setScopes(String scopes) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'setScope'");
}
@Override
public String getUseAsUsername() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getUseAsUsername'");
}
@Override
public void setUseAsUsername(String useAsUsername) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'setUseAsUsername'");
}
@Override
public String getIssuer() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getIssuer'");
}
@Override
public void setIssuer(String issuer) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'setIssuer'");
}
@Override
public String getClientSecret() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getClientSecret'");
}
@Override
public void setClientSecret(String clientSecret) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'setClientSecret'");
}
@Override
public String getClientId() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getClientId'");
}
@Override
public void setClientId(String clientId) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'setClientId'");
}
}

View File

@@ -0,0 +1,11 @@
package stirling.software.SPDF.model;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class SignatureFile {
private String fileName;
private String category; // "Personal" or "Shared"
}

View File

@@ -0,0 +1,8 @@
package stirling.software.SPDF.model.api.misc;
public enum HighContrastColorCombination {
WHITE_TEXT_ON_BLACK,
BLACK_TEXT_ON_WHITE,
YELLOW_TEXT_ON_BLACK,
GREEN_TEXT_ON_BLACK,
}

View File

@@ -0,0 +1,7 @@
package stirling.software.SPDF.model.api.misc;
public enum ReplaceAndInvert {
HIGH_CONTRAST_COLOR,
CUSTOM_COLOR,
FULL_INVERSION,
}

View File

@@ -0,0 +1,40 @@
package stirling.software.SPDF.model.api.misc;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import stirling.software.SPDF.model.api.PDFFile;
@Data
@EqualsAndHashCode(callSuper = true)
public class ReplaceAndInvertColorRequest extends PDFFile {
@Schema(
description = "Replace and Invert color options of a pdf.",
allowableValues = {"HIGH_CONTRAST_COLOR", "CUSTOM_COLOR", "FULL_INVERSION"})
private ReplaceAndInvert replaceAndInvertOption;
@Schema(
description =
"If HIGH_CONTRAST_COLOR option selected, then pick the default color option for text and background.",
allowableValues = {
"WHITE_TEXT_ON_BLACK",
"BLACK_TEXT_ON_WHITE",
"YELLOW_TEXT_ON_BLACK",
"GREEN_TEXT_ON_BLACK"
})
private HighContrastColorCombination highContrastColorCombination;
@Schema(
description =
"If CUSTOM_COLOR option selected, then pick the custom color for background. "
+ "Expected color value should be 24bit decimal value of a color")
private String backGroundColor;
@Schema(
description =
"If CUSTOM_COLOR option selected, then pick the custom color for text. "
+ "Expected color value should be 24bit decimal value of a color")
private String textColor;
}

View File

@@ -50,4 +50,7 @@ public class SignPDFWithCertRequest extends PDFFile {
description =
"The page number where the signature should be visible. This is required if showSignature is set to true")
private Integer pageNumber;
@Schema(description = "Whether to visually show a signature logo along with the signature")
private boolean showLogo;
}

View File

@@ -10,8 +10,10 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.PDFText;
@Slf4j
public class TextFinder extends PDFTextStripper {
private final String searchText;
@@ -92,7 +94,7 @@ public class TextFinder extends PDFTextStripper {
public List<PDFText> getTextLocations(PDDocument document) throws Exception {
this.getText(document);
System.out.println(
log.debug(
"Found "
+ textOccurrences.size()
+ " occurrences of '"

View File

@@ -7,17 +7,20 @@ import java.io.InputStream;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.config.PdfMetadataService;
import stirling.software.SPDF.model.PdfMetadata;
import stirling.software.SPDF.model.api.PDFFile;
@Component
public class CustomPDDocumentFactory {
private static final Logger logger = LoggerFactory.getLogger(CustomPDDocumentFactory.class);
private final PdfMetadataService pdfMetadataService;
@Autowired
@@ -31,6 +34,36 @@ public class CustomPDDocumentFactory {
return document;
}
public byte[] createNewBytesBasedOnOldDocument(byte[] oldDocument) throws IOException {
PDDocument document = Loader.loadPDF(oldDocument);
return createNewBytesBasedOnOldDocument(document);
}
public byte[] createNewBytesBasedOnOldDocument(File oldDocument) throws IOException {
PDDocument document = Loader.loadPDF(oldDocument);
return createNewBytesBasedOnOldDocument(document);
}
public byte[] createNewBytesBasedOnOldDocument(PDDocument oldDocument) throws IOException {
pdfMetadataService.setMetadataToPdf(
oldDocument, pdfMetadataService.extractMetadataFromPdf(oldDocument), true);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
oldDocument.save(baos);
oldDocument.close();
return baos.toByteArray();
}
public PDDocument createNewDocumentBasedOnOldDocument(byte[] oldDocument) throws IOException {
PDDocument document = Loader.loadPDF(oldDocument);
return createNewDocumentBasedOnOldDocument(document);
}
public PDDocument createNewDocumentBasedOnOldDocument(File oldDocument) throws IOException {
PDDocument document = Loader.loadPDF(oldDocument);
return createNewDocumentBasedOnOldDocument(document);
}
public PDDocument createNewDocumentBasedOnOldDocument(PDDocument oldDocument)
throws IOException {
PDDocument document = new PDDocument();
@@ -71,6 +104,7 @@ public class CustomPDDocumentFactory {
public PDDocument load(byte[] input) throws IOException {
PDDocument document = Loader.loadPDF(input);
pdfMetadataService.setDefaultMetadata(document);
removezeropassword(document);
return document;
}
@@ -96,5 +130,17 @@ public class CustomPDDocumentFactory {
return document;
}
private PDDocument removezeropassword(PDDocument document) throws IOException {
if (document.isEncrypted()) {
try {
logger.info("Removing security from the source document");
document.setAllSecurityToBeRemoved(true);
} catch (Exception e) {
logger.warn("Cannot decrypt the pdf");
}
}
return document;
}
// Add other load methods as needed, following the same pattern
}

View File

@@ -0,0 +1,56 @@
package stirling.software.SPDF.service;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.search.Search;
@Service
public class MetricsAggregatorService {
private final MeterRegistry meterRegistry;
private final PostHogService postHogService;
private final Map<String, Double> lastSentMetrics = new ConcurrentHashMap<>();
@Autowired
public MetricsAggregatorService(MeterRegistry meterRegistry, PostHogService postHogService) {
this.meterRegistry = meterRegistry;
this.postHogService = postHogService;
}
@Scheduled(fixedRate = 900000) // Run every 15 minutes
public void aggregateAndSendMetrics() {
Map<String, Object> metrics = new HashMap<>();
Search.in(meterRegistry)
.name("http.requests")
.counters()
.forEach(
counter -> {
String key =
String.format(
"http_requests_%s_%s",
counter.getId().getTag("method"),
counter.getId().getTag("uri").replace("/", "_"));
double currentCount = counter.count();
double lastCount = lastSentMetrics.getOrDefault(key, 0.0);
double difference = currentCount - lastCount;
if (difference > 0) {
metrics.put(key, difference);
lastSentMetrics.put(key, currentCount);
}
});
// Send aggregated metrics to PostHog
if (!metrics.isEmpty()) {
postHogService.captureEvent("aggregated_metrics", metrics);
}
}
}

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.config;
package stirling.software.SPDF.service;
import java.util.Calendar;
@@ -15,16 +15,16 @@ import stirling.software.SPDF.model.PdfMetadata;
public class PdfMetadataService {
private final ApplicationProperties applicationProperties;
private final String appVersion;
private final String stirlingPDFLabel;
private final UserServiceInterface userService;
@Autowired
public PdfMetadataService(
ApplicationProperties applicationProperties,
@Qualifier("appVersion") String appVersion,
@Qualifier("StirlingPDFLabel") String stirlingPDFLabel,
@Autowired(required = false) UserServiceInterface userService) {
this.applicationProperties = applicationProperties;
this.appVersion = appVersion;
this.stirlingPDFLabel = stirlingPDFLabel;
this.userService = userService;
}
@@ -59,51 +59,40 @@ public class PdfMetadataService {
private void setNewDocumentMetadata(PDDocument pdf, PdfMetadata pdfMetadata) {
String creator = "Stirling-PDF";
String creator = stirlingPDFLabel;
// if (applicationProperties
// .getEnterpriseEdition()
// .getCustomMetadata()
// .isAutoUpdateMetadata()) {
if (applicationProperties
.getEnterpriseEdition()
.getCustomMetadata()
.isAutoUpdateMetadata()) {
// producer =
//
// applicationProperties.getEnterpriseEdition().getCustomMetadata().getProducer();
// creator =
// applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator();
// title = applicationProperties.getEnterpriseEdition().getCustomMetadata().getTitle();
creator = applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator();
pdf.getDocumentInformation().setProducer(stirlingPDFLabel);
}
// if ("{filename}".equals(title)) {
// title = "Filename"; // Replace with actual filename logic
// } else if ("{unchanged}".equals(title)) {
// title = pdfMetadata.getTitle(); // Keep the original title
// }
// }
pdf.getDocumentInformation().setCreator(creator + " " + appVersion);
pdf.getDocumentInformation().setCreator(creator);
pdf.getDocumentInformation().setCreationDate(Calendar.getInstance());
}
private void setCommonMetadata(PDDocument pdf, PdfMetadata pdfMetadata) {
String producer = "Stirling-PDF";
String title = pdfMetadata.getTitle();
pdf.getDocumentInformation().setTitle(title);
pdf.getDocumentInformation().setProducer(producer + " " + appVersion);
pdf.getDocumentInformation().setProducer(stirlingPDFLabel);
pdf.getDocumentInformation().setSubject(pdfMetadata.getSubject());
pdf.getDocumentInformation().setKeywords(pdfMetadata.getKeywords());
pdf.getDocumentInformation().setModificationDate(Calendar.getInstance());
String author = pdfMetadata.getAuthor();
// if (applicationProperties
// .getEnterpriseEdition()
// .getCustomMetadata()
// .isAutoUpdateMetadata()) {
// author = applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor();
if (applicationProperties
.getEnterpriseEdition()
.getCustomMetadata()
.isAutoUpdateMetadata()) {
author = applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor();
// if (userService != null) {
// author = author.replace("username", userService.getCurrentUsername());
// }
// }
if (userService != null) {
author = author.replace("username", userService.getCurrentUsername());
}
}
pdf.getDocumentInformation().setAuthor(author);
}
}

View File

@@ -0,0 +1,387 @@
package stirling.software.SPDF.service;
import java.io.File;
import java.lang.management.*;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import com.posthog.java.PostHog;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.ApplicationProperties;
@Service
public class PostHogService {
private final PostHog postHog;
private final String uniqueId;
private final ApplicationProperties applicationProperties;
private final UserServiceInterface userService;
@Autowired
public PostHogService(
PostHog postHog,
@Qualifier("UUID") String uuid,
ApplicationProperties applicationProperties,
@Autowired(required = false) UserServiceInterface userService) {
this.postHog = postHog;
this.uniqueId = uuid;
this.applicationProperties = applicationProperties;
this.userService = userService;
captureSystemInfo();
}
private void captureSystemInfo() {
if (!Boolean.parseBoolean(applicationProperties.getSystem().getEnableAnalytics())) {
return;
}
try {
postHog.capture(uniqueId, "system_info_captured", captureServerMetrics());
} catch (Exception e) {
// Handle exceptions
}
}
public void captureEvent(String eventName, Map<String, Object> properties) {
if (!Boolean.parseBoolean(applicationProperties.getSystem().getEnableAnalytics())) {
return;
}
postHog.capture(uniqueId, eventName, properties);
}
public Map<String, Object> captureServerMetrics() {
Map<String, Object> metrics = new HashMap<>();
try {
// System info
metrics.put("os_name", System.getProperty("os.name"));
metrics.put("os_version", System.getProperty("os.version"));
metrics.put("java_version", System.getProperty("java.version"));
metrics.put("user_name", System.getProperty("user.name"));
metrics.put("user_home", System.getProperty("user.home"));
metrics.put("user_dir", System.getProperty("user.dir"));
// CPU and Memory
metrics.put("cpu_cores", Runtime.getRuntime().availableProcessors());
metrics.put("total_memory", Runtime.getRuntime().totalMemory());
metrics.put("free_memory", Runtime.getRuntime().freeMemory());
// Network and Server Identity
InetAddress localHost = InetAddress.getLocalHost();
metrics.put("ip_address", localHost.getHostAddress());
metrics.put("hostname", localHost.getHostName());
metrics.put("mac_address", getMacAddress());
// JVM info
metrics.put("jvm_vendor", System.getProperty("java.vendor"));
metrics.put("jvm_version", System.getProperty("java.vm.version"));
// Locale and Timezone
metrics.put("system_language", System.getProperty("user.language"));
metrics.put("system_country", System.getProperty("user.country"));
metrics.put("timezone", TimeZone.getDefault().getID());
metrics.put("locale", Locale.getDefault().toString());
// Disk info
File root = new File(".");
metrics.put("total_disk_space", root.getTotalSpace());
metrics.put("free_disk_space", root.getFreeSpace());
// Process info
metrics.put("process_id", ProcessHandle.current().pid());
// JVM metrics
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
metrics.put("jvm_uptime_ms", runtimeMXBean.getUptime());
metrics.put("jvm_start_time", runtimeMXBean.getStartTime());
// Memory metrics
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
metrics.put("heap_memory_usage", memoryMXBean.getHeapMemoryUsage().getUsed());
metrics.put("non_heap_memory_usage", memoryMXBean.getNonHeapMemoryUsage().getUsed());
// CPU metrics
OperatingSystemMXBean osMXBean = ManagementFactory.getOperatingSystemMXBean();
metrics.put("system_load_average", osMXBean.getSystemLoadAverage());
// Thread metrics
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
metrics.put("thread_count", threadMXBean.getThreadCount());
metrics.put("daemon_thread_count", threadMXBean.getDaemonThreadCount());
metrics.put("peak_thread_count", threadMXBean.getPeakThreadCount());
// Garbage collection metrics
for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) {
metrics.put("gc_" + gcBean.getName() + "_count", gcBean.getCollectionCount());
metrics.put("gc_" + gcBean.getName() + "_time", gcBean.getCollectionTime());
}
// Network interfaces
metrics.put("network_interfaces", getNetworkInterfacesInfo());
// Docker detection and stats
boolean isDocker = isRunningInDocker();
metrics.put("is_docker", isDocker);
if (isDocker) {
metrics.put("docker_metrics", getDockerMetrics());
}
metrics.put("application_properties", captureApplicationProperties());
if (userService != null) {
metrics.put("total_users_created", userService.getTotalUsersCount());
}
} catch (Exception e) {
metrics.put("error", e.getMessage());
}
return metrics;
}
private boolean isRunningInDocker() {
return Files.exists(Paths.get("/.dockerenv"));
}
private Map<String, Object> getDockerMetrics() {
Map<String, Object> dockerMetrics = new HashMap<>();
// Network-related Docker info
dockerMetrics.put("docker_network_mode", System.getenv("DOCKER_NETWORK_MODE"));
// Container name (if set)
String containerName = System.getenv("CONTAINER_NAME");
if (containerName != null && !containerName.isEmpty()) {
dockerMetrics.put("container_name", containerName);
}
// Docker compose information
String composeProject = System.getenv("COMPOSE_PROJECT_NAME");
String composeService = System.getenv("COMPOSE_SERVICE_NAME");
if (composeProject != null && composeService != null) {
dockerMetrics.put("compose_project", composeProject);
dockerMetrics.put("compose_service", composeService);
}
// Kubernetes-specific info (if running in K8s)
String k8sPodName = System.getenv("KUBERNETES_POD_NAME");
if (k8sPodName != null) {
dockerMetrics.put("k8s_pod_name", k8sPodName);
dockerMetrics.put("k8s_namespace", System.getenv("KUBERNETES_NAMESPACE"));
dockerMetrics.put("k8s_node_name", System.getenv("KUBERNETES_NODE_NAME"));
}
// New environment variables
dockerMetrics.put("version_tag", System.getenv("VERSION_TAG"));
dockerMetrics.put("docker_enable_security", System.getenv("DOCKER_ENABLE_SECURITY"));
dockerMetrics.put("fat_docker", System.getenv("FAT_DOCKER"));
return dockerMetrics;
}
private void addIfNotEmpty(Map<String, Object> map, String key, Object value) {
if (value != null) {
if (value instanceof String) {
String strValue = (String) value;
if (!StringUtils.isBlank(strValue)) {
map.put(key, strValue.trim());
}
} else {
map.put(key, value);
}
}
}
public Map<String, Object> captureApplicationProperties() {
Map<String, Object> properties = new HashMap<>();
// Capture Legal properties
addIfNotEmpty(
properties,
"legal_termsAndConditions",
applicationProperties.getLegal().getTermsAndConditions());
addIfNotEmpty(
properties,
"legal_privacyPolicy",
applicationProperties.getLegal().getPrivacyPolicy());
addIfNotEmpty(
properties,
"legal_accessibilityStatement",
applicationProperties.getLegal().getAccessibilityStatement());
addIfNotEmpty(
properties,
"legal_cookiePolicy",
applicationProperties.getLegal().getCookiePolicy());
addIfNotEmpty(
properties, "legal_impressum", applicationProperties.getLegal().getImpressum());
// Capture Security properties
addIfNotEmpty(
properties,
"security_enableLogin",
applicationProperties.getSecurity().getEnableLogin());
addIfNotEmpty(
properties,
"security_csrfDisabled",
applicationProperties.getSecurity().getCsrfDisabled());
addIfNotEmpty(
properties,
"security_loginAttemptCount",
applicationProperties.getSecurity().getLoginAttemptCount());
addIfNotEmpty(
properties,
"security_loginResetTimeMinutes",
applicationProperties.getSecurity().getLoginResetTimeMinutes());
addIfNotEmpty(
properties,
"security_loginMethod",
applicationProperties.getSecurity().getLoginMethod());
// Capture OAuth2 properties (excluding sensitive information)
addIfNotEmpty(
properties,
"security_oauth2_enabled",
applicationProperties.getSecurity().getOauth2().getEnabled());
if (applicationProperties.getSecurity().getOauth2().getEnabled()) {
addIfNotEmpty(
properties,
"security_oauth2_autoCreateUser",
applicationProperties.getSecurity().getOauth2().getAutoCreateUser());
addIfNotEmpty(
properties,
"security_oauth2_blockRegistration",
applicationProperties.getSecurity().getOauth2().getBlockRegistration());
addIfNotEmpty(
properties,
"security_oauth2_useAsUsername",
applicationProperties.getSecurity().getOauth2().getUseAsUsername());
addIfNotEmpty(
properties,
"security_oauth2_provider",
applicationProperties.getSecurity().getOauth2().getProvider());
}
// Capture System properties
addIfNotEmpty(
properties,
"system_defaultLocale",
applicationProperties.getSystem().getDefaultLocale());
addIfNotEmpty(
properties,
"system_googlevisibility",
applicationProperties.getSystem().getGooglevisibility());
addIfNotEmpty(
properties, "system_showUpdate", applicationProperties.getSystem().isShowUpdate());
addIfNotEmpty(
properties,
"system_showUpdateOnlyAdmin",
applicationProperties.getSystem().getShowUpdateOnlyAdmin());
addIfNotEmpty(
properties,
"system_customHTMLFiles",
applicationProperties.getSystem().isCustomHTMLFiles());
addIfNotEmpty(
properties,
"system_tessdataDir",
applicationProperties.getSystem().getTessdataDir());
addIfNotEmpty(
properties,
"system_enableAlphaFunctionality",
applicationProperties.getSystem().getEnableAlphaFunctionality());
addIfNotEmpty(
properties,
"system_enableAnalytics",
applicationProperties.getSystem().getEnableAnalytics());
// Capture UI properties
addIfNotEmpty(properties, "ui_appName", applicationProperties.getUi().getAppName());
addIfNotEmpty(
properties,
"ui_homeDescription",
applicationProperties.getUi().getHomeDescription());
addIfNotEmpty(
properties, "ui_appNameNavbar", applicationProperties.getUi().getAppNameNavbar());
// Capture Metrics properties
addIfNotEmpty(
properties, "metrics_enabled", applicationProperties.getMetrics().getEnabled());
// Capture EnterpriseEdition properties
addIfNotEmpty(
properties,
"enterpriseEdition_enabled",
applicationProperties.getEnterpriseEdition().isEnabled());
if (applicationProperties.getEnterpriseEdition().isEnabled()) {
addIfNotEmpty(
properties,
"enterpriseEdition_customMetadata_autoUpdateMetadata",
applicationProperties
.getEnterpriseEdition()
.getCustomMetadata()
.isAutoUpdateMetadata());
addIfNotEmpty(
properties,
"enterpriseEdition_customMetadata_author",
applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor());
addIfNotEmpty(
properties,
"enterpriseEdition_customMetadata_creator",
applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator());
addIfNotEmpty(
properties,
"enterpriseEdition_customMetadata_producer",
applicationProperties.getEnterpriseEdition().getCustomMetadata().getProducer());
}
// Capture AutoPipeline properties
addIfNotEmpty(
properties,
"autoPipeline_outputFolder",
applicationProperties.getAutoPipeline().getOutputFolder());
return properties;
}
private String getMacAddress() {
try {
Enumeration<NetworkInterface> networkInterfaces =
NetworkInterface.getNetworkInterfaces();
while (networkInterfaces.hasMoreElements()) {
NetworkInterface ni = networkInterfaces.nextElement();
byte[] hardwareAddress = ni.getHardwareAddress();
if (hardwareAddress != null) {
String[] hexadecimal = new String[hardwareAddress.length];
for (int i = 0; i < hardwareAddress.length; i++) {
hexadecimal[i] = String.format("%02X", hardwareAddress[i]);
}
return String.join("-", hexadecimal);
}
}
} catch (Exception e) {
// Handle exception
}
return "Unknown";
}
private Map<String, String> getNetworkInterfacesInfo() {
Map<String, String> interfacesInfo = new HashMap<>();
try {
Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
while (nets.hasMoreElements()) {
NetworkInterface netint = nets.nextElement();
interfacesInfo.put(netint.getName(), netint.getDisplayName());
}
} catch (Exception e) {
interfacesInfo.put("error", e.getMessage());
}
return interfacesInfo;
}
}

View File

@@ -0,0 +1,100 @@
package stirling.software.SPDF.service;
import java.io.FileNotFoundException;
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.List;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
import org.thymeleaf.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.SignatureFile;
@Service
@Slf4j
public class SignatureService {
private static final String SIGNATURE_BASE_PATH = "customFiles/signatures/";
private static final String ALL_USERS_FOLDER = "ALL_USERS";
public boolean hasAccessToFile(String username, String fileName) throws IOException {
validateFileName(fileName);
// Check if file exists in user's personal folder or ALL_USERS folder
Path userPath = Paths.get(SIGNATURE_BASE_PATH, username, fileName);
Path allUsersPath = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER, fileName);
return Files.exists(userPath) || Files.exists(allUsersPath);
}
public List<SignatureFile> getAvailableSignatures(String username) {
List<SignatureFile> signatures = new ArrayList<>();
// Get signatures from user's personal folder
if (!StringUtils.isEmptyOrWhitespace(username)) {
Path userFolder = Paths.get(SIGNATURE_BASE_PATH, username);
if (Files.exists(userFolder)) {
try {
signatures.addAll(getSignaturesFromFolder(userFolder, "Personal"));
} catch (IOException e) {
log.error("Error reading user signatures folder", e);
}
}
}
// Get signatures from ALL_USERS folder
Path allUsersFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
if (Files.exists(allUsersFolder)) {
try {
signatures.addAll(getSignaturesFromFolder(allUsersFolder, "Shared"));
} catch (IOException e) {
log.error("Error reading shared signatures folder", e);
}
}
return signatures;
}
private List<SignatureFile> getSignaturesFromFolder(Path folder, String category)
throws IOException {
return Files.list(folder)
.filter(path -> isImageFile(path))
.map(path -> new SignatureFile(path.getFileName().toString(), category))
.collect(Collectors.toList());
}
public byte[] getSignatureBytes(String username, String fileName) throws IOException {
validateFileName(fileName);
// First try user's personal folder
Path userPath = Paths.get(SIGNATURE_BASE_PATH, username, fileName);
if (Files.exists(userPath)) {
return Files.readAllBytes(userPath);
}
// Then try ALL_USERS folder
Path allUsersPath = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER, fileName);
if (Files.exists(allUsersPath)) {
return Files.readAllBytes(allUsersPath);
}
throw new FileNotFoundException("Signature file not found");
}
private boolean isImageFile(Path path) {
String fileName = path.getFileName().toString().toLowerCase();
return fileName.endsWith(".jpg")
|| fileName.endsWith(".jpeg")
|| fileName.endsWith(".png")
|| fileName.endsWith(".gif");
}
private void validateFileName(String fileName) {
if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) {
throw new IllegalArgumentException("Invalid filename");
}
}
}

View File

@@ -0,0 +1,42 @@
package stirling.software.SPDF.service.misc;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.Factories.ReplaceAndInvertColorFactory;
import stirling.software.SPDF.model.api.misc.HighContrastColorCombination;
import stirling.software.SPDF.model.api.misc.ReplaceAndInvert;
import stirling.software.SPDF.utils.misc.ReplaceAndInvertColorStrategy;
@Service
public class ReplaceAndInvertColorService {
private ReplaceAndInvertColorFactory replaceAndInvertColorFactory;
@Autowired
public ReplaceAndInvertColorService(ReplaceAndInvertColorFactory replaceAndInvertColorFactory) {
this.replaceAndInvertColorFactory = replaceAndInvertColorFactory;
}
public InputStreamResource replaceAndInvertColor(
MultipartFile file,
ReplaceAndInvert replaceAndInvertOption,
HighContrastColorCombination highContrastColorCombination,
String backGroundColor,
String textColor)
throws IOException {
ReplaceAndInvertColorStrategy replaceColorStrategy =
replaceAndInvertColorFactory.replaceAndInvert(
file,
replaceAndInvertOption,
highContrastColorCombination,
backGroundColor,
textColor);
return replaceColorStrategy.replace();
}
}

View File

@@ -0,0 +1,21 @@
package stirling.software.SPDF.utils;
import org.owasp.html.HtmlPolicyBuilder;
import org.owasp.html.PolicyFactory;
import org.owasp.html.Sanitizers;
public class CustomHtmlSanitizer {
private static final PolicyFactory POLICY =
Sanitizers.FORMATTING
.and(Sanitizers.BLOCKS)
.and(Sanitizers.STYLES)
.and(Sanitizers.LINKS)
.and(Sanitizers.TABLES)
.and(Sanitizers.IMAGES)
.and(new HtmlPolicyBuilder().disallowElements("noscript").toFactory());
public static String sanitize(String html) {
String htmlAfter = POLICY.sanitize(html);
return htmlAfter;
}
}

View File

@@ -2,16 +2,23 @@ package stirling.software.SPDF.utils;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
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.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import io.github.pixee.security.ZipSecurity;
@@ -33,19 +40,25 @@ public class FileToPdf {
try {
if (fileName.endsWith(".html")) {
tempInputFile = Files.createTempFile("input_", ".html");
Files.write(tempInputFile, fileBytes);
} else {
String sanitizedHtml =
sanitizeHtmlContent(new String(fileBytes, StandardCharsets.UTF_8));
Files.write(tempInputFile, sanitizedHtml.getBytes(StandardCharsets.UTF_8));
} else if (fileName.endsWith(".zip")) {
tempInputFile = Files.createTempFile("input_", ".zip");
Files.write(tempInputFile, fileBytes);
sanitizeHtmlFilesInZip(tempInputFile);
} else {
throw new IllegalArgumentException("Unsupported file format: " + fileName);
}
List<String> command = new ArrayList<>();
if (!htmlFormatsInstalled) {
command.add("weasyprint");
command.add("-e utf-8");
command.add("-e");
command.add("utf-8");
command.add("-v");
command.add(tempInputFile.toString());
command.add(tempOutputFile.toString());
} else {
command.add("ebook-convert");
command.add(tempInputFile.toString());
@@ -54,10 +67,8 @@ public class FileToPdf {
command.add("a4");
if (request != null && request.getZoom() != 1.0) {
// Create a temporary CSS file
File tempCssFile = Files.createTempFile("customStyle", ".css").toFile();
try (FileWriter writer = new FileWriter(tempCssFile)) {
// Write the CSS rule to the file
writer.write("body { zoom: " + request.getZoom() + "; }");
}
command.add("--extra-css");
@@ -65,9 +76,7 @@ public class FileToPdf {
}
}
ProcessExecutorResult returnCode;
returnCode =
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
.runCommandWithOutputHandling(command);
@@ -78,8 +87,6 @@ public class FileToPdf {
throw e;
}
} finally {
// Clean up temporary files
Files.deleteIfExists(tempOutputFile);
Files.deleteIfExists(tempInputFile);
}
@@ -87,6 +94,81 @@ public class FileToPdf {
return pdfBytes;
}
private static String sanitizeHtmlContent(String htmlContent) {
return CustomHtmlSanitizer.sanitize(htmlContent);
}
private static void sanitizeHtmlFilesInZip(Path zipFilePath) throws IOException {
Path tempUnzippedDir = Files.createTempDirectory("unzipped_");
try (ZipInputStream zipIn =
ZipSecurity.createHardenedInputStream(
new ByteArrayInputStream(Files.readAllBytes(zipFilePath)))) {
ZipEntry entry = zipIn.getNextEntry();
while (entry != null) {
Path filePath = tempUnzippedDir.resolve(entry.getName());
if (!entry.isDirectory()) {
Files.createDirectories(filePath.getParent());
if (entry.getName().toLowerCase().endsWith(".html")
|| entry.getName().toLowerCase().endsWith(".htm")) {
String content = new String(zipIn.readAllBytes(), StandardCharsets.UTF_8);
String sanitizedContent = sanitizeHtmlContent(content);
Files.write(filePath, sanitizedContent.getBytes(StandardCharsets.UTF_8));
} else {
Files.copy(zipIn, filePath);
}
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
}
}
// Repack the sanitized files
zipDirectory(tempUnzippedDir, zipFilePath);
// Clean up
deleteDirectory(tempUnzippedDir);
}
private static void zipDirectory(Path sourceDir, Path zipFilePath) throws IOException {
try (ZipOutputStream zos =
new ZipOutputStream(new FileOutputStream(zipFilePath.toFile()))) {
Files.walk(sourceDir)
.filter(path -> !Files.isDirectory(path))
.forEach(
path -> {
ZipEntry zipEntry =
new ZipEntry(sourceDir.relativize(path).toString());
try {
zos.putNextEntry(zipEntry);
Files.copy(path, zos);
zos.closeEntry();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
}
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 =

View File

@@ -5,18 +5,28 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.NetworkInterface;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.UUID;
import org.simpleyaml.configuration.file.YamlFile;
import org.simpleyaml.configuration.file.YamlFileWrapper;
import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
@@ -262,4 +272,81 @@ public class GeneralUtils {
}
return true;
}
public static boolean isValidUUID(String uuid) {
if (uuid == null) {
return false;
}
try {
UUID.fromString(uuid);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
public static void saveKeyToConfig(String id, String key) throws IOException {
saveKeyToConfig(id, key, true);
}
public static void saveKeyToConfig(String id, String key, boolean autoGenerated)
throws IOException {
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
final YamlFile settingsYml = new YamlFile(path.toFile());
DumperOptions yamlOptionssettingsYml =
((SimpleYamlImplementation) settingsYml.getImplementation()).getDumperOptions();
yamlOptionssettingsYml.setSplitLines(false);
settingsYml.loadWithComments();
YamlFileWrapper writer = settingsYml.path(id).set(key);
if (autoGenerated) {
writer.comment("# Automatically Generated Settings (Do Not Edit Directly)");
}
settingsYml.save();
}
public static String generateMachineFingerprint() {
try {
// Get the MAC address
StringBuilder sb = new StringBuilder();
InetAddress ip = InetAddress.getLocalHost();
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
if (network == null) {
Enumeration<NetworkInterface> networks = NetworkInterface.getNetworkInterfaces();
while (networks.hasMoreElements()) {
NetworkInterface net = networks.nextElement();
byte[] mac = net.getHardwareAddress();
if (mac != null) {
for (int i = 0; i < mac.length; i++) {
sb.append(String.format("%02X", mac[i]));
}
break; // Use the first network interface with a MAC address
}
}
} else {
byte[] mac = network.getHardwareAddress();
if (mac != null) {
for (int i = 0; i < mac.length; i++) {
sb.append(String.format("%02X", mac[i]));
}
}
}
// Hash the MAC address for privacy and consistency
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest(sb.toString().getBytes(StandardCharsets.UTF_8));
StringBuilder fingerprint = new StringBuilder();
for (byte b : hash) {
fingerprint.append(String.format("%02x", b));
}
return fingerprint.toString();
} catch (Exception e) {
return "GenericID";
}
}
}

View File

@@ -1,13 +1,31 @@
package stirling.software.SPDF.utils;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import javax.imageio.ImageIO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
import com.drew.metadata.exif.ExifSubIFDDirectory;
public class ImageProcessingUtils {
private static final Logger logger = LoggerFactory.getLogger(PdfUtils.class);
static BufferedImage convertColorType(BufferedImage sourceImage, String colorType) {
BufferedImage convertedImage;
switch (colorType) {
@@ -59,4 +77,51 @@ public class ImageProcessingUtils {
return data;
}
}
public static double extractImageOrientation(InputStream is) throws IOException {
try {
Metadata metadata = ImageMetadataReader.readMetadata(is);
ExifSubIFDDirectory directory =
metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
if (directory == null) {
return 0;
}
int orientationTag = directory.getInt(ExifSubIFDDirectory.TAG_ORIENTATION);
switch (orientationTag) {
case 1:
return 0;
case 6:
return 90;
case 3:
return 180;
case 8:
return 270;
default:
logger.warn("Unknown orientation tag: {}", orientationTag);
return 0;
}
} catch (ImageProcessingException | MetadataException e) {
return 0;
}
}
public static BufferedImage applyOrientation(BufferedImage image, double orientation) {
if (orientation == 0) {
return image;
}
AffineTransform transform =
AffineTransform.getRotateInstance(
Math.toRadians(orientation),
image.getWidth() / 2.0,
image.getHeight() / 2.0);
AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
return op.filter(image, null);
}
public static BufferedImage loadImageWithExifOrientation(MultipartFile file)
throws IOException {
BufferedImage image = ImageIO.read(file.getInputStream());
double orientation = extractImageOrientation(file.getInputStream());
return applyOrientation(image, orientation);
}
}

View File

@@ -191,7 +191,6 @@ public class PDFToFile {
Files.deleteIfExists(tempInputFile);
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
}
System.out.println("fileBytes=" + fileBytes.length);
return WebResponseUtils.bytesToWebResponse(
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
}

View File

@@ -194,7 +194,8 @@ public class PdfUtils {
pdfDocument.close();
// Assumes the expectedPageSize is in the format "widthxheight", e.g. "595x842" for A4
// Assumes the expectedPageSize is in the format "widthxheight", e.g. "595x842"
// for A4
String[] dimensions = expectedPageSize.split("x");
float expectedPageWidth = Float.parseFloat(dimensions[0]);
float expectedPageHeight = Float.parseFloat(dimensions[1]);
@@ -407,7 +408,7 @@ public class PdfUtils {
addImageToDocument(doc, pdImage, fitOption, autoRotate);
}
} else {
BufferedImage image = ImageIO.read(file.getInputStream());
BufferedImage image = ImageProcessingUtils.loadImageWithExifOrientation(file);
BufferedImage convertedImage =
ImageProcessingUtils.convertColorType(image, colorType);
// Use JPEGFactory if it's JPEG since JPEG is lossy

View File

@@ -17,6 +17,7 @@ public class RequestUriUtils {
|| requestURI.startsWith(contextPath + "/public/")
|| requestURI.startsWith(contextPath + "/pdfjs/")
|| requestURI.startsWith(contextPath + "/login")
|| requestURI.startsWith(contextPath + "/error")
|| requestURI.endsWith(".svg")
|| requestURI.endsWith(".png")
|| requestURI.endsWith(".ico")

View File

@@ -0,0 +1,164 @@
package stirling.software.SPDF.utils.misc;
import java.awt.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import java.util.Set;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDPageTree;
import org.apache.pdfbox.pdmodel.font.*;
import org.apache.pdfbox.text.TextPosition;
import org.springframework.core.io.InputStreamResource;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.model.api.misc.HighContrastColorCombination;
import stirling.software.SPDF.model.api.misc.ReplaceAndInvert;
public class CustomColorReplaceStrategy extends ReplaceAndInvertColorStrategy {
private String textColor;
private String backgroundColor;
private HighContrastColorCombination highContrastColorCombination;
public CustomColorReplaceStrategy(
MultipartFile file,
ReplaceAndInvert replaceAndInvert,
String textColor,
String backgroundColor,
HighContrastColorCombination highContrastColorCombination) {
super(file, replaceAndInvert);
this.textColor = textColor;
this.backgroundColor = backgroundColor;
this.highContrastColorCombination = highContrastColorCombination;
}
@Override
public InputStreamResource replace() throws IOException {
// If ReplaceAndInvert is HighContrastColor option, then get the colors of text and
// background from static
if (replaceAndInvert == ReplaceAndInvert.HIGH_CONTRAST_COLOR) {
String[] colors =
HighContrastColorReplaceDecider.getColors(
replaceAndInvert, highContrastColorCombination);
this.textColor = colors[0];
this.backgroundColor = colors[1];
}
// Create a temporary file, with the original filename from the multipart file
File file = Files.createTempFile("temp", getFileInput().getOriginalFilename()).toFile();
// Transfer the content of the multipart file to the file
getFileInput().transferTo(file);
try (PDDocument document = Loader.loadPDF(file)) {
PDPageTree pages = document.getPages();
for (PDPage page : pages) {
PdfTextStripperCustom pdfTextStripperCustom = new PdfTextStripperCustom();
// Get text positions
List<List<TextPosition>> charactersByArticle =
pdfTextStripperCustom.processPageCustom(page);
// Begin a new content stream
PDPageContentStream contentStream =
new PDPageContentStream(
document, page, PDPageContentStream.AppendMode.APPEND, true, true);
// Set the new text color
contentStream.setNonStrokingColor(Color.decode(this.textColor));
// Draw the text with the new color
for (List<TextPosition> textPositions : charactersByArticle) {
for (TextPosition text : textPositions) {
// Move to the text position
contentStream.beginText();
contentStream.newLineAtOffset(
text.getX(), page.getMediaBox().getHeight() - text.getY());
PDFont font = null;
String unicodeText = text.getUnicode();
try {
font = PDFontFactory.createFont(text.getFont().getCOSObject());
} catch (IOException io) {
System.out.println("Primary font not found, using fallback font.");
font = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
}
// if a character is not supported by font, then look for supported font
try {
byte[] bytes = font.encode(unicodeText);
} catch (IOException io) {
System.out.println("text could not be encoded ");
font = checkSupportedFontForCharacter(unicodeText);
} catch (IllegalArgumentException ie) {
System.out.println("text not supported by font ");
font = checkSupportedFontForCharacter(unicodeText);
} finally {
// if any other font is not supported, then replace default character *
if (font == null) {
font = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
unicodeText = "*";
}
}
contentStream.setFont(font, text.getFontSize());
contentStream.showText(unicodeText);
contentStream.endText();
}
}
// Close the content stream
contentStream.close();
// Use a content stream to overlay the background color
try (PDPageContentStream contentStreamBg =
new PDPageContentStream(
document,
page,
PDPageContentStream.AppendMode.PREPEND,
true,
true)) {
// Set background color (e.g., light yellow)
contentStreamBg.setNonStrokingColor(Color.decode(this.backgroundColor));
contentStreamBg.addRect(
0, 0, page.getMediaBox().getWidth(), page.getMediaBox().getHeight());
contentStreamBg.fill();
}
}
// 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;
}
}
private PDFont checkSupportedFontForCharacter(String unicodeText) {
Set<String> fonts = Standard14Fonts.getNames();
for (String font : fonts) {
Standard14Fonts.FontName fontName = Standard14Fonts.getMappedFontName(font);
PDFont currentFont = new PDType1Font(fontName);
try {
byte[] bytes = currentFont.encode(unicodeText);
return currentFont;
} catch (IOException io) {
System.out.println("text could not be encoded ");
} catch (IllegalArgumentException ie) {
System.out.println("text not supported by font ");
}
}
return null;
}
}

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