Compare commits

...

210 Commits

Author SHA1 Message Date
Anthony Stirling
a875ae3034 saml test 2024-09-04 15:20:44 +01:00
Anthony Stirling
2989d8d416 2024-09-03 14:10:14 +02:00
Ludy
c1f78d0f9b Validates the file name (#1793) 2024-09-02 21:03:04 +01:00
Ludy
b31d565c75 removes duplicate dependencies (#1792) 2024-09-02 21:01:50 +01:00
github-actions[bot]
8eecab51f5 📝 Update README: Translation Progress Table (#1789)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-02 12:59:15 +01:00
Ludy
085c5c06d3 Update messages_de_DE.properties (#1784)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-09-02 12:51:10 +01:00
Anthony Stirling
2bfd81a495 Delete .github/FUNDING.yml 2024-09-02 11:23:07 +01:00
Anthony Stirling
bbf8015c50 Update README.md 2024-09-02 11:22:39 +01:00
Anthony Stirling
da6b66e199 Update LICENSE to MIT 2024-09-02 11:01:17 +01:00
Ludy
42677fbd5d Fix: wrong selected repository; disable Push - missing permission (#1785) 2024-08-31 21:48:40 +01:00
Anthony Stirling
7d73337461 Update README.md 2024-08-31 15:35:38 +01:00
Ludy
a14b78ff91 Extends the checking of message*.properties (#1781) 2024-08-31 14:54:11 +01:00
Ludy
09e963b160 correction action bot (#1782) 2024-08-31 15:30:18 +02:00
github-actions[bot]
6ffccc5f52 📝 Update README: Translation Progress Table (#1777)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-30 21:54:39 +01:00
NeilJared
b871ff94cd Updated es_ES translation (#1776)
* Update messages_es_ES.properties

Updated es_ES translation (100% done)

* Update messages_es_ES.properties

Updated es_ES translation (100% completed)
2024-08-30 22:18:37 +02:00
Anthony Stirling
68bc4d82de Add image support to multi-tool page (#1769)
* Add image support to multi-tool page

Related to #278

* changes to support image types

* final touches

---------

Co-authored-by: a <a>
2024-08-29 10:07:31 +01:00
github-actions[bot]
316021453f Update 3rd Party Licenses (#1767)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-08-27 20:33:10 +01:00
dependabot[bot]
c1a01ac283 Bump org.springframework.boot from 3.3.2 to 3.3.3 (#1764)
Bumps [org.springframework.boot](https://github.com/spring-projects/spring-boot) from 3.3.2 to 3.3.3.
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.2...v3.3.3)

---
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-08-27 10:56:44 +01:00
dependabot[bot]
c42fbbbf8c Bump springBootVersion from 3.3.2 to 3.3.3 (#1763)
Bumps `springBootVersion` from 3.3.2 to 3.3.3.

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

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

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

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

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

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

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

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

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

---
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-08-27 10:56:37 +01:00
github-actions[bot]
aa66538c54 📝 Update README: Translation Progress Table (#1766)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-27 10:46:50 +01:00
Anthony Stirling
47314a0f38 Extract images enhancements (#1757)
* fix

* extarct images

* langs

* logging

* cuke fix

---------

Co-authored-by: a <a>
2024-08-27 10:46:18 +01:00
Ludy
63dfcfe688 Change display name WEPB to WEBP (#1762)
Update pdf-to-img.html
2024-08-26 20:17:45 +01:00
Ludy
1732531174 Introduces the checking of message_*.properties (#1758)
* Introduces the checking of message_*.properties

* Update check_properties.yml
2024-08-25 22:04:28 +01:00
maxi322
363f5a4c23 Update german translation (#1759)
Co-authored-by: maxi322 <maxi322@users.noreply.github.com>
2024-08-25 21:33:06 +01:00
github-actions[bot]
d75e4f1318 💾 Update Version (#1755)
💾 Sync Versions
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-24 17:14:47 +01:00
Anthony Stirling
4088132597 Update build.gradle 2024-08-24 17:14:16 +01:00
github-actions[bot]
7b9d419267 Update 3rd Party Licenses (#1754)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-08-24 17:12:27 +01:00
Anthony Stirling
9b710b489e Fixes #1552 and #1554 (#1753)
* fix

* cleanups!

* fix

* fix for #1552 pipeline not accepting non pdfs

* fix for #1154 font not accepting numbers etc

* Update User.java

---------

Co-authored-by: a <a>
2024-08-24 17:08:51 +01:00
Anthony Stirling
e2ff418c89 Increase linecounts to check #1618 (#1752)
Increase linecounts to check #1618
2024-08-24 15:32:33 +01:00
albanobattistella
be006f08a6 Update messages_it_IT.properties (#1751) 2024-08-24 15:34:02 +02:00
Ludy
b007c805bf Removes double equal signs (#1750) 2024-08-23 22:55:01 +01:00
github-actions[bot]
24be10eed4 Update 3rd Party Licenses (#1746)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-08-23 22:49:31 +01:00
Dimitris Kaitantzidis
0854a1d26e Fixes LazyInitializationException in User entity (#1749)
Temp integration of playground dist files of pdfme as-is to investigate the result
2024-08-23 21:37:45 +01:00
Ludy
33c7bb7e13 Add: Make Login Attempt Service deactivatable (#1747) 2024-08-23 14:46:09 +01:00
Anthony Stirling
cdf31622e2 Fixes for eager loading (#1748)
* fix

* cleanups!

* fix

---------

Co-authored-by: a <a>
2024-08-23 14:45:53 +01:00
Anthony Stirling
c7e5987342 Cleanup logs (#1739)
* fix

* cleanups!

---------

Co-authored-by: a <a>
2024-08-23 11:52:45 +01:00
Ludy
d3ef335c24 Fix: validations is not a permitted attribute - dropdown (#1745) 2024-08-23 11:52:31 +01:00
Ludy
b23784f598 Fix: authentication ApiKey NullPointerException (#1744) 2024-08-23 12:10:58 +02:00
Ludy
90cbcde029 Fix: Missing multi-selection and Python validation (#1740)
Missing multi-selection and Python validation
2024-08-23 09:28:06 +01:00
Anthony Stirling
382edc01f8 Multiple flag fix (#1742)
* fix

* multiple file logic cleanup

* fix

---------

Co-authored-by: a <a>
2024-08-23 09:17:50 +01:00
github-actions[bot]
dee912f075 📝 Update README: Translation Progress Table (#1734)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-22 21:47:37 +01:00
albanobattistella
faed367cde Update messages_it_IT.properties (#1732) 2024-08-22 11:54:55 +01:00
Anthony Stirling
788744c1be Update pull_request_template.md 2024-08-21 20:43:13 +01:00
github-actions[bot]
686b88d21d 📝 Update README: Translation Progress Table (#1731)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-21 11:37:45 +01:00
Ludy
1a594b27ab Fix: introduces the verification of the python installation (#1730)
* Fix: introduces the verification of the python installation

* Update ExtractImageScansController.java

* Update CheckProgramInstall.java
2024-08-21 11:16:29 +01:00
Anthony Stirling
9f0088c839 Create release.yml 2024-08-20 22:56:15 +01:00
github-actions[bot]
5564a6e730 💾 Update Version (#1727)
💾 Sync Versions
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-20 17:49:33 +01:00
Anthony Stirling
8a58647ffd Update build.gradle 2024-08-20 17:49:10 +01:00
Anthony Stirling
37dcae282a ExtractImagesController. null checks 2024-08-20 17:20:18 +01:00
Ludy
58618b3a21 Add: Convert PDF to WebP (#1666)
* Add PDF to WebP

* add swagger param

* back

* creates a custom image for Docker from pymupdf

* Converting with pdf2image and Pillow instead of pymupdf

* webp remove to pdf-to-img

* remove mupdf
2024-08-20 16:17:54 +01:00
github-actions[bot]
4a4c7faf47 📝 Update README: Translation Progress Table (#1725)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-20 10:59:08 +01:00
github-actions[bot]
66324a5bdc Update 3rd Party Licenses (#1724)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-08-20 10:57:53 +01:00
Guilherme L. Leite Marques
3bd18f7c5e Updated pt_BR ignored translations; Improved pt_BR translation (#1705)
* Updated pt_BR ignored translations

* Fixed blank lines
2024-08-20 10:57:42 +01:00
dependabot[bot]
e693bbb2bd Bump com.bucket4j:bucket4j_jdk17-core from 8.13.1 to 8.14.0 (#1720)
Bumps [com.bucket4j:bucket4j_jdk17-core](https://github.com/bucket4j/bucket4j) from 8.13.1 to 8.14.0.
- [Release notes](https://github.com/bucket4j/bucket4j/releases)
- [Commits](https://github.com/bucket4j/bucket4j/commits/8.14.0)

---
updated-dependencies:
- dependency-name: com.bucket4j:bucket4j_jdk17-core
  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-08-20 10:55:00 +01:00
Црнобог
f11ad92fa5 Typing error in README.md (#1721)
Misstype |  Sebian => Serbian
2024-08-20 10:54:44 +01:00
Anthony Stirling
f443a4e0de reduce google font size (#1723)
* Delete src/main/resources/static/fonts/google-symbol.woff2

* Add files via upload
2024-08-20 09:57:12 +01:00
PingLin8888
fa0152aa2d Fix ConcurrentModificationException by modifying resources outside the iteration. (#1719)
Fix ConcurrentModificationException by collecting XObject names

- Changed  to use a list to collect XObject names before removal.
- Avoids ConcurrentModificationException by modifying resources outside the iteration.

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-19 19:42:55 +01:00
Ludy
e1d0f2cd3e Fix: YamlFile - String length limit disable (#1716)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-19 15:03:05 +01:00
Ludy
81e2a77e57 Fix: Failed authentication #1704 (#1708)
* Fix: Failed authentication #1704

* Update account.html
2024-08-19 15:02:40 +01:00
Ludy
6c9a4e8acc Add Option to Specify Installation Method in Bug Report Template (#1710)
* Add Option to Specify Installation Method in Bug Report Template

* Update 1-bug.yml
2024-08-19 15:00:23 +01:00
albanobattistella
8e5b3ea7f1 Update messages_it_IT.properties (#1713) 2024-08-19 15:00:12 +01:00
Ludy
b47f8a2c17 corrects the link to the country selection (#1717) 2024-08-19 15:00:00 +01:00
Ludy
56a07bbf3a increases some versions in the workflows (#1707) 2024-08-18 13:07:14 +01:00
github-actions[bot]
987d793ef4 Update 3rd Party Licenses (#1701)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-08-16 13:27:18 +01:00
github-actions[bot]
ea85d76f5b 💾 Update Version (#1700)
💾 Sync Versions
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-16 13:24:53 +01:00
Anthony Stirling
fc762329a8 Update build.gradle 2024-08-16 13:24:32 +01:00
github-actions[bot]
48bae227f6 💾 Update Version (#1699)
💾 Sync Versions
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-16 12:52:25 +01:00
Anthony Stirling
9773138612 Update build.gradle 2024-08-16 12:51:59 +01:00
github-actions[bot]
1927801894 Update 3rd Party Licenses (#1698)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-08-16 12:47:46 +01:00
Anthony Stirling
3cd6f462a8 Update build.gradle to use commmon versions (#1697)
* Update build.gradle

* Update build.gradle
2024-08-16 12:44:25 +01:00
github-actions[bot]
6dab3980fc Update 3rd Party Licenses (#1696)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-08-16 12:15:55 +01:00
Anthony Stirling
ea2d755808 Update licenses-update.yml 2024-08-16 12:15:07 +01:00
dependabot[bot]
05efcedea8 Bump ch.qos.logback:logback-classic from 1.5.6 to 1.5.7 (#1685)
* Bump ch.qos.logback:logback-classic from 1.5.6 to 1.5.7

Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.6 to 1.5.7.
- [Commits](https://github.com/qos-ch/logback/compare/v_1.5.6...v_1.5.7)

---
updated-dependencies:
- dependency-name: ch.qos.logback:logback-classic
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update build.gradle

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-16 12:03:59 +01:00
Ludy
29fcbf30d7 Admin panel - Enhanced User Management & Fix: #1630 (#1658)
* Prevents SSO login due to faulty verification

* add translation & fix show error message

* Update settings.yml.template

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-16 11:57:37 +01:00
github-actions[bot]
2cbe34ea24 Update 3rd Party Licenses (#1695)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-08-16 11:52:51 +01:00
dependabot[bot]
29f43c010e Bump io.micrometer:micrometer-core from 1.13.0 to 1.13.3 (#1689)
Bumps [io.micrometer:micrometer-core](https://github.com/micrometer-metrics/micrometer) from 1.13.0 to 1.13.3.
- [Release notes](https://github.com/micrometer-metrics/micrometer/releases)
- [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.13.0...v1.13.3)

---
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>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-16 11:51:40 +01:00
Anthony Stirling
8602f38fbf Update build.yml 2024-08-16 11:44:49 +01:00
Anthony Stirling
f5258c593b Update auto-labeler.yml 2024-08-16 11:15:37 +01:00
Anthony Stirling
e89ac84928 Update build.yml 2024-08-16 11:09:17 +01:00
Anthony Stirling
8997855922 Update auto-labeler.yml 2024-08-16 11:02:38 +01:00
Anthony Stirling
09c93cebe3 PR changes (#1693)
* Update licenses-update.yml

* Update build.yml

* Update test.yml

* Delete .github/workflows/test.yml

* Update build.yml

* Update build.yml

* Update auto-labeler.yml

* Update build.yml

* Update auto-labeler.yml

* Update labeler-config.yml
2024-08-16 11:00:10 +01:00
github-actions[bot]
00d65596d1 Update 3rd Party Licenses (#1687)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-08-16 08:48:42 +01:00
dependabot[bot]
851b43fadf Bump com.github.jk1.dependency-license-report from 2.8 to 2.9 (#1690)
Bumps com.github.jk1.dependency-license-report from 2.8 to 2.9.

---
updated-dependencies:
- dependency-name: com.github.jk1.dependency-license-report
  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-08-16 00:04:43 +02:00
dependabot[bot]
d5ac560452 Bump org.apache.pdfbox:pdfbox from 3.0.2 to 3.0.3 (#1691)
Bumps org.apache.pdfbox:pdfbox from 3.0.2 to 3.0.3.

---
updated-dependencies:
- dependency-name: org.apache.pdfbox:pdfbox
  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-08-15 23:57:45 +02:00
dependabot[bot]
4ea323b879 Bump edu.sc.seis.launch4j from 3.0.5 to 3.0.6 (#1686)
Bumps edu.sc.seis.launch4j from 3.0.5 to 3.0.6.

---
updated-dependencies:
- dependency-name: edu.sc.seis.launch4j
  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-08-15 21:41:03 +00:00
dependabot[bot]
5c84ae1b53 Bump org.springframework:spring-webmvc from 6.1.9 to 6.1.12 (#1680)
Bumps [org.springframework:spring-webmvc](https://github.com/spring-projects/spring-framework) from 6.1.9 to 6.1.12.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.9...v6.1.12)

---
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-08-15 21:36:26 +00:00
dependabot[bot]
96a8898c15 Bump com.bucket4j:bucket4j_jdk17-core from 8.12.1 to 8.13.1 (#1683)
Bumps [com.bucket4j:bucket4j_jdk17-core](https://github.com/bucket4j/bucket4j) from 8.12.1 to 8.13.1.
- [Release notes](https://github.com/bucket4j/bucket4j/releases)
- [Commits](https://github.com/bucket4j/bucket4j/commits)

---
updated-dependencies:
- dependency-name: com.bucket4j:bucket4j_jdk17-core
  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-08-15 21:34:04 +00:00
dependabot[bot]
1cca13334e Bump org.projectlombok:lombok from 1.18.32 to 1.18.34 (#1684)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.32 to 1.18.34.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.32...v1.18.34)

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  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-08-15 23:31:57 +02:00
dependabot[bot]
264763dd4c Bump io.github.pixee:java-security-toolkit from 1.1.3 to 1.2.0 (#1667)
Bumps [io.github.pixee:java-security-toolkit](https://github.com/pixee/java-security-toolkit) from 1.1.3 to 1.2.0.
- [Release notes](https://github.com/pixee/java-security-toolkit/releases)
- [Commits](https://github.com/pixee/java-security-toolkit/compare/v1.1.3...v1.2.0)

---
updated-dependencies:
- dependency-name: io.github.pixee:java-security-toolkit
  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-08-15 22:01:41 +01:00
dependabot[bot]
eafbfb8dbf Bump org.apache.pdfbox:xmpbox from 3.0.2 to 3.0.3 (#1670)
Bumps org.apache.pdfbox:xmpbox from 3.0.2 to 3.0.3.

---
updated-dependencies:
- dependency-name: org.apache.pdfbox:xmpbox
  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-08-15 22:01:03 +01:00
Anthony Stirling
6fa7c2e5e1 Update dependabot.yml 2024-08-15 22:00:02 +01:00
Anthony Stirling
909054a49d Update dependabot.yml 2024-08-15 21:57:37 +01:00
kmau
711501a382 Update messages_it_IT.properties - remove image in home (#1661) 2024-08-15 09:46:54 +00:00
github-actions[bot]
e45b512087 📝 Update README: Translation Progress Table (#1675)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-15 09:45:19 +00:00
Ludy
d32da95f55 tessdata available to local Windows users (#1677)
tessdata available to local Windows users
2024-08-15 11:43:56 +02:00
Rafael Martins
b54d73d723 Updated brazilian portuguese translation (#1673)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-14 10:59:06 +00:00
Ludy
503a1c9526 redesign footer (#1674)
* redesign footer

* remove about test
2024-08-13 23:54:33 +02:00
Ludy
f176558a39 Fix: Conditional Attribute Binding for the multiple Attribute in the File Selector Fragment (#1665) 2024-08-12 17:22:32 +01:00
github-actions[bot]
68c387086c 📝 Update README: Translation Progress Table (#1655)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-09 08:58:31 +01:00
Anthony Stirling
f165439d26 Update remove-pages.html
#1656
2024-08-09 08:57:29 +01:00
tkymmm
6649ffd7a0 Updated Japanese translation (#1654)
* Update messages_ja_JP.properties

Updated Japanese translation

* Update messages_ja_JP.properties

Updated Japanese translation
2024-08-09 07:39:35 +01:00
github-actions[bot]
8dbbacb09e 📝 Update README: Translation Progress Table (#1651)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-08 22:29:02 +01:00
albanobattistella
908b409155 Update messages_it_IT.properties (#1649) 2024-08-08 22:06:13 +01:00
github-actions[bot]
4ad716f281 Update 3rd Party Licenses (#1648)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-08-08 21:41:40 +01:00
Diallo
148feda83f Bug fix UI crash when url is unrechable (#1642)
* feat: Add URL  reachability check in ConvertWebsiteToPDF

* Add tests for URL reachability in ConvertWebsiteToPdfTest

* test: Update URL in ConvertWebsiteToPdfTest for testing
2024-08-08 20:35:15 +00:00
dependabot[bot]
771b312ee8 Bump com.twelvemonkeys.imageio:imageio-tiff from 3.10.1 to 3.11.0 (#1503)
* Bump com.twelvemonkeys.imageio:imageio-tiff from 3.10.1 to 3.11.0

Bumps com.twelvemonkeys.imageio:imageio-tiff from 3.10.1 to 3.11.0.

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

Signed-off-by: dependabot[bot] <support@github.com>

* Update build.gradle

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-08 21:33:14 +01:00
github-actions[bot]
00a0670954 📝 Update README: Translation Progress Table (#1647)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-08 21:17:18 +01:00
github-actions[bot]
39423c247c 💾 Update Version (#1646)
💾 Sync Versions
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-08 21:17:08 +01:00
Anthony Stirling
6d8d0bad56 langs 2024-08-08 21:15:41 +01:00
Anthony Stirling
a3374745f8 formatting 2024-08-08 21:13:59 +01:00
dependabot[bot]
d65a637a46 Bump alpine from 3.20.0 to 3.20.1 (#1505)
* Bump alpine from 3.20.0 to 3.20.1

Bumps alpine from 3.20.0 to 3.20.1.

---
updated-dependencies:
- dependency-name: alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update Dockerfile

* Update Dockerfile-fat

* Update Dockerfile-ultra-lite

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-08 20:49:56 +01:00
PingLin8888
d0bf385d69 Issue1632 remove images (#1645)
* Implemented PdfImageRemovalService.java and PdfImageRemovalController.java. Image can be removed testing using Postman, but the file size doesn't change.

* Fix removal logic in service file to decrease file size.

* Implement "Remove Image" feature on the website

Updated the front-end code to integrate the "Remove Image" feature. The new functionality is now fully operational on the website, allowing users to remove images as expected.

* Add comments to PdfImageRemovalController and PdfImageRemovalService.

* Change the google material icon in navbar, homepage and remove-image-pdf.html.
2024-08-08 20:38:36 +01:00
github-actions[bot]
bc35745768 📝 Update README: Translation Progress Table (#1640)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-07 22:19:08 +01:00
HimaGirija
e50391a44a Added multithreaded feature for image extraction (#1641)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-07 22:16:57 +01:00
arsvendg
96b080528b Changes norwegian translation (#1639)
* Minor correction

* Endringer oversettelser

* Changes norwegian translation
2024-08-07 09:44:44 +01:00
Anthony Stirling
f35cbc4310 enhancement auto have label 2024-08-06 10:57:18 +01:00
github-actions[bot]
c09fc1541f 📝 Update README: Translation Progress Table (#1636)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-06 10:51:50 +01:00
Anthony Stirling
dff53310a7 lang 2024-08-06 10:50:47 +01:00
mylk13
ec537c6fde Add a checkbox to WatermarkController to convert the pdf to pdf-image (#1633)
* Add a checkbox to WatermarkController to convert the pdf to pdf-image

* 381: Fix messages_en_GB

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-06 08:11:52 +00:00
dependabot[bot]
ce70796fff Bump org.mockito:mockito-inline from 3.12.4 to 5.2.0 (#1635)
Bumps [org.mockito:mockito-inline](https://github.com/mockito/mockito) from 3.12.4 to 5.2.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.12.4...v5.2.0)

---
updated-dependencies:
- dependency-name: org.mockito:mockito-inline
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-06 09:10:14 +01:00
Mateusz Tylec
7db7192d95 Update polish translation (#1631) 2024-08-04 21:26:55 +01:00
github-actions[bot]
d00e7fe958 Update 3rd Party Licenses (#1627)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-08-03 13:47:59 +01:00
Anthony Stirling
510f39ad41 Update examples.feature 2024-08-03 13:47:47 +01:00
Ludy
950a0c4b21 Bump org.springframework.boot from 3.3.0 to 3.3.2 & Gradle 8 compatibility (#1626)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-03 13:18:51 +01:00
Ludy
e6793bd04a Fix: fail JUnit test (#1625) 2024-08-03 12:52:50 +01:00
Anthony Stirling
0f60974a57 Update examples.feature (#1624) 2024-08-03 10:44:17 +01:00
dependabot[bot]
0ed4c16dc0 Bump io.spring.dependency-management from 1.1.5 to 1.1.6 (#1579)
Bumps [io.spring.dependency-management](https://github.com/spring-gradle-plugins/dependency-management-plugin) from 1.1.5 to 1.1.6.
- [Release notes](https://github.com/spring-gradle-plugins/dependency-management-plugin/releases)
- [Commits](https://github.com/spring-gradle-plugins/dependency-management-plugin/compare/v1.1.5...v1.1.6)

---
updated-dependencies:
- dependency-name: io.spring.dependency-management
  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-08-03 10:33:57 +01:00
Manohar Mannam
ea6d4a293e blank pages returns removed pages for verification #1574 (#1619)
separated blank and non-blank pages and created unified ZIP archive

Co-authored-by: mannam <101550345+ManoharMannam@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-03 09:30:53 +00:00
Anthony Stirling
191e79da18 Update test.yml (#1623)
* Update test.yml

* Update SPdfApplication.java
2024-08-03 10:29:34 +01:00
Ludy
c54c18b247 Add: Irish and Danish to the table (#1622) 2024-08-03 10:16:26 +01:00
github-actions[bot]
39cbb5e7d9 📝 Update README: Translation Progress Table (#1615)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-01 00:10:12 +01:00
LizardWizardGB
3df0474ed2 Danish language (#1606)
* Update languages.html

Added an entry for "Danish".

* Update languages.html

filename of flag was wrong.

* Danish flag svg

* Create messages_da_DK.properties

Initial commit of danish translation.

* Update messages_da_DK.properties

* Update messages_da_DK.properties
2024-07-31 21:25:57 +01:00
congyuluo
9ff2cb63d0 Refactored Identifiers (#1609) 2024-07-31 21:25:48 +01:00
github-actions[bot]
d8087d8c55 📝 Update README: Translation Progress Table (#1613)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-07-31 21:25:37 +01:00
Aindriú Mac Giolla Eoin
0dfb4d77c0 Added Irish Language (#1607)
Adding Irish Language
2024-07-31 18:17:01 +00:00
Ludy
065f53e577 Optimize Editor and Git Ignore Settings for Improved Consistency and Security (#1611) 2024-07-31 18:49:52 +01:00
github-actions[bot]
c899f605a9 📝 Update README: Translation Progress Table (#1601)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-07-27 09:11:41 +01:00
DeH40
47de0f84db Update messages_zh_CN.properties (#1599)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-07-27 09:11:16 +01:00
Ludy
543b96c033 Add: Vietnam to the table (#1600)
* Add: Vietnam to the table

* Update labeler-config.yml
2024-07-27 08:37:22 +01:00
github-actions[bot]
c1126e57bd 📝 Update README: Translation Progress Table (#1598)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-07-26 16:01:23 +01:00
an-777
7c5077006d Update messages_zh_TW.properties: Translate English sentence to Traditional Chinese (#1596)
* Update messages_zh_TW.properties

* fix eof
2024-07-26 13:19:20 +01:00
github-actions[bot]
3e7889cee8 📝 Update README: Translation Progress Table (#1597)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-07-26 13:06:48 +01:00
Son Tran Lam
281047f42a Translate to Vietnamese (#1591)
* Translate to Vietnamese

* Add translation for Vietnamese

* - Remove  invalid properties and duplicated values.
- Add blank lines to align with the original format of en_GB file

* fix eof

* add empty lines to align with the template format. The number of line is equal with the reference en_GB file now

* change some translations to be more natural and easier to understand
2024-07-26 13:01:17 +01:00
github-actions[bot]
07f85ea8b4 📝 Update README: Translation Progress Table (#1587)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-07-26 13:00:54 +01:00
Jean-Baptiste WITTNER
e07f73dce7 [Helm][K8S] Add rootPath helm (#1593)
* Update of values.yaml to use a new variable to manage the application rootpath

* Use rootPath of values in deployment to manage rootPath and probes
2024-07-24 21:58:04 +01:00
albanobattistella
bfe38c71e8 Update messages_it_IT.properties (#1588) 2024-07-23 20:18:00 +01:00
Clara Bujeda
072090d41b Update of what was missing in messages_es_ES.properties (#1586)
I have translated what was missing from translating so everything would be translated.
2024-07-23 18:51:46 +01:00
Ludy
560936e182 remove new lines and obsolete spaces (#1585) 2024-07-23 16:57:21 +01:00
Ludy
6eb79e65fa minor changes in the DEV tools and more (#1578) 2024-07-22 21:15:10 +01:00
github-actions[bot]
cbe92269f4 Update 3rd Party Licenses (#1572)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-07-20 10:49:09 +01:00
dependabot[bot]
81871a6f10 Bump springBootVersion from 3.3.0 to 3.3.2 (#1570)
Bumps `springBootVersion` from 3.3.0 to 3.3.2.

Updates `org.springframework.boot:spring-boot-starter-web` from 3.3.0 to 3.3.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.0...v3.3.2)

Updates `org.springframework.boot:spring-boot-starter-jetty` from 3.3.0 to 3.3.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.0...v3.3.2)

Updates `org.springframework.boot:spring-boot-starter-thymeleaf` from 3.3.0 to 3.3.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.0...v3.3.2)

Updates `org.springframework.boot:spring-boot-starter-security` from 3.3.0 to 3.3.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.0...v3.3.2)

Updates `org.springframework.boot:spring-boot-starter-data-jpa` from 3.3.0 to 3.3.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.0...v3.3.2)

Updates `org.springframework.boot:spring-boot-starter-oauth2-client` from 3.3.0 to 3.3.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.0...v3.3.2)

Updates `org.springframework.boot:spring-boot-starter-test` from 3.3.0 to 3.3.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.0...v3.3.2)

Updates `org.springframework.boot:spring-boot-starter-actuator` from 3.3.0 to 3.3.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.0...v3.3.2)

Updates `org.springframework.boot:spring-boot-devtools` from 3.3.0 to 3.3.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.0...v3.3.2)

---
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-07-20 10:42:03 +01:00
github-actions[bot]
cf2a7896da 📝 Update README: Translation Progress Table (#1571)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-07-20 10:41:32 +01:00
Oğuz Ersen
6a3d95ba09 Add missing Turkish translation (#1549)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-07-20 09:15:03 +00:00
Ludy
85ed0c38d1 Adding declaration as repository component & changing primary key type (#1559)
* Adding declaration as repository component & changing primary key type

* Update AuthorityRepository.java

* Update PersistentLoginRepository.java

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-07-20 10:13:49 +01:00
Ludy
6c7dc34640 Add: Label manager (#1560) 2024-07-20 09:57:27 +01:00
Anthony Stirling
ecfdfa5644 Update init-without-ocr.sh 2024-07-20 09:56:39 +01:00
Anthony Stirling
11e279bd12 Remove calibre for now 2024-07-20 09:54:46 +01:00
Anthony Stirling
929f0bbbe5 version bump, multi file fix and disable survey (#1550)
* version bump, multi file fix and disable survey

* example test stuff

* logs

* Update docker-compose-latest.yml

---------

Co-authored-by: a <a>
2024-07-20 09:53:58 +01:00
Ludy
5751b1ac2d adds Thai to the languages ​​table (#1555)
This PR makes #1554 obsolete
2024-07-11 23:35:01 +01:00
taesaeng28
4bf78ffd5d Added support for Thai language (#1551)
* Added support for Thai language

* Correct Thai national flag proportions in SVG

* Remove th_TH from env.LANGS

causer
Failed tests:
Stirling-PDF-Regression
Some tests failed.
Error: Process completed with exit code 1.

* fix incorrect syntax
2024-07-10 18:05:29 +00:00
pixeebot[bot]
b7d37deb85 Refactored to use parameterized SQL APIs (#1545)
Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>
2024-07-09 21:18:32 +01:00
github-actions[bot]
2a65fd0825 📝 Update README: Translation Progress Table (#1538)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-07-07 11:45:50 +01:00
Ludy
422264a288 added non-translatable strings (#1537)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-07-06 21:54:04 +00:00
github-actions[bot]
695fbb0150 📝 Update README: Translation Progress Table (#1536)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-07-06 21:48:53 +00:00
Ludy
a17105e650 Create stale.yml (#1530)
* Create stale.yml

* Update stale.yml
2024-07-06 20:25:38 +00:00
Ludy
32ac38e93f Add missing translations strings (#1535)
* Add missing translations strings

* Update messages_de_DE.properties
2024-07-06 19:48:39 +01:00
Ludy
3c0d2b908f Update messages_de_DE.properties (#1532)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-07-06 18:17:30 +00:00
Ludy
ab62a93a0d Fix labeler 2 (#1534) 2024-07-06 19:15:19 +01:00
Ludy
5189708d25 Fix labeler (#1533)
* Update labeler.yml

* Update labeler.yml
2024-07-06 17:48:06 +01:00
Ludy
19831c050c Add labeler action for pull requests (#1529)
* Automatically label PRs

* Update labeler-config.yml
2024-07-06 15:43:53 +01:00
albanobattistella
e426d99145 Update messages_it_IT.properties (#1527) 2024-07-06 12:27:19 +00:00
Ludy
4088208fc8 adding documentation for database import and export (#1528)
* adding documentation for database import and export

* Update DATABASE.md
2024-07-06 13:24:32 +01:00
albanobattistella
31ce5b1221 Update messages_it_IT.properties (#1526)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-07-05 20:50:22 +01:00
Ludy
be05db22f5 Preparation for Switching to a New Database Version (#1521)
* preparing to switch to a new database version

* add PreAuthorize

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-07-05 20:48:33 +01:00
Ludy
79927416e5 standardize the layout (#1525) 2024-07-04 21:13:03 +00:00
Shawn Johnston
f95ee31bbd Highlight color selection for the Compare PDFs page. (#1515)
* Update compare.html

* Update compare.html

* Conform to text requirements

Changed text in some labels to conform to Thymeleaf format.

* Add GB to th:text for label
2024-07-04 21:11:41 +00:00
Ludy
40042c37f2 [Bugfix] the Manifest Syntax error (#1524)
Fixes the Manifest Syntax error
2024-07-04 21:05:45 +00:00
Ludy
1c90b65bca removes empty list entries (#1523) 2024-07-04 21:04:21 +00:00
Ludy
2971425544 [Bugfix] Prevents the deletion of productive data (#1522)
prevents the deletion of productive data
2024-07-04 22:02:35 +01:00
Prerak Trivedi
a9ee698432 Update README.md (#1518)
removed dead link to a screenshot
2024-07-02 20:29:22 +01:00
github-actions[bot]
15b5d51957 📝 Update README: Translation Progress Table (#1514)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-06-30 15:11:18 +01:00
eruditus-ginkgo
92893b8d4c Update messages_zh_CN.properties (#1513)
Translate untranslated text to Simplified Chinese;
Standardize Chinese expressions for identical English phrases;
Remove spaces between Chinese and English text for consistency.
2024-06-29 17:45:31 +01:00
Ludy
3a6969cad0 Fix: synchronizing the browser settings to the database #1481 (#1510)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-06-26 20:48:50 +00:00
Ludy
88e8663d44 Rename from translation_status.toml to ignore_translation.toml and more (#1511) 2024-06-26 21:47:20 +01:00
github-actions[bot]
a2d7490d45 📝 Update README: Translation Progress Table (#1507)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-06-25 23:21:57 +01:00
Alex
c363a1c4e0 Add some translations and add ignored translations (#1501)
* Create ignore_translation.toml

Added ignored translations

* Update messages_nl_NL.properties

Changed a few translations to be better

* Update ignore_translation.toml

Deleted wrong ignore
2024-06-25 23:16:09 +01:00
Anthony Stirling
9e84836cfe Update pipeline.html 2024-06-22 23:29:36 +01:00
ProvaTeams
c8a2789caf Missing description for Split function in top bar (#1492) 2024-06-20 20:39:59 +01:00
Anthony Stirling
202b996c2b Update README.md 2024-06-19 21:05:56 +01:00
github-actions[bot]
e766a5f583 Update 3rd Party Licenses (#1490)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-06-19 18:37:43 +01:00
dependabot[bot]
c7d18939fc Bump org.springframework:spring-webmvc from 6.1.5 to 6.1.9 (#1462)
Bumps [org.springframework:spring-webmvc](https://github.com/spring-projects/spring-framework) from 6.1.5 to 6.1.9.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.5...v6.1.9)

---
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>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-06-19 18:25:09 +01:00
dependabot[bot]
34e198d8e6 Bump com.twelvemonkeys.imageio:imageio-jpeg from 3.10.1 to 3.11.0 (#1486)
Bumps com.twelvemonkeys.imageio:imageio-jpeg from 3.10.1 to 3.11.0.

---
updated-dependencies:
- dependency-name: com.twelvemonkeys.imageio:imageio-jpeg
  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-06-19 18:24:09 +01:00
anthonyp-cns
f6a31e6ed0 Update README.md adjusting system to security as shown later in the readme for consistency (#1488)
Update README.md

Updated readme changing system to security tag to make enabling logins easy to find / implement for new users via docker environment variable
2024-06-19 06:14:52 +01:00
github-actions[bot]
7768dc9fd9 📝 Update README: Translation Progress Table (#1485)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-06-17 20:37:27 +01:00
Alex
b739d3847e Update messages_nl_NL.properties (#1483)
Added new translations
2024-06-17 20:36:41 +01:00
M0C
a3e5cb51b0 Polish translation (#1482)
pigers full polish translation
2024-06-17 20:36:29 +01:00
Anthony Stirling
ab7a41d155 Update README.md 2024-06-17 18:40:07 +01:00
Anthony Stirling
3576c32c52 add fat jar 2024-06-16 15:42:45 +01:00
Ludy
f43fe15193 further bug fixes when using context path (#1475)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-06-15 22:07:09 +01:00
arsvendg
234ae17dc8 Edit norwegian translations (#1476)
* Minor correction

* Endringer oversettelser
2024-06-15 21:40:52 +01:00
github-actions[bot]
7ee14ac794 📝 Update README: Translation Progress Table (#1473)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-06-15 16:08:07 +01:00
albanobattistella
ba604bda3e Update messages_it_IT.properties (#1472) 2024-06-15 15:21:08 +01:00
Ludy
036c10fc27 added: Differentiate login methods and more (#1471)
- Added Portuguese in the table (README.md)
- ApplicationProperties.class diluted, provider outsourced to its own class
- Added UnsupportedProviderException to indicate a meaningful error
- Closes #1357
- Closes #1238
2024-06-15 13:15:09 +01:00
github-actions[bot]
baa9410242 📝 Update README: Translation Progress Table (#1469)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-06-15 12:07:20 +01:00
Ludy
ab2fe5ef81 Add: missing string pdfOrganiser.mode.10 (#1468)
* Add: missing string pdfOrganiser.mode.10

* Update messages_it_IT.properties
2024-06-15 12:06:38 +01:00
github-actions[bot]
a926289fb0 Update 3rd Party Licenses (#1466)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-06-15 11:50:58 +01:00
Anthony Stirling
b711be3061 Update build.gradle 2024-06-15 11:50:31 +01:00
Ludy
f07ba9192b Updating build.gradle libraries (#1465) 2024-06-15 11:47:16 +01:00
github-actions[bot]
dc2f891632 Update 3rd Party Licenses (#1464)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-06-15 10:32:23 +01:00
Ludy
fe9c5a7351 Change: method write and read settings.yml #1441 (#1463)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-06-15 09:05:31 +01:00
268 changed files with 13985 additions and 3998 deletions

13
.github/FUNDING.yml vendored
View File

@@ -1,13 +0,0 @@
# These are supported funding model platforms
github: Frooodle # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: ['https://www.paypal.com/donate/?hosted_button_id=MN7JPG5G6G3JL'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -10,7 +10,19 @@ body:
Thanks for taking the time to fill out this bug report! Thanks for taking the time to fill out this bug report!
This issue form is for reporting bugs only. Please fill out the following sections to help us understand the issue you are facing. This issue form is for reporting bugs only. Please fill out the following sections to help us understand the issue you are facing.
- type: dropdown
id: installation-method
attributes:
label: Installation Method
description: |
Indicate whether you are using Docker or a local installation.
options:
- Docker
- Docker ultra lite
- Docker fat
- Local Installation
- type: textarea - type: textarea
id: problem id: problem
validations: validations:

View File

@@ -1,6 +1,8 @@
name: Feature Request name: Feature Request
description: Submit a new feature request. description: Submit a new feature request.
title: "[Feature Request]: " title: "[Feature Request]: "
labels:
- enhancement
body: body:
- type: markdown - type: markdown
attributes: attributes:

View File

@@ -9,6 +9,8 @@ updates:
directory: "/" # Location of package manifests directory: "/" # Location of package manifests
schedule: schedule:
interval: "weekly" interval: "weekly"
open-pull-requests-limit: 10
rebase-strategy: "auto"
- package-ecosystem: "docker" - package-ecosystem: "docker"
directory: "/" # Location of Dockerfile directory: "/" # Location of Dockerfile
schedule: schedule:

54
.github/labeler-config.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
Translation:
- changed-files:
- any-glob-to-any-file: 'src/main/resources/messages_*_*.properties'
- any-glob-to-any-file: 'scripts/ignore_translation.toml'
- any-glob-to-any-file: 'src/main/resources/templates/fragments/languages.html'
Front End:
- changed-files:
- any-glob-to-any-file: 'src/main/resources/templates/**/*'
- any-glob-to-any-file: 'src/main/resources/static/**/*'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/**'
Java:
- changed-files:
- any-glob-to-any-file: 'src/main/java/**/*.java'
Back End:
- changed-files:
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/model/provider/**/*'
- any-glob-to-any-file: 'src/main/resources/settings.yml.template'
- any-glob-to-any-file: 'src/main/resources/banner.txt'
Security:
- changed-files:
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/model/provider/**/*'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/model/AuthenticationType.java'
API:
- changed-files:
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/MetricsController.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/**/*'
Documentation:
- changed-files:
- any-glob-to-any-file: '**/*.md'
- any-glob-to-any-file: 'scripts/counter_translation.py'
- any-glob-to-any-file: 'scripts/ignore_translation.toml'
Docker:
- changed-files:
- any-glob-to-any-file: 'Dockerfile'
- any-glob-to-any-file: 'Dockerfile-*'
- any-glob-to-any-file: 'exampleYmlFiles/*.yml'
Test:
- changed-files:
- any-glob-to-any-file: 'cucumber/**/*'
- any-glob-to-any-file: 'src/test**/*'
Github:
- changed-files:
- any-glob-to-any-file: '.github/**/*'

93
.github/labels.yml vendored Normal file
View File

@@ -0,0 +1,93 @@
# Labels names are important as they are used by Release Drafter to decide
# regarding where to record them in changelog or if to skip them.
#
# The repository labels will be automatically configured using this file and
# the GitHub Action https://github.com/marketplace/actions/github-labeler.
- name: "Back End"
color: "20CE6C"
description: "Issues related to back-end development"
from_name: "Back end"
- name: "Bug"
description: "Something isn't working"
color: "EB9CA6"
from_name: "bug"
- name: "dependencies"
description: "Pull requests that update a dependency file"
color: "5AA8FC"
- name: "Docker"
description: "Pull requests that update Docker code"
color: "1FCEFF"
from_name: "docker"
- name: "Documentation"
description: "Improvements or additions to documentation"
color: "35ABFF"
from_name: "documentation"
- name: "Done for next release"
color: "0CDBD1"
- name: "Done"
color: "60F13B"
- name: "duplicate"
description: "This issue or pull request already exists"
color: "CDD1D5"
- name: "enhancement"
description: "New feature or request"
color: "A0EEEE"
- name: "fix needs confirmation"
color: "60A1E7"
description: "Fix needs to be confirmed"
- name: "Front End"
color: "BBD2F1"
description: "Issues related to front-end development"
- name: "github-actions"
description: "Pull requests that update GitHub Actions code"
color: "999999"
from_name: "github_actions"
- name: "good first issue"
description: "Good for newcomers"
color: "C1B8FF"
- name: "help wanted"
description: "Extra attention is needed"
color: "00E6C4"
- name: "invalid"
description: "This doesn't seem right"
color: "E5E566"
- name: "Java"
description: "Pull requests that update Java code"
color: "FF9E1F"
from_name: "java"
- name: "Long-term Enhancement"
color: "BFDEC3"
description: "Enhancements planned for the long term"
- name: "more-info-needed"
color: "00E4F8"
description: "More information is needed"
- name: "needs investigation"
color: "B8C3A7"
description: "Issues that require further investigation"
- name: "Prioritised enhancement"
color: "4BA2EE"
description: "High-priority enhancements"
- name: "question"
description: "Further information is requested"
color: "D97EE5"
- name: "Translation"
color: "9FABF9"
from_name: "translation"
- name: "upstream"
color: "DEDEDE"
- name: "v2"
color: "FFFF00"
- name: "wontfix"
description: "This will not be worked on"
color: "FFFFFF"
- name: "Security"
color: "000000"
description: "Security-related issues or pull requests"
- name: "API"
color: "FFFF00"
description: "API-related issues or pull requests"
- name: "Test"
color: "FF9E1F"
description: "Testing-related issues or pull requests"
- name: "Stale"
color: "000000"

View File

@@ -10,9 +10,3 @@ Closes #(issue_number)
- [ ] I have performed a self-review of my own code - [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] My changes generate no new warnings - [ ] My changes generate no new warnings
## Contributor License Agreement
By submitting this pull request, I acknowledge and agree that my 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.
(This does not change the general open-source nature of Stirling-PDF, simply moving from one license to another license)

32
.github/release.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
changelog:
exclude:
labels:
- Documentation
- Test
- Github
categories:
- title: Bug Fixes
labels:
- Bug
- title: Enhancements
labels:
- enhancement
- title: Minor Enhancements
labels:
- Java
- Front End
- title: Docker Updates
labels:
- Docker
- title: Translation Changes
labels:
- Translation
- title: Other Changes
labels:
- "*"

View File

@@ -0,0 +1,254 @@
"""
Author: Ludy87
Description: This script processes .properties files for localization checks. It compares translation files in a branch with
a reference file to ensure consistency. The script performs two main checks:
1. Verifies that the number of lines (including comments and empty lines) in the translation files matches the reference file.
2. Ensures that all keys in the translation files are present in the reference file and vice versa.
The script also provides functionality to update the translation files to match the reference file by adding missing keys and
adjusting the format.
Usage:
python script_name.py --reference-file <path_to_reference_file> --branch <branch_name> [--files <list_of_changed_files>]
"""
import copy
import glob
import os
import argparse
import re
def parse_properties_file(file_path):
"""Parses a .properties file and returns a list of objects (including comments, empty lines, and line numbers)."""
properties_list = []
with open(file_path, "r", encoding="utf-8") as file:
for line_number, line in enumerate(file, start=1):
stripped_line = line.strip()
# Empty lines
if not stripped_line:
properties_list.append(
{"line_number": line_number, "type": "empty", "content": ""}
)
continue
# Comments
if stripped_line.startswith("#"):
properties_list.append(
{
"line_number": line_number,
"type": "comment",
"content": stripped_line,
}
)
continue
# Key-value pairs
match = re.match(r"^([^=]+)=(.*)$", line)
if match:
key, value = match.groups()
properties_list.append(
{
"line_number": line_number,
"type": "entry",
"key": key.strip(),
"value": value.strip(),
}
)
return properties_list
def write_json_file(file_path, updated_properties):
updated_lines = {entry["line_number"]: entry for entry in updated_properties}
# Sort by line numbers and retain comments and empty lines
all_lines = sorted(set(updated_lines.keys()))
original_format = []
for line in all_lines:
if line in updated_lines:
entry = updated_lines[line]
else:
entry = None
ref_entry = updated_lines[line]
if ref_entry["type"] in ["comment", "empty"]:
original_format.append(ref_entry)
elif entry is None:
# Add missing entries from the reference file
original_format.append(ref_entry)
elif entry["type"] == "entry":
# Replace entries with those from the current JSON
original_format.append(entry)
# Write back in the original format
with open(file_path, "w", encoding="utf-8") as file:
for entry in original_format:
if entry["type"] == "comment":
file.write(f"{entry['content']}\n")
elif entry["type"] == "empty":
file.write(f"{entry['content']}\n")
elif entry["type"] == "entry":
file.write(f"{entry['key']}={entry['value']}\n")
def update_missing_keys(reference_file, file_list, branch=""):
reference_properties = parse_properties_file(reference_file)
for file_path in file_list:
basename_current_file = os.path.basename(branch + file_path)
if (
basename_current_file == os.path.basename(reference_file)
or not file_path.endswith(".properties")
or not basename_current_file.startswith("messages_")
):
continue
current_properties = parse_properties_file(branch + file_path)
updated_properties = []
for ref_entry in reference_properties:
ref_entry_copy = copy.deepcopy(ref_entry)
for current_entry in current_properties:
if current_entry["type"] == "entry":
if ref_entry_copy["type"] != "entry":
continue
if ref_entry_copy["key"] == current_entry["key"]:
ref_entry_copy["value"] = current_entry["value"]
updated_properties.append(ref_entry_copy)
write_json_file(branch + file_path, updated_properties)
def check_for_missing_keys(reference_file, file_list, branch):
update_missing_keys(reference_file, file_list, branch + "/")
def read_properties(file_path):
with open(file_path, "r", encoding="utf-8") as file:
return file.read().splitlines()
def check_for_differences(reference_file, file_list, branch):
reference_branch = reference_file.split("/")[0]
basename_reference_file = os.path.basename(reference_file)
report = []
report.append(
f"### 📋 Checking with the file `{basename_reference_file}` from the `{reference_branch}` - Checking the `{branch}`"
)
reference_lines = read_properties(reference_file)
has_differences = False
only_reference_file = True
for file_path in file_list:
basename_current_file = os.path.basename(branch + "/" + file_path)
if (
basename_current_file == basename_reference_file
or not file_path.endswith(".properties")
or not basename_current_file.startswith("messages_")
):
continue
only_reference_file = False
report.append(f"#### 🗂️ **Checking File:** `{basename_current_file}`...")
current_lines = read_properties(branch + "/" + file_path)
reference_line_count = len(reference_lines)
current_line_count = len(current_lines)
if reference_line_count != current_line_count:
report.append("")
report.append("- **Test 1 Status:** ❌ Failed")
has_differences = True
if reference_line_count > current_line_count:
report.append(
f" - **Issue:** Missing lines! Comments, empty lines, or translation strings are missing. Details: {reference_line_count} (reference) vs {current_line_count} (current)."
)
elif reference_line_count < current_line_count:
report.append(
f" - **Issue:** Too many lines! Check your translation files! Details: {reference_line_count} (reference) vs {current_line_count} (current)."
)
# update_missing_keys(reference_file, [file_path], branch + "/")
else:
report.append("- **Test 1 Status:** ✅ Passed")
# Check for missing or extra keys
current_keys = []
reference_keys = []
for line in current_lines:
if not line.startswith("#") and line != "" and "=" in line:
key, _ = line.split("=", 1)
current_keys.append(key)
for line in reference_lines:
if not line.startswith("#") and line != "" and "=" in line:
key, _ = line.split("=", 1)
reference_keys.append(key)
current_keys_set = set(current_keys)
reference_keys_set = set(reference_keys)
missing_keys = current_keys_set.difference(reference_keys_set)
extra_keys = reference_keys_set.difference(current_keys_set)
missing_keys_list = list(missing_keys)
extra_keys_list = list(extra_keys)
if missing_keys_list or extra_keys_list:
has_differences = True
missing_keys_str = "`, `".join(missing_keys_list)
extra_keys_str = "`, `".join(extra_keys_list)
report.append("- **Test 2 Status:** ❌ Failed")
if missing_keys_list:
report.append(
f" - **Issue:** There are keys in ***{basename_current_file}*** `{missing_keys_str}` that are not present in ***{basename_reference_file}***!"
)
if extra_keys_list:
report.append(
f" - **Issue:** There are keys in ***{basename_reference_file}*** `{extra_keys_str}` that are not present in ***{basename_current_file}***!"
)
# update_missing_keys(reference_file, [file_path], branch + "/")
else:
report.append("- **Test 2 Status:** ✅ Passed")
# if has_differences:
# report.append("")
# report.append(f"#### 🚧 ***{basename_current_file}*** will be corrected...")
report.append("")
report.append("---")
report.append("")
# update_file_list = glob.glob(branch + "/src/**/messages_*.properties", recursive=True)
# update_missing_keys(reference_file, update_file_list)
# report.append("---")
# report.append("")
if has_differences:
report.append("## ❌ Overall Check Status: **_Failed_**")
else:
report.append("## ✅ Overall Check Status: **_Success_**")
if not only_reference_file:
print("\n".join(report))
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Find missing keys")
parser.add_argument(
"--reference-file",
required=True,
help="Path to the reference file.",
)
parser.add_argument(
"--branch",
type=str,
required=True,
help="Branch name.",
)
parser.add_argument(
"--files",
nargs="+",
required=False,
help="List of changed files, separated by spaces.",
)
args = parser.parse_args()
file_list = args.files
if file_list is None:
file_list = glob.glob(
os.getcwd() + "/src/**/messages_*.properties", recursive=True
)
update_missing_keys(args.reference_file, file_list)
else:
check_for_differences(args.reference_file, file_list, args.branch)

View File

@@ -1,4 +1,5 @@
"""check_tabulator.py""" """check_tabulator.py"""
import argparse import argparse
import sys import sys

20
.github/workflows/auto-labeler.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: "Pull Request Labeler"
on:
pull_request_target:
types: [opened, synchronize]
jobs:
labeler:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Apply Labels
uses: actions/labeler@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
configuration-path: .github/labeler-config.yml
sync-labels: true

View File

@@ -1,4 +1,4 @@
name: "Build repo" name: Build repo
on: on:
push: push:
@@ -17,20 +17,72 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix:
jdk-version: [17, 21]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up JDK 17 - name: Set up JDK ${{ matrix.jdk-version }}
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: "17" java-version: ${{ matrix.jdk-version }}
distribution: "temurin" distribution: "temurin"
- uses: gradle/actions/setup-gradle@v3 - name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
with: with:
gradle-version: 8.7 gradle-version: 8.7
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew build --no-build-cache run: ./gradlew build --no-build-cache
docker-compose-tests:
# if: github.event_name == 'push' && github.ref == 'refs/heads/main' ||
# (github.event_name == 'pull_request' &&
# contains(github.event.pull_request.labels.*.name, 'licenses') == false &&
# (
# contains(github.event.pull_request.labels.*.name, 'Front End') ||
# contains(github.event.pull_request.labels.*.name, 'Java') ||
# contains(github.event.pull_request.labels.*.name, 'Back End') ||
# contains(github.event.pull_request.labels.*.name, 'Security') ||
# contains(github.event.pull_request.labels.*.name, 'API') ||
# contains(github.event.pull_request.labels.*.name, 'Docker') ||
# contains(github.event.pull_request.labels.*.name, 'Test')
# )
# )
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Java 17
uses: actions/setup-java@v4
with:
java-version: "17"
distribution: "adopt"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Install Docker Compose
run: |
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.29.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.7"
- name: Pip requirements
run: |
pip install -r ./cucumber/requirements.txt
- name: Run Docker Compose Tests
run: |
chmod +x ./test.sh
./test.sh

202
.github/workflows/check_properties.yml vendored Normal file
View File

@@ -0,0 +1,202 @@
name: Check Properties Files
on:
pull_request_target:
types: [opened, synchronize, reopened]
paths:
- "src/main/resources/messages_*.properties"
push:
paths:
- "src/main/resources/messages_en_GB.properties"
permissions:
contents: write
pull-requests: write
jobs:
check-files:
if: github.event_name == 'pull_request_target'
runs-on: ubuntu-latest
steps:
- name: Checkout PR branch
uses: actions/checkout@v4
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.ref }}
path: pr-branch
fetch-depth: 0
- name: Checkout main branch
uses: actions/checkout@v4
with:
ref: main
path: main-branch
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install GitHub CLI
run: sudo apt-get update && sudo apt-get install -y gh
- name: Fetch PR changed files
id: fetch-pr-changes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Fetching PR changed files..."
cd pr-branch
gh repo set-default ${{ github.repository }}
gh pr view ${{ github.event.pull_request.number }} --json files -q ".files[].path" > ../changed_files.txt
cd ..
echo $(cat changed_files.txt)
BRANCH_PATH="pr-branch"
echo "BRANCH_PATH=${BRANCH_PATH}" >> $GITHUB_ENV
CHANGED_FILES=$(cat changed_files.txt | tr '\n' ' ')
echo "CHANGED_FILES=${CHANGED_FILES}" >> $GITHUB_ENV
echo "Changed files: ${CHANGED_FILES}"
echo "Branch: ${BRANCH_PATH}"
- name: Determine reference file
id: determine-file
run: |
echo "Determining reference file..."
if echo "${{ env.CHANGED_FILES }}" | grep -q 'src/main/resources/messages_en_GB.properties'; then
echo "REFERENCE_FILE=pr-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
else
echo "REFERENCE_FILE=main-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
fi
echo "REFERENCE_FILE=${{ env.REFERENCE_FILE }}"
- name: Show REFERENCE_FILE
run: echo "Reference file is set to ${{ env.REFERENCE_FILE }}"
- name: Run Python script to check files
id: run-check
run: |
python main-branch/.github/scripts/check_language_properties.py --reference-file ${{ env.REFERENCE_FILE }} --branch ${{ env.BRANCH_PATH }} --files ${{ env.CHANGED_FILES }} > failure.txt || true
- name: Capture output
id: capture-output
run: |
if [ -f failure.txt ] && [ -s failure.txt ]; then
echo "Test failed, capturing output..."
ERROR_OUTPUT=$(cat failure.txt)
echo "ERROR_OUTPUT<<EOF" >> $GITHUB_ENV
echo "$ERROR_OUTPUT" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
echo $ERROR_OUTPUT
else
echo "No errors found."
echo "ERROR_OUTPUT=" >> $GITHUB_ENV
fi
- name: Post comment on PR
if: env.ERROR_OUTPUT != ''
uses: actions/github-script@v7
with:
script: |
const { GITHUB_REPOSITORY, ERROR_OUTPUT } = process.env;
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
const prNumber = context.issue.number;
// Find existing comment
const comments = await github.rest.issues.listComments({
owner: repoOwner,
repo: repoName,
issue_number: prNumber
});
const comment = comments.data.find(c => c.body.includes("## 🚀 Translation Verification Summary"));
// Only allow the action user to update comments
const expectedActor = "github-actions[bot]";
if (comment && comment.user.login === expectedActor) {
// Update existing comment
await github.rest.issues.updateComment({
owner: repoOwner,
repo: repoName,
comment_id: comment.id,
body: `## 🚀 Translation Verification Summary\n\n\n${ERROR_OUTPUT}\n`
});
console.log("Updated existing comment.");
} else if (!comment) {
// Create new comment if no existing comment is found
await github.rest.issues.createComment({
owner: repoOwner,
repo: repoName,
issue_number: prNumber,
body: `## 🚀 Translation Verification Summary\n\n\n${ERROR_OUTPUT}\n`
});
console.log("Created new comment.");
} else {
console.log("Comment update attempt denied. Actor does not match.");
}
# - name: Set up git config
# run: |
# git config --global user.name "github-actions[bot]"
# git config --global user.email "github-actions[bot]@users.noreply.github.com"
# - name: Add translation keys
# run: |
# cd ${{ env.BRANCH_PATH }}
# git add src/main/resources/messages_*.properties
# git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
# git commit -m "Update translation files" || echo "No changes to commit"
# - name: Push
# if: env.CHANGES_DETECTED == 'true'
# run: |
# cd pr-branch
# git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.event.pull_request.head.repo.full_name }}.git
# git push origin ${{ github.head_ref }} || echo "Push failed: possibly no changes to push"
update-translations-main:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Run Python script to check files
id: run-check
run: |
python .github/scripts/check_language_properties.py --reference-file src/main/resources/messages_en_GB.properties --branch main
- name: Set up git config
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: Add translation keys
run: |
git add src/main/resources/messages_*.properties
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
- name: Create Pull Request
id: cpr
if: env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Update translation files"
committer: GitHub Action <action@github.com>
author: GitHub Action <action@github.com>
signoff: true
branch: update_translation_files
title: "Update translation files"
body: |
Auto-generated by [create-pull-request][1]
[1]: https://github.com/peter-evans/create-pull-request
labels: Translation
draft: false
delete-branch: true

View File

@@ -25,7 +25,7 @@ jobs:
java-version: "17" java-version: "17"
distribution: "adopt" distribution: "adopt"
- uses: gradle/actions/setup-gradle@v3 - uses: gradle/actions/setup-gradle@v4
- name: Run Gradle Command - name: Run Gradle Command
run: ./gradlew clean generateLicenseReport run: ./gradlew clean generateLicenseReport
@@ -36,8 +36,8 @@ jobs:
- name: Set up git config - name: Set up git config
run: | run: |
git config --global user.email "GitHub Action <action@github.com>" git config --global user.name "github-actions[bot]"
git config --global user.name "GitHub Action <action@github.com>" git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: Run git add - name: Run git add
run: | run: |
@@ -45,6 +45,7 @@ jobs:
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
- name: Create Pull Request - name: Create Pull Request
id: cpr
if: env.CHANGES_DETECTED == 'true' if: env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@v6 uses: peter-evans/create-pull-request@v6
with: with:
@@ -57,6 +58,22 @@ jobs:
title: "Update 3rd Party Licenses" title: "Update 3rd Party Licenses"
body: | body: |
Auto-generated by [create-pull-request][1] Auto-generated by [create-pull-request][1]
[1]: https://github.com/peter-evans/create-pull-request [1]: https://github.com/peter-evans/create-pull-request
labels: licenses
draft: false draft: false
delete-branch: true delete-branch: true
- name: Auto approve
if: steps.cpr.outputs.pull-request-operation == 'created'
run: gh pr review --approve "${{ steps.cpr.outputs.pull-request-number }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Enable auto-merge
if: steps.cpr.outputs.pull-request-operation == 'created'
uses: peter-evans/enable-pull-request-automerge@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
merge-method: squash # Choose the merge method: merge, squash, or rebase

24
.github/workflows/manage-label.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Manage labels
on:
schedule:
- cron: "30 20 * * *"
permissions:
contents: read
issues: write
jobs:
labeler:
name: Labeler
runs-on: ubuntu-latest
steps:
- name: Check out the repository
uses: actions/checkout@v4
- name: Run Labeler
uses: crazy-max/ghaction-github-labeler@v5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
yaml-file: .github/labels.yml
skip-delete: true

View File

@@ -22,7 +22,7 @@ jobs:
java-version: "17" java-version: "17"
distribution: "temurin" distribution: "temurin"
- uses: gradle/actions/setup-gradle@v3 - uses: gradle/actions/setup-gradle@v4
with: with:
gradle-version: 8.7 gradle-version: 8.7
@@ -72,7 +72,7 @@ jobs:
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }} type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push main Dockerfile - name: Build and push main Dockerfile
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
builder: ${{ steps.buildx.outputs.name }} builder: ${{ steps.buildx.outputs.name }}
context: . context: .
@@ -98,7 +98,7 @@ jobs:
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }} type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
- name: Build and push Dockerfile-ultra-lite - name: Build and push Dockerfile-ultra-lite
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
if: github.ref != 'refs/heads/main' if: github.ref != 'refs/heads/main'
with: with:
context: . context: .
@@ -111,7 +111,6 @@ jobs:
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }} build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8 platforms: linux/amd64,linux/arm64/v8
- name: Generate tags fat - name: Generate tags fat
id: meta3 id: meta3
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
@@ -125,7 +124,7 @@ jobs:
type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' }} type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' }}
- name: Build and push main Dockerfile fat - name: Build and push main Dockerfile fat
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
if: github.ref != 'refs/heads/main' if: github.ref != 'refs/heads/main'
with: with:
builder: ${{ steps.buildx.outputs.name }} builder: ${{ steps.buildx.outputs.name }}

View File

@@ -27,7 +27,7 @@ jobs:
java-version: "17" java-version: "17"
distribution: "temurin" distribution: "temurin"
- uses: gradle/actions/setup-gradle@v3 - uses: gradle/actions/setup-gradle@v4
with: with:
gradle-version: 8.7 gradle-version: 8.7

32
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Close stale issues
on:
schedule:
- cron: "30 0 * * *"
workflow_dispatch:
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: 30 days stale issues
uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 30
days-before-close: 7
stale-issue-message: >
This issue has been automatically marked as stale because it has had no recent activity.
It will be closed if no further activity occurs. Thank you for your contributions.
close-issue-message: >
This issue has been automatically closed because it has had no recent activity after being marked as stale.
Please reopen if you need further assistance.
stale-issue-label: "Stale"
remove-stale-when-updated: true
only-issue-labels: "more-info-needed"
days-before-pr-stale: -1 # Prevents PRs from being marked as stale
days-before-pr-close: -1 # Prevents PRs from being closed
start-date: '2024-07-06T00:00:00Z' # ISO 8601 Format

View File

@@ -18,7 +18,7 @@ jobs:
java-version: "17" java-version: "17"
distribution: "temurin" distribution: "temurin"
- uses: gradle/actions/setup-gradle@v3 - uses: gradle/actions/setup-gradle@v4
- name: Generate Swagger documentation - name: Generate Swagger documentation
run: ./gradlew generateOpenApiDocs run: ./gradlew generateOpenApiDocs

View File

@@ -7,7 +7,7 @@ on:
paths: paths:
- "build.gradle" - "build.gradle"
- "src/main/resources/messages_*.properties" - "src/main/resources/messages_*.properties"
- "scripts/translation_status.toml" - "scripts/ignore_translation.toml"
permissions: permissions:
contents: write contents: write
@@ -17,9 +17,9 @@ jobs:
sync-versions: sync-versions:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"
- name: Install dependencies - name: Install dependencies
@@ -28,15 +28,15 @@ jobs:
run: python .github/scripts/gradle_to_chart.py run: python .github/scripts/gradle_to_chart.py
- name: Set up git config - name: Set up git config
run: | run: |
git config --global user.email "GitHub Action <action@github.com>" git config --global user.name "github-actions[bot]"
git config --global user.name "GitHub Action <action@github.com>" git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: Run git add - name: Run git add
run: | run: |
git add . git add .
git diff --staged --quiet || git commit -m ":floppy_disk: Sync Versions git diff --staged --quiet || git commit -m ":floppy_disk: Sync Versions
> Made via sync_files.yml" || echo "no changes" > Made via sync_files.yml" || echo "no changes"
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v6.0.1 uses: peter-evans/create-pull-request@v6
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Update files commit-message: Update files
@@ -51,12 +51,13 @@ jobs:
[1]: https://github.com/peter-evans/create-pull-request [1]: https://github.com/peter-evans/create-pull-request
draft: false draft: false
delete-branch: true delete-branch: true
labels: github-actions
sync-readme: sync-readme:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"
- name: Install dependencies - name: Install dependencies
@@ -65,15 +66,15 @@ jobs:
run: python scripts/counter_translation.py run: python scripts/counter_translation.py
- name: Set up git config - name: Set up git config
run: | run: |
git config --global user.email "GitHub Action <action@github.com>" git config --global user.name "github-actions[bot]"
git config --global user.name "GitHub Action <action@github.com>" git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: Run git add - name: Run git add
run: | run: |
git add . git add .
git diff --staged --quiet || git commit -m ":memo: Sync README git diff --staged --quiet || git commit -m ":memo: Sync README
> Made via sync_files.yml" || echo "no changes" > Made via sync_files.yml" || echo "no changes"
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v6.0.1 uses: peter-evans/create-pull-request@v6
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Update files commit-message: Update files
@@ -88,3 +89,4 @@ jobs:
[1]: https://github.com/peter-evans/create-pull-request [1]: https://github.com/peter-evans/create-pull-request
draft: false draft: false
delete-branch: true delete-branch: true
labels: Documentation,Translation,github-actions

View File

@@ -1,47 +0,0 @@
name: Docker Compose Tests
on:
pull_request:
paths:
- "src/**"
- "**.gradle"
- "!src/main/java/resources/messages*"
- "exampleYmlFiles/**"
- "Dockerfile"
- "Dockerfile**"
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Java 17
uses: actions/setup-java@v4
with:
java-version: "17"
distribution: "adopt"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Install Docker Compose
run: |
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.26.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# sudo chmod +x /usr/local/bin/docker-compose
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.7"
- name: Pip requirements
run: |
pip install -r ./cucumber/requirements.txt
- name: Run Docker Compose Tests
run: |
chmod +x ./test.sh
./test.sh

47
.gitignore vendored
View File

@@ -1,5 +1,3 @@
### Eclipse ### ### Eclipse ###
.metadata .metadata
bin/ bin/
@@ -22,7 +20,6 @@ customFiles/
configs/ configs/
watchedFolders/ watchedFolders/
# Gradle # Gradle
.gradle .gradle
.lock .lock
@@ -119,12 +116,48 @@ watchedFolders/
*.db *.db
/build /build
/.vscode # Byte-compiled / optimized / DLL files
/.idea __pycache__/
*.py[cod]
*.pyo
# Virtual environments
.env*
.venv*
env*/
venv*/
ENV/
env.bak/
venv.bak/
# VS Code
/.vscode/**/*
!/.vscode/settings.json
# IntelliJ IDEA
.idea/
*.iml
out/
# Ignore Mac DS_Store files # Ignore Mac DS_Store files
.DS_Store .DS_Store
**/.DS_Store **/.DS_Store
#cucumber # cucumber
/cucumber/reports/** /cucumber/reports/**
# Certs
*.p12
*.pem
*.crt
*.cer
*.der
*.key
*.csr
# cache
.ruff_cache
.mypy_cache
.pytest_cache
.ipynb_checkpoints

View File

@@ -6,9 +6,11 @@ repos:
args: args:
- --fix - --fix
- --line-length=127 - --line-length=127
files: ^((.github/scripts)/.+)?[^/]+\.py$ files: ^((.github/scripts|scripts)/.+)?[^/]+\.py$
exclude: (split_photos.py)
- id: ruff-format - id: ruff-format
files: ^((.github/scripts)/.+)?[^/]+\.py$ files: ^((.github/scripts|scripts)/.+)?[^/]+\.py$
exclude: (split_photos.py)
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.2.6 rev: v2.2.6
hooks: hooks:
@@ -33,5 +35,5 @@ repos:
# args: ["--replace_with= "] # args: ["--replace_with= "]
entry: python .github/scripts/check_tabulator.py entry: python .github/scripts/check_tabulator.py
language: python language: python
exclude: ^src/main/resources/static/pdfjs/ exclude: ^(src/main/resources/static/pdfjs|src/main/resources/static/pdfjs-legacy)
files: ^.*(\.html|\.css|\.js)$ files: ^.*(\.html|\.css|\.js)$

53
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,53 @@
{
"java.compile.nullAnalysis.mode": "automatic",
"files.eol": "auto",
"java.configuration.updateBuildConfiguration": "interactive",
"black-formatter.args": ["--line-length", "127"],
"flake8.args": ["--max-line-length", "127"],
"pylint.args": ["max-line-length", "127"],
"[java]": {
"editor.tabSize": 4,
"editor.detectIndentation": false,
"editor.rulers": [127]
},
"[python]": {
"editor.tabSize": 2,
"editor.detectIndentation": false,
"editor.rulers": [127]
},
"[gradle-build]": {
"editor.tabSize": 4,
"editor.detectIndentation": false,
"editor.rulers": [127]
},
"[gradle]": {
"editor.tabSize": 4,
"editor.detectIndentation": false,
"editor.rulers": [127]
},
"[html]": {
"editor.tabSize": 2,
"editor.rulers": [127],
"files.trimFinalNewlines": false,
"files.insertFinalNewline": false
},
"[javascript]": {
"editor.tabSize": 2,
"editor.rulers": [127]
},
"[yaml]": {
"files.trimFinalNewlines": false,
"files.insertFinalNewline": false
},
"diffEditor.maxComputationTime": 0,
"editor.wordSegmenterLocales": null,
"editor.guides.bracketPairs": "active",
"editor.guides.bracketPairsHorizontal": "active",
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true,
"editor.indentSize": "tabSize",
"editor.stickyScroll.enabled": false,
"editor.minimap.enabled": false,
"editor.formatOnSave": true
}

40
DATABASE.md Normal file
View File

@@ -0,0 +1,40 @@
# New Database Backup and Import Functionality
**Full activation will take place on approximately January 5th, 2025!**
Why is the waiting time six months?
There are users who only install updates sporadically; if they skip the preparation, it can/will lead to data loss in the database.
## Functionality Overview
The newly introduced feature enhances the application with robust database backup and import capabilities. This feature is designed to ensure data integrity and provide a straightforward way to manage database backups. Here's how it works:
1. Automatic Backup Creation
- The system automatically creates a database backup every day at midnight. This ensures that there is always a recent backup available, minimizing the risk of data loss.
2. Manual Backup Export
- Admin actions that modify the user database trigger a manual export of the database. This keeps the backup up-to-date with the latest changes and provides an extra layer of data security.
3. Importing Database Backups
- Admin users can import a database backup either via the web interface or API endpoints. This allows for easy restoration of the database to a previous state in case of data corruption or other issues.
- The import process ensures that the database structure and data are correctly restored, maintaining the integrity of the application.
4. Managing Backup Files
- Admins can view a list of all existing backup files, along with their creation dates and sizes. This helps in managing storage and identifying the most recent or relevant backups.
- Backup files can be downloaded for offline storage or transferred to other environments, providing flexibility in database management.
- Unnecessary backup files can be deleted through the interface to free up storage space and maintain an organized backup directory.
## User Interface
### Web Interface
1. Upload SQL files to import database backups.
2. View details of existing backups, such as file names, creation dates, and sizes.
3. Download backup files for offline storage.
4. Delete outdated or unnecessary backup files.
### API Endpoints
1. Import database backups by uploading SQL files.
2. Download backup files.
3. Delete backup files.
This new functionality streamlines database management, ensuring that backups are always available and easy to manage, thus improving the reliability and resilience of the application.

View File

@@ -1,5 +1,5 @@
# Main stage # Main stage
FROM alpine:3.20.0 FROM alpine:3.20.2
# Copy necessary files # Copy necessary files
COPY scripts /scripts COPY scripts /scripts
@@ -39,16 +39,16 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
libreoffice \ libreoffice \
# pdftohtml # pdftohtml
poppler-utils \ poppler-utils \
# OCR MY PDF (unpaper for descew and other advanced featues) # OCR MY PDF (unpaper for descew and other advanced features)
ocrmypdf \ ocrmypdf \
tesseract-ocr-data-eng \ tesseract-ocr-data-eng \
# CV # CV
py3-opencv \ py3-opencv \
# python3/pip # python3/pip
python3 && \ python3 \
wget https://bootstrap.pypa.io/get-pip.py -qO - | python3 - --break-system-packages --no-cache-dir --upgrade && \ py3-pip && \
# uno unoconv and HTML # uno unoconv and HTML
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint && \ pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint pdf2image pillow && \
mv /usr/share/tessdata /usr/share/tessdata-original && \ mv /usr/share/tessdata /usr/share/tessdata-original && \
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \ mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
fc-cache -f -v && \ fc-cache -f -v && \

View File

@@ -9,10 +9,10 @@ COPY . .
# Build the application with DOCKER_ENABLE_SECURITY=false # Build the application with DOCKER_ENABLE_SECURITY=false
RUN DOCKER_ENABLE_SECURITY=true \ RUN DOCKER_ENABLE_SECURITY=true \
./gradlew clean build ./gradlew clean build
# Main stage # Main stage
FROM alpine:3.20.0 FROM alpine:3.20.2
# Copy necessary files # Copy necessary files
COPY scripts /scripts COPY scripts /scripts
@@ -31,8 +31,8 @@ ENV DOCKER_ENABLE_SECURITY=false \
PGID=1000 \ PGID=1000 \
UMASK=022 \ UMASK=022 \
FAT_DOCKER=true \ FAT_DOCKER=true \
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=true INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
# JDK for app # JDK for app
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
@@ -45,7 +45,6 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
tini \ tini \
bash \ bash \
curl \ curl \
calibre@testing \
shadow \ shadow \
su-exec \ su-exec \
openssl \ openssl \
@@ -62,10 +61,10 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
# CV # CV
py3-opencv \ py3-opencv \
# python3/pip # python3/pip
python3 && \ python3 \
wget https://bootstrap.pypa.io/get-pip.py -qO - | python3 - --break-system-packages --no-cache-dir --upgrade && \ py3-pip && \
# uno unoconv and HTML # uno unoconv and HTML
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint && \ pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint pdf2image pillow && \
mv /usr/share/tessdata /usr/share/tessdata-original && \ mv /usr/share/tessdata /usr/share/tessdata-original && \
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \ mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
fc-cache -f -v && \ fc-cache -f -v && \

View File

@@ -1,5 +1,5 @@
# use alpine # use alpine
FROM alpine:3.20.0 FROM alpine:3.20.2
ARG VERSION_TAG ARG VERSION_TAG

View File

@@ -15,7 +15,7 @@
| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | | | file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | | | img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | | | pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
| pdf-to-img | | ✔️ | | | | | | | | ✔️ | | | pdf-to-img | | ✔️ | | | | ✔️ | | | | ✔️ | |
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | | | pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | | | pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | |
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | | | pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
@@ -44,4 +44,4 @@
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
| repair | | | | ✔️ | ✔️ | | | ✔️ | | | | | repair | | | | ✔️ | ✔️ | | | ✔️ | | | |
| show-javascript | | | | ✔️ | | | | | | | ✔️ | | show-javascript | | | | ✔️ | | | | | | | ✔️ |
| sign | | | | ✔️ | | | | | | | ✔️ | | sign | | | | ✔️ | | | | | | | ✔️ |

695
LICENSE
View File

@@ -1,674 +1,21 @@
GNU GENERAL PUBLIC LICENSE MIT License
Version 3, 29 June 2007
Copyright (c) 2024 Stirling Tools
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies Permission is hereby granted, free of charge, to any person obtaining a copy
of this license document, but changing it is not allowed. of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
Preamble to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
The GNU General Public License is a free, copyleft license for furnished to do so, subject to the following conditions:
software and other kinds of works.
The above copyright notice and this permission notice shall be included in all
The licenses for most software and other practical works are designed copies or substantial portions of the Software.
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
share and change all versions of a program--to make sure it remains free IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
software for all its users. We, the Free Software Foundation, use the FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
GNU General Public License for most of our software; it applies also to AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
any other work released this way by its authors. You can apply it to LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
your programs, too. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

178
README.md
View File

@@ -5,10 +5,9 @@
[![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/Cn8pWhQRxZ) [![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/Cn8pWhQRxZ)
[![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Stirling-Tools/Stirling-PDF/) [![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Stirling-Tools/Stirling-PDF/)
[![GitHub Repo stars](https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social)](https://github.com/Stirling-Tools/stirling-pdf) [![GitHub Repo stars](https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social)](https://github.com/Stirling-Tools/stirling-pdf)
[![Paypal Donate](https://img.shields.io/badge/Paypal%20Donate-yellow?style=flat&logo=paypal)](https://www.paypal.com/donate/?hosted_button_id=MN7JPG5G6G3JL)
[![Github Sponsor](https://img.shields.io/badge/Github%20Sponsor-yellow?style=flat&logo=github)](https://github.com/sponsors/Frooodle)
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af) [![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
[<img src="https://www.ssdnodes.com/wp-content/uploads/2023/11/footer-logo.svg" alt="Name" height="40">](https://www.ssdnodes.com/manage/aff.php?aff=2216&register=true)
This is a robust, locally hosted web-based PDF manipulation tool using Docker. It enables you to carry out various operations on PDF files, including splitting, merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application has evolved to encompass a comprehensive set of features, addressing all your PDF requirements. This is a robust, locally hosted web-based PDF manipulation tool using Docker. It enables you to carry out various operations on PDF files, including splitting, merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application has evolved to encompass a comprehensive set of features, addressing all your PDF requirements.
@@ -21,10 +20,12 @@ All files and PDFs exist either exclusively on the client side, reside in server
## Features ## Features
- Dark mode support. - Dark mode support.
- Custom download options (see [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/images/settings.png) for example) - Custom download options
- Parallel file processing and downloads - Parallel file processing and downloads
- Custom 'Pipelines' to run multiple features in a queue
- API for integration with external scripts - API for integration with external scripts
- Optional Login and Authentication support (see [here](https://github.com/Stirling-Tools/Stirling-PDF/tree/main#login-authentication) for documentation) - Optional Login and Authentication support (see [here](https://github.com/Stirling-Tools/Stirling-PDF/tree/main#login-authentication) for documentation)
- Database Backup and Import (see [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DATABASE.md) for documentation)
## **PDF Features** ## **PDF Features**
@@ -44,6 +45,7 @@ All files and PDFs exist either exclusively on the client side, reside in server
- Auto Split PDF (With physically scanned page dividers). - Auto Split PDF (With physically scanned page dividers).
- Extract page(s). - Extract page(s).
- Convert PDF to a single page. - Convert PDF to a single page.
- Overlay PDFs ontop of each other
### **Conversion Operations** ### **Conversion Operations**
@@ -80,9 +82,11 @@ All files and PDFs exist either exclusively on the client side, reside in server
- Edit metadata. - Edit metadata.
- Flatten PDFs. - Flatten PDFs.
- Get all information on a PDF to view or export as JSON. - Get all information on a PDF to view or export as JSON.
- Show/Detect embedded Javascript
For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md) For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
Demo of the app is available [here](https://stirlingpdf.io). username: demo, password: demo
Demo of the app is available [here](https://stirlingpdf.io).
## Technologies used ## Technologies used
@@ -105,33 +109,36 @@ Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LocalRunGui
https://hub.docker.com/r/frooodle/s-pdf https://hub.docker.com/r/frooodle/s-pdf
Stirling PDF has 2 different versions, a Full version and ultra-Lite version. Depending on the types of features you use you may want a smaller image to save on space. Stirling PDF has 3 different versions, a Full version and ultra-Lite version as well as a 'Fat' version. Depending on the types of features you use you may want a smaller image to save on space.
To see what the different versions offer please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md) To see what the different versions offer please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md)
For people that don't mind about space optimization just use the latest tag. For people that don't mind about space optimization just use the latest tag.
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest?label=Stirling-PDF%20Full) ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest?label=Stirling-PDF%20Full)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest-ultra-lite?label=Stirling-PDF%20Ultra-Lite) ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest-ultra-lite?label=Stirling-PDF%20Ultra-Lite)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest-fat?label=Stirling-PDF%20Fat)
Docker Run Please note in below examples you may need to change the volume paths as needed, current examples install them to the current working directory
eg ``./extraConfigs:/configs`` to ``/opt/stirlingpdf/extraConfigs:/configs``
### Docker Run
```bash ```bash
docker run -d \ docker run -d \
-p 8080:8080 \ -p 8080:8080 \
-v /location/of/trainingData:/usr/share/tessdata \ -v ./trainingData:/usr/share/tessdata \
-v /location/of/extraConfigs:/configs \ -v ./extraConfigs:/configs \
-v /location/of/logs:/logs \ -v ./logs:/logs \
-e DOCKER_ENABLE_SECURITY=false \ -e DOCKER_ENABLE_SECURITY=false \
-e INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \ -e INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
-e LANGS=en_GB \ -e LANGS=en_GB \
--name stirling-pdf \ --name stirling-pdf \
frooodle/s-pdf:latest frooodle/s-pdf:latest
Can also add these for customisation but are not required Can also add these for customisation but are not required
-v /location/of/customFiles:/customFiles \ -v /location/of/customFiles:/customFiles \
``` ```
Docker Compose ### Docker Compose
```yaml ```yaml
version: '3.3' version: '3.3'
@@ -141,10 +148,10 @@ services:
ports: ports:
- '8080:8080' - '8080:8080'
volumes: volumes:
- /location/of/trainingData:/usr/share/tessdata #Required for extra OCR languages - ./trainingData:/usr/share/tessdata #Required for extra OCR languages
- /location/of/extraConfigs:/configs - ./extraConfigs:/configs
# - /location/of/customFiles:/customFiles/ # - ./customFiles:/customFiles/
# - /location/of/logs:/logs/ # - ./logs:/logs/
environment: environment:
- DOCKER_ENABLE_SECURITY=false - DOCKER_ENABLE_SECURITY=false
- INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false - INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
@@ -159,41 +166,46 @@ Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR
## Supported Languages ## Supported Languages
Stirling PDF currently supports 32! Stirling PDF currently supports 38!
| Language | Progress | | Language | Progress |
| ------------------------------------------- | -------------------------------------- | | ------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![44%](https://geps.dev/progress/44) |
| Basque (Euskara) (eu_ES) | ![60%](https://geps.dev/progress/60) |
| Bulgarian (Български) (bg_BG) | ![92%](https://geps.dev/progress/92) |
| Catalan (Català) (ca_CA) | ![47%](https://geps.dev/progress/47) |
| Croatian (Hrvatski) (hr_HR) | ![92%](https://geps.dev/progress/92) |
| Czech (Česky) (cs_CZ) | ![88%](https://geps.dev/progress/88) |
| Danish (Dansk) (da_DK) | ![9%](https://geps.dev/progress/9) |
| Dutch (Nederlands) (nl_NL) | ![94%](https://geps.dev/progress/94) |
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) | | English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) | | English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| Arabic (العربية) (ar_AR) | ![46%](https://geps.dev/progress/46) | | French (Français) (fr_FR) | ![91%](https://geps.dev/progress/91) |
| German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) | | German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) |
| French (Français) (fr_FR) | ![93%](https://geps.dev/progress/93) | | Greek (Ελληνικά) (el_GR) | ![80%](https://geps.dev/progress/80) |
| Spanish (Español) (es_ES) | ![93%](https://geps.dev/progress/93) | | Hindi (हिंदी) (hi_IN) | ![75%](https://geps.dev/progress/75) |
| Simplified Chinese (简体中文) (zh_CN) | ![94%](https://geps.dev/progress/94) | | Hungarian (Magyar) (hu_HU) | ![74%](https://geps.dev/progress/74) |
| Traditional Chinese (繁體中文) (zh_TW) | ![98%](https://geps.dev/progress/98) | | Indonesia (Bahasa Indonesia) (id_ID) | ![74%](https://geps.dev/progress/74) |
| Catalan (Català) (ca_CA) | ![49%](https://geps.dev/progress/49) | | Irish (Gaeilge) (ga_IE) | ![96%](https://geps.dev/progress/96) |
| Italian (Italiano) (it_IT) | ![98%](https://geps.dev/progress/98) | | Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) |
| Swedish (Svenska) (sv_SE) | ![40%](https://geps.dev/progress/40) | | Japanese (日本語) (ja_JP) | ![90%](https://geps.dev/progress/90) |
| Polish (Polski) (pl_PL) | ![42%](https://geps.dev/progress/42) | | Korean (한국어) (ko_KR) | ![82%](https://geps.dev/progress/82) |
| Romanian (Română) (ro_RO) | ![39%](https://geps.dev/progress/39) | | Norwegian (Norsk) (no_NB) | ![96%](https://geps.dev/progress/96) |
| Korean (한국어) (ko_KR) | ![86%](https://geps.dev/progress/86) | | Polish (Polski) (pl_PL) | ![90%](https://geps.dev/progress/90) |
| Portuguese Brazilian (Português) (pt_BR) | ![61%](https://geps.dev/progress/61) | | Portuguese (Português) (pt_PT) | ![76%](https://geps.dev/progress/76) |
| Russian (Русский) (ru_RU) | ![86%](https://geps.dev/progress/86) | | Portuguese Brazilian (Português) (pt_BR) | ![99%](https://geps.dev/progress/99) |
| Basque (Euskara) (eu_ES) | ![63%](https://geps.dev/progress/63) | | Romanian (Română) (ro_RO) | ![38%](https://geps.dev/progress/38) |
| Japanese (日本語) (ja_JP) | ![86%](https://geps.dev/progress/86) | | Russian (Русский) (ru_RU) | ![82%](https://geps.dev/progress/82) |
| Dutch (Nederlands) (nl_NL) | ![83%](https://geps.dev/progress/83) | | Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![76%](https://geps.dev/progress/76) |
| Greek (Ελληνικά) (el_GR) | ![84%](https://geps.dev/progress/84) | | Simplified Chinese (简体中文) (zh_CN) | ![96%](https://geps.dev/progress/96) |
| Turkish (Türkçe) (tr_TR) | ![96%](https://geps.dev/progress/96) | | Slovakian (Slovensky) (sk_SK) | ![90%](https://geps.dev/progress/90) |
| Indonesia (Bahasa Indonesia) (id_ID) | ![78%](https://geps.dev/progress/78) | | Spanish (Español) (es_ES) | ![99%](https://geps.dev/progress/99) |
| Hindi (हिंदी) (hi_IN) | ![79%](https://geps.dev/progress/79) | | Swedish (Svenska) (sv_SE) | ![38%](https://geps.dev/progress/38) |
| Hungarian (Magyar) (hu_HU) | ![77%](https://geps.dev/progress/77) | | Thai (ไทย) (th_TH) | ![97%](https://geps.dev/progress/97) |
| Bulgarian (Български) (bg_BG) | ![96%](https://geps.dev/progress/96) | | Traditional Chinese (繁體中文) (zh_TW) | ![96%](https://geps.dev/progress/96) |
| Sebian Latin alphabet (Srpski) (sr_LATN_RS) | ![80%](https://geps.dev/progress/80) | | Turkish (Türkçe) (tr_TR) | ![97%](https://geps.dev/progress/97) |
| Ukrainian (Українська) (uk_UA) | ![85%](https://geps.dev/progress/85) | | Ukrainian (Українська) (uk_UA) | ![87%](https://geps.dev/progress/87) |
| Slovakian (Slovensky) (sk_SK) | ![93%](https://geps.dev/progress/93) | | Vietnamese (Tiếng Việt) (vi_VN) | ![97%](https://geps.dev/progress/97) |
| Czech (Česky) (cs_CZ) | ![92%](https://geps.dev/progress/92) |
| Croatian (Hrvatski) (hr_HR) | ![97%](https://geps.dev/progress/97) |
| Norwegian (Norsk) (no_NB) | ![97%](https://geps.dev/progress/97) |
## Contributing (creating issues, translations, fixing bugs, etc.) ## Contributing (creating issues, translations, fixing bugs, etc.)
@@ -205,7 +217,7 @@ Stirling PDF allows easy customization of the app.
Includes things like Includes things like
- Custom application name - Custom application name
- Custom slogans, icons, HTML, images CSS etc (via file overrides) - Custom slogans, icons, HTML, images CSS etc (via file overrides)
There are two options for this, either using the generated settings file ``settings.yml`` There are two options for this, either using the generated settings file ``settings.yml``
This file is located in the ``/configs`` directory and follows standard YAML formatting This file is located in the ``/configs`` directory and follows standard YAML formatting
@@ -214,11 +226,11 @@ Environment variables are also supported and would override the settings file
For example in the settings.yml you have For example in the settings.yml you have
```yaml ```yaml
system: security:
enableLogin: 'true' enableLogin: 'true'
``` ```
To have this via an environment variable you would have ``SYSTEM_ENABLELOGIN`` To have this via an environment variable you would have ``SECURITY_ENABLELOGIN``
The Current list of settings is The Current list of settings is
@@ -226,37 +238,39 @@ The Current list of settings is
security: security:
enableLogin: false # set to 'true' to enable login enableLogin: false # set to 'true' to enable login
csrfDisabled: true # Set to 'true' to disable CSRF protection (not recommended for production) csrfDisabled: true # Set to 'true' to disable CSRF protection (not recommended for production)
loginAttemptCount: 5 # lock user account after 5 tries loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
# initialLogin: loginMethod: all # 'all' (Login Username/Password and OAuth2[must be enabled and configured]), 'normal'(only Login with Username/Password) or 'oauth2'(only Login with OAuth2)
# username: "admin" # Initial username for the first login initialLogin:
# password: "stirling" # Initial password for the first login username: '' # Initial username for the first login
# oauth2: password: '' # Initial password for the first login
# enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work) oauth2:
# issuer: "" # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) end-point enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
# clientId: "" # Client ID from your provider client:
# clientSecret: "" # Client Secret from your provider keycloak:
# autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users issuer: '' # URL of the Keycloak realm's OpenID Connect Discovery endpoint
# useAsUsername: "email" # Default is 'email'; custom fields can be used as the username clientId: '' # Client ID for Keycloak OAuth2
# scopes: "openid, profile, email" # Specify the scopes for which the application will request permissions clientSecret: '' # Client Secret for Keycloak OAuth2
# provider: "google" # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak' scopes: openid, profile, email # Scopes for Keycloak OAuth2
# client: useAsUsername: preferred_username # Field to use as the username for Keycloak OAuth2
# google: google:
# clientId: "" # Client ID for Google OAuth2 clientId: '' # Client ID for Google OAuth2
# clientSecret: "" # Client Secret for Google OAuth2 clientSecret: '' # Client Secret for Google OAuth2
# scopes: "https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile" # Scopes for Google OAuth2 scopes: https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile # Scopes for Google OAuth2
# useAsUsername: "email" # Field to use as the username for Google OAuth2 useAsUsername: email # Field to use as the username for Google OAuth2
# github: github:
# clientId: "" # Client ID for GitHub OAuth2 clientId: '' # Client ID for GitHub OAuth2
# clientSecret: "" # Client Secret for GitHub OAuth2 clientSecret: '' # Client Secret for GitHub OAuth2
# scopes: "read:user" # Scope for GitHub OAuth2 scopes: read:user # Scope for GitHub OAuth2
# useAsUsername: "login" # Field to use as the username for GitHub OAuth2 useAsUsername: login # Field to use as the username for GitHub OAuth2
# keycloak: issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) end-point
# issuer: "http://192.168.0.123:8888/realms/stirling-pdf" # URL of the Keycloak realm's OpenID Connect Discovery endpoint clientId: '' # Client ID from your provider
# clientId: "stirling-pdf" # Client ID for Keycloak OAuth2 clientSecret: '' # Client Secret from your provider
# clientSecret: "" # Client Secret for Keycloak OAuth2 autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
# scopes: "openid, profile, email" # Scopes for Keycloak OAuth2 blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
# useAsUsername: "email" # Field to use as the username for Keycloak OAuth2 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'
system: 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)
@@ -267,9 +281,9 @@ system:
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template html files customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template html files
ui: ui:
appName: null # Application's visible name appName: '' # Application's visible name
homeDescription: null # Short description or tagline shown on homepage. homeDescription: '' # Short description or tagline shown on homepage.
appNameNavbar: null # Name displayed on the navigation bar appNameNavbar: '' # Name displayed on the navigation bar
endpoints: endpoints:
toRemove: [] # List endpoints to disable (e.g. ['img-to-pdf', 'remove-pages']) toRemove: [] # List endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
@@ -303,7 +317,7 @@ For those wanting to use Stirling-PDFs backend API to link with their own custom
![stirling-login](images/login-light.png) ![stirling-login](images/login-light.png)
### Prerequisites: ### Prerequisites
- User must have the folder ./configs volumed within docker so that it is retained during updates. - User must have the folder ./configs volumed within docker so that it is retained during updates.
- Docker users must download the security jar version by setting ``DOCKER_ENABLE_SECURITY`` to ``true`` in environment variables. - Docker users must download the security jar version by setting ``DOCKER_ENABLE_SECURITY`` to ``true`` in environment variables.

View File

@@ -1,4 +1,7 @@
| Technology | Ultra-Lite | Full | |All versions in a Docker environment can download Calibre as a optional extra at runtime to support `book-to-pdf` and `pdf-to-book` using parameter ``INSTALL_BOOK_AND_ADVANCED_HTML_OPS``.
The 'Fat' container contains all those found in 'Full' with security jar along with this Calibre install.
Technology | Ultra-Lite | Full |
| ---------- | :--------: | :---: | | ---------- | :--------: | :---: |
| Java | ✔️ | ✔️ | | Java | ✔️ | ✔️ |
| JavaScript | ✔️ | ✔️ | | JavaScript | ✔️ | ✔️ |

View File

@@ -1,24 +1,43 @@
plugins { plugins {
id 'java' id "java"
id 'org.springframework.boot' version '3.2.4' id "org.springframework.boot" version "3.3.3"
id 'io.spring.dependency-management' version '1.1.3' id "io.spring.dependency-management" version "1.1.6"
id 'org.springdoc.openapi-gradle-plugin' version '1.8.0' id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
id "io.swagger.swaggerhub" version "1.3.2" id "io.swagger.swaggerhub" version "1.3.2"
id 'edu.sc.seis.launch4j' version '3.0.5' id "edu.sc.seis.launch4j" version "3.0.6"
id 'com.diffplug.spotless' version '6.25.0' id "com.diffplug.spotless" version "6.25.0"
id 'com.github.jk1.dependency-license-report' version '2.6' id "com.github.jk1.dependency-license-report" version "2.9"
//id "nebula.lint" version "19.0.3"
} }
import com.github.jk1.license.render.* import com.github.jk1.license.render.*
group = 'stirling.software' ext {
version = '0.26.1' springBootVersion = "3.3.3"
pdfboxVersion = "3.0.3"
logbackVersion = "1.5.7"
imageioVersion = "3.11.0"
lombokVersion = "1.18.34"
bouncycastleVersion = "1.78.1"
}
//17 is lowest but we support and recommend 21 group = "stirling.software"
sourceCompatibility = '17' version = "0.28.3"
java {
// 17 is lowest but we support and recommend 21
sourceCompatibility = JavaVersion.VERSION_17
}
repositories { repositories {
mavenCentral() 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 { licenseReport {
@@ -28,15 +47,19 @@ licenseReport {
sourceSets { sourceSets {
main { main {
java { java {
if (System.getenv('DOCKER_ENABLE_SECURITY') == 'false') { if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") {
exclude 'stirling/software/SPDF/config/security/**' exclude "stirling/software/SPDF/config/security/**"
exclude 'stirling/software/SPDF/controller/api/UserController.java' exclude "stirling/software/SPDF/controller/api/UserController.java"
exclude 'stirling/software/SPDF/controller/web/AccountWebController.java' exclude "stirling/software/SPDF/controller/api/DatabaseController.java"
exclude 'stirling/software/SPDF/model/ApiKeyAuthenticationToken.java' exclude "stirling/software/SPDF/controller/web/AccountWebController.java"
exclude 'stirling/software/SPDF/model/Authority.java' exclude "stirling/software/SPDF/controller/web/DatabaseWebController.java"
exclude 'stirling/software/SPDF/model/PersistentLogin.java' exclude "stirling/software/SPDF/model/ApiKeyAuthenticationToken.java"
exclude 'stirling/software/SPDF/model/User.java' exclude "stirling/software/SPDF/model/AttemptCounter.java"
exclude 'stirling/software/SPDF/repository/**' exclude "stirling/software/SPDF/model/Authority.java"
exclude "stirling/software/SPDF/model/PersistentLogin.java"
exclude "stirling/software/SPDF/model/SessionEntity.java"
exclude "stirling/software/SPDF/model/User.java"
exclude "stirling/software/SPDF/repository/**"
} }
} }
} }
@@ -49,34 +72,34 @@ openApi {
} }
launch4j { launch4j {
icon = "${projectDir}/src/main/resources/static/favicon.ico" icon = "${projectDir}/src/main/resources/static/favicon.ico"
outfile="Stirling-PDF.exe" outfile="Stirling-PDF.exe"
headerType="console" headerType="console"
jarTask = tasks.bootJar jarTask = tasks.bootJar
errTitle="Encountered error, Do you have Java 21?" errTitle="Encountered error, Do you have Java 21?"
downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe" downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe"
variables=["BROWSER_OPEN=true", "ENDPOINTS_GROUPS_TO_REMOVE=CLI"] variables=["BROWSER_OPEN=true", "ENDPOINTS_GROUPS_TO_REMOVE=CLI"]
jreMinVersion="17" jreMinVersion="17"
mutexName="Stirling-PDF" mutexName="Stirling-PDF"
windowTitle="Stirling-PDF" windowTitle="Stirling-PDF"
messagesStartupError="An error occurred while starting Stirling-PDF" messagesStartupError="An error occurred while starting Stirling-PDF"
//messagesJreNotFoundError="This application requires a Java Runtime Environment, Please download Java 17." // messagesJreNotFoundError="This application requires a Java Runtime Environment, Please download Java 17."
messagesJreVersionError="You are running the wrong version of Java, Please download Java 21." messagesJreVersionError="You are running the wrong version of Java, Please download Java 21."
messagesLauncherError="Java is corrupted. Please uninstall and then install Java 21." messagesLauncherError="Java is corrupted. Please uninstall and then install Java 21."
messagesInstanceAlreadyExists="Stirling-PDF is already running." messagesInstanceAlreadyExists="Stirling-PDF is already running."
} }
spotless { spotless {
java { java {
target project.fileTree('src/main/java') target project.fileTree('src/main/java')
googleJavaFormat('1.19.1').aosp().reorderImports(false) googleJavaFormat("1.22.0").aosp().reorderImports(false)
importOrder('java', 'javax', 'org', 'com', 'net', 'io') importOrder("java", "javax", "org", "com", "net", "io")
toggleOffOn() toggleOffOn()
trimTrailingWhitespace() trimTrailingWhitespace()
indentWithSpaces() indentWithSpaces()
@@ -84,130 +107,148 @@ spotless {
} }
} }
//gradleLint {
// rules=['unused-dependency']
// }
tasks.wrapper {
gradleVersion = "8.7"
}
//tasks.withType(JavaCompile) {
// options.compilerArgs << "-Xlint:deprecation"
//}
configurations.all {
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
}
dependencies { dependencies {
//security updates //security updates
implementation 'ch.qos.logback:logback-classic:1.5.3' implementation "org.springframework:spring-webmvc:6.1.9"
implementation 'ch.qos.logback:logback-core:1.5.3'
implementation 'org.springframework:spring-webmvc:6.1.5'
implementation("io.github.pixee:java-security-toolkit:1.1.3") implementation("io.github.pixee:java-security-toolkit:1.2.0")
// implementation "org.yaml:snakeyaml:2.2"
implementation 'com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4'
implementation 'org.yaml:snakeyaml:2.2'
// Exclude Tomcat and include Jetty // Exclude Tomcat and include Jetty
implementation('org.springframework.boot:spring-boot-starter-web:3.2.4') { implementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion")
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat' implementation "org.springframework.boot:spring-boot-starter-jetty:$springBootVersion"
}
implementation 'org.springframework.boot:spring-boot-starter-jetty:3.2.4'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.2.4'
if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') { implementation "org.springframework.boot:spring-boot-starter-thymeleaf:$springBootVersion"
implementation 'org.springframework.boot:spring-boot-starter-security:3.2.4'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE' if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
implementation "org.springframework.boot:spring-boot-starter-data-jpa:3.2.4" implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client:3.2.4' runtimeOnly "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"
//2.2.x requires rebuild of DB file.. need migration path //2.2.x requires rebuild of DB file.. need migration path
implementation "com.h2database:h2:2.1.214" runtimeOnly "com.h2database:h2:2.1.214"
} // implementation "com.h2database:h2:2.2.224"
implementation 'org.springframework.security:spring-security-saml2-service-provider:6.3.3'
}
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.2.4' testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
// Batik // Batik
implementation 'org.apache.xmlgraphics:batik-all:1.17' implementation "org.apache.xmlgraphics:batik-all:1.17"
// TwelveMonkeys // TwelveMonkeys
implementation 'com.twelvemonkeys.imageio:imageio-batik:3.10.1' runtimeOnly "com.twelvemonkeys.imageio:imageio-batik:$imageioVersion"
implementation 'com.twelvemonkeys.imageio:imageio-bmp:3.10.1' runtimeOnly "com.twelvemonkeys.imageio:imageio-bmp:$imageioVersion"
// implementation 'com.twelvemonkeys.imageio:imageio-hdr:3.10.1' // runtimeOnly "com.twelvemonkeys.imageio:imageio-hdr:$imageioVersion"
// implementation 'com.twelvemonkeys.imageio:imageio-icns:3.10.1' // runtimeOnly "com.twelvemonkeys.imageio:imageio-icns:$imageioVersion"
// implementation 'com.twelvemonkeys.imageio:imageio-iff:3.10.1' // runtimeOnly "com.twelvemonkeys.imageio:imageio-iff:$imageioVersion"
implementation 'com.twelvemonkeys.imageio:imageio-jpeg:3.10.1' runtimeOnly "com.twelvemonkeys.imageio:imageio-jpeg:$imageioVersion"
// implementation 'com.twelvemonkeys.imageio:imageio-pcx:3.10.1' // runtimeOnly "com.twelvemonkeys.imageio:imageio-pcx:$imageioVersion@
// implementation 'com.twelvemonkeys.imageio:imageio-pict:3.10.1' // runtimeOnly "com.twelvemonkeys.imageio:imageio-pict:$imageioVersion"
// implementation 'com.twelvemonkeys.imageio:imageio-pnm:3.10.1' // runtimeOnly "com.twelvemonkeys.imageio:imageio-pnm:$imageioVersion"
// implementation 'com.twelvemonkeys.imageio:imageio-psd:3.10.1' // runtimeOnly "com.twelvemonkeys.imageio:imageio-psd:$imageioVersion"
// implementation 'com.twelvemonkeys.imageio:imageio-sgi:3.10.1' // runtimeOnly "com.twelvemonkeys.imageio:imageio-sgi:$imageioVersion"
// implementation 'com.twelvemonkeys.imageio:imageio-tga:3.10.1' // runtimeOnly "com.twelvemonkeys.imageio:imageio-tga:$imageioVersion"
// implementation 'com.twelvemonkeys.imageio:imageio-thumbsdb:3.10.1' // runtimeOnly "com.twelvemonkeys.imageio:imageio-thumbsdb:$imageioVersion"
implementation 'com.twelvemonkeys.imageio:imageio-tiff:3.10.1' runtimeOnly "com.twelvemonkeys.imageio:imageio-tiff:$imageioVersion"
implementation 'com.twelvemonkeys.imageio:imageio-webp:3.10.1' runtimeOnly "com.twelvemonkeys.imageio:imageio-webp:$imageioVersion"
// implementation 'com.twelvemonkeys.imageio:imageio-xwd:3.10.1' // runtimeOnly "com.twelvemonkeys.imageio:imageio-xwd:$imageioVersion"
implementation 'commons-io:commons-io:2.16.1'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
implementation "commons-io:commons-io:2.16.1"
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0"
//general PDF //general PDF
// https://mvnrepository.com/artifact/com.opencsv/opencsv // https://mvnrepository.com/artifact/com.opencsv/opencsv
implementation ('com.opencsv:opencsv:5.9') { implementation ("com.opencsv:opencsv:5.9") {
exclude group: 'commons-logging', module: 'commons-logging' exclude group: "commons-logging", module: "commons-logging"
} }
implementation ('org.apache.pdfbox:pdfbox:3.0.2'){ implementation ("org.apache.pdfbox:pdfbox:$pdfboxVersion") {
exclude group: 'commons-logging', module: 'commons-logging' exclude group: "commons-logging", module: "commons-logging"
} }
implementation ('org.apache.pdfbox:xmpbox:3.0.2'){ implementation ("org.apache.pdfbox:xmpbox:$pdfboxVersion") {
exclude group: 'commons-logging', module: 'commons-logging' exclude group: "commons-logging", module: "commons-logging"
} }
implementation 'org.bouncycastle:bcprov-jdk18on:1.77' implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
implementation 'org.bouncycastle:bcpkix-jdk18on:1.77'
implementation 'org.springframework.boot:spring-boot-starter-actuator:3.2.4' implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation 'io.micrometer:micrometer-core:1.12.4' implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
implementation group: 'com.google.zxing', name: 'core', version: '3.5.3' implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
implementation "io.micrometer:micrometer-core:1.13.3"
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
// https://mvnrepository.com/artifact/org.commonmark/commonmark // https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation 'org.commonmark:commonmark:0.22.0' implementation "org.commonmark:commonmark:0.22.0"
implementation 'org.commonmark:commonmark-ext-gfm-tables:0.22.0' implementation "org.commonmark:commonmark-ext-gfm-tables:0.22.0"
// https://mvnrepository.com/artifact/com.github.vladimir-bukhtoyarov/bucket4j-core // https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0' implementation "com.bucket4j:bucket4j_jdk17-core:8.14.0"
implementation "com.fathzer:javaluator:3.0.4"
implementation 'com.fathzer:javaluator:3.0.3' developmentOnly("org.springframework.boot:spring-boot-devtools:$springBootVersion")
compileOnly "org.projectlombok:lombok:$lombokVersion"
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
developmentOnly("org.springframework.boot:spring-boot-devtools:3.2.4") testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
compileOnly 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.32'
} }
tasks.withType(JavaCompile).configureEach { tasks.withType(JavaCompile).configureEach {
dependsOn 'spotlessApply' options.encoding = "UTF-8"
dependsOn "spotlessApply"
} }
compileJava { compileJava {
options.compilerArgs << '-parameters' options.compilerArgs << "-parameters"
} }
task writeVersion { task writeVersion {
def propsFile = file('src/main/resources/version.properties') def propsFile = file("src/main/resources/version.properties")
def props = new Properties() def props = new Properties()
props.setProperty('version', version) props.setProperty("version", version)
props.store(propsFile.newWriter(), null) props.store(propsFile.newWriter(), null)
} }
swaggerhubUpload { swaggerhubUpload {
//dependsOn generateOpenApiDocs // Depends on your task generating Swagger docs //dependsOn generateOpenApiDocs // Depends on your task generating Swagger docs
api 'Stirling-PDF' // The name of your API on SwaggerHub api "Stirling-PDF" // The name of your API on SwaggerHub
owner 'Frooodle' // Your SwaggerHub username (or organization name) owner "Frooodle" // Your SwaggerHub username (or organization name)
version project.version // The version of your API version project.version // The version of your API
inputFile './SwaggerDoc.json' // The path to your Swagger docs inputFile "./SwaggerDoc.json" // The path to your Swagger docs
token "${System.getenv('SWAGGERHUB_API_KEY')}" // Your SwaggerHub API key, passed as an environment variable token "${System.getenv("SWAGGERHUB_API_KEY")}" // Your SwaggerHub API key, passed as an environment variable
oas '3.0.0' // The version of the OpenAPI Specification you're using oas "3.0.0" // The version of the OpenAPI Specification you"re using
} }
jar { jar {
enabled = false enabled = false
manifest { manifest {
attributes 'Implementation-Title': 'Stirling-PDF', attributes "Implementation-Title": "Stirling-PDF",
'Implementation-Version': project.version "Implementation-Version": project.version
} }
} }
tasks.named('test') { tasks.named("test") {
useJUnitPlatform() useJUnitPlatform()
} }
task printVersion { task printVersion {
println project.version println project.version
} }

View File

@@ -1,5 +1,5 @@
apiVersion: v2 apiVersion: v2
appVersion: 0.26.1 appVersion: 0.28.3
description: locally hosted web application that allows you to perform various operations description: locally hosted web application that allows you to perform various operations
on PDF files on PDF files
home: https://github.com/Stirling-Tools/Stirling-PDF home: https://github.com/Stirling-Tools/Stirling-PDF

View File

@@ -62,8 +62,10 @@ spec:
imagePullPolicy: {{ .Values.image.pullPolicy }} imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext: securityContext:
{{- toYaml .Values.containerSecurityContext | nindent 10 }} {{- toYaml .Values.containerSecurityContext | nindent 10 }}
{{- if .Values.envs }}
env: env:
- name: SYSTEM_ROOTURIPATH
value: {{ .Values.rootPath}}
{{- if .Values.envs }}
{{ toYaml .Values.envs | indent 8 }} {{ toYaml .Values.envs | indent 8 }}
{{- end }} {{- end }}
{{- if .Values.extraArgs }} {{- if .Values.extraArgs }}
@@ -75,13 +77,13 @@ spec:
containerPort: 8080 containerPort: 8080
livenessProbe: livenessProbe:
httpGet: httpGet:
path: / path: {{ .Values.rootPath}}
port: http port: http
{{ toYaml .Values.probes.livenessHttpGetConfig | indent 12 }} {{ toYaml .Values.probes.livenessHttpGetConfig | indent 12 }}
{{ toYaml .Values.probes.liveness | indent 10 }} {{ toYaml .Values.probes.liveness | indent 10 }}
readinessProbe: readinessProbe:
httpGet: httpGet:
path: / path: {{ .Values.rootPath}}
port: http port: http
{{ toYaml .Values.probes.readinessHttpGetConfig | indent 12 }} {{ toYaml .Values.probes.readinessHttpGetConfig | indent 12 }}
{{ toYaml .Values.probes.readiness | indent 10 }} {{ toYaml .Values.probes.readiness | indent 10 }}

View File

@@ -15,6 +15,9 @@ secret:
commonLabels: {} commonLabels: {}
# team_name: dev # team_name: dev
# rootpath for the application
rootPath: /
envs: [] envs: []
# - name: UI_APP_NAME # - name: UI_APP_NAME
# value: "Stirling PDF" # value: "Stirling PDF"
@@ -24,8 +27,6 @@ envs: []
# value: "Stirling PDF" # value: "Stirling PDF"
# - name: ALLOW_GOOGLE_VISIBILITY # - name: ALLOW_GOOGLE_VISIBILITY
# value: "true" # value: "true"
# - name: APP_ROOT_PATH
# value: "/"
# - name: APP_LOCALE # - name: APP_LOCALE
# value: "en_GB" # value: "en_GB"

View File

@@ -0,0 +1,106 @@
%PDF-1.3
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
4 0 obj
<<
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
>>
endobj
7 0 obj
<<
/Author (anonymous) /CreationDate (D:20240718233034+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240718233034+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
8 0 obj
<<
/Count 3 /Kids [ 3 0 R 4 0 R 5 0 R ] /Type /Pages
>>
endobj
9 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 210
>>
stream
Gap@Gb79+X'F"5[`EfJOD4:mD<%*=m+N>oDG,>NK`<U'B^0WYY,dWl^i_UcRk`<"L=<NPC$BtQ<5l$3<Y!?BuoCSYQ6GSt25lpqr0IrP?S[b)9%M"e'HHFqcRO'9eRaR0'DYi*Y.:nEMFAoTM;rPL%EF]`CfoELVl_Q,"LS:%iI;Nc[&bG.*65O]ecfK1'*<>5P_s[usI/ph*0pV~>endstream
endobj
10 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
>>
stream
Gap@Gb79+X'F"5Y`EfJOV2A9=!fB]F'tK1LS`,]G+MiTenb&V2-^hqa(5IE#Nr59/!"Qm*5_(BdF!0&h!Yhk/A+\iS'%6tuO$O)9LaZS+flr([1p2&#RS1p/gT[B;rDj-=&=iqUlj(P^/5U@eCFqn4:<lU`l`.HXqG-',hJH.DI.(6L\luSAW`Q'oje[qgVLVIXg%PXe+,<$7('~>endstream
endobj
11 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
>>
stream
Gap@GbmK%f(e+0_`ODoa2.):e/i+N3r(.o*Qf\gSNb(bt4FIubi@GIOE=p8Ir3;CbQ@KuG^cdJhODZKQ*upt+*rdZ%!mFmN$*.P)K;`s#]G=8AO3s3DGB.RCOn?[F]bEIg,a>25?B%dh\Z/C6opFE'el@I,P\u\V\]:*JYrrsNJ&d,11VL;$h!43eGu&1X6$+5-h\Vr6!+>4Je,~>endstream
endobj
xref
0 12
0000000000 65535 f
0000000073 00000 n
0000000104 00000 n
0000000211 00000 n
0000000404 00000 n
0000000598 00000 n
0000000792 00000 n
0000000860 00000 n
0000001156 00000 n
0000001227 00000 n
0000001527 00000 n
0000001827 00000 n
trailer
<<
/ID
[<0d5cf047e754e05f8d574f067785875c><0d5cf047e754e05f8d574f067785875c>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 7 0 R
/Root 6 0 R
/Size 12
>>
startxref
2127
%%EOF

View File

@@ -0,0 +1,106 @@
%PDF-1.3
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
4 0 obj
<<
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
>>
endobj
7 0 obj
<<
/Author (anonymous) /CreationDate (D:20240718233034+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240718233034+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
8 0 obj
<<
/Count 3 /Kids [ 3 0 R 4 0 R 5 0 R ] /Type /Pages
>>
endobj
9 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 207
>>
stream
Gap@G:CDb.*/<p2MVk["e@)7*Z0@"b%+@f/9pA%_U<oOkVp?PnGRb81iPg?0i?(]%^_CSf##%;<!7Ne/-%RR^p@t7hKYZ9eJVHV]fjjHIB:6DrW+2\p16@*`r^CpQZZH'2Pjqd<.&hM2UO%$Wi$te%4QmS;<E"QS\!deQG_XtuEK>b(UbS>%`/0S`k\\5'TNY0mmgH?`8]i_0~>endstream
endobj
10 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 207
>>
stream
Gap@G]afWJ'Lm;=if<;s>V*7BTJ]oQ@P!(q5S+WG1%>L@?8Ue;c>[fY&&IOd5@t@TY@+q.5T<Z'81"J("KhsBa+&u4"n'#6)AjfImh)%$0tVC:aGk",=aJJH#/4]i.WJr9c"cibYm:M-44<%FFlG0Cl\Z'nmo7C"TR+7dk3T#iD(9Pq'\;rQku%o>A_`50SO&7M04=8M'O<Am~>endstream
endobj
11 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
>>
stream
Gap@GYmu@>'Ld5[if35r/JNaJ.A.7fP9RpSN*8k^-sEER0,enq1Rsuo@R/uCO-^&Y`F'9d^a?9)?ns+F&dXm[HMgPn6Ep+%TRk5Nh+!(+[H#H:U^.^(YL,PKS'%j/:3O\hJVEK-UUekJTd[A$N^((K^#0Du`i@,/^f5KiUISGr")3/+f9NF8NO1+iUgm^b"X\cE^+[:s!0]Gu6i~>endstream
endobj
xref
0 12
0000000000 65535 f
0000000073 00000 n
0000000104 00000 n
0000000211 00000 n
0000000404 00000 n
0000000598 00000 n
0000000792 00000 n
0000000860 00000 n
0000001156 00000 n
0000001227 00000 n
0000001524 00000 n
0000001822 00000 n
trailer
<<
/ID
[<407fc55425168745e56176202aad30c9><407fc55425168745e56176202aad30c9>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 7 0 R
/Root 6 0 R
/Size 12
>>
startxref
2122
%%EOF

View File

@@ -0,0 +1,106 @@
%PDF-1.3
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
4 0 obj
<<
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
>>
endobj
7 0 obj
<<
/Author (anonymous) /CreationDate (D:20240718233034+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240718233034+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
8 0 obj
<<
/Count 3 /Kids [ 3 0 R 4 0 R 5 0 R ] /Type /Pages
>>
endobj
9 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
>>
stream
Gap@G]+0EH(e/_@iZH]:>:>hu1e>07BJg5<'#:.C1n)e#(QJ6R1Rsuo_gpn.+0-H5$/#"iYR[B.9\'>7!aDAC*rf/t&6O#aH<?-7IT'\?X(&TcABG=ON*Nq`4k=o&p@3,0*31r<)TAP2Pk94p0\"R-_sY1$AYo[8B\?4R>feLAB\mpjZhp"`@J3;"Fm97#9+W,"eb95\+#p\^HN~>endstream
endobj
10 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
>>
stream
Gap@G]+0EX'Eriuig+>QHNeD'#n%Sq#n%BW`C'uDUOYK)HdS4E9JMsp+HUmDj&H-t*4?UamXX0peVspk"i_@ba+&u"J>UYDKV_^G,7V==aTZZ<YO7:sNSQ[6"Ja-29NtYjd#=`J@D'h+[QW=:EEb?A<k!f+\`g^?,Vgp7_)91[lR\f.Tkf7VIPLVYM&deF!aYt9Ip^"N",3F'*W~>endstream
endobj
11 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
>>
stream
Gap@G]+0EH(e/_@iZH]:>J`g!jPCLm;?AgU"fdk"PQZD\d?lRI_oWc[$tp^]O\:3fK8kWeX2&Jcg0+RoJ]j;2j*upu!b4.o&f)b$I@7CfIYjP^#\VjhC=QhQ]^lV-@<0Tam!0.+Dn@("AK%N,Uc7hb+6VoQ$q2q[7]BB92RoY/.j2N028i1jNf'@<1+Fqf$1&"8omHk`#DHP>OT~>endstream
endobj
xref
0 12
0000000000 65535 f
0000000073 00000 n
0000000104 00000 n
0000000211 00000 n
0000000404 00000 n
0000000598 00000 n
0000000792 00000 n
0000000860 00000 n
0000001156 00000 n
0000001227 00000 n
0000001526 00000 n
0000001826 00000 n
trailer
<<
/ID
[<80da26147a484f2b7573da8151a93d2e><80da26147a484f2b7573da8151a93d2e>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 7 0 R
/Root 6 0 R
/Size 12
>>
startxref
2126
%%EOF

Binary file not shown.

View File

@@ -0,0 +1,106 @@
%PDF-1.3
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
4 0 obj
<<
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
>>
endobj
7 0 obj
<<
/Author (anonymous) /CreationDate (D:20240718233034+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240718233034+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
8 0 obj
<<
/Count 3 /Kids [ 3 0 R 4 0 R 5 0 R ] /Type /Pages
>>
endobj
9 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 206
>>
stream
Gap@G\IO3f&4Lr[@S4&T2aReWZ3N'9",Ncra>5AuK^J(o@r?=EP>b]h[L@XZ8q7#[c:#H2:^/=b,p3^,&f-Q.'H%!U?%N\iVa1pLMlh/41\A8@dF5@0al:-1?L;D%LpL3g\9`.3c6N/Mp=sE/nO%^@%Cc3`]e`qqS@[pkUWemMZC<P\fkqa55u)*hIUoU437-gb!e_*&B/,&~>endstream
endobj
10 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
>>
stream
Gap@G\IO3V'LdA_ig"8P1PS=kA5Q_GQ\P]*S3\>Q`jHYt?8UdkV`6]UV*On)+1VMV+A@.iF:*6sWfM9f"s.NmVuMto!p7-+,Rb<.h,pdi-&OQ5KO\RRFj.j"A)ScTQ7$hudF^TnZ'XuQA5"O]rYkt><-DJmj'"Ri>n!4`^m409XX`e)AR'*rGsn6m79.18+^ba=qRuss"-A3k+9~>endstream
endobj
11 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 210
>>
stream
Gap@G]+0EH(e/_@iZH]:.1fBHK`Xl'[i1&AjX(\k8hbgo(QJ6R1Rsuo6_I1A5Gg$JL;D#$J2CX;+Cf*cUHk2%H1XmpWe+qZ5moJ#B]>b%%[d,mfSSkS4A:Q4NlOFfrL7eA,s45"eUSakM;927AA,1"-LZ)&nZ/ah=8_X7:?ZMj@J@;r7d`t]Z0\d39M%:$k8[S5D"2oSap4s80l?~>endstream
endobj
xref
0 12
0000000000 65535 f
0000000073 00000 n
0000000104 00000 n
0000000211 00000 n
0000000404 00000 n
0000000598 00000 n
0000000792 00000 n
0000000860 00000 n
0000001156 00000 n
0000001227 00000 n
0000001523 00000 n
0000001823 00000 n
trailer
<<
/ID
[<88edee24ee67bd7d6b7cf53cfa2222b0><88edee24ee67bd7d6b7cf53cfa2222b0>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 7 0 R
/Root 6 0 R
/Size 12
>>
startxref
2124
%%EOF

View File

@@ -0,0 +1,106 @@
%PDF-1.3
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
4 0 obj
<<
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
>>
endobj
7 0 obj
<<
/Author (anonymous) /CreationDate (D:20240718233034+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240718233034+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
8 0 obj
<<
/Count 3 /Kids [ 3 0 R 4 0 R 5 0 R ] /Type /Pages
>>
endobj
9 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
>>
stream
Gap@GYmu@>'Ld5[if35rI0]sG)F[U^"c>T)"\\os-r:1V0,enq1Rsuo,*67.@k7U.LRF-P.e"CM2V!>iYi<g`nXh!K?n@$t^rY1$+^0'>=B8H6e;F1WmG#,(eS00(Qe9&:O@nI879DTsT,njXAB?`8:>,Hn3*RV!qh4;&@6%]<9Y*>QZ].Z5o;RAZXg7d[#+bphHs_Ep!QR2TZ2~>endstream
endobj
10 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 210
>>
stream
Gap@G]+0EH(e/_@iZH]:>=,iY1bE)XN?M;1'J/>i&HY;gks]*rj:!DKpb8@`prC#N+9E#o#-<G*!#p7e6j-1sX2k5S,6XmM"taYkfK^k">%usEeEk=sR<UT"dm`rXD;!S`_jS9LU+(R%e'V%WSMfHP.pXZEQqTQq=&D[I[PS(41(NIAZ1R/U?:Z=hSXu!NDF)bpG2F+/I/q/u1-Y~>endstream
endobj
11 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
>>
stream
Gap@G_$YcZ'LhbF`EQB$nqi=8S<;#HbK3&f>rnodRPo`Vf4P[3cJidY(I=[K5NWCT'<lHgci?oCRVNST&[k#q4oSC0FWgAt1pD4d_(hIRjn_Nt+cFgJlfm[1U8@/M4r^Pk<@F!@e?%/!-Vq;]nfdLi9]P2M)ck9?)%oNXa_\N<-d"(pjlH%-G`T@Sj&P(j6.@#Xh\Vr6!1iI2/H~>endstream
endobj
xref
0 12
0000000000 65535 f
0000000073 00000 n
0000000104 00000 n
0000000211 00000 n
0000000404 00000 n
0000000598 00000 n
0000000792 00000 n
0000000860 00000 n
0000001156 00000 n
0000001227 00000 n
0000001526 00000 n
0000001827 00000 n
trailer
<<
/ID
[<4fcc82a085fe71e34a32d1b23c8b939f><4fcc82a085fe71e34a32d1b23c8b939f>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 7 0 R
/Root 6 0 R
/Size 12
>>
startxref
2127
%%EOF

View File

@@ -14,3 +14,8 @@ def after_scenario(context, scenario):
os.remove('response_file') os.remove('response_file')
if hasattr(context, 'file_name') and os.path.exists(context.file_name): if hasattr(context, 'file_name') and os.path.exists(context.file_name):
os.remove(context.file_name) os.remove(context.file_name)
# Remove any temporary files
for temp_file in os.listdir('.'):
if temp_file.startswith('genericNonCustomisableName') or temp_file.startswith('temp_image_'):
os.remove(temp_file)

View File

@@ -1,4 +1,4 @@
@example @example @general
Feature: API Validation Feature: API Validation
@positive @password @positive @password
@@ -92,10 +92,10 @@ Feature: API Validation
| threshold | 90 | | threshold | 90 |
| whitePercent | 99.9 | | whitePercent | 99.9 |
When I send the API request to the endpoint "/api/v1/misc/remove-blanks" When I send the API request to the endpoint "/api/v1/misc/remove-blanks"
Then the response content type should be "application/pdf" Then the response content type should be "application/octet-stream"
And the response file should have extension ".zip"
And the response ZIP should contain 1 files
And the response file should have size greater than 0 And the response file should have size greater than 0
And the response PDF should contain 0 pages
And the response status code should be 200
@positive @flatten @positive @flatten
Scenario: Flatten PDF Scenario: Flatten PDF
@@ -127,4 +127,4 @@ Feature: API Validation
And the response PDF metadata should include "Title" as "Sample Title" And the response PDF metadata should include "Title" as "Sample Title"
And the response status code should be 200 And the response status code should be 200

View File

@@ -32,7 +32,7 @@ Feature: API Validation
@ocr @positive @ocr @positive
Scenario: Extract Image Scans Scenario: Extract Image Scans
Given I generate a PDF file as "fileInput" Given I generate a PDF file as "fileInput"
And the pdf contains 3 images on 2 pages And the pdf contains 3 images of size 300x300 on 2 pages
And the request data includes And the request data includes
| parameter | value | | parameter | value |
| angleThreshold | 5 | | angleThreshold | 5 |
@@ -125,8 +125,7 @@ Feature: API Validation
@ocr @ocr
Scenario: PDFA Scenario: PDFA
Given I generate a PDF file as "fileInput" Given I use an example file at "exampleFiles/pdfa2.pdf" as parameter "fileInput"
And the pdf contains 3 pages with random text
And the request data includes And the request data includes
| parameter | value | | parameter | value |
| outputFormat | pdfa | | outputFormat | pdfa |
@@ -137,8 +136,7 @@ Feature: API Validation
@ocr @ocr
Scenario: PDFA1 Scenario: PDFA1
Given I generate a PDF file as "fileInput" Given I use an example file at "exampleFiles/pdfa1.pdf" as parameter "fileInput"
And the pdf contains 3 pages with random text
And the request data includes And the request data includes
| parameter | value | | parameter | value |
| outputFormat | pdfa-1 | | outputFormat | pdfa-1 |
@@ -149,8 +147,7 @@ Feature: API Validation
@compress @ghostscript @positive @compress @ghostscript @positive
Scenario: Compress Scenario: Compress
Given I generate a PDF file as "fileInput" Given I use an example file at "exampleFiles/ghost3.pdf" as parameter "fileInput"
And the pdf contains 3 pages with random text
And the request data includes And the request data includes
| parameter | value | | parameter | value |
| optimizeLevel | 4 | | optimizeLevel | 4 |
@@ -161,8 +158,7 @@ Feature: API Validation
@compress @ghostscript @positive @compress @ghostscript @positive
Scenario: Compress Scenario: Compress
Given I generate a PDF file as "fileInput" Given I use an example file at "exampleFiles/ghost2.pdf" as parameter "fileInput"
And the pdf contains 3 pages with random text
And the request data includes And the request data includes
| parameter | value | | parameter | value |
| optimizeLevel | 1 | | optimizeLevel | 1 |
@@ -175,8 +171,7 @@ Feature: API Validation
@compress @ghostscript @positive @compress @ghostscript @positive
Scenario: Compress Scenario: Compress
Given I generate a PDF file as "fileInput" Given I use an example file at "exampleFiles/ghost1.pdf" as parameter "fileInput"
And the pdf contains 3 pages with random text
And the request data includes And the request data includes
| parameter | value | | parameter | value |
| optimizeLevel | 1 | | optimizeLevel | 1 |

View File

@@ -94,3 +94,21 @@ Feature: API Validation
| 1 | 10 | 2 | 10 | | 1 | 10 | 2 | 10 |
@extract-images
Scenario Outline: Extract Image Scans duplicates
Given I use an example file at "exampleFiles/images.pdf" as parameter "fileInput"
And the request data includes
| parameter | value |
| format | <format> |
When I send the API request to the endpoint "/api/v1/misc/extract-images"
Then the response content type should be "application/octet-stream"
And the response file should have extension ".zip"
And the response ZIP should contain 2 files
And the response file should have size greater than 0
And the response status code should be 200
Examples:
| format |
| png |
| gif |
| jpeg |

View File

@@ -6,11 +6,14 @@ import io
import random import random
import string import string
from reportlab.lib.pagesizes import letter from reportlab.lib.pagesizes import letter
from reportlab.lib.utils import ImageReader
from reportlab.pdfgen import canvas from reportlab.pdfgen import canvas
import mimetypes import mimetypes
import requests import requests
import zipfile import zipfile
import shutil import shutil
import re
from PIL import Image, ImageDraw
######### #########
# GIVEN # # GIVEN #
@@ -43,8 +46,6 @@ def step_use_example_file(context, filePath, fileInput):
except FileNotFoundError: except FileNotFoundError:
raise FileNotFoundError(f"The example file '{filePath}' does not exist.") raise FileNotFoundError(f"The example file '{filePath}' does not exist.")
@given('the pdf contains {page_count:d} pages') @given('the pdf contains {page_count:d} pages')
def step_pdf_contains_pages(context, page_count): def step_pdf_contains_pages(context, page_count):
writer = PdfWriter() writer = PdfWriter()
@@ -66,8 +67,6 @@ def step_pdf_contains_blank_pages(context, page_count):
context.files[context.param_name].close() context.files[context.param_name].close()
context.files[context.param_name] = open(context.file_name, 'rb') context.files[context.param_name] = open(context.file_name, 'rb')
def create_black_box_image(file_name, size): def create_black_box_image(file_name, size):
can = canvas.Canvas(file_name, pagesize=size) can = canvas.Canvas(file_name, pagesize=size)
width, height = size width, height = size
@@ -76,36 +75,75 @@ def create_black_box_image(file_name, size):
can.showPage() can.showPage()
can.save() can.save()
def create_pdf_with_black_boxes(file_name, image_count, page_count): @given(u'the pdf contains {image_count:d} images of size {width:d}x{height:d} on {page_count:d} pages')
page_width, page_height = letter def step_impl(context, image_count, width, height, page_count):
box_size = 72 # 1 inch by 1 inch black box context.param_name = "fileInput"
context.file_name = "genericNonCustomisableName.pdf"
create_pdf_with_images_and_boxes(context.file_name, image_count, page_count, width, height)
if not hasattr(context, 'files'):
context.files = {}
context.files[context.param_name] = open(context.file_name, 'rb')
def add_black_boxes_to_image(image):
if isinstance(image, str):
image = Image.open(image)
draw = ImageDraw.Draw(image)
draw.rectangle([(0, 0), image.size], fill=(0, 0, 0)) # Fill image with black
return image
def create_pdf_with_images_and_boxes(file_name, image_count, page_count, image_width, image_height):
page_width, page_height = max(letter[0], image_width), max(letter[1], image_height)
boxes_per_page = image_count // page_count + (1 if image_count % page_count != 0 else 0) boxes_per_page = image_count // page_count + (1 if image_count % page_count != 0 else 0)
writer = PdfWriter() writer = PdfWriter()
box_counter = 0 box_counter = 0
for page in range(page_count): for page in range(page_count):
packet = io.BytesIO() packet = io.BytesIO()
can = canvas.Canvas(packet, pagesize=letter) can = canvas.Canvas(packet, pagesize=(page_width, page_height))
for i in range(boxes_per_page): for i in range(boxes_per_page):
if box_counter >= image_count: if box_counter >= image_count:
break break
x = (i % (page_width // box_size)) * box_size
y = page_height - ((i // (page_width // box_size) + 1) * box_size) # Simulating a dynamic image creation (replace this with your actual image creation logic)
can.setFillColorRGB(0, 0, 0) # For demonstration, we'll create a simple black image
can.rect(x, y, box_size, box_size, fill=1) dummy_image = Image.new('RGB', (image_width, image_height), color='white') # Create a white image
dummy_image = add_black_boxes_to_image(dummy_image) # Add black boxes
# Convert the PIL Image to bytes to pass to drawImage
image_bytes = io.BytesIO()
dummy_image.save(image_bytes, format='PNG')
image_bytes.seek(0)
# Check if the image fits in the current page dimensions
x = (i % (page_width // image_width)) * image_width
y = page_height - (((i % (page_height // image_height)) + 1) * image_height)
if x + image_width > page_width or y < 0:
break
# Add the image to the PDF
can.drawImage(ImageReader(image_bytes), x, y, width=image_width, height=image_height)
box_counter += 1 box_counter += 1
can.showPage() can.showPage()
can.save() can.save()
packet.seek(0) packet.seek(0)
new_pdf = PdfReader(packet) new_pdf = PdfReader(packet)
writer.add_page(new_pdf.pages[0]) writer.add_page(new_pdf.pages[0])
# Write the PDF to file
with open(file_name, 'wb') as f: with open(file_name, 'wb') as f:
writer.write(f) writer.write(f)
# Clean up temporary image files
for i in range(image_count):
temp_image_path = f"temp_image_{i}.png"
if os.path.exists(temp_image_path):
os.remove(temp_image_path)
@given('the pdf contains {image_count:d} images on {page_count:d} pages') @given('the pdf contains {image_count:d} images on {page_count:d} pages')
def step_pdf_contains_images(context, image_count, page_count): def step_pdf_contains_images(context, image_count, page_count):
if not hasattr(context, 'param_name'): if not hasattr(context, 'param_name'):
@@ -118,7 +156,6 @@ def step_pdf_contains_images(context, image_count, page_count):
context.files[context.param_name].close() context.files[context.param_name].close()
context.files[context.param_name] = open(context.file_name, 'rb') context.files[context.param_name] = open(context.file_name, 'rb')
@given('the pdf contains {page_count:d} pages with random text') @given('the pdf contains {page_count:d} pages with random text')
def step_pdf_contains_pages_with_random_text(context, page_count): def step_pdf_contains_pages_with_random_text(context, page_count):
buffer = io.BytesIO() buffer = io.BytesIO()
@@ -186,6 +223,21 @@ def save_generated_pdf(context, filename):
# WHEN # # WHEN #
######## ########
@when('I send a GET request to "{endpoint}"')
def step_send_get_request(context, endpoint):
base_url = "http://localhost:8080"
full_url = f"{base_url}{endpoint}"
response = requests.get(full_url)
context.response = response
@when('I send a GET request to "{endpoint}" with parameters')
def step_send_get_request_with_params(context, endpoint):
base_url = "http://localhost:8080"
params = {row['parameter']: row['value'] for row in context.table}
full_url = f"{base_url}{endpoint}"
response = requests.get(full_url, params=params)
context.response = response
@when('I send the API request to the endpoint "{endpoint}"') @when('I send the API request to the endpoint "{endpoint}"')
def step_send_api_request(context, endpoint): def step_send_api_request(context, endpoint):
url = f"http://localhost:8080{endpoint}" url = f"http://localhost:8080{endpoint}"
@@ -278,7 +330,6 @@ def step_save_response_file(context, filename):
f.write(context.response.content) f.write(context.response.content)
print(f"Saved response content to {filename}") print(f"Saved response content to {filename}")
@then('the response PDF should contain {page_count:d} pages') @then('the response PDF should contain {page_count:d} pages')
def step_check_response_pdf_page_count(context, page_count): def step_check_response_pdf_page_count(context, page_count):
response_file = io.BytesIO(context.response.content) response_file = io.BytesIO(context.response.content)
@@ -305,3 +356,26 @@ def step_check_response_zip_doc_page_count(context, doc_count, pages_per_doc):
reader = PdfReader(pdf_file) reader = PdfReader(pdf_file)
actual_pages_per_doc = len(reader.pages) actual_pages_per_doc = len(reader.pages)
assert actual_pages_per_doc == pages_per_doc, f"Expected {pages_per_doc} pages per document but got {actual_pages_per_doc} pages in document {file_name}" assert actual_pages_per_doc == pages_per_doc, f"Expected {pages_per_doc} pages per document but got {actual_pages_per_doc} pages in document {file_name}"
@then('the JSON value of "{key}" should be "{expected_value}"')
def step_check_json_value(context, key, expected_value):
actual_value = context.response.json().get(key)
assert actual_value == expected_value, \
f"Expected JSON value for '{key}' to be '{expected_value}' but got '{actual_value}'"
@then('JSON list entry containing "{identifier_key}" as "{identifier_value}" should have "{target_key}" as "{target_value}"')
def step_check_json_list_entry(context, identifier_key, identifier_self, target_key, target_value):
json_response = context.response.json()
for entry in json_response:
if entry.get(identifier_key) == identifier_value:
assert entry.get(target_key) == target_value, \
f"Expected {target_key} to be {target_value} in entry where {identifier_key} is {identifier_value}, but found {entry.get(target_key)}"
break
else:
raise AssertionError(f"No entry with {identifier_key} as {identifier_value} found")
@then('the response should match the regex "{pattern}"')
def step_response_matches_regex(context, pattern):
response_text = context.response.text
assert re.match(pattern, response_text), \
f"Response '{response_text}' does not match the expected pattern '{pattern}'"

View File

@@ -22,7 +22,6 @@ services:
DOCKER_ENABLE_SECURITY: "false" DOCKER_ENABLE_SECURITY: "false"
SECURITY_ENABLELOGIN: "false" SECURITY_ENABLELOGIN: "false"
LANGS: "en_GB,en_US,ar_AR,de_DE,fr_FR,es_ES,zh_CN,zh_TW,ca_CA,it_IT,sv_SE,pl_PL,ro_RO,ko_KR,pt_BR,ru_RU,el_GR,hi_IN,hu_HU,tr_TR,id_ID" LANGS: "en_GB,en_US,ar_AR,de_DE,fr_FR,es_ES,zh_CN,zh_TW,ca_CA,it_IT,sv_SE,pl_PL,ro_RO,ko_KR,pt_BR,ru_RU,el_GR,hi_IN,hu_HU,tr_TR,id_ID"
INSTALL_BOOK_AND_ADVANCED_HTML_OPS: "true"
SYSTEM_DEFAULTLOCALE: en-US SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF UI_APPNAME: Stirling-PDF
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest

View File

@@ -79,7 +79,9 @@ def write_readme(progress_list: list[tuple[str, int]]) -> None:
file.writelines(content) file.writelines(content)
def compare_files(default_file_path, file_paths, translation_status_file) -> list[tuple[str, int]]: def compare_files(
default_file_path, file_paths, ignore_translation_file
) -> list[tuple[str, int]]:
"""Compares the default properties file with other """Compares the default properties file with other
properties files in the directory. properties files in the directory.
@@ -92,18 +94,24 @@ def compare_files(default_file_path, file_paths, translation_status_file) -> lis
language and progress percentage. language and progress percentage.
""" # noqa: D205 """ # noqa: D205
num_lines = sum( num_lines = sum(
1 for line in open(default_file_path, encoding="utf-8") if line.strip() and not line.strip().startswith("#") 1
for line in open(default_file_path, encoding="utf-8")
if line.strip() and not line.strip().startswith("#")
) )
result_list = [] result_list = []
sort_translation_status: tomlkit.TOMLDocument sort_ignore_translation: tomlkit.TOMLDocument
# read toml # read toml
with open(translation_status_file, encoding="utf-8") as f: with open(ignore_translation_file, encoding="utf-8") as f:
sort_translation_status = tomlkit.parse(f.read()) sort_ignore_translation = tomlkit.parse(f.read())
for file_path in file_paths: for file_path in file_paths:
language = os.path.basename(file_path).split("messages_", 1)[1].split(".properties", 1)[0] language = (
os.path.basename(file_path)
.split("messages_", 1)[1]
.split(".properties", 1)[0]
)
fails = 0 fails = 0
if "en_GB" in language or "en_US" in language: if "en_GB" in language or "en_US" in language:
@@ -111,21 +119,25 @@ def compare_files(default_file_path, file_paths, translation_status_file) -> lis
result_list.append(("en_US", 100)) result_list.append(("en_US", 100))
continue continue
if language not in sort_translation_status: if language not in sort_ignore_translation:
sort_translation_status[language] = tomlkit.table() sort_ignore_translation[language] = tomlkit.table()
if ( if (
"ignore" not in sort_translation_status[language] "ignore" not in sort_ignore_translation[language]
or len(sort_translation_status[language].get("ignore", [])) < 1 or len(sort_ignore_translation[language].get("ignore", [])) < 1
): ):
sort_translation_status[language]["ignore"] = tomlkit.array(["language.direction"]) sort_ignore_translation[language]["ignore"] = tomlkit.array(
["language.direction"]
)
# if "missing" not in sort_translation_status[language]: # if "missing" not in sort_ignore_translation[language]:
# sort_translation_status[language]["missing"] = tomlkit.array() # sort_ignore_translation[language]["missing"] = tomlkit.array()
# elif "language.direction" in sort_translation_status[language]["missing"]: # elif "language.direction" in sort_ignore_translation[language]["missing"]:
# sort_translation_status[language]["missing"].remove("language.direction") # sort_ignore_translation[language]["missing"].remove("language.direction")
with open(default_file_path, encoding="utf-8") as default_file, open(file_path, encoding="utf-8") as file: with open(default_file_path, encoding="utf-8") as default_file, open(
file_path, encoding="utf-8"
) as file:
for _ in range(5): for _ in range(5):
next(default_file) next(default_file)
try: try:
@@ -133,34 +145,45 @@ def compare_files(default_file_path, file_paths, translation_status_file) -> lis
except StopIteration: except StopIteration:
fails = num_lines fails = num_lines
for line_num, (line_default, line_file) in enumerate(zip(default_file, file), start=6): for line_num, (line_default, line_file) in enumerate(
zip(default_file, file), start=6
):
try: try:
# Ignoring empty lines and lines start with # # Ignoring empty lines and lines start with #
if line_default.strip() == "" or line_default.startswith("#"): if line_default.strip() == "" or line_default.startswith("#"):
continue continue
default_key, default_value = line_default.split("=", 1) default_key, default_value = line_default.split("=", 1)
file_key, file_value = line_file.split("=", 1) file_key, file_value = line_file.split("=", 1)
if ( if (
default_value.strip() == file_value.strip() default_value.strip() == file_value.strip()
and default_key.strip() not in sort_translation_status[language]["ignore"] and default_key.strip()
not in sort_ignore_translation[language]["ignore"]
): ):
print(f"{language}: Line {line_num} is missing the translation.") print(
# if default_key.strip() not in sort_translation_status[language]["missing"]: f"{language}: Line {line_num} is missing the translation."
)
# if default_key.strip() not in sort_ignore_translation[language]["missing"]:
# missing_array = tomlkit.array() # missing_array = tomlkit.array()
# missing_array.append(default_key.strip()) # missing_array.append(default_key.strip())
# missing_array.multiline(True) # missing_array.multiline(True)
# sort_translation_status[language]["missing"].extend(missing_array) # sort_ignore_translation[language]["missing"].extend(missing_array)
fails += 1 fails += 1
# elif default_key.strip() in sort_translation_status[language]["ignore"]: # elif default_key.strip() in sort_ignore_translation[language]["ignore"]:
# if default_key.strip() in sort_translation_status[language]["missing"]: # if default_key.strip() in sort_ignore_translation[language]["missing"]:
# sort_translation_status[language]["missing"].remove(default_key.strip()) # sort_ignore_translation[language]["missing"].remove(default_key.strip())
if default_value.strip() != file_value.strip(): if default_value.strip() != file_value.strip():
# if default_key.strip() in sort_translation_status[language]["missing"]: # if default_key.strip() in sort_ignore_translation[language]["missing"]:
# sort_translation_status[language]["missing"].remove(default_key.strip()) # sort_ignore_translation[language]["missing"].remove(default_key.strip())
if default_key.strip() in sort_translation_status[language]["ignore"]: if (
sort_translation_status[language]["ignore"].remove(default_key.strip()) default_key.strip()
in sort_ignore_translation[language]["ignore"]
):
sort_ignore_translation[language]["ignore"].remove(
default_key.strip()
)
except ValueError:
print(f"{line_default}|{line_file}")
exit(1)
except IndexError: except IndexError:
pass pass
@@ -171,9 +194,9 @@ def compare_files(default_file_path, file_paths, translation_status_file) -> lis
int((num_lines - fails) * 100 / num_lines), int((num_lines - fails) * 100 / num_lines),
) )
) )
translation_status = convert_to_multiline(sort_translation_status) ignore_translation = convert_to_multiline(sort_ignore_translation)
with open(translation_status_file, "w", encoding="utf-8") as file: with open(ignore_translation_file, "w", encoding="utf-8") as file:
file.write(tomlkit.dumps(translation_status)) file.write(tomlkit.dumps(ignore_translation))
unique_data = list(set(result_list)) unique_data = list(set(result_list))
unique_data.sort(key=lambda x: x[1], reverse=True) unique_data.sort(key=lambda x: x[1], reverse=True)
@@ -187,6 +210,8 @@ if __name__ == "__main__":
reference_file = os.path.join(directory, "messages_en_GB.properties") reference_file = os.path.join(directory, "messages_en_GB.properties")
scripts_directory = os.path.join(os.getcwd(), "scripts") scripts_directory = os.path.join(os.getcwd(), "scripts")
translation_state_file = os.path.join(scripts_directory, "translation_status.toml") translation_state_file = os.path.join(scripts_directory, "ignore_translation.toml")
write_readme(compare_files(reference_file, messages_file_paths, translation_state_file)) write_readme(
compare_files(reference_file, messages_file_paths, translation_state_file)
)

View File

@@ -10,7 +10,11 @@ ignore = [
[ca_CA] [ca_CA]
ignore = [ ignore = [
'PDFToText.tags',
'adminUserSettings.admin',
'language.direction', 'language.direction',
'survey.button',
'watermark.type.1',
] ]
[cs_CZ] [cs_CZ]
@@ -21,6 +25,11 @@ ignore = [
'text', 'text',
] ]
[da_DK]
ignore = [
'language.direction',
]
[de_DE] [de_DE]
ignore = [ ignore = [
'AddStampRequest.alphabet', 'AddStampRequest.alphabet',
@@ -48,6 +57,7 @@ ignore = [
ignore = [ ignore = [
'adminUserSettings.roles', 'adminUserSettings.roles',
'color', 'color',
'error',
'language.direction', 'language.direction',
'no', 'no',
'showJS.tags', 'showJS.tags',
@@ -60,8 +70,31 @@ ignore = [
[fr_FR] [fr_FR]
ignore = [ ignore = [
'AddStampRequest.alphabet',
'AddStampRequest.position',
'AddStampRequest.rotation',
'PDFToBook.selectText.1',
'addPageNumbers.selectText.3',
'adminUserSettings.actions',
'alphabet',
'compare.document.1',
'compare.document.2',
'info',
'language.direction', 'language.direction',
'licenses.license',
'licenses.module',
'licenses.nav',
'licenses.version',
'pdfOrganiser.mode',
'pipeline.title',
'pipelineOptions.pipelineHeader',
'sponsor', 'sponsor',
'watermark.type.2',
]
[ga_IE]
ignore = [
'language.direction',
] ]
[hi_IN] [hi_IN]
@@ -71,6 +104,7 @@ ignore = [
[hr_HR] [hr_HR]
ignore = [ ignore = [
'PDFToBook.selectText.1',
'font', 'font',
'home.pipeline.title', 'home.pipeline.title',
'info', 'info',
@@ -114,22 +148,42 @@ ignore = [
[nl_NL] [nl_NL]
ignore = [ ignore = [
'HTMLToPDF.print',
'adjustContrast.contrast',
'compare.document.1',
'compare.document.2',
'error',
'getPdfInfo.downloadJson',
'help',
'info',
'language.direction', 'language.direction',
'navbar.allTools',
'printFile.submit',
'showJS.downloadJS',
'sponsor',
] ]
[no_NB] [no_NB]
ignore = [ ignore = [
'PDFToBook.selectText.1',
'adminUserSettings.admin',
'info',
'language.direction', 'language.direction',
'oops',
'sponsor',
] ]
[pl_PL] [pl_PL]
ignore = [ ignore = [
'PDFToBook.selectText.1',
'language.direction', 'language.direction',
] ]
[pt_BR] [pt_BR]
ignore = [ ignore = [
'changeMetadata.trapped',
'language.direction', 'language.direction',
'pipelineOptions.pipelineHeader',
] ]
[pt_PT] [pt_PT]
@@ -149,12 +203,20 @@ ignore = [
[sk_SK] [sk_SK]
ignore = [ ignore = [
'adminUserSettings.admin',
'home.multiTool.title',
'info',
'language.direction', 'language.direction',
'navbar.sections.security',
'text',
'watermark.type.1',
] ]
[sr_LATN_RS] [sr_LATN_RS]
ignore = [ ignore = [
'language.direction', 'language.direction',
'licenses.version',
'poweredBy',
] ]
[sv_SE] [sv_SE]
@@ -162,6 +224,14 @@ ignore = [
'language.direction', 'language.direction',
] ]
[th_TH]
ignore = [
'language.direction',
'pipeline.title',
'pipelineOptions.pipelineHeader',
'showJS.tags',
]
[tr_TR] [tr_TR]
ignore = [ ignore = [
'language.direction', 'language.direction',
@@ -172,6 +242,14 @@ ignore = [
'language.direction', 'language.direction',
] ]
[vi_VN]
ignore = [
'language.direction',
'pipeline.title',
'pipelineOptions.pipelineHeader',
'showJS.tags',
]
[zh_CN] [zh_CN]
ignore = [ ignore = [
'language.direction', 'language.direction',

View File

@@ -12,7 +12,8 @@ fi
umask "$UMASK" || true umask "$UMASK" || true
if [[ "$INSTALL_BOOK_AND_ADVANCED_HTML_OPS" == "true" && "$FAT_DOCKER" != "true" ]]; then if [[ "$INSTALL_BOOK_AND_ADVANCED_HTML_OPS" == "true" && "$FAT_DOCKER" != "true" ]]; then
apk add --no-cache calibre@testing echo "issue with calibre in current version, feature currently disabled on Stirling-PDF"
#apk add --no-cache calibre@testing
fi fi
if [[ "$FAT_DOCKER" != "true" ]]; then if [[ "$FAT_DOCKER" != "true" ]]; then

174
scripts/png_to_webp.py Normal file
View File

@@ -0,0 +1,174 @@
"""
Author: Ludy87
Description: This script converts a PDF file to WebP images. It includes functionality to resize images if they exceed specified dimensions and handle conversion of PDF pages to WebP format.
Example
-------
To convert a PDF file to WebP images with each page as a separate WebP file:
python script.py input.pdf output_directory
To convert a PDF file to a single WebP image:
python script.py input.pdf output_directory --single
To adjust the DPI resolution for rendering PDF pages:
python script.py input.pdf output_directory --dpi 150
"""
import argparse
import os
from pdf2image import convert_from_path
from PIL import Image
def resize_image(input_image_path, output_image_path, max_size=(16383, 16383)):
"""
Resize the image if its dimensions exceed the maximum allowed size and save it as WebP.
Parameters
----------
input_image_path : str
Path to the input image file.
output_image_path : str
Path where the output WebP image will be saved.
max_size : tuple of int, optional
Maximum allowed dimensions for the image (width, height). Default is (16383, 16383).
Returns
-------
None
"""
try:
# Open the image
image = Image.open(input_image_path)
width, height = image.size
max_width, max_height = max_size
# Check if the image dimensions exceed the maximum allowed dimensions
if width > max_width or height > max_height:
# Calculate the scaling ratio
ratio = min(max_width / width, max_height / height)
new_width = int(width * ratio)
new_height = int(height * ratio)
# Resize the image
resized_image = image.resize((new_width, new_height), Image.LANCZOS)
resized_image.save(output_image_path, format="WEBP", quality=100)
print(
f"The image was successfully resized to ({new_width}, {new_height}) and saved as WebP: {output_image_path}"
)
else:
# If dimensions are within the allowed limits, save the image directly
image.save(output_image_path, format="WEBP", quality=100)
print(f"The image was successfully saved as WebP: {output_image_path}")
except Exception as e:
print(f"An error occurred: {e}")
def convert_image_to_webp(input_image, output_file):
"""
Convert an image to WebP format, resizing it if it exceeds the maximum dimensions.
Parameters
----------
input_image : str
Path to the input image file.
output_file : str
Path where the output WebP image will be saved.
Returns
-------
None
"""
# Resize the image if it exceeds the maximum dimensions
resize_image(input_image, output_file, max_size=(16383, 16383))
def pdf_to_webp(pdf_path, output_dir, dpi=300):
"""
Convert each page of a PDF file to WebP images.
Parameters
----------
pdf_path : str
Path to the input PDF file.
output_dir : str
Directory where the WebP images will be saved.
dpi : int, optional
DPI resolution for rendering PDF pages. Default is 300.
Returns
-------
None
"""
# Convert the PDF to a list of images
images = convert_from_path(pdf_path, dpi=dpi)
for page_number, image in enumerate(images):
# Define temporary PNG path
temp_png_path = os.path.join(output_dir, f"temp_page_{page_number + 1}.png")
image.save(temp_png_path, format="PNG")
# Define the output path for WebP
output_path = os.path.join(output_dir, f"page_{page_number + 1}.webp")
# Convert PNG to WebP
convert_image_to_webp(temp_png_path, output_path)
# Delete the temporary PNG file
os.remove(temp_png_path)
def main(pdf_image_path, output_dir, dpi=300, single_images_flag=False):
"""
Main function to handle conversion from PDF to WebP images.
Parameters
----------
pdf_image_path : str
Path to the input PDF file or image.
output_dir : str
Directory where the WebP images will be saved.
dpi : int, optional
DPI resolution for rendering PDF pages. Default is 300.
single_images_flag : bool, optional
If True, combine all pages into a single WebP image. Default is False.
Returns
-------
None
"""
if single_images_flag:
# Combine all pages into a single WebP image
output_path = os.path.join(output_dir, "combined_image.webp")
convert_image_to_webp(pdf_image_path, output_path)
else:
# Convert each PDF page to a separate WebP image
pdf_to_webp(pdf_image_path, output_dir, dpi)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Convert a PDF file to WebP images.")
parser.add_argument("pdf_path", help="The path to the input PDF file.")
parser.add_argument(
"output_dir", help="The directory where the WebP images should be saved."
)
parser.add_argument(
"--dpi",
type=int,
default=300,
help="The DPI resolution for rendering the PDF pages (default: 300).",
)
parser.add_argument(
"--single",
action="store_true",
help="Combine all pages into a single WebP image.",
)
args = parser.parse_args()
os.makedirs(args.output_dir, exist_ok=True)
main(
args.pdf_path,
args.output_dir,
dpi=args.dpi,
single_images_flag=args.single,
)

View File

@@ -45,7 +45,6 @@ public class SPdfApplication {
// Check if the BROWSER_OPEN environment variable is set to true // Check if the BROWSER_OPEN environment variable is set to true
String browserOpenEnv = env.getProperty("BROWSER_OPEN"); String browserOpenEnv = env.getProperty("BROWSER_OPEN");
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv); boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
if (browserOpen) { if (browserOpen) {
try { try {
String url = "http://localhost:" + getNonStaticPort(); String url = "http://localhost:" + getNonStaticPort();
@@ -66,6 +65,7 @@ public class SPdfApplication {
public static void main(String[] args) throws IOException, InterruptedException { public static void main(String[] args) throws IOException, InterruptedException {
SpringApplication app = new SpringApplication(SPdfApplication.class); SpringApplication app = new SpringApplication(SPdfApplication.class);
app.setAdditionalProfiles("default");
app.addInitializers(new ConfigInitializer()); app.addInitializers(new ConfigInitializer());
Map<String, String> propertyFiles = new HashMap<>(); Map<String, String> propertyFiles = new HashMap<>();
@@ -79,13 +79,14 @@ public class SPdfApplication {
// custom javs settings file // custom javs settings file
if (Files.exists(Paths.get("configs/custom_settings.yml"))) { if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
String existing = propertyFiles.getOrDefault("spring.config.additional-location", ""); String existingLocation =
if (!existing.isEmpty()) { propertyFiles.getOrDefault("spring.config.additional-location", "");
existing += ","; if (!existingLocation.isEmpty()) {
existingLocation += ",";
} }
propertyFiles.put( propertyFiles.put(
"spring.config.additional-location", "spring.config.additional-location",
existing + "file:configs/custom_settings.yml"); existingLocation + "file:configs/custom_settings.yml");
} else { } else {
logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist."); logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
} }

View File

@@ -126,13 +126,12 @@ public class AppConfig {
} }
@Bean(name = "directoryFilter") @Bean(name = "directoryFilter")
public Predicate<Path> processPDFOnlyFilter() { public Predicate<Path> processOnlyFiles() {
return path -> { return path -> {
if (Files.isDirectory(path)) { if (Files.isDirectory(path)) {
return !path.toString().contains("processing"); return !path.toString().contains("processing");
} else { } else {
String fileName = path.getFileName().toString(); return true;
return fileName.endsWith(".pdf");
} }
}; };
} }

View File

@@ -22,7 +22,8 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
"error", "error",
"erroroauth", "erroroauth",
"file", "file",
"messageType"); "messageType",
"infoMessage");
@Override @Override
public boolean preHandle( public boolean preHandle(
@@ -31,25 +32,25 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
String queryString = request.getQueryString(); String queryString = request.getQueryString();
if (queryString != null && !queryString.isEmpty()) { if (queryString != null && !queryString.isEmpty()) {
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
Map<String, String> parameters = new HashMap<>(); Map<String, String> allowedParameters = new HashMap<>();
// Keep only the allowed parameters // Keep only the allowed parameters
String[] queryParameters = queryString.split("&"); String[] queryParameters = queryString.split("&");
for (String param : queryParameters) { for (String param : queryParameters) {
String[] keyValue = param.split("="); String[] keyValuePair = param.split("=");
if (keyValue.length != 2) { if (keyValuePair.length != 2) {
continue; continue;
} }
if (ALLOWED_PARAMS.contains(keyValue[0])) { if (ALLOWED_PARAMS.contains(keyValuePair[0])) {
parameters.put(keyValue[0], keyValue[1]); allowedParameters.put(keyValuePair[0], keyValuePair[1]);
} }
} }
// If there are any parameters that are not allowed // If there are any parameters that are not allowed
if (parameters.size() != queryParameters.length) { if (allowedParameters.size() != queryParameters.length) {
// Construct new query string // Construct new query string
StringBuilder newQueryString = new StringBuilder(); StringBuilder newQueryString = new StringBuilder();
for (Map.Entry<String, String> entry : parameters.entrySet()) { for (Map.Entry<String, String> entry : allowedParameters.entrySet()) {
if (newQueryString.length() > 0) { if (newQueryString.length() > 0) {
newQueryString.append("&"); newQueryString.append("&");
} }

View File

@@ -4,17 +4,28 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.List; import java.util.List;
import org.simpleyaml.configuration.comments.CommentType;
import org.simpleyaml.configuration.file.YamlFile;
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.context.ApplicationContextInitializer; import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
public class ConfigInitializer public class ConfigInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> { implements ApplicationContextInitializer<ConfigurableApplicationContext> {
private static final Logger logger = LoggerFactory.getLogger(ConfigInitializer.class);
@Override @Override
public void initialize(ConfigurableApplicationContext applicationContext) { public void initialize(ConfigurableApplicationContext applicationContext) {
try { try {
@@ -44,95 +55,101 @@ public class ConfigInitializer
} }
} }
} else { } else {
// Path templatePath =
// Paths.get( // Define the path to the config settings file
// getClass() Path settingsPath = Paths.get("configs", "settings.yml");
// .getClassLoader() // Load the template resource
// .getResource("settings.yml.template") URL settingsTemplateResource =
// .toURI()); getClass().getClassLoader().getResource("settings.yml.template");
// Path userPath = Paths.get("configs", "settings.yml"); if (settingsTemplateResource == null) {
// throw new IOException("Resource not found: settings.yml.template");
// List<String> templateLines = Files.readAllLines(templatePath); }
// List<String> userLines =
// Files.exists(userPath) ? Files.readAllLines(userPath) : new // Create a temporary file to copy the resource content
// ArrayList<>(); Path tempTemplatePath = Files.createTempFile("settings.yml", ".template");
//
// List<String> resultLines = new ArrayList<>(); try (InputStream in = settingsTemplateResource.openStream()) {
// int position = 0; Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING);
// for (String templateLine : templateLines) { }
// // Check if the line is a comment
// if (templateLine.trim().startsWith("#")) { final YamlFile settingsTemplateFile = new YamlFile(tempTemplatePath.toFile());
// String entry = templateLine.trim().substring(1).trim(); DumperOptions yamlOptionsSettingsTemplateFile =
// if (!entry.isEmpty()) { ((SimpleYamlImplementation) settingsTemplateFile.getImplementation())
// // Check if this comment has been uncommented in userLines .getDumperOptions();
// String key = entry.split(":")[0].trim(); yamlOptionsSettingsTemplateFile.setSplitLines(false);
// addLine(resultLines, userLines, templateLine, key, position); settingsTemplateFile.loadWithComments();
// } else {
// resultLines.add(templateLine); final YamlFile settingsFile = new YamlFile(settingsPath.toFile());
// } DumperOptions yamlOptionsSettingsFile =
// } ((SimpleYamlImplementation) settingsFile.getImplementation())
// // Check if the line is a key-value pair .getDumperOptions();
// else if (templateLine.contains(":")) { yamlOptionsSettingsFile.setSplitLines(false);
// String key = templateLine.split(":")[0].trim(); settingsFile.loadWithComments();
// addLine(resultLines, userLines, templateLine, key, position);
// } // Load headers and comments
// // Handle empty lines String header = settingsTemplateFile.getHeader();
// else if (templateLine.trim().length() == 0) {
// resultLines.add(""); // Create a new file for temporary settings
// } final YamlFile tempSettingFile = new YamlFile(settingsPath.toFile());
// position++; DumperOptions yamlOptionsTempSettingFile =
// } ((SimpleYamlImplementation) tempSettingFile.getImplementation())
// .getDumperOptions();
// // Write the result to the user settings file yamlOptionsTempSettingFile.setSplitLines(false);
// Files.write(userPath, resultLines); tempSettingFile.createNewFile(true);
tempSettingFile.setHeader(header);
// Get all keys from the template
List<String> keys =
Arrays.asList(settingsTemplateFile.getKeys(true).toArray(new String[0]));
for (String key : keys) {
if (!key.contains(".")) {
// Add blank lines and comments to specific sections
tempSettingFile
.path(key)
.comment(settingsTemplateFile.getComment(key))
.blankLine();
continue;
}
// Copy settings from the template to the settings.yml file
changeConfigItemFromCommentToKeyValue(
settingsTemplateFile, settingsFile, tempSettingFile, key);
}
// Save the settings.yml file
tempSettingFile.save();
} }
// Create custom settings file if it doesn't exist
Path customSettingsPath = Paths.get("configs", "custom_settings.yml"); Path customSettingsPath = Paths.get("configs", "custom_settings.yml");
if (!Files.exists(customSettingsPath)) { if (!Files.exists(customSettingsPath)) {
Files.createFile(customSettingsPath); Files.createFile(customSettingsPath);
} }
} }
// TODO check parent value instead of just indent lines for duplicate keys (like enabled etc) private void changeConfigItemFromCommentToKeyValue(
private static void addLine( final YamlFile settingsTemplateFile,
List<String> resultLines, final YamlFile settingsFile,
List<String> userLines, final YamlFile tempSettingFile,
String templateLine, String path) {
String key, if (settingsFile.get(path) == null && settingsTemplateFile.get(path) != null) {
int position) { // If the key is only in the template, add it to the temporary settings with comments
boolean added = false; tempSettingFile
int templateIndentationLevel = getIndentationLevel(templateLine); .path(path)
int pos = 0; .set(settingsTemplateFile.get(path))
for (String settingsLine : userLines) { .comment(settingsTemplateFile.getComment(path, CommentType.BLOCK))
if (settingsLine.trim().startsWith(key + ":") && position == pos) { .commentSide(settingsTemplateFile.getComment(path, CommentType.SIDE));
int settingsIndentationLevel = getIndentationLevel(settingsLine); } else if (settingsFile.get(path) != null && settingsTemplateFile.get(path) != null) {
// Check if it is correct settingsLine and has the same parent as templateLine // If the key is in both, update the temporary settings with the main settings' value
if (settingsIndentationLevel == templateIndentationLevel) { // and comments
resultLines.add(settingsLine); tempSettingFile
added = true; .path(path)
break; .set(settingsFile.get(path))
} .comment(settingsTemplateFile.getComment(path, CommentType.BLOCK))
} .commentSide(settingsTemplateFile.getComment(path, CommentType.SIDE));
pos++; } else {
// Log if the key is not found in both YAML files
logger.info("Key not found in both YAML files: " + path);
} }
if (!added) {
resultLines.add(templateLine);
}
}
private static int getIndentationLevel(String line) {
int indentationLevel = 0;
String trimmedLine = line.trim();
if (trimmedLine.startsWith("#")) {
line = trimmedLine.substring(1);
}
for (char c : line.toCharArray()) {
if (c == ' ') {
indentationLevel++;
} else {
break;
}
}
return indentationLevel;
} }
} }

View File

@@ -0,0 +1,16 @@
package stirling.software.SPDF.config;
import java.io.IOException;
import java.util.List;
import stirling.software.SPDF.utils.FileInfo;
public interface DatabaseBackupInterface {
void exportDatabase() throws IOException;
boolean importDatabase();
boolean hasBackup();
List<FileInfo> getBackupList();
}

View File

@@ -137,6 +137,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Other", "auto-rename"); addEndpointToGroup("Other", "auto-rename");
addEndpointToGroup("Other", "get-info-on-pdf"); addEndpointToGroup("Other", "get-info-on-pdf");
addEndpointToGroup("Other", "show-javascript"); addEndpointToGroup("Other", "show-javascript");
addEndpointToGroup("Other", "remove-image-pdf");
// CLI // CLI
addEndpointToGroup("CLI", "compress-pdf"); addEndpointToGroup("CLI", "compress-pdf");
@@ -165,6 +166,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Python", REMOVE_BLANKS); addEndpointToGroup("Python", REMOVE_BLANKS);
addEndpointToGroup("Python", "html-to-pdf"); addEndpointToGroup("Python", "html-to-pdf");
addEndpointToGroup("Python", "url-to-pdf"); addEndpointToGroup("Python", "url-to-pdf");
addEndpointToGroup("Python", "pdf-to-img");
// openCV // openCV
addEndpointToGroup("OpenCV", "extract-image-scans"); addEndpointToGroup("OpenCV", "extract-image-scans");
@@ -221,6 +223,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "split-pdf-by-sections"); addEndpointToGroup("Java", "split-pdf-by-sections");
addEndpointToGroup("Java", REMOVE_BLANKS); addEndpointToGroup("Java", REMOVE_BLANKS);
addEndpointToGroup("Java", "pdf-to-text"); addEndpointToGroup("Java", "pdf-to-text");
addEndpointToGroup("Java", "remove-image-pdf");
// Javascript // Javascript
addEndpointToGroup("Javascript", "pdf-organizer"); addEndpointToGroup("Javascript", "pdf-organizer");

View File

@@ -3,9 +3,8 @@ package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import java.util.Optional; import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InternalAuthenticationServiceException; import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
@@ -15,17 +14,16 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFa
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
@Slf4j
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private LoginAttemptService loginAttemptService; private LoginAttemptService loginAttemptService;
private UserService userService; private UserService userService;
private static final Logger logger =
LoggerFactory.getLogger(CustomAuthenticationFailureHandler.class);
public CustomAuthenticationFailureHandler( public CustomAuthenticationFailureHandler(
final LoginAttemptService loginAttemptService, UserService userService) { final LoginAttemptService loginAttemptService, UserService userService) {
this.loginAttemptService = loginAttemptService; this.loginAttemptService = loginAttemptService;
@@ -39,14 +37,17 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
AuthenticationException exception) AuthenticationException exception)
throws IOException, ServletException { throws IOException, ServletException {
if (exception instanceof DisabledException) {
log.error("User is deactivated: ", exception);
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
return;
}
String ip = request.getRemoteAddr(); String ip = request.getRemoteAddr();
logger.error("Failed login attempt from IP: {}", ip); log.error("Failed login attempt from IP: {}", ip);
String contextPath = request.getContextPath(); if (exception instanceof LockedException) {
getRedirectStrategy().sendRedirect(request, response, "/login?error=locked");
if (exception.getClass().isAssignableFrom(InternalAuthenticationServiceException.class)
|| "Password must not be null".equalsIgnoreCase(exception.getMessage())) {
response.sendRedirect(contextPath + "/login?error=oauth2AuthenticationError");
return; return;
} }
@@ -54,20 +55,25 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
Optional<User> optUser = userService.findByUsernameIgnoreCase(username); Optional<User> optUser = userService.findByUsernameIgnoreCase(username);
if (username != null && optUser.isPresent() && !isDemoUser(optUser)) { if (username != null && optUser.isPresent() && !isDemoUser(optUser)) {
logger.info( log.info(
"Remaining attempts for user {}: {}", "Remaining attempts for user {}: {}",
optUser.get().getUsername(), username,
loginAttemptService.getRemainingAttempts(username)); loginAttemptService.getRemainingAttempts(username));
loginAttemptService.loginFailed(username); loginAttemptService.loginFailed(username);
if (loginAttemptService.isBlocked(username) if (loginAttemptService.isBlocked(username) || exception instanceof LockedException) {
|| exception.getClass().isAssignableFrom(LockedException.class)) { getRedirectStrategy().sendRedirect(request, response, "/login?error=locked");
response.sendRedirect(contextPath + "/login?error=locked");
return; return;
} }
} }
if (exception.getClass().isAssignableFrom(BadCredentialsException.class) if (exception instanceof BadCredentialsException
|| exception.getClass().isAssignableFrom(UsernameNotFoundException.class)) { || exception instanceof UsernameNotFoundException) {
response.sendRedirect(contextPath + "/login?error=badcredentials"); getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
return;
}
if (exception instanceof InternalAuthenticationServiceException
|| "Password must not be null".equalsIgnoreCase(exception.getMessage())) {
getRedirectStrategy()
.sendRedirect(request, response, "/login?error=oauth2AuthenticationError");
return; return;
} }

View File

@@ -10,15 +10,20 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.utils.RequestUriUtils; import stirling.software.SPDF.utils.RequestUriUtils;
@Slf4j
public class CustomAuthenticationSuccessHandler public class CustomAuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler { extends SavedRequestAwareAuthenticationSuccessHandler {
private LoginAttemptService loginAttemptService; private LoginAttemptService loginAttemptService;
private UserService userService;
public CustomAuthenticationSuccessHandler(LoginAttemptService loginAttemptService) { public CustomAuthenticationSuccessHandler(
LoginAttemptService loginAttemptService, UserService userService) {
this.loginAttemptService = loginAttemptService; this.loginAttemptService = loginAttemptService;
this.userService = userService;
} }
@Override @Override
@@ -27,6 +32,10 @@ public class CustomAuthenticationSuccessHandler
throws ServletException, IOException { throws ServletException, IOException {
String userName = request.getParameter("username"); String userName = request.getParameter("username");
if (userService.isUserDisabled(userName)) {
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
return;
}
loginAttemptService.loginSucceeded(userName); loginAttemptService.loginSucceeded(userName);
// Get the saved request // Get the saved request

View File

@@ -2,32 +2,26 @@ package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
@Autowired SessionRegistry sessionRegistry;
@Override @Override
public void onLogoutSuccess( public void onLogoutSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication) HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException { throws IOException, ServletException {
HttpSession session = request.getSession(false);
if (session != null) { if (request.getParameter("userIsDisabled") != null) {
String sessionId = session.getId(); getRedirectStrategy()
sessionRegistry.removeSessionInformation(sessionId); .sendRedirect(request, response, "/login?erroroauth=userIsDisabled");
session.invalidate(); return;
logger.debug("Session invalidated: " + sessionId);
} }
response.sendRedirect(request.getContextPath() + "/login?logout=true"); getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
} }
} }

View File

@@ -1,34 +1,40 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import org.slf4j.Logger; import org.simpleyaml.configuration.file.YamlFile;
import org.slf4j.LoggerFactory; import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.DatabaseBackupInterface;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
@Component @Component
@Slf4j
public class InitialSecuritySetup { public class InitialSecuritySetup {
@Autowired private UserService userService; @Autowired private UserService userService;
@Autowired private ApplicationProperties applicationProperties; @Autowired private ApplicationProperties applicationProperties;
private static final Logger logger = LoggerFactory.getLogger(InitialSecuritySetup.class); @Autowired private DatabaseBackupInterface databaseBackupHelper;
@PostConstruct @PostConstruct
public void init() { public void init() throws IllegalArgumentException, IOException {
if (!userService.hasUsers()) { if (databaseBackupHelper.hasBackup() && !userService.hasUsers()) {
databaseBackupHelper.importDatabase();
} else if (!userService.hasUsers()) {
initializeAdminUser(); initializeAdminUser();
} else {
databaseBackupHelper.exportDatabase();
} }
initializeInternalApiUser(); initializeInternalApiUser();
} }
@@ -42,12 +48,11 @@ public class InitialSecuritySetup {
} }
} }
private void initializeAdminUser() { private void initializeAdminUser() throws IOException {
String initialUsername = String initialUsername =
applicationProperties.getSecurity().getInitialLogin().getUsername(); applicationProperties.getSecurity().getInitialLogin().getUsername();
String initialPassword = String initialPassword =
applicationProperties.getSecurity().getInitialLogin().getPassword(); applicationProperties.getSecurity().getInitialLogin().getPassword();
if (initialUsername != null if (initialUsername != null
&& !initialUsername.isEmpty() && !initialUsername.isEmpty()
&& initialPassword != null && initialPassword != null
@@ -55,9 +60,9 @@ public class InitialSecuritySetup {
&& !userService.findByUsernameIgnoreCase(initialUsername).isPresent()) { && !userService.findByUsernameIgnoreCase(initialUsername).isPresent()) {
try { try {
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
logger.info("Admin user created: " + initialUsername); log.info("Admin user created: " + initialUsername);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
logger.error("Failed to initialize security setup", e); log.error("Failed to initialize security setup", e);
System.exit(1); System.exit(1);
} }
} else { } else {
@@ -65,54 +70,41 @@ public class InitialSecuritySetup {
} }
} }
private void createDefaultAdminUser() { private void createDefaultAdminUser() throws IllegalArgumentException, IOException {
String defaultUsername = "admin"; String defaultUsername = "admin";
String defaultPassword = "stirling"; String defaultPassword = "stirling";
if (!userService.findByUsernameIgnoreCase(defaultUsername).isPresent()) { if (!userService.findByUsernameIgnoreCase(defaultUsername).isPresent()) {
userService.saveUser(defaultUsername, defaultPassword, Role.ADMIN.getRoleId(), true); userService.saveUser(defaultUsername, defaultPassword, Role.ADMIN.getRoleId(), true);
logger.info("Default admin user created: " + defaultUsername); log.info("Default admin user created: " + defaultUsername);
} }
} }
private void initializeInternalApiUser() { private void initializeInternalApiUser() throws IllegalArgumentException, IOException {
if (!userService.usernameExistsIgnoreCase(Role.INTERNAL_API_USER.getRoleId())) { if (!userService.usernameExistsIgnoreCase(Role.INTERNAL_API_USER.getRoleId())) {
userService.saveUser( userService.saveUser(
Role.INTERNAL_API_USER.getRoleId(), Role.INTERNAL_API_USER.getRoleId(),
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
Role.INTERNAL_API_USER.getRoleId()); Role.INTERNAL_API_USER.getRoleId());
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId()); userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
logger.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId()); log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId());
} }
} }
private void saveKeyToConfig(String key) throws IOException { private void saveKeyToConfig(String key) throws IOException {
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
List<String> lines = Files.readAllLines(path);
boolean keyFound = false;
// Search for the existing key to replace it or place to add it final YamlFile settingsYml = new YamlFile(path.toFile());
for (int i = 0; i < lines.size(); i++) { DumperOptions yamlOptionssettingsYml =
if (lines.get(i).startsWith("AutomaticallyGenerated:")) { ((SimpleYamlImplementation) settingsYml.getImplementation()).getDumperOptions();
keyFound = true; yamlOptionssettingsYml.setSplitLines(false);
if (i + 1 < lines.size() && lines.get(i + 1).trim().startsWith("key:")) {
lines.set(i + 1, " key: " + key);
break;
} else {
lines.add(i + 1, " key: " + key);
break;
}
}
}
// If the section doesn't exist, append it settingsYml.loadWithComments();
if (!keyFound) {
lines.add("# Automatically Generated Settings (Do Not Edit Directly)");
lines.add("AutomaticallyGenerated:");
lines.add(" key: " + key);
}
// Write back to the file settingsYml
Files.write(path, lines); .path("AutomaticallyGenerated.key")
.set(key)
.comment("# Automatically Generated Settings (Do Not Edit Directly)");
settingsYml.save();
} }
private boolean isValidUUID(String uuid) { private boolean isValidUUID(String uuid) {

View File

@@ -3,29 +3,32 @@ package stirling.software.SPDF.config.security;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.AttemptCounter; import stirling.software.SPDF.model.AttemptCounter;
@Service @Service
@Slf4j
public class LoginAttemptService { public class LoginAttemptService {
@Autowired ApplicationProperties applicationProperties; @Autowired private ApplicationProperties applicationProperties;
private static final Logger logger = LoggerFactory.getLogger(LoginAttemptService.class);
private int MAX_ATTEMPT; private int MAX_ATTEMPT;
private long ATTEMPT_INCREMENT_TIME; private long ATTEMPT_INCREMENT_TIME;
private ConcurrentHashMap<String, AttemptCounter> attemptsCache; private ConcurrentHashMap<String, AttemptCounter> attemptsCache;
private boolean isBlockedEnabled = true;
@PostConstruct @PostConstruct
public void init() { public void init() {
MAX_ATTEMPT = applicationProperties.getSecurity().getLoginAttemptCount(); MAX_ATTEMPT = applicationProperties.getSecurity().getLoginAttemptCount();
if (MAX_ATTEMPT == -1) {
isBlockedEnabled = false;
log.info("Login attempt tracking is disabled.");
}
ATTEMPT_INCREMENT_TIME = ATTEMPT_INCREMENT_TIME =
TimeUnit.MINUTES.toMillis( TimeUnit.MINUTES.toMillis(
applicationProperties.getSecurity().getLoginResetTimeMinutes()); applicationProperties.getSecurity().getLoginResetTimeMinutes());
@@ -33,14 +36,16 @@ public class LoginAttemptService {
} }
public void loginSucceeded(String key) { public void loginSucceeded(String key) {
if (key == null || key.trim().isEmpty()) { if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
return; return;
} }
attemptsCache.remove(key.toLowerCase()); attemptsCache.remove(key.toLowerCase());
} }
public void loginFailed(String key) { public void loginFailed(String key) {
if (key == null || key.trim().isEmpty()) return; if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
return;
}
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase()); AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
if (attemptCounter == null) { if (attemptCounter == null) {
@@ -55,7 +60,9 @@ public class LoginAttemptService {
} }
public boolean isBlocked(String key) { public boolean isBlocked(String key) {
if (key == null || key.trim().isEmpty()) return false; if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
return false;
}
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase()); AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
if (attemptCounter == null) { if (attemptCounter == null) {
return false; return false;
@@ -65,7 +72,9 @@ public class LoginAttemptService {
} }
public int getRemainingAttempts(String key) { public int getRemainingAttempts(String key) {
if (key == null || key.trim().isEmpty()) return MAX_ATTEMPT; if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
return Integer.MAX_VALUE; // Arbitrarily high number if tracking is disabled
}
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase()); AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
if (attemptCounter == null) { if (attemptCounter == null) {

View File

@@ -6,27 +6,20 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; 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.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 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.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
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.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
@@ -37,22 +30,29 @@ import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationF
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
import stirling.software.SPDF.config.security.saml.CustomSAMLAuthenticationFailureHandler;
import stirling.software.SPDF.config.security.saml.CustomSAMLAuthenticationSuccessHandler;
import stirling.software.SPDF.config.security.saml.SAMLLogoutSuccessHandler;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.GithubProvider;
import stirling.software.SPDF.model.ApplicationProperties.GoogleProvider;
import stirling.software.SPDF.model.ApplicationProperties.KeycloakProvider;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.JPATokenRepositoryImpl; import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
@Configuration @Configuration
@EnableWebSecurity() @EnableWebSecurity
@EnableMethodSecurity @EnableMethodSecurity
public class SecurityConfiguration { public class SecurityConfiguration {
@Autowired private CustomUserDetailsService userDetailsService; @Autowired private CustomUserDetailsService userDetailsService;
@Autowired(required = false)
private GrantedAuthoritiesMapper userAuthoritiesMapper;
@Autowired(required = false)
private OpenSaml4AuthenticationProvider samlAuthenticationProvider;
@Autowired(required = false)
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
private static final Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class); private static final Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
@Bean @Bean
@@ -73,11 +73,7 @@ public class SecurityConfiguration {
@Autowired private LoginAttemptService loginAttemptService; @Autowired private LoginAttemptService loginAttemptService;
@Autowired private FirstLoginFilter firstLoginFilter; @Autowired private FirstLoginFilter firstLoginFilter;
@Autowired private SessionPersistentRegistry sessionRegistry;
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
@@ -94,7 +90,7 @@ public class SecurityConfiguration {
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(10) .maximumSessions(10)
.maxSessionsPreventsLogin(false) .maxSessionsPreventsLogin(false)
.sessionRegistry(sessionRegistry()) .sessionRegistry(sessionRegistry)
.expiredUrl("/login?logout=true")); .expiredUrl("/login?logout=true"));
http.formLogin( http.formLogin(
@@ -103,7 +99,7 @@ public class SecurityConfiguration {
.loginPage("/login") .loginPage("/login")
.successHandler( .successHandler(
new CustomAuthenticationSuccessHandler( new CustomAuthenticationSuccessHandler(
loginAttemptService)) loginAttemptService, userService))
.defaultSuccessUrl("/") .defaultSuccessUrl("/")
.failureHandler( .failureHandler(
new CustomAuthenticationFailureHandler( new CustomAuthenticationFailureHandler(
@@ -141,6 +137,7 @@ public class SecurityConfiguration {
return trimmedUri.startsWith("/login") return trimmedUri.startsWith("/login")
|| trimmedUri.startsWith("/oauth") || trimmedUri.startsWith("/oauth")
|| trimmedUri.startsWith("/saml2")
|| trimmedUri.endsWith(".svg") || trimmedUri.endsWith(".svg")
|| trimmedUri.startsWith( || trimmedUri.startsWith(
"/register") "/register")
@@ -155,12 +152,15 @@ public class SecurityConfiguration {
}) })
.permitAll() .permitAll()
.anyRequest() .anyRequest()
.authenticated()) .authenticated());
.authenticationProvider(authenticationProvider());
// Handle OAUTH2 Logins // Handle OAUTH2 Logins
if (applicationProperties.getSecurity().getOAUTH2() != null if (applicationProperties.getSecurity().getOAUTH2() != null
&& applicationProperties.getSecurity().getOAUTH2().getEnabled()) { && applicationProperties.getSecurity().getOAUTH2().getEnabled()
&& !applicationProperties
.getSecurity()
.getLoginMethod()
.equalsIgnoreCase("normal")) {
http.oauth2Login( http.oauth2Login(
oauth2 -> oauth2 ->
@@ -187,14 +187,41 @@ public class SecurityConfiguration {
userService, userService,
loginAttemptService)) loginAttemptService))
.userAuthoritiesMapper( .userAuthoritiesMapper(
userAuthoritiesMapper()))) userAuthoritiesMapper)))
.logout( .logout(
logout -> logout ->
logout.logoutSuccessHandler( logout.logoutSuccessHandler(
new CustomOAuth2LogoutSuccessHandler( new CustomOAuth2LogoutSuccessHandler(
this.applicationProperties, applicationProperties)));
sessionRegistry())) }
.invalidateHttpSession(true));
// Handle SAML Logins
if (applicationProperties.getSecurity().getSAML() != null
&& applicationProperties.getSecurity().getSAML().getEnabled()
&& !applicationProperties
.getSecurity()
.getLoginMethod()
.equalsIgnoreCase("normal")) {
http.saml2Login(
saml2 ->
saml2.relyingPartyRegistrationRepository(
relyingPartyRegistrationRepository)
.loginProcessingUrl("/login/saml2/sso/stirling")
.loginPage("/saml2")
.authenticationManager(
new ProviderManager(
samlAuthenticationProvider))
.successHandler(
new CustomSAMLAuthenticationSuccessHandler(
loginAttemptService, userService))
.failureHandler(
new CustomSAMLAuthenticationFailureHandler()))
.saml2Metadata(Customizer.withDefaults())
.logout(
logout ->
logout.logoutSuccessHandler(
new SAMLLogoutSuccessHandler()));
} }
} else { } else {
http.csrf(csrf -> csrf.disable()) http.csrf(csrf -> csrf.disable())
@@ -204,192 +231,12 @@ public class SecurityConfiguration {
return http.build(); return http.build();
} }
// Client Registration Repository for OAUTH2 OIDC Login
@Bean
@ConditionalOnProperty(
value = "security.oauth2.enabled",
havingValue = "true",
matchIfMissing = false)
public ClientRegistrationRepository clientRegistrationRepository() {
List<ClientRegistration> registrations = new ArrayList<>();
githubClientRegistration().ifPresent(registrations::add);
oidcClientRegistration().ifPresent(registrations::add);
googleClientRegistration().ifPresent(registrations::add);
keycloakClientRegistration().ifPresent(registrations::add);
if (registrations.isEmpty()) {
logger.error("At least one OAuth2 provider must be configured");
System.exit(1);
}
return new InMemoryClientRegistrationRepository(registrations);
}
private Optional<ClientRegistration> googleClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
GoogleProvider google = client.getGoogle();
return google != null && google.isSettingsValid()
? Optional.of(
ClientRegistration.withRegistrationId(google.getName())
.clientId(google.getClientId())
.clientSecret(google.getClientSecret())
.scope(google.getScopes())
.authorizationUri(google.getAuthorizationuri())
.tokenUri(google.getTokenuri())
.userInfoUri(google.getUserinfouri())
.userNameAttributeName(google.getUseAsUsername())
.clientName(google.getClientName())
.redirectUri("{baseUrl}/login/oauth2/code/" + google.getName())
.authorizationGrantType(
org.springframework.security.oauth2.core
.AuthorizationGrantType.AUTHORIZATION_CODE)
.build())
: Optional.empty();
}
private Optional<ClientRegistration> keycloakClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
KeycloakProvider keycloak = client.getKeycloak();
return keycloak != null && keycloak.isSettingsValid()
? Optional.of(
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
.registrationId(keycloak.getName())
.clientId(keycloak.getClientId())
.clientSecret(keycloak.getClientSecret())
.scope(keycloak.getScopes())
.userNameAttributeName(keycloak.getUseAsUsername())
.clientName(keycloak.getClientName())
.build())
: Optional.empty();
}
private Optional<ClientRegistration> githubClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
GithubProvider github = client.getGithub();
return github != null && github.isSettingsValid()
? Optional.of(
ClientRegistration.withRegistrationId(github.getName())
.clientId(github.getClientId())
.clientSecret(github.getClientSecret())
.scope(github.getScopes())
.authorizationUri(github.getAuthorizationuri())
.tokenUri(github.getTokenuri())
.userInfoUri(github.getUserinfouri())
.userNameAttributeName(github.getUseAsUsername())
.clientName(github.getClientName())
.redirectUri("{baseUrl}/login/oauth2/code/" + github.getName())
.authorizationGrantType(
org.springframework.security.oauth2.core
.AuthorizationGrantType.AUTHORIZATION_CODE)
.build())
: Optional.empty();
}
private Optional<ClientRegistration> oidcClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
if (oauth == null
|| oauth.getIssuer() == null
|| oauth.getIssuer().isEmpty()
|| oauth.getClientId() == null
|| oauth.getClientId().isEmpty()
|| oauth.getClientSecret() == null
|| oauth.getClientSecret().isEmpty()
|| oauth.getScopes() == null
|| oauth.getScopes().isEmpty()
|| oauth.getUseAsUsername() == null
|| oauth.getUseAsUsername().isEmpty()) {
return Optional.empty();
}
return Optional.of(
ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
.registrationId("oidc")
.clientId(oauth.getClientId())
.clientSecret(oauth.getClientSecret())
.scope(oauth.getScopes())
.userNameAttributeName(oauth.getUseAsUsername())
.clientName("OIDC")
.build());
}
/*
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.
*/
@Bean
@ConditionalOnProperty(
value = "security.oauth2.enabled",
havingValue = "true",
matchIfMissing = false)
GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(
authority -> {
// Add existing OAUTH2 Authorities
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
// Add Authorities from database for existing user, if user is present.
if (authority instanceof OAuth2UserAuthority oauth2Auth) {
String useAsUsername =
applicationProperties
.getSecurity()
.getOAUTH2()
.getUseAsUsername();
Optional<User> userOpt =
userService.findByUsernameIgnoreCase(
(String) oauth2Auth.getAttributes().get(useAsUsername));
if (userOpt.isPresent()) {
User user = userOpt.get();
if (user != null) {
mappedAuthorities.add(
new SimpleGrantedAuthority(
userService.findRole(user).getAuthority()));
}
}
}
});
return mappedAuthorities;
};
}
@Bean @Bean
public IPRateLimitingFilter rateLimitingFilter() { public IPRateLimitingFilter rateLimitingFilter() {
int maxRequestsPerIp = 1000000; // Example limit TODO add config level int maxRequestsPerIp = 1000000; // Example limit TODO add config level
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp); return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
} }
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean @Bean
public PersistentTokenRepository persistentTokenRepository() { public PersistentTokenRepository persistentTokenRepository() {
return new JPATokenRepositoryImpl(); return new JPATokenRepositoryImpl();

View File

@@ -1,6 +1,9 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
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.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
@@ -8,9 +11,11 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
@@ -18,15 +23,17 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApiKeyAuthenticationToken; import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
import stirling.software.SPDF.model.User;
@Component @Component
public class UserAuthenticationFilter extends OncePerRequestFilter { public class UserAuthenticationFilter extends OncePerRequestFilter {
@Autowired private UserDetailsService userDetailsService;
@Autowired @Lazy private UserService userService; @Autowired @Lazy private UserService userService;
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
@Autowired @Autowired
@Qualifier("loginEnabled") @Qualifier("loginEnabled")
public boolean loginEnabledValue; public boolean loginEnabledValue;
@@ -51,15 +58,20 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
try { try {
// Use API key to authenticate. This requires you to have an authentication // Use API key to authenticate. This requires you to have an authentication
// provider for API keys. // provider for API keys.
UserDetails userDetails = userService.loadUserByApiKey(apiKey); Optional<User> user = userService.getUserByApiKey(apiKey);
if (userDetails == null) { if (!user.isPresent()) {
response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Invalid API Key."); response.getWriter().write("Invalid API Key.");
return; return;
} }
authentication = List<SimpleGrantedAuthority> authorities =
new ApiKeyAuthenticationToken( user.get().getAuthorities().stream()
userDetails, apiKey, userDetails.getAuthorities()); .map(
authority ->
new SimpleGrantedAuthority(
authority.getAuthority()))
.collect(Collectors.toList());
authentication = new ApiKeyAuthenticationToken(user.get(), apiKey, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (AuthenticationException e) { } catch (AuthenticationException e) {
// If API key authentication fails, deny the request // If API key authentication fails, deny the request
@@ -87,6 +99,43 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
} }
} }
// Check if the authenticated user is disabled and invalidate their session if so
if (authentication != null && authentication.isAuthenticated()) {
Object principal = authentication.getPrincipal();
String username = null;
if (principal instanceof UserDetails) {
username = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) {
username = ((OAuth2User) principal).getName();
} else if (principal instanceof String) {
username = (String) principal;
}
List<SessionInformation> sessionsInformations =
sessionPersistentRegistry.getAllSessions(principal, false);
if (username != null) {
boolean isUserExists = userService.usernameExistsIgnoreCase(username);
boolean isUserDisabled = userService.isUserDisabled(username);
if (!isUserExists || isUserDisabled) {
for (SessionInformation sessionsInformation : sessionsInformations) {
sessionsInformation.expireNow();
sessionPersistentRegistry.expireSession(sessionsInformation.getSessionId());
}
}
if (!isUserExists) {
response.sendRedirect(request.getContextPath() + "/logout?badcredentials=true");
return;
}
if (isUserDisabled) {
response.sendRedirect(request.getContextPath() + "/logout?userIsDisabled=true");
return;
}
}
}
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }

View File

@@ -19,7 +19,6 @@ import org.springframework.web.filter.OncePerRequestFilter;
import io.github.bucket4j.Bandwidth; import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket; import io.github.bucket4j.Bucket;
import io.github.bucket4j.ConsumptionProbe; import io.github.bucket4j.ConsumptionProbe;
import io.github.bucket4j.Refill;
import io.github.pixee.security.Newlines; import io.github.pixee.security.Newlines;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
@@ -142,7 +141,10 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
private Bucket createUserBucket(int limitPerDay) { private Bucket createUserBucket(int limitPerDay) {
Bandwidth limit = Bandwidth limit =
Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1))); Bandwidth.builder()
.capacity(limitPerDay)
.refillIntervally(limitPerDay, Duration.ofDays(1))
.build();
return Bucket.builder().addLimit(limit).build(); return Bucket.builder().addLimit(limit).build();
} }
} }

View File

@@ -1,10 +1,7 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.util.Collection; import java.io.IOException;
import java.util.HashMap; import java.util.*;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -14,11 +11,15 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import stirling.software.SPDF.config.DatabaseBackupInterface;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface; import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.AuthenticationType; import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.Authority; import stirling.software.SPDF.model.Authority;
@@ -38,12 +39,17 @@ public class UserService implements UserServiceInterface {
@Autowired private MessageSource messageSource; @Autowired private MessageSource messageSource;
@Autowired private SessionPersistentRegistry sessionRegistry;
@Autowired DatabaseBackupInterface databaseBackupHelper;
// Handle OAUTH2 login and user auto creation. // Handle OAUTH2 login and user auto creation.
public boolean processOAuth2PostLogin(String username, boolean autoCreateUser) { public boolean processOAuth2PostLogin(String username, boolean autoCreateUser)
throws IllegalArgumentException, IOException {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
return false; return false;
} }
Optional<User> existingUser = userRepository.findByUsernameIgnoreCase(username); Optional<User> existingUser = findByUsernameIgnoreCase(username);
if (existingUser.isPresent()) { if (existingUser.isPresent()) {
return true; return true;
} }
@@ -55,8 +61,8 @@ public class UserService implements UserServiceInterface {
} }
public Authentication getAuthentication(String apiKey) { public Authentication getAuthentication(String apiKey) {
User user = getUserByApiKey(apiKey); Optional<User> user = getUserByApiKey(apiKey);
if (user == null) { if (!user.isPresent()) {
throw new UsernameNotFoundException("API key is not valid"); throw new UsernameNotFoundException("API key is not valid");
} }
@@ -64,7 +70,7 @@ public class UserService implements UserServiceInterface {
return new UsernamePasswordAuthenticationToken( return new UsernamePasswordAuthenticationToken(
user, // principal (typically the user) user, // principal (typically the user)
null, // credentials (we don't expose the password or API key here) null, // credentials (we don't expose the password or API key here)
getAuthorities(user) // user's authorities (roles/permissions) getAuthorities(user.get()) // user's authorities (roles/permissions)
); );
} }
@@ -79,18 +85,17 @@ public class UserService implements UserServiceInterface {
String apiKey; String apiKey;
do { do {
apiKey = UUID.randomUUID().toString(); apiKey = UUID.randomUUID().toString();
} while (userRepository.findByApiKey(apiKey) != null); // Ensure uniqueness } while (userRepository.findByApiKey(apiKey).isPresent()); // Ensure uniqueness
return apiKey; return apiKey;
} }
public User addApiKeyToUser(String username) { public User addApiKeyToUser(String username) {
User user = Optional<User> user = findByUsernameIgnoreCase(username);
userRepository if (user.isPresent()) {
.findByUsernameIgnoreCase(username) user.get().setApiKey(generateApiKey());
.orElseThrow(() -> new UsernameNotFoundException("User not found")); return userRepository.save(user.get());
}
user.setApiKey(generateApiKey()); throw new UsernameNotFoundException("User not found");
return userRepository.save(user);
} }
public User refreshApiKeyForUser(String username) { public User refreshApiKeyForUser(String username) {
@@ -99,39 +104,40 @@ public class UserService implements UserServiceInterface {
public String getApiKeyForUser(String username) { public String getApiKeyForUser(String username) {
User user = User user =
userRepository findByUsernameIgnoreCase(username)
.findByUsernameIgnoreCase(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found")); .orElseThrow(() -> new UsernameNotFoundException("User not found"));
return user.getApiKey(); return user.getApiKey();
} }
public boolean isValidApiKey(String apiKey) { public boolean isValidApiKey(String apiKey) {
return userRepository.findByApiKey(apiKey) != null; return userRepository.findByApiKey(apiKey).isPresent();
} }
public User getUserByApiKey(String apiKey) { public Optional<User> getUserByApiKey(String apiKey) {
return userRepository.findByApiKey(apiKey); return userRepository.findByApiKey(apiKey);
} }
public UserDetails loadUserByApiKey(String apiKey) { public Optional<User> loadUserByApiKey(String apiKey) {
User user = userRepository.findByApiKey(apiKey); Optional<User> user = userRepository.findByApiKey(apiKey);
if (user != null) {
// Convert your User entity to a UserDetails object with authorities if (user.isPresent()) {
return new org.springframework.security.core.userdetails.User( return user;
user.getUsername(),
user.getPassword(), // you might not need this for API key auth
getAuthorities(user));
} }
return null; // or throw an exception return null; // or throw an exception
} }
public boolean validateApiKeyForUser(String username, String apiKey) { public boolean validateApiKeyForUser(String username, String apiKey) {
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username); Optional<User> userOpt = findByUsernameIgnoreCase(username);
return userOpt.isPresent() && apiKey.equals(userOpt.get().getApiKey()); return userOpt.isPresent() && apiKey.equals(userOpt.get().getApiKey());
} }
public void saveUser(String username, AuthenticationType authenticationType) public void saveUser(String username, AuthenticationType authenticationType)
throws IllegalArgumentException { throws IllegalArgumentException, IOException {
saveUser(username, authenticationType, Role.USER.getRoleId());
}
public void saveUser(String username, AuthenticationType authenticationType, String role)
throws IllegalArgumentException, IOException {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage()); throw new IllegalArgumentException(getInvalidUsernameMessage());
} }
@@ -139,12 +145,14 @@ public class UserService implements UserServiceInterface {
user.setUsername(username); user.setUsername(username);
user.setEnabled(true); user.setEnabled(true);
user.setFirstLogin(false); user.setFirstLogin(false);
user.addAuthority(new Authority(Role.USER.getRoleId(), user)); user.addAuthority(new Authority(role, user));
user.setAuthenticationType(authenticationType); user.setAuthenticationType(authenticationType);
userRepository.save(user); userRepository.save(user);
databaseBackupHelper.exportDatabase();
} }
public void saveUser(String username, String password) throws IllegalArgumentException { public void saveUser(String username, String password)
throws IllegalArgumentException, IOException {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage()); throw new IllegalArgumentException(getInvalidUsernameMessage());
} }
@@ -154,10 +162,11 @@ public class UserService implements UserServiceInterface {
user.setEnabled(true); user.setEnabled(true);
user.setAuthenticationType(AuthenticationType.WEB); user.setAuthenticationType(AuthenticationType.WEB);
userRepository.save(user); userRepository.save(user);
databaseBackupHelper.exportDatabase();
} }
public void saveUser(String username, String password, String role, boolean firstLogin) public void saveUser(String username, String password, String role, boolean firstLogin)
throws IllegalArgumentException { throws IllegalArgumentException, IOException {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage()); throw new IllegalArgumentException(getInvalidUsernameMessage());
} }
@@ -169,15 +178,16 @@ public class UserService implements UserServiceInterface {
user.setAuthenticationType(AuthenticationType.WEB); user.setAuthenticationType(AuthenticationType.WEB);
user.setFirstLogin(firstLogin); user.setFirstLogin(firstLogin);
userRepository.save(user); userRepository.save(user);
databaseBackupHelper.exportDatabase();
} }
public void saveUser(String username, String password, String role) public void saveUser(String username, String password, String role)
throws IllegalArgumentException { throws IllegalArgumentException, IOException {
saveUser(username, password, role, false); saveUser(username, password, role, false);
} }
public void deleteUser(String username) { public void deleteUser(String username) {
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username); Optional<User> userOpt = findByUsernameIgnoreCase(username);
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
for (Authority authority : userOpt.get().getAuthorities()) { for (Authority authority : userOpt.get().getAuthorities()) {
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) { if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
@@ -186,28 +196,28 @@ public class UserService implements UserServiceInterface {
} }
userRepository.delete(userOpt.get()); userRepository.delete(userOpt.get());
} }
invalidateUserSessions(username);
} }
public boolean usernameExists(String username) { public boolean usernameExists(String username) {
return userRepository.findByUsername(username).isPresent(); return findByUsername(username).isPresent();
} }
public boolean usernameExistsIgnoreCase(String username) { public boolean usernameExistsIgnoreCase(String username) {
return userRepository.findByUsernameIgnoreCase(username).isPresent(); return findByUsernameIgnoreCase(username).isPresent();
} }
public boolean hasUsers() { public boolean hasUsers() {
long userCount = userRepository.count(); long userCount = userRepository.count();
if (userRepository if (findByUsernameIgnoreCase(Role.INTERNAL_API_USER.getRoleId()).isPresent()) {
.findByUsernameIgnoreCase(Role.INTERNAL_API_USER.getRoleId())
.isPresent()) {
userCount -= 1; userCount -= 1;
} }
return userCount > 0; return userCount > 0;
} }
public void updateUserSettings(String username, Map<String, String> updates) { public void updateUserSettings(String username, Map<String, String> updates)
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username); throws IOException {
Optional<User> userOpt = findByUsernameIgnoreCaseWithSettings(username);
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
User user = userOpt.get(); User user = userOpt.get();
Map<String, String> settingsMap = user.getSettings(); Map<String, String> settingsMap = user.getSettings();
@@ -220,6 +230,7 @@ public class UserService implements UserServiceInterface {
user.setSettings(settingsMap); user.setSettings(settingsMap);
userRepository.save(user); userRepository.save(user);
databaseBackupHelper.exportDatabase();
} }
} }
@@ -231,32 +242,47 @@ public class UserService implements UserServiceInterface {
return userRepository.findByUsernameIgnoreCase(username); return userRepository.findByUsernameIgnoreCase(username);
} }
public Optional<User> findByUsernameIgnoreCaseWithSettings(String username) {
return userRepository.findByUsernameIgnoreCaseWithSettings(username);
}
public Authority findRole(User user) { public Authority findRole(User user) {
return authorityRepository.findByUserId(user.getId()); return authorityRepository.findByUserId(user.getId());
} }
public void changeUsername(User user, String newUsername) throws IllegalArgumentException { public void changeUsername(User user, String newUsername)
throws IllegalArgumentException, IOException {
if (!isUsernameValid(newUsername)) { if (!isUsernameValid(newUsername)) {
throw new IllegalArgumentException(getInvalidUsernameMessage()); throw new IllegalArgumentException(getInvalidUsernameMessage());
} }
user.setUsername(newUsername); user.setUsername(newUsername);
userRepository.save(user); userRepository.save(user);
databaseBackupHelper.exportDatabase();
} }
public void changePassword(User user, String newPassword) { public void changePassword(User user, String newPassword) throws IOException {
user.setPassword(passwordEncoder.encode(newPassword)); user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user); userRepository.save(user);
databaseBackupHelper.exportDatabase();
} }
public void changeFirstUse(User user, boolean firstUse) { public void changeFirstUse(User user, boolean firstUse) throws IOException {
user.setFirstLogin(firstUse); user.setFirstLogin(firstUse);
userRepository.save(user); userRepository.save(user);
databaseBackupHelper.exportDatabase();
} }
public void changeRole(User user, String newRole) { public void changeRole(User user, String newRole) throws IOException {
Authority userAuthority = this.findRole(user); Authority userAuthority = this.findRole(user);
userAuthority.setAuthority(newRole); userAuthority.setAuthority(newRole);
authorityRepository.save(userAuthority); authorityRepository.save(userAuthority);
databaseBackupHelper.exportDatabase();
}
public void changeUserEnabled(User user, Boolean enbeled) throws IOException {
user.setEnabled(enbeled);
userRepository.save(user);
databaseBackupHelper.exportDatabase();
} }
public boolean isPasswordCorrect(User user, String currentPassword) { public boolean isPasswordCorrect(User user, String currentPassword) {
@@ -280,14 +306,40 @@ public class UserService implements UserServiceInterface {
} }
public boolean hasPassword(String username) { public boolean hasPassword(String username) {
Optional<User> user = userRepository.findByUsernameIgnoreCase(username); Optional<User> user = findByUsernameIgnoreCase(username);
return user.isPresent() && user.get().hasPassword(); return user.isPresent() && user.get().hasPassword();
} }
public boolean isAuthenticationTypeByUsername( public boolean isAuthenticationTypeByUsername(
String username, AuthenticationType authenticationType) { String username, AuthenticationType authenticationType) {
Optional<User> user = userRepository.findByUsernameIgnoreCase(username); Optional<User> user = findByUsernameIgnoreCase(username);
return user.isPresent() return user.isPresent()
&& authenticationType.name().equalsIgnoreCase(user.get().getAuthenticationType()); && authenticationType.name().equalsIgnoreCase(user.get().getAuthenticationType());
} }
public boolean isUserDisabled(String username) {
Optional<User> userOpt = findByUsernameIgnoreCase(username);
return userOpt.map(user -> !user.isEnabled()).orElse(false);
}
public void invalidateUserSessions(String username) {
String usernameP = "";
for (Object principal : sessionRegistry.getAllPrincipals()) {
for (SessionInformation sessionsInformation :
sessionRegistry.getAllSessions(principal, false)) {
if (principal instanceof UserDetails) {
UserDetails userDetails = (UserDetails) principal;
usernameP = userDetails.getUsername();
} else if (principal instanceof OAuth2User) {
OAuth2User oAuth2User = (OAuth2User) principal;
usernameP = oAuth2User.getName();
} else if (principal instanceof String) {
usernameP = (String) principal;
}
if (usernameP.equalsIgnoreCase(username)) {
sessionRegistry.expireSession(sessionsInformation.getSessionId());
}
}
}
}
} }

View File

@@ -0,0 +1,228 @@
package stirling.software.SPDF.config.security.database;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
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.utils.FileInfo;
@Slf4j
@Configuration
public class DatabaseBackupHelper implements DatabaseBackupInterface {
@Value("${spring.datasource.url}")
private String url;
private Path backupPath = Paths.get("configs/db/backup/");
@Override
public boolean hasBackup() {
// Check if there is at least one backup
return !getBackupList().isEmpty();
}
@Override
public List<FileInfo> getBackupList() {
// Check if the backup directory exists, and create it if it does not
ensureBackupDirectoryExists();
List<FileInfo> backupFiles = new ArrayList<>();
// Read the backup directory and filter for files with the prefix "backup_" and suffix
// ".sql"
try (DirectoryStream<Path> stream =
Files.newDirectoryStream(
backupPath,
path ->
path.getFileName().toString().startsWith("backup_")
&& path.getFileName().toString().endsWith(".sql"))) {
for (Path entry : stream) {
BasicFileAttributes attrs = Files.readAttributes(entry, BasicFileAttributes.class);
LocalDateTime modificationDate =
LocalDateTime.ofInstant(
attrs.lastModifiedTime().toInstant(), ZoneId.systemDefault());
LocalDateTime creationDate =
LocalDateTime.ofInstant(
attrs.creationTime().toInstant(), ZoneId.systemDefault());
long fileSize = attrs.size();
backupFiles.add(
new FileInfo(
entry.getFileName().toString(),
entry.toString(),
modificationDate,
fileSize,
creationDate));
}
} catch (IOException e) {
log.error("Error reading backup directory: {}", e.getMessage(), e);
}
return backupFiles;
}
// Imports a database backup from the specified file.
public boolean importDatabaseFromUI(String fileName) throws IOException {
return this.importDatabaseFromUI(getBackupFilePath(fileName));
}
// Imports a database backup from the specified path.
public boolean importDatabaseFromUI(Path tempTemplatePath) throws IOException {
boolean success = executeDatabaseScript(tempTemplatePath);
if (success) {
LocalDateTime dateNow = LocalDateTime.now();
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
Path insertOutputFilePath =
this.getBackupFilePath("backup_user_" + dateNow.format(myFormatObj) + ".sql");
Files.copy(tempTemplatePath, insertOutputFilePath);
Files.deleteIfExists(tempTemplatePath);
}
return success;
}
@Override
public boolean importDatabase() {
if (!this.hasBackup()) return false;
List<FileInfo> backupList = this.getBackupList();
backupList.sort(Comparator.comparing(FileInfo::getModificationDate).reversed());
return executeDatabaseScript(Paths.get(backupList.get(0).getFilePath()));
}
@Override
public void exportDatabase() throws IOException {
// Check if the backup directory exists, and create it if it does not
ensureBackupDirectoryExists();
// Filter and delete old backups if there are more than 5
List<FileInfo> filteredBackupList =
this.getBackupList().stream()
.filter(backup -> !backup.getFileName().startsWith("backup_user_"))
.collect(Collectors.toList());
if (filteredBackupList.size() > 5) {
filteredBackupList.sort(
Comparator.comparing(
p -> p.getFileName().substring(7, p.getFileName().length() - 4)));
Files.deleteIfExists(Paths.get(filteredBackupList.get(0).getFilePath()));
log.info("Deleted oldest backup: {}", filteredBackupList.get(0).getFileName());
}
LocalDateTime dateNow = LocalDateTime.now();
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
Path insertOutputFilePath =
this.getBackupFilePath("backup_" + dateNow.format(myFormatObj) + ".sql");
String query = "SCRIPT SIMPLE COLUMNS DROP to ?;";
try (Connection conn = DriverManager.getConnection(url, "sa", "");
PreparedStatement stmt = conn.prepareStatement(query)) {
stmt.setString(1, insertOutputFilePath.toString());
stmt.execute();
log.info("Database export completed: {}", insertOutputFilePath);
} catch (SQLException e) {
log.error("Error during database export: {}", e.getMessage(), e);
}
}
// Retrieves the H2 database version.
public String getH2Version() {
String version = "Unknown";
try (Connection conn = DriverManager.getConnection(url, "sa", "")) {
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) {
if (rs.next()) {
version = rs.getString("version");
log.info("H2 Database Version: {}", version);
}
}
} catch (SQLException e) {
log.error("Error retrieving H2 version: {}", e.getMessage(), e);
}
return version;
}
// Deletes a backup file.
public boolean deleteBackupFile(String fileName) throws IOException {
if (!isValidFileName(fileName)) {
log.error("Invalid file name: {}", fileName);
return false;
}
Path filePath = this.getBackupFilePath(fileName);
if (Files.deleteIfExists(filePath)) {
log.info("Deleted backup file: {}", fileName);
return true;
} else {
log.error("File not found or could not be deleted: {}", fileName);
return false;
}
}
// Gets the Path object for a given backup file name.
public Path getBackupFilePath(String fileName) {
Path filePath = Paths.get(backupPath.toString(), fileName).normalize();
if (!filePath.startsWith(backupPath)) {
throw new SecurityException("Path traversal detected");
}
return filePath;
}
private boolean executeDatabaseScript(Path scriptPath) {
String query = "RUNSCRIPT from ?;";
try (Connection conn = DriverManager.getConnection(url, "sa", "");
PreparedStatement stmt = conn.prepareStatement(query)) {
stmt.setString(1, scriptPath.toString());
stmt.execute();
log.info("Database import completed: {}", scriptPath);
return true;
} catch (SQLException e) {
log.error("Error during database import: {}", e.getMessage(), e);
return false;
}
}
private void ensureBackupDirectoryExists() {
if (Files.notExists(backupPath)) {
try {
Files.createDirectories(backupPath);
} catch (IOException e) {
log.error("Error creating directories: {}", e.getMessage());
}
}
}
private boolean isValidFileName(String fileName) {
// Check for invalid characters or sequences
return fileName != null
&& !fileName.contains("..")
&& !fileName.contains("/")
&& !fileName.contains("\\")
&& !fileName.contains(":")
&& !fileName.contains("*")
&& !fileName.contains("?")
&& !fileName.contains("\"")
&& !fileName.contains("<")
&& !fileName.contains(">")
&& !fileName.contains("|");
}
}

View File

@@ -0,0 +1,18 @@
package stirling.software.SPDF.config.security.database;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class ScheduledTasks {
@Autowired private DatabaseBackupHelper databaseBackupService;
@Scheduled(cron = "0 0 0 * * ?")
public void performBackup() throws IOException {
databaseBackupService.exportDatabase();
}
}

View File

@@ -2,8 +2,8 @@ package stirling.software.SPDF.config.security.oauth2;
import java.io.IOException; import java.io.IOException;
import org.slf4j.Logger; import org.springframework.security.authentication.BadCredentialsException;
import org.slf4j.LoggerFactory; import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
@@ -13,19 +13,34 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFa
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CustomOAuth2AuthenticationFailureHandler public class CustomOAuth2AuthenticationFailureHandler
extends SimpleUrlAuthenticationFailureHandler { extends SimpleUrlAuthenticationFailureHandler {
private static final Logger logger =
LoggerFactory.getLogger(CustomOAuth2AuthenticationFailureHandler.class);
@Override @Override
public void onAuthenticationFailure( public void onAuthenticationFailure(
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
AuthenticationException exception) AuthenticationException exception)
throws IOException, ServletException { throws IOException, ServletException {
if (exception instanceof BadCredentialsException) {
log.error("BadCredentialsException", exception);
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
return;
}
if (exception instanceof DisabledException) {
log.error("User is deactivated: ", exception);
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
return;
}
if (exception instanceof LockedException) {
log.error("Account locked: ", exception);
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
return;
}
if (exception instanceof OAuth2AuthenticationException) { if (exception instanceof OAuth2AuthenticationException) {
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError(); OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
@@ -34,17 +49,13 @@ public class CustomOAuth2AuthenticationFailureHandler
if (error.getErrorCode().equals("Password must not be null")) { if (error.getErrorCode().equals("Password must not be null")) {
errorCode = "userAlreadyExistsWeb"; errorCode = "userAlreadyExistsWeb";
} }
logger.error("OAuth2 Authentication error: " + errorCode); log.error("OAuth2 Authentication error: " + errorCode);
log.error("OAuth2AuthenticationException", exception);
getRedirectStrategy() getRedirectStrategy()
.sendRedirect(request, response, "/logout?erroroauth=" + errorCode); .sendRedirect(request, response, "/logout?erroroauth=" + errorCode);
return; return;
} else if (exception instanceof LockedException) {
logger.error("Account locked: ", exception);
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
return;
} else {
logger.error("Unhandled authentication exception", exception);
super.onAuthenticationFailure(request, response, exception);
} }
log.error("Unhandled authentication exception", exception);
super.onAuthenticationFailure(request, response, exception);
} }
} }

View File

@@ -2,10 +2,9 @@ package stirling.software.SPDF.config.security.oauth2;
import java.io.IOException; import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.SavedRequest; import org.springframework.security.web.savedrequest.SavedRequest;
@@ -26,9 +25,6 @@ public class CustomOAuth2AuthenticationSuccessHandler
private LoginAttemptService loginAttemptService; private LoginAttemptService loginAttemptService;
private static final Logger logger =
LoggerFactory.getLogger(CustomOAuth2AuthenticationSuccessHandler.class);
private ApplicationProperties applicationProperties; private ApplicationProperties applicationProperties;
private UserService userService; private UserService userService;
@@ -46,6 +42,17 @@ public class CustomOAuth2AuthenticationSuccessHandler
HttpServletRequest request, HttpServletResponse response, Authentication authentication) HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws ServletException, IOException { throws ServletException, IOException {
Object principal = authentication.getPrincipal();
String username = "";
if (principal instanceof OAuth2User) {
OAuth2User oauthUser = (OAuth2User) principal;
username = oauthUser.getName();
} else if (principal instanceof UserDetails) {
UserDetails oauthUser = (UserDetails) principal;
username = oauthUser.getUsername();
}
// Get the saved request // Get the saved request
HttpSession session = request.getSession(false); HttpSession session = request.getSession(false);
String contextPath = request.getContextPath(); String contextPath = request.getContextPath();
@@ -59,11 +66,8 @@ public class CustomOAuth2AuthenticationSuccessHandler
// Redirect to the original destination // Redirect to the original destination
super.onAuthenticationSuccess(request, response, authentication); super.onAuthenticationSuccess(request, response, authentication);
} else { } else {
OAuth2User oauthUser = (OAuth2User) authentication.getPrincipal();
OAUTH2 oAuth = applicationProperties.getSecurity().getOAUTH2(); OAUTH2 oAuth = applicationProperties.getSecurity().getOAUTH2();
String username = oauthUser.getName();
if (loginAttemptService.isBlocked(username)) { if (loginAttemptService.isBlocked(username)) {
if (session != null) { if (session != null) {
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST"); session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
@@ -78,15 +82,21 @@ public class CustomOAuth2AuthenticationSuccessHandler
&& oAuth.getAutoCreateUser()) { && oAuth.getAutoCreateUser()) {
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true"); response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
return; return;
} else { }
try { try {
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser()); if (oAuth.getBlockRegistration()
response.sendRedirect(contextPath + "/"); && !userService.usernameExistsIgnoreCase(username)) {
return; response.sendRedirect(contextPath + "/logout?oauth2_admin_blocked_user=true");
} catch (IllegalArgumentException e) {
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
return; return;
} }
if (principal instanceof OAuth2User) {
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
}
response.sendRedirect(contextPath + "/");
return;
} catch (IllegalArgumentException e) {
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
return;
} }
} }
} }

View File

@@ -2,33 +2,26 @@ package stirling.software.SPDF.config.security.oauth2;
import java.io.IOException; import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.Provider; import stirling.software.SPDF.model.Provider;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.UrlUtils; import stirling.software.SPDF.utils.UrlUtils;
@Slf4j
public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private static final Logger logger =
LoggerFactory.getLogger(CustomOAuth2LogoutSuccessHandler.class);
private final SessionRegistry sessionRegistry;
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
public CustomOAuth2LogoutSuccessHandler( public CustomOAuth2LogoutSuccessHandler(ApplicationProperties applicationProperties) {
ApplicationProperties applicationProperties, SessionRegistry sessionRegistry) {
this.sessionRegistry = sessionRegistry;
this.applicationProperties = applicationProperties; this.applicationProperties = applicationProperties;
} }
@@ -41,6 +34,15 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
String issuer = null; String issuer = null;
String clientId = 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(); OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
if (authentication instanceof OAuth2AuthenticationToken) { if (authentication instanceof OAuth2AuthenticationToken) {
@@ -51,10 +53,9 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
Provider provider = oauth.getClient().get(registrationId); Provider provider = oauth.getClient().get(registrationId);
issuer = provider.getIssuer(); issuer = provider.getIssuer();
clientId = provider.getClientId(); clientId = provider.getClientId();
} catch (Exception e) { } catch (UnsupportedProviderException e) {
logger.error("exception", e); log.error(e.getMessage());
} }
} else { } else {
registrationId = oauth.getProvider() != null ? oauth.getProvider() : ""; registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
issuer = oauth.getIssuer(); issuer = oauth.getIssuer();
@@ -69,18 +70,16 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
param = "erroroauth=" + sanitizeInput(errorMessage); param = "erroroauth=" + sanitizeInput(errorMessage);
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) { } else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
param = "error=oauth2AutoCreateDisabled"; 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; String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
HttpSession session = request.getSession(false);
if (session != null) {
String sessionId = session.getId();
sessionRegistry.removeSessionInformation(sessionId);
session.invalidate();
logger.info("Session invalidated: " + sessionId);
}
switch (registrationId.toLowerCase()) { switch (registrationId.toLowerCase()) {
case "keycloak": case "keycloak":
// Add Keycloak specific logout URL if needed // Add Keycloak specific logout URL if needed
@@ -91,13 +90,13 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
+ clientId + clientId
+ "&post_logout_redirect_uri=" + "&post_logout_redirect_uri="
+ response.encodeRedirectURL(redirect_url); + response.encodeRedirectURL(redirect_url);
logger.info("Redirecting to Keycloak logout URL: " + logoutUrl); log.info("Redirecting to Keycloak logout URL: " + logoutUrl);
response.sendRedirect(logoutUrl); response.sendRedirect(logoutUrl);
break; break;
case "github": case "github":
// Add GitHub specific logout URL if needed // Add GitHub specific logout URL if needed
String githubLogoutUrl = "https://github.com/logout"; String githubLogoutUrl = "https://github.com/logout";
logger.info("Redirecting to GitHub logout URL: " + githubLogoutUrl); log.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
response.sendRedirect(githubLogoutUrl); response.sendRedirect(githubLogoutUrl);
break; break;
case "google": case "google":
@@ -105,13 +104,14 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
// String googleLogoutUrl = // String googleLogoutUrl =
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue=" // "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
// + response.encodeRedirectURL(redirect_url); // + response.encodeRedirectURL(redirect_url);
// logger.info("Redirecting to Google logout URL: " + googleLogoutUrl); log.info("Google does not have a specific logout URL");
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
// response.sendRedirect(googleLogoutUrl); // response.sendRedirect(googleLogoutUrl);
// break; // break;
default: default:
String redirectUrl = request.getContextPath() + "/login?" + param; String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
logger.info("Redirecting to default logout URL: " + redirectUrl); log.info("Redirecting to default logout URL: " + defaultRedirectUrl);
response.sendRedirect(redirectUrl); response.sendRedirect(defaultRedirectUrl);
break; break;
} }
} }

View File

@@ -0,0 +1,211 @@
package stirling.software.SPDF.config.security.oauth2;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
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.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.UserService;
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.User;
import stirling.software.SPDF.model.provider.GithubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider;
@Configuration
@Slf4j
public class OAuth2Config {
@Autowired ApplicationProperties applicationProperties;
@Autowired @Lazy private UserService userService;
// Client Registration Repository for OAUTH2 OIDC Login
@Bean
@ConditionalOnProperty(
value = "security.oauth2.enabled",
havingValue = "true",
matchIfMissing = false)
public ClientRegistrationRepository clientRegistrationRepository() {
List<ClientRegistration> registrations = new ArrayList<>();
githubClientRegistration().ifPresent(registrations::add);
oidcClientRegistration().ifPresent(registrations::add);
googleClientRegistration().ifPresent(registrations::add);
keycloakClientRegistration().ifPresent(registrations::add);
if (registrations.isEmpty()) {
log.error("At least one OAuth2 provider must be configured");
System.exit(1);
}
return new InMemoryClientRegistrationRepository(registrations);
}
private Optional<ClientRegistration> googleClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
GoogleProvider google = client.getGoogle();
return google != null && google.isSettingsValid()
? Optional.of(
ClientRegistration.withRegistrationId(google.getName())
.clientId(google.getClientId())
.clientSecret(google.getClientSecret())
.scope(google.getScopes())
.authorizationUri(google.getAuthorizationuri())
.tokenUri(google.getTokenuri())
.userInfoUri(google.getUserinfouri())
.userNameAttributeName(google.getUseAsUsername())
.clientName(google.getClientName())
.redirectUri("{baseUrl}/login/oauth2/code/" + google.getName())
.authorizationGrantType(
org.springframework.security.oauth2.core
.AuthorizationGrantType.AUTHORIZATION_CODE)
.build())
: Optional.empty();
}
private Optional<ClientRegistration> keycloakClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
KeycloakProvider keycloak = client.getKeycloak();
return keycloak != null && keycloak.isSettingsValid()
? Optional.of(
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
.registrationId(keycloak.getName())
.clientId(keycloak.getClientId())
.clientSecret(keycloak.getClientSecret())
.scope(keycloak.getScopes())
.userNameAttributeName(keycloak.getUseAsUsername())
.clientName(keycloak.getClientName())
.build())
: Optional.empty();
}
private Optional<ClientRegistration> githubClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
GithubProvider github = client.getGithub();
return github != null && github.isSettingsValid()
? Optional.of(
ClientRegistration.withRegistrationId(github.getName())
.clientId(github.getClientId())
.clientSecret(github.getClientSecret())
.scope(github.getScopes())
.authorizationUri(github.getAuthorizationuri())
.tokenUri(github.getTokenuri())
.userInfoUri(github.getUserinfouri())
.userNameAttributeName(github.getUseAsUsername())
.clientName(github.getClientName())
.redirectUri("{baseUrl}/login/oauth2/code/" + github.getName())
.authorizationGrantType(
org.springframework.security.oauth2.core
.AuthorizationGrantType.AUTHORIZATION_CODE)
.build())
: Optional.empty();
}
private Optional<ClientRegistration> oidcClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
if (oauth == null
|| oauth.getIssuer() == null
|| oauth.getIssuer().isEmpty()
|| oauth.getClientId() == null
|| oauth.getClientId().isEmpty()
|| oauth.getClientSecret() == null
|| oauth.getClientSecret().isEmpty()
|| oauth.getScopes() == null
|| oauth.getScopes().isEmpty()
|| oauth.getUseAsUsername() == null
|| oauth.getUseAsUsername().isEmpty()) {
return Optional.empty();
}
return Optional.of(
ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
.registrationId("oidc")
.clientId(oauth.getClientId())
.clientSecret(oauth.getClientSecret())
.scope(oauth.getScopes())
.userNameAttributeName(oauth.getUseAsUsername())
.clientName("OIDC")
.build());
}
/*
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.
*/
@Bean
@ConditionalOnProperty(
value = "security.oauth2.enabled",
havingValue = "true",
matchIfMissing = false)
GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(
authority -> {
// Add existing OAUTH2 Authorities
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
// Add Authorities from database for existing user, if user is present.
if (authority instanceof OAuth2UserAuthority oauth2Auth) {
String useAsUsername =
applicationProperties
.getSecurity()
.getOAUTH2()
.getUseAsUsername();
Optional<User> userOpt =
userService.findByUsernameIgnoreCase(
(String) oauth2Auth.getAttributes().get(useAsUsername));
if (userOpt.isPresent()) {
User user = userOpt.get();
if (user != null) {
mappedAuthorities.add(
new SimpleGrantedAuthority(
userService.findRole(user).getAuthority()));
}
}
}
});
return mappedAuthorities;
};
}
}

View File

@@ -0,0 +1,51 @@
package stirling.software.SPDF.config.security.saml;
import java.io.IOException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
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 CustomSAMLAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
if (exception instanceof BadCredentialsException) {
log.error("BadCredentialsException", exception);
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
return;
}
if (exception instanceof DisabledException) {
log.error("User is deactivated: ", exception);
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
return;
}
if (exception instanceof LockedException) {
log.error("Account locked: ", exception);
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
return;
}
if (exception instanceof Saml2AuthenticationException) {
log.error("SAML2 Authentication error: ", exception);
getRedirectStrategy()
.sendRedirect(request, response, "/logout?error=saml2AuthenticationError");
return;
}
log.error("Unhandled authentication exception", exception);
super.onAuthenticationFailure(request, response, exception);
}
}

View File

@@ -0,0 +1,60 @@
package stirling.software.SPDF.config.security.saml;
import java.io.IOException;
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.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.LoginAttemptService;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.utils.RequestUriUtils;
@Slf4j
public class CustomSAMLAuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler {
private LoginAttemptService loginAttemptService;
private UserService userService;
public CustomSAMLAuthenticationSuccessHandler(
LoginAttemptService loginAttemptService, UserService userService) {
this.loginAttemptService = loginAttemptService;
this.userService = userService;
}
@Override
public void onAuthenticationSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
String userName = request.getParameter("username");
if (userService.isUserDisabled(userName)) {
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
return;
}
loginAttemptService.loginSucceeded(userName);
// Get the saved request
HttpSession session = request.getSession(false);
SavedRequest savedRequest =
(session != null)
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
: null;
if (savedRequest != null
&& !RequestUriUtils.isStaticResource(
request.getContextPath(), savedRequest.getRedirectUrl())) {
// Redirect to the original destination
super.onAuthenticationSuccess(request, response, authentication);
} else {
// Redirect to the root URL (considering context path)
getRedirectStrategy().sendRedirect(request, response, "/");
}
}
}

View File

@@ -0,0 +1,38 @@
package stirling.software.SPDF.config.security.saml;
import java.io.IOException;
import org.springframework.security.core.Authentication;
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;
@Slf4j
public class SAMLLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
@Override
public void onLogoutSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
String redirectUrl = determineTargetUrl(request, response, authentication);
if (response.isCommitted()) {
log.debug("Response has already been committed. Unable to redirect to " + redirectUrl);
return;
}
getRedirectStrategy().sendRedirect(request, response, redirectUrl);
}
protected String determineTargetUrl(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) {
// Default to the root URL
return "/";
}
}

View File

@@ -0,0 +1,216 @@
package stirling.software.SPDF.config.security.saml;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver;
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.DefaultRelyingPartyRegistrationResolver;
import org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
@Configuration
@Slf4j
public class SamlConfig {
@Autowired ApplicationProperties applicationProperties;
@Bean
public OpenSaml4AuthenticationProvider openSaml4AuthenticationProvider() {
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseAuthenticationConverter(
responseToken -> {
Saml2AuthenticationToken token = responseToken.getToken();
log.info("Received SAML response: {}", token.getSaml2Response());
// Your custom conversion logic here
// For now, we'll just return the token as is
return token;
});
return provider;
}
@Bean
@ConditionalOnProperty(
value = "security.saml.enabled",
havingValue = "true",
matchIfMissing = false)
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
RelyingPartyRegistration registration =
RelyingPartyRegistration.withRegistrationId(
applicationProperties.getSecurity().getSAML().getRegistrationId())
.entityId(applicationProperties.getSecurity().getSAML().getEntityId())
.assertionConsumerServiceLocation(
applicationProperties.getSecurity().getSAML().getSpBaseUrl()
+ "/login/saml2/sso/stirling")
.singleLogoutServiceLocation(
applicationProperties.getSecurity().getSAML().getSpBaseUrl()
+ "/logout/saml2/slo")
.singleLogoutServiceResponseLocation(
applicationProperties.getSecurity().getSAML().getSpBaseUrl()
+ "/logout/saml2/slo")
.signingX509Credentials(credentials -> credentials.add(signingCredential()))
.assertingPartyDetails(
party ->
party.entityId(
applicationProperties
.getSecurity()
.getSAML()
.getEntityId())
.singleSignOnServiceLocation(
applicationProperties
.getSecurity()
.getSAML()
.getIdpMetadataLocation())
.wantAuthnRequestsSigned(true)
.verificationX509Credentials(
c -> c.add(this.realmCertificate())))
.build();
return new InMemoryRelyingPartyRegistrationRepository(registration);
}
private Saml2X509Credential signingCredential() {
log.info("Starting to load signing credential");
try {
Resource storeResource =
applicationProperties
.getSecurity()
.getSAML()
.getKeystore()
.getKeystoreResource();
log.info("Keystore resource: {}", storeResource.getDescription());
KeyStore keyStore = KeyStore.getInstance("JKS");
try (InputStream is = storeResource.getInputStream()) {
keyStore.load(
is,
applicationProperties
.getSecurity()
.getSAML()
.getKeystore()
.getKeystorePassword()
.toCharArray());
log.info("Keystore loaded successfully");
}
String keyAlias =
applicationProperties.getSecurity().getSAML().getKeystore().getKeyAlias();
log.info("Attempting to retrieve private key with alias: {}", keyAlias);
PrivateKey privateKey =
(PrivateKey)
keyStore.getKey(
keyAlias,
applicationProperties
.getSecurity()
.getSAML()
.getKeystore()
.getKeyPassword()
.toCharArray());
if (privateKey == null) {
log.error("Private key not found for alias: {}", keyAlias);
throw new RuntimeException("Private key not found in keystore");
}
log.info("Private key retrieved successfully");
X509Certificate certificate = (X509Certificate) keyStore.getCertificate(keyAlias);
if (certificate == null) {
log.info("Certificate not found for alias: {}", keyAlias);
throw new RuntimeException("Certificate not found in keystore");
}
log.info(
"Certificate retrieved successfully. Subject: {}",
certificate.getSubjectX500Principal());
log.info("Signing credential created successfully");
return Saml2X509Credential.signing(privateKey, certificate);
} catch (Exception e) {
log.error("Error loading signing credential", e);
throw new RuntimeException("Error loading signing credential", e);
}
}
private Saml2X509Credential realmCertificate() {
log.info("Starting to load realm certificate");
try {
Resource storeResource =
applicationProperties
.getSecurity()
.getSAML()
.getKeystore()
.getKeystoreResource();
log.info("Keystore resource: {}", storeResource.getDescription());
KeyStore keyStore = KeyStore.getInstance("JKS");
try (InputStream is = storeResource.getInputStream()) {
keyStore.load(
is,
applicationProperties
.getSecurity()
.getSAML()
.getKeystore()
.getKeystorePassword()
.toCharArray());
log.info("Keystore loaded successfully");
}
String realmCertificateAlias =
applicationProperties
.getSecurity()
.getSAML()
.getKeystore()
.getRealmCertificateAlias();
log.info(
"Attempting to retrieve realm certificate with alias: {}",
realmCertificateAlias);
X509Certificate certificate =
(X509Certificate) keyStore.getCertificate(realmCertificateAlias);
if (certificate == null) {
log.error("Realm certificate not found for alias: {}", realmCertificateAlias);
throw new RuntimeException("Realm certificate not found in keystore");
}
log.info(
"Realm certificate retrieved successfully. Subject: {}",
certificate.getSubjectX500Principal());
log.info("Realm certificate credential created successfully");
return Saml2X509Credential.verification(certificate);
} catch (Exception e) {
log.error("Error loading realm certificate", e);
throw new RuntimeException("Error loading realm certificate", e);
}
}
@Bean
@ConditionalOnProperty(
value = "security.saml.enabled",
havingValue = "true",
matchIfMissing = false)
public Saml2MetadataFilter metadataFilter(RelyingPartyRegistrationRepository registrations) {
DefaultRelyingPartyRegistrationResolver registrationResolver =
new DefaultRelyingPartyRegistrationResolver(registrations);
OpenSamlMetadataResolver metadataResolver = new OpenSamlMetadataResolver();
return new Saml2MetadataFilter(registrationResolver, metadataResolver);
}
}

View File

@@ -0,0 +1,26 @@
package stirling.software.SPDF.config.security.session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpSessionEvent;
import jakarta.servlet.http.HttpSessionListener;
import lombok.extern.slf4j.Slf4j;
@Component
@Slf4j
public class CustomHttpSessionListener implements HttpSessionListener {
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
@Override
public void sessionCreated(HttpSessionEvent se) {
log.info("Session created: " + se.getSession().getId());
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
log.info("Session destroyed: " + se.getSession().getId());
sessionPersistentRegistry.expireSession(se.getSession().getId());
}
}

View File

@@ -0,0 +1,183 @@
package stirling.software.SPDF.config.security.session;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Component;
import jakarta.transaction.Transactional;
import stirling.software.SPDF.model.SessionEntity;
@Component
public class SessionPersistentRegistry implements SessionRegistry {
private final SessionRepository sessionRepository;
@Value("${server.servlet.session.timeout:30m}")
private Duration defaultMaxInactiveInterval;
public SessionPersistentRegistry(SessionRepository sessionRepository) {
this.sessionRepository = sessionRepository;
}
@Override
public List<Object> getAllPrincipals() {
List<SessionEntity> sessions = sessionRepository.findAll();
List<Object> principals = new ArrayList<>();
for (SessionEntity session : sessions) {
principals.add(session.getPrincipalName());
}
return principals;
}
@Override
public List<SessionInformation> getAllSessions(
Object principal, boolean includeExpiredSessions) {
List<SessionInformation> sessionInformations = new ArrayList<>();
String principalName = null;
if (principal instanceof UserDetails) {
principalName = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) {
principalName = ((OAuth2User) principal).getName();
} else if (principal instanceof String) {
principalName = (String) principal;
}
if (principalName != null) {
List<SessionEntity> sessionEntities =
sessionRepository.findByPrincipalName(principalName);
for (SessionEntity sessionEntity : sessionEntities) {
if (includeExpiredSessions || !sessionEntity.isExpired()) {
sessionInformations.add(
new SessionInformation(
sessionEntity.getPrincipalName(),
sessionEntity.getSessionId(),
sessionEntity.getLastRequest()));
}
}
}
return sessionInformations;
}
@Override
@Transactional
public void registerNewSession(String sessionId, Object principal) {
String principalName = null;
if (principal instanceof UserDetails) {
principalName = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) {
principalName = ((OAuth2User) principal).getName();
} else if (principal instanceof String) {
principalName = (String) principal;
}
if (principalName != null) {
SessionEntity sessionEntity = new SessionEntity();
sessionEntity.setSessionId(sessionId);
sessionEntity.setPrincipalName(principalName);
sessionEntity.setLastRequest(new Date()); // Set lastRequest to the current date
sessionEntity.setExpired(false);
sessionRepository.save(sessionEntity);
}
}
@Override
@Transactional
public void removeSessionInformation(String sessionId) {
sessionRepository.deleteById(sessionId);
}
@Override
@Transactional
public void refreshLastRequest(String sessionId) {
Optional<SessionEntity> sessionEntityOpt = sessionRepository.findById(sessionId);
if (sessionEntityOpt.isPresent()) {
SessionEntity sessionEntity = sessionEntityOpt.get();
sessionEntity.setLastRequest(new Date()); // Update lastRequest to the current date
sessionRepository.save(sessionEntity);
}
}
@Override
public SessionInformation getSessionInformation(String sessionId) {
Optional<SessionEntity> sessionEntityOpt = sessionRepository.findById(sessionId);
if (sessionEntityOpt.isPresent()) {
SessionEntity sessionEntity = sessionEntityOpt.get();
return new SessionInformation(
sessionEntity.getPrincipalName(),
sessionEntity.getSessionId(),
sessionEntity.getLastRequest());
}
return null;
}
// Retrieve all non-expired sessions
public List<SessionEntity> getAllSessionsNotExpired() {
return sessionRepository.findByExpired(false);
}
// Retrieve all sessions
public List<SessionEntity> getAllSessions() {
return sessionRepository.findAll();
}
// Mark a session as expired
public void expireSession(String sessionId) {
Optional<SessionEntity> sessionEntityOpt = sessionRepository.findById(sessionId);
if (sessionEntityOpt.isPresent()) {
SessionEntity sessionEntity = sessionEntityOpt.get();
sessionEntity.setExpired(true); // Set expired to true
sessionRepository.save(sessionEntity);
}
}
// Get the maximum inactive interval for sessions
public int getMaxInactiveInterval() {
return (int) defaultMaxInactiveInterval.getSeconds();
}
// Retrieve a session entity by session ID
public SessionEntity getSessionEntity(String sessionId) {
return sessionRepository.findBySessionId(sessionId);
}
// Update session details by principal name
public void updateSessionByPrincipalName(
String principalName, boolean expired, Date lastRequest) {
sessionRepository.saveByPrincipalName(expired, lastRequest, principalName);
}
// Find the latest session for a given principal name
public Optional<SessionEntity> findLatestSession(String principalName) {
List<SessionEntity> allSessions = sessionRepository.findByPrincipalName(principalName);
if (allSessions.isEmpty()) {
return Optional.empty();
}
// Sort sessions by lastRequest in descending order
Collections.sort(
allSessions,
new Comparator<SessionEntity>() {
@Override
public int compare(SessionEntity s1, SessionEntity s2) {
// Sort by lastRequest in descending order
return s2.getLastRequest().compareTo(s1.getLastRequest());
}
});
// The first session in the list is the latest session for the given principal name
return Optional.of(allSessions.get(0));
}
}

View File

@@ -0,0 +1,20 @@
package stirling.software.SPDF.config.security.session;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.session.SessionRegistryImpl;
@Configuration
public class SessionRegistryConfig {
@Bean
public SessionRegistryImpl sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
public SessionPersistentRegistry sessionPersistentRegistry(
SessionRepository sessionRepository) {
return new SessionPersistentRegistry(sessionRepository);
}
}

View File

@@ -0,0 +1,31 @@
package stirling.software.SPDF.config.security.session;
import java.util.Date;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import jakarta.transaction.Transactional;
import stirling.software.SPDF.model.SessionEntity;
@Repository
public interface SessionRepository extends JpaRepository<SessionEntity, String> {
List<SessionEntity> findByPrincipalName(String principalName);
List<SessionEntity> findByExpired(boolean expired);
SessionEntity findBySessionId(String sessionId);
@Modifying
@Transactional
@Query(
"UPDATE SessionEntity s SET s.expired = :expired, s.lastRequest = :lastRequest WHERE s.principalName = :principalName")
void saveByPrincipalName(
@Param("expired") boolean expired,
@Param("lastRequest") Date lastRequest,
@Param("principalName") String principalName);
}

View File

@@ -0,0 +1,35 @@
package stirling.software.SPDF.config.security.session;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.stereotype.Component;
@Component
public class SessionScheduled {
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
@Scheduled(cron = "0 0/5 * * * ?")
public void expireSessions() {
Instant now = Instant.now();
for (Object principal : sessionPersistentRegistry.getAllPrincipals()) {
List<SessionInformation> sessionInformations =
sessionPersistentRegistry.getAllSessions(principal, false);
for (SessionInformation sessionInformation : sessionInformations) {
Date lastRequest = sessionInformation.getLastRequest();
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
Instant expirationTime =
lastRequest.toInstant().plus(maxInactiveInterval, ChronoUnit.SECONDS);
if (now.isAfter(expirationTime)) {
sessionPersistentRegistry.expireSession(sessionInformation.getSessionId());
}
}
}
}
}

View File

@@ -0,0 +1,144 @@
package stirling.software.SPDF.controller.api;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import org.eclipse.jetty.http.HttpStatus;
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.security.access.prepost.PreAuthorize;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.database.DatabaseBackupHelper;
@Slf4j
@Controller
@RequestMapping("/api/v1/database")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@Tag(name = "Database", description = "Database APIs")
public class DatabaseController {
@Autowired DatabaseBackupHelper databaseBackupHelper;
@Hidden
@PostMapping(consumes = "multipart/form-data", value = "import-database")
@Operation(
summary = "Import database backup",
description = "This endpoint imports a database backup from a SQL file.")
public String importDatabase(
@RequestParam("fileInput") MultipartFile file, RedirectAttributes redirectAttributes)
throws IllegalArgumentException, IOException {
if (file == null || file.isEmpty()) {
redirectAttributes.addAttribute("error", "fileNullOrEmpty");
return "redirect:/database";
}
log.info("Received file: {}", file.getOriginalFilename());
Path tempTemplatePath = Files.createTempFile("backup_", ".sql");
try (InputStream in = file.getInputStream()) {
Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING);
boolean importSuccess = databaseBackupHelper.importDatabaseFromUI(tempTemplatePath);
if (importSuccess) {
redirectAttributes.addAttribute("infoMessage", "importIntoDatabaseSuccessed");
} else {
redirectAttributes.addAttribute("error", "failedImportFile");
}
} catch (Exception e) {
log.error("Error importing database: {}", e.getMessage());
redirectAttributes.addAttribute("error", "failedImportFile");
}
return "redirect:/database";
}
@Hidden
@GetMapping("/import-database-file/{fileName}")
public String importDatabaseFromBackupUI(@PathVariable String fileName)
throws IllegalArgumentException, IOException {
if (fileName == null || fileName.isEmpty()) {
return "redirect:/database?error=fileNullOrEmpty";
}
// Check if the file exists in the backup list
boolean fileExists =
databaseBackupHelper.getBackupList().stream()
.anyMatch(backup -> backup.getFileName().equals(fileName));
if (!fileExists) {
log.error("File {} not found in backup list", fileName);
return "redirect:/database?error=fileNotFound";
}
log.info("Received file: {}", fileName);
if (databaseBackupHelper.importDatabaseFromUI(fileName)) {
log.info("File {} imported to database", fileName);
return "redirect:/database?infoMessage=importIntoDatabaseSuccessed";
}
return "redirect:/database?error=failedImportFile";
}
@Hidden
@GetMapping("/delete/{fileName}")
@Operation(
summary = "Delete a database backup file",
description =
"This endpoint deletes a database backup file with the specified file name.")
public String deleteFile(@PathVariable String fileName) {
if (fileName == null || fileName.isEmpty()) {
throw new IllegalArgumentException("File must not be null or empty");
}
try {
if (databaseBackupHelper.deleteBackupFile(fileName)) {
log.info("Deleted file: {}", fileName);
} else {
log.error("Failed to delete file: {}", fileName);
return "redirect:/database?error=failedToDeleteFile";
}
} catch (IOException e) {
log.error("Error deleting file: {}", e.getMessage());
return "redirect:/database?error=" + e.getMessage();
}
return "redirect:/database";
}
@Hidden
@GetMapping("/download/{fileName}")
@Operation(
summary = "Download a database backup file",
description =
"This endpoint downloads a database backup file with the specified file name.")
public ResponseEntity<?> downloadFile(@PathVariable String fileName) {
if (fileName == null || fileName.isEmpty()) {
throw new IllegalArgumentException("File must not be null or empty");
}
try {
Path filePath = databaseBackupHelper.getBackupFilePath(fileName);
InputStreamResource resource = new InputStreamResource(Files.newInputStream(filePath));
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(Files.size(filePath))
.body(resource);
} catch (IOException e) {
log.error("Error downloading file: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.SEE_OTHER_303)
.location(URI.create("/database?error=downloadFailed"))
.build();
}
}
}

View File

@@ -0,0 +1,82 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.service.PdfImageRemovalService;
import stirling.software.SPDF.utils.WebResponseUtils;
/**
* Controller class for handling PDF image removal requests. Provides an endpoint to remove images
* from a PDF file to reduce its size.
*/
@RestController
@RequestMapping("/api/v1/general")
public class PdfImageRemovalController {
// Service for removing images from PDFs
@Autowired private PdfImageRemovalService pdfImageRemovalService;
/**
* Constructor for dependency injection of PdfImageRemovalService.
*
* @param pdfImageRemovalService The service used for removing images from PDFs.
*/
public PdfImageRemovalController(PdfImageRemovalService pdfImageRemovalService) {
this.pdfImageRemovalService = pdfImageRemovalService;
}
/**
* Endpoint to remove images from a PDF file.
*
* <p>This method processes the uploaded PDF file, removes all images, and returns the modified
* PDF file with a new name indicating that images were removed.
*
* @param file The PDF file with images to be removed.
* @return ResponseEntity containing the modified PDF file as byte array with appropriate
* content type and filename.
* @throws IOException If an error occurs while processing the PDF file.
*/
@PostMapping(consumes = "multipart/form-data", value = "/remove-image-pdf")
@Operation(
summary = "Remove images from file to reduce the file size.",
description =
"This endpoint remove images from file to reduce the file size.Input:PDF Output:PDF Type:MISO")
public ResponseEntity<byte[]> removeImages(@ModelAttribute PDFFile file) throws IOException {
MultipartFile pdf = file.getFileInput();
// Convert the MultipartFile to a byte array
byte[] pdfBytes = pdf.getBytes();
// Load the PDF document from the byte array
PDDocument document = Loader.loadPDF(pdfBytes);
// Remove images from the PDF document using the service
PDDocument modifiedDocument = pdfImageRemovalService.removeImagesFromPdf(document);
// Create a ByteArrayOutputStream to hold the modified PDF data
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// Save the modified PDF document to the output stream
modifiedDocument.save(outputStream);
modifiedDocument.close();
// Generate a new filename for the modified PDF
String mergedFileName =
pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_images.pdf";
// Convert the byte array to a web response and return it
return WebResponseUtils.bytesToWebResponse(outputStream.toByteArray(), mergedFileName);
}
}

View File

@@ -1,7 +1,9 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.io.IOException;
import java.security.Principal; import java.security.Principal;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@@ -11,8 +13,8 @@ import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionInformation; import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
@@ -29,6 +31,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.config.security.UserService; import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.model.api.user.UsernameAndPass; import stirling.software.SPDF.model.api.user.UsernameAndPass;
@@ -40,9 +44,12 @@ public class UserController {
@Autowired private UserService userService; @Autowired private UserService userService;
@Autowired SessionPersistentRegistry sessionRegistry;
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/register") @PostMapping("/register")
public String register(@ModelAttribute UsernameAndPass requestModel, Model model) { public String register(@ModelAttribute UsernameAndPass requestModel, Model model)
throws IOException {
if (userService.usernameExistsIgnoreCase(requestModel.getUsername())) { if (userService.usernameExistsIgnoreCase(requestModel.getUsername())) {
model.addAttribute("error", "Username already exists"); model.addAttribute("error", "Username already exists");
return "register"; return "register";
@@ -63,7 +70,8 @@ public class UserController {
@RequestParam(name = "newUsername") String newUsername, @RequestParam(name = "newUsername") String newUsername,
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
RedirectAttributes redirectAttributes) { RedirectAttributes redirectAttributes)
throws IOException {
if (!userService.isUsernameValid(newUsername)) { if (!userService.isUsernameValid(newUsername)) {
return new RedirectView("/account?messageType=invalidUsername", true); return new RedirectView("/account?messageType=invalidUsername", true);
@@ -116,7 +124,8 @@ public class UserController {
@RequestParam(name = "newPassword") String newPassword, @RequestParam(name = "newPassword") String newPassword,
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
RedirectAttributes redirectAttributes) { RedirectAttributes redirectAttributes)
throws IOException {
if (principal == null) { if (principal == null) {
return new RedirectView("/change-creds?messageType=notAuthenticated", true); return new RedirectView("/change-creds?messageType=notAuthenticated", true);
} }
@@ -149,7 +158,8 @@ public class UserController {
@RequestParam(name = "newPassword") String newPassword, @RequestParam(name = "newPassword") String newPassword,
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
RedirectAttributes redirectAttributes) { RedirectAttributes redirectAttributes)
throws IOException {
if (principal == null) { if (principal == null) {
return new RedirectView("/account?messageType=notAuthenticated", true); return new RedirectView("/account?messageType=notAuthenticated", true);
} }
@@ -176,7 +186,8 @@ public class UserController {
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/updateUserSettings") @PostMapping("/updateUserSettings")
public String updateUserSettings(HttpServletRequest request, Principal principal) { public String updateUserSettings(HttpServletRequest request, Principal principal)
throws IOException {
Map<String, String[]> paramMap = request.getParameterMap(); Map<String, String[]> paramMap = request.getParameterMap();
Map<String, String> updates = new HashMap<>(); Map<String, String> updates = new HashMap<>();
@@ -197,11 +208,13 @@ public class UserController {
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/saveUser") @PostMapping("/admin/saveUser")
public RedirectView saveUser( public RedirectView saveUser(
@RequestParam(name = "username") String username, @RequestParam String username,
@RequestParam(name = "password") String password, @RequestParam(name = "password", required = false) String password,
@RequestParam(name = "role") String role, @RequestParam(name = "role") String role,
@RequestParam(name = "authType") String authType,
@RequestParam(name = "forceChange", required = false, defaultValue = "false") @RequestParam(name = "forceChange", required = false, defaultValue = "false")
boolean forceChange) { boolean forceChange)
throws IllegalArgumentException, IOException {
if (!userService.isUsernameValid(username)) { if (!userService.isUsernameValid(username)) {
return new RedirectView("/addUsers?messageType=invalidUsername", true); return new RedirectView("/addUsers?messageType=invalidUsername", true);
@@ -230,7 +243,15 @@ public class UserController {
return new RedirectView("/addUsers?messageType=invalidRole", true); return new RedirectView("/addUsers?messageType=invalidRole", true);
} }
userService.saveUser(username, password, role, forceChange); if (authType.equalsIgnoreCase(AuthenticationType.OAUTH2.toString())) {
userService.saveUser(username, AuthenticationType.OAUTH2, role);
} else {
if (password.isBlank()) {
return new RedirectView("/addUsers?messageType=invalidPassword", true);
}
userService.saveUser(username, password, role, forceChange);
}
return new RedirectView( return new RedirectView(
"/addUsers", true); // Redirect to account page after adding the user "/addUsers", true); // Redirect to account page after adding the user
} }
@@ -240,7 +261,8 @@ public class UserController {
public RedirectView changeRole( public RedirectView changeRole(
@RequestParam(name = "username") String username, @RequestParam(name = "username") String username,
@RequestParam(name = "role") String role, @RequestParam(name = "role") String role,
Authentication authentication) { Authentication authentication)
throws IOException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username); Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
@@ -271,6 +293,60 @@ public class UserController {
User user = userOpt.get(); User user = userOpt.get();
userService.changeRole(user, role); userService.changeRole(user, role);
return new RedirectView(
"/addUsers", true); // Redirect to account page after adding the user
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/changeUserEnabled/{username}")
public RedirectView changeUserEnabled(
@PathVariable("username") String username,
@RequestParam("enabled") boolean enabled,
Authentication authentication)
throws IOException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (!userOpt.isPresent()) {
return new RedirectView("/addUsers?messageType=userNotFound", true);
}
if (!userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=userNotFound", true);
}
// Get the currently authenticated username
String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username
if (currentUsername.equalsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=disabledCurrentUser", true);
}
User user = userOpt.get();
userService.changeUserEnabled(user, enabled);
if (!enabled) {
// Invalidate all sessions if the user is being disabled
List<Object> principals = sessionRegistry.getAllPrincipals();
String userNameP = "";
for (Object principal : principals) {
List<SessionInformation> sessionsInformations =
sessionRegistry.getAllSessions(principal, false);
if (principal instanceof UserDetails) {
userNameP = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) {
userNameP = ((OAuth2User) principal).getName();
} else if (principal instanceof String) {
userNameP = (String) principal;
}
if (userNameP.equalsIgnoreCase(username)) {
for (SessionInformation sessionsInformation : sessionsInformations) {
sessionRegistry.expireSession(sessionsInformation.getSessionId());
}
}
}
}
return new RedirectView( return new RedirectView(
"/addUsers", true); // Redirect to account page after adding the user "/addUsers", true); // Redirect to account page after adding the user
} }
@@ -278,7 +354,7 @@ public class UserController {
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/deleteUser/{username}") @PostMapping("/admin/deleteUser/{username}")
public RedirectView deleteUser( public RedirectView deleteUser(
@PathVariable(name = "username") String username, Authentication authentication) { @PathVariable("username") String username, Authentication authentication) {
if (!userService.usernameExistsIgnoreCase(username)) { if (!userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=deleteUsernameExists", true); return new RedirectView("/addUsers?messageType=deleteUsernameExists", true);
@@ -291,27 +367,18 @@ public class UserController {
if (currentUsername.equalsIgnoreCase(username)) { if (currentUsername.equalsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=deleteCurrentUser", true); return new RedirectView("/addUsers?messageType=deleteCurrentUser", true);
} }
invalidateUserSessions(username);
// Invalidate all sessions before deleting the user
List<SessionInformation> sessionsInformations =
sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
for (SessionInformation sessionsInformation : sessionsInformations) {
sessionRegistry.expireSession(sessionsInformation.getSessionId());
sessionRegistry.removeSessionInformation(sessionsInformation.getSessionId());
}
userService.deleteUser(username); userService.deleteUser(username);
return new RedirectView("/addUsers", true); return new RedirectView("/addUsers", true);
} }
@Autowired private SessionRegistry sessionRegistry;
private void invalidateUserSessions(String username) {
for (Object principal : sessionRegistry.getAllPrincipals()) {
if (principal instanceof UserDetails) {
UserDetails userDetails = (UserDetails) principal;
if (userDetails.getUsername().equals(username)) {
for (SessionInformation session :
sessionRegistry.getAllSessions(principal, false)) {
session.expireNow();
}
}
}
}
}
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/get-api-key") @PostMapping("/get-api-key")
public ResponseEntity<String> getApiKey(Principal principal) { public ResponseEntity<String> getApiKey(Principal principal) {

View File

@@ -1,8 +1,18 @@
package stirling.software.SPDF.controller.api.converters; package stirling.software.SPDF.controller.api.converters;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.net.URLConnection; import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.commons.io.FileUtils;
import org.apache.pdfbox.rendering.ImageType; import org.apache.pdfbox.rendering.ImageType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -20,7 +30,10 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest; import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest; import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
import stirling.software.SPDF.utils.CheckProgramInstall;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@@ -60,15 +73,87 @@ public class ConvertImgPDFController {
result = result =
PdfUtils.convertFromPdf( PdfUtils.convertFromPdf(
pdfBytes, pdfBytes,
imageFormat.toUpperCase(), imageFormat.equalsIgnoreCase("webp") ? "png" : imageFormat.toUpperCase(),
colorTypeResult, colorTypeResult,
singleImage, singleImage,
Integer.valueOf(dpi), Integer.valueOf(dpi),
filename); filename);
if (result == null || result.length == 0) { if (result == null || result.length == 0) {
logger.error("resultant bytes for {} is null, error converting ", filename); logger.error("resultant bytes for {} is null, error converting ", filename);
} }
if (imageFormat.equalsIgnoreCase("webp") && !CheckProgramInstall.isPythonAvailable()) {
throw new IOException("Python is not installed. Required for WebP conversion.");
} else if (imageFormat.equalsIgnoreCase("webp")
&& CheckProgramInstall.isPythonAvailable()) {
// Write the output stream to a temp file
Path tempFile = Files.createTempFile("temp_png", ".png");
try (FileOutputStream fos = new FileOutputStream(tempFile.toFile())) {
fos.write(result);
fos.flush();
}
String pythonVersion = CheckProgramInstall.getAvailablePythonCommand();
List<String> command = new ArrayList<>();
command.add(pythonVersion);
command.add("./scripts/png_to_webp.py"); // Python script to handle the conversion
// Create a temporary directory for the output WebP files
Path tempOutputDir = Files.createTempDirectory("webp_output");
if (singleImage) {
// Run the Python script to convert PNG to WebP
command.add(tempFile.toString());
command.add(tempOutputDir.toString());
command.add("--single");
} else {
// Save the uploaded PDF to a temporary file
Path tempPdfPath = Files.createTempFile("temp_pdf", ".pdf");
file.transferTo(tempPdfPath.toFile());
// Run the Python script to convert PDF to WebP
command.add(tempPdfPath.toString());
command.add(tempOutputDir.toString());
}
command.add("--dpi");
command.add(dpi);
ProcessExecutorResult resultProcess =
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
.runCommandWithOutputHandling(command);
// Find all WebP files in the output directory
List<Path> webpFiles =
Files.walk(tempOutputDir)
.filter(path -> path.toString().endsWith(".webp"))
.collect(Collectors.toList());
if (webpFiles.isEmpty()) {
logger.error("No WebP files were created in: {}", tempOutputDir.toString());
throw new IOException("No WebP files were created. " + resultProcess.getMessages());
}
byte[] bodyBytes = new byte[0];
if (webpFiles.size() == 1) {
// Return the single WebP file directly
Path webpFilePath = webpFiles.get(0);
bodyBytes = Files.readAllBytes(webpFilePath);
} else {
// Create a ZIP file containing all WebP images
ByteArrayOutputStream zipOutputStream = new ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(zipOutputStream)) {
for (Path webpFile : webpFiles) {
zos.putNextEntry(new ZipEntry(webpFile.getFileName().toString()));
Files.copy(webpFile, zos);
zos.closeEntry();
}
}
bodyBytes = zipOutputStream.toByteArray();
}
// Clean up the temporary files
Files.deleteIfExists(tempFile);
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
result = bodyBytes;
}
if (singleImage) { if (singleImage) {
String docName = filename + "." + imageFormat; String docName = filename + "." + imageFormat;
MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat)); MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat));

View File

@@ -39,6 +39,12 @@ public class ConvertWebsiteToPDF {
if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) { if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
throw new IllegalArgumentException("Invalid URL format provided."); throw new IllegalArgumentException("Invalid URL format provided.");
} }
// validate the URL is reachable
if (!GeneralUtils.isURLReachable(URL)) {
throw new IllegalArgumentException("URL is not reachable, please provide a valid URL.");
}
Path tempOutputFile = null; Path tempOutputFile = null;
byte[] pdfBytes; byte[] pdfBytes;
try { try {

View File

@@ -33,7 +33,7 @@ public class AutoRenameController {
private static final Logger logger = LoggerFactory.getLogger(AutoRenameController.class); private static final Logger logger = LoggerFactory.getLogger(AutoRenameController.class);
private static final float TITLE_FONT_SIZE_THRESHOLD = 20.0f; private static final float TITLE_FONT_SIZE_THRESHOLD = 20.0f;
private static final int LINE_LIMIT = 11; private static final int LINE_LIMIT = 200;
@PostMapping(consumes = "multipart/form-data", value = "/auto-rename") @PostMapping(consumes = "multipart/form-data", value = "/auto-rename")
@Operation( @Operation(

View File

@@ -1,12 +1,12 @@
package stirling.software.SPDF.controller.api.misc; package stirling.software.SPDF.controller.api.misc;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.zip.ZipEntry;
import java.util.stream.IntStream; import java.util.zip.ZipOutputStream;
import org.apache.pdfbox.Loader; import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
@@ -17,6 +17,7 @@ import org.apache.pdfbox.text.PDFTextStripper;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -50,31 +51,31 @@ public class BlankPageController {
int threshold = request.getThreshold(); int threshold = request.getThreshold();
float whitePercent = request.getWhitePercent(); float whitePercent = request.getWhitePercent();
PDDocument document = null; try (PDDocument document = Loader.loadPDF(inputFile.getBytes())) {
try {
document = Loader.loadPDF(inputFile.getBytes());
PDPageTree pages = document.getDocumentCatalog().getPages(); PDPageTree pages = document.getDocumentCatalog().getPages();
PDFTextStripper textStripper = new PDFTextStripper(); PDFTextStripper textStripper = new PDFTextStripper();
List<Integer> pagesToKeepIndex = new ArrayList<>(); List<PDPage> nonBlankPages = new ArrayList<>();
List<PDPage> blankPages = new ArrayList<>();
int pageIndex = 0; int pageIndex = 0;
PDFRenderer pdfRenderer = new PDFRenderer(document); PDFRenderer pdfRenderer = new PDFRenderer(document);
pdfRenderer.setSubsamplingAllowed(true); pdfRenderer.setSubsamplingAllowed(true);
for (PDPage page : pages) { for (PDPage page : pages) {
logger.info("checking page " + pageIndex); logger.info("checking page {}", pageIndex);
textStripper.setStartPage(pageIndex + 1); textStripper.setStartPage(pageIndex + 1);
textStripper.setEndPage(pageIndex + 1); textStripper.setEndPage(pageIndex + 1);
String pageText = textStripper.getText(document); String pageText = textStripper.getText(document);
boolean hasText = !pageText.trim().isEmpty(); boolean hasText = !pageText.trim().isEmpty();
Boolean blank = true; boolean blank = true;
if (hasText) { if (hasText) {
logger.info("page " + pageIndex + " has text, not blank"); logger.info("page {} has text, not blank", pageIndex);
blank = false; blank = false;
} else { } else {
boolean hasImages = PdfUtils.hasImagesOnPage(page); boolean hasImages = PdfUtils.hasImagesOnPage(page);
if (hasImages) { if (hasImages) {
logger.info("page " + pageIndex + " has image, running blank detection"); logger.info("page {} has image, running blank detection", pageIndex);
// Render image and save as temp file // Render image and save as temp file
BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 30); BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 30);
blank = isBlankImage(image, threshold, whitePercent, threshold); blank = isBlankImage(image, threshold, whitePercent, threshold);
@@ -82,34 +83,57 @@ public class BlankPageController {
} }
if (blank) { if (blank) {
logger.info("Skipping, Image was blank for page #" + pageIndex); logger.info("Skipping, Image was blank for page #{}", pageIndex);
blankPages.add(page);
} else { } else {
logger.info("page " + pageIndex + " has image which is not blank"); logger.info("page {} has image which is not blank", pageIndex);
pagesToKeepIndex.add(pageIndex); nonBlankPages.add(page);
} }
pageIndex++; pageIndex++;
} }
// Remove pages not present in pagesToKeepIndex
List<Integer> pageIndices = ByteArrayOutputStream baos = new ByteArrayOutputStream();
IntStream.range(0, pages.getCount()).boxed().collect(Collectors.toList()); ZipOutputStream zos = new ZipOutputStream(baos);
Collections.reverse(pageIndices); // Reverse to prevent index shifting during removal
for (Integer i : pageIndices) { String filename =
if (!pagesToKeepIndex.contains(i)) { Filenames.toSimpleFileName(inputFile.getOriginalFilename())
pages.remove(i); .replaceFirst("[.][^.]+$", "");
}
if (!nonBlankPages.isEmpty()) {
createZipEntry(zos, nonBlankPages, filename + "_nonBlankPages.pdf");
} else {
createZipEntry(zos, blankPages, filename + "_allBlankPages.pdf");
} }
return WebResponseUtils.pdfDocToWebResponse( if (!nonBlankPages.isEmpty() && !blankPages.isEmpty()) {
document, createZipEntry(zos, blankPages, filename + "_blankPages.pdf");
Filenames.toSimpleFileName(inputFile.getOriginalFilename()) }
.replaceFirst("[.][^.]+$", "")
+ "_blanksRemoved.pdf"); zos.close();
logger.info("Returning ZIP file: {}", filename + "_processed.zip");
return WebResponseUtils.boasToWebResponse(
baos, filename + "_processed.zip", MediaType.APPLICATION_OCTET_STREAM);
} catch (IOException e) { } catch (IOException e) {
logger.error("exception", e); logger.error("exception", e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
} finally { }
if (document != null) document.close(); }
public void createZipEntry(ZipOutputStream zos, List<PDPage> pages, String entryName)
throws IOException {
try (PDDocument document = new PDDocument()) {
for (PDPage page : pages) {
document.addPage(page);
}
ZipEntry zipEntry = new ZipEntry(entryName);
zos.putNextEntry(zipEntry);
document.save(zos);
zos.closeEntry();
} }
} }

View File

@@ -99,7 +99,7 @@ public class CompressController {
List<String> command = new ArrayList<>(); List<String> command = new ArrayList<>();
command.add("gs"); command.add("gs");
command.add("-sDEVICE=pdfwrite"); command.add("-sDEVICE=pdfwrite");
command.add("-dCompatibilityLevel=1.4"); command.add("-dCompatibilityLevel=1.5");
switch (optimizeLevel) { switch (optimizeLevel) {
case 1: case 1:

View File

@@ -32,6 +32,7 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest; import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
import stirling.software.SPDF.utils.CheckProgramInstall;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@@ -76,6 +77,11 @@ public class ExtractImageScansController {
Path tempZipFile = null; Path tempZipFile = null;
List<Path> tempDirs = new ArrayList<>(); List<Path> tempDirs = new ArrayList<>();
if (!CheckProgramInstall.isPythonAvailable()) {
throw new IOException("Python is not installed.");
}
String pythonVersion = CheckProgramInstall.getAvailablePythonCommand();
try { try {
// Check if input file is a PDF // Check if input file is a PDF
if ("pdf".equalsIgnoreCase(extension)) { if ("pdf".equalsIgnoreCase(extension)) {
@@ -117,7 +123,7 @@ public class ExtractImageScansController {
List<String> command = List<String> command =
new ArrayList<>( new ArrayList<>(
Arrays.asList( Arrays.asList(
"python3", pythonVersion,
"./scripts/split_photos.py", "./scripts/split_photos.py",
images.get(i), images.get(i),
tempDir.toString(), tempDir.toString(),

View File

@@ -1,13 +1,19 @@
package stirling.software.SPDF.controller.api.misc; package stirling.software.SPDF.controller.api.misc;
import java.awt.Graphics2D; import java.awt.*;
import java.awt.Image;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage; import java.awt.image.RenderedImage;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.zip.Deflater; import java.util.zip.Deflater;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
@@ -33,7 +39,8 @@ import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFWithImageFormatRequest; import stirling.software.SPDF.model.api.PDFExtractImagesRequest;
import stirling.software.SPDF.utils.ImageProcessingUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@@ -47,16 +54,19 @@ public class ExtractImagesController {
@Operation( @Operation(
summary = "Extract images from a PDF file", summary = "Extract images from a PDF file",
description = description =
"This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input:PDF Output:IMAGE/ZIP Type:SIMO") "This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input: PDF Output: IMAGE/ZIP Type: SIMO")
public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFWithImageFormatRequest request) public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFExtractImagesRequest request)
throws IOException { throws IOException, InterruptedException, ExecutionException {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
String format = request.getFormat(); String format = request.getFormat();
boolean allowDuplicates = request.isAllowDuplicates();
System.out.println( System.out.println(
System.currentTimeMillis() + "file=" + file.getName() + ", format=" + format); System.currentTimeMillis() + " file=" + file.getName() + ", format=" + format);
PDDocument document = Loader.loadPDF(file.getBytes()); PDDocument document = Loader.loadPDF(file.getBytes());
// Determine if multithreading should be used based on PDF size or number of pages
boolean useMultithreading = shouldUseMultithreading(file, document);
// Create ByteArrayOutputStream to write zip file to byte array // Create ByteArrayOutputStream to write zip file to byte array
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -66,71 +76,58 @@ public class ExtractImagesController {
// Set compression level // Set compression level
zos.setLevel(Deflater.BEST_COMPRESSION); zos.setLevel(Deflater.BEST_COMPRESSION);
int imageIndex = 1;
String filename = String filename =
Filenames.toSimpleFileName(file.getOriginalFilename()) Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", ""); .replaceFirst("[.][^.]+$", "");
int pageNum = 0; Set<byte[]> processedImages = new HashSet<>();
Set<Integer> processedImages = new HashSet<>();
// Iterate over each page
for (PDPage page : document.getPages()) {
++pageNum;
// Extract images from page
for (COSName name : page.getResources().getXObjectNames()) {
if (page.getResources().isImageXObject(name)) {
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
int imageHash = image.hashCode();
if (processedImages.contains(imageHash)) {
continue; // Skip already processed images
}
processedImages.add(imageHash);
// Convert image to desired format if (useMultithreading) {
RenderedImage renderedImage = image.getImage(); // Executor service to handle multithreading
BufferedImage bufferedImage = null; ExecutorService executor =
if ("png".equalsIgnoreCase(format)) { Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
bufferedImage = Set<Future<Void>> futures = new HashSet<>();
new BufferedImage(
renderedImage.getWidth(),
renderedImage.getHeight(),
BufferedImage.TYPE_INT_ARGB);
} else if ("jpeg".equalsIgnoreCase(format) || "jpg".equalsIgnoreCase(format)) {
bufferedImage =
new BufferedImage(
renderedImage.getWidth(),
renderedImage.getHeight(),
BufferedImage.TYPE_INT_RGB);
} else if ("gif".equalsIgnoreCase(format)) {
bufferedImage =
new BufferedImage(
renderedImage.getWidth(),
renderedImage.getHeight(),
BufferedImage.TYPE_BYTE_INDEXED);
}
// Write image to zip file // Iterate over each page
String imageName = for (int pgNum = 0; pgNum < document.getPages().getCount(); pgNum++) {
filename + "_" + imageIndex + " (Page " + pageNum + ")." + format; PDPage page = document.getPage(pgNum);
ZipEntry zipEntry = new ZipEntry(imageName); int pageNum = document.getPages().indexOf(page) + 1;
zos.putNextEntry(zipEntry); // Submit a task for processing each page
Future<Void> future =
executor.submit(
() -> {
extractImagesFromPage(
page,
format,
filename,
pageNum,
processedImages,
zos,
allowDuplicates);
return null;
});
Graphics2D g = bufferedImage.createGraphics(); futures.add(future);
g.drawImage((Image) renderedImage, 0, 0, null); }
g.dispose();
// Write image bytes to zip file
ByteArrayOutputStream imageBaos = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, format, imageBaos);
zos.write(imageBaos.toByteArray());
zos.closeEntry(); // Wait for all tasks to complete
imageIndex++; for (Future<Void> future : futures) {
} future.get();
}
// Close executor service
executor.shutdown();
} else {
// Single-threaded extraction
for (int pgNum = 0; pgNum < document.getPages().getCount(); pgNum++) {
PDPage page = document.getPage(pgNum);
extractImagesFromPage(
page, format, filename, pgNum + 1, processedImages, zos, allowDuplicates);
} }
} }
// Close ZipOutputStream and PDDocument // Close PDDocument and ZipOutputStream
zos.close();
document.close(); document.close();
zos.close();
// Create ByteArrayResource from byte array // Create ByteArrayResource from byte array
byte[] zipContents = baos.toByteArray(); byte[] zipContents = baos.toByteArray();
@@ -138,4 +135,85 @@ public class ExtractImagesController {
return WebResponseUtils.boasToWebResponse( return WebResponseUtils.boasToWebResponse(
baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM); baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM);
} }
private boolean shouldUseMultithreading(MultipartFile file, PDDocument document) {
// Criteria: Use multithreading if file size > 10MB or number of pages > 20
long fileSizeInMB = file.getSize() / (1024 * 1024);
int numberOfPages = document.getPages().getCount();
return fileSizeInMB > 10 || numberOfPages > 20;
}
private void extractImagesFromPage(
PDPage page,
String format,
String filename,
int pageNum,
Set<byte[]> processedImages,
ZipOutputStream zos,
boolean allowDuplicates)
throws IOException {
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
logger.error("MD5 algorithm not available for extractImages hash.", e);
return;
}
if (page.getResources() == null || page.getResources().getXObjectNames() == null) {
return;
}
int count = 1;
for (COSName name : page.getResources().getXObjectNames()) {
if (page.getResources().isImageXObject(name)) {
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
if (!allowDuplicates) {
byte[] data = ImageProcessingUtils.getImageData(image.getImage());
byte[] imageHash = md.digest(data);
synchronized (processedImages) {
if (processedImages.stream()
.anyMatch(hash -> Arrays.equals(hash, imageHash))) {
continue; // Skip already processed images
}
processedImages.add(imageHash);
}
}
RenderedImage renderedImage = image.getImage();
// Convert to standard RGB colorspace if needed
BufferedImage bufferedImage = convertToRGB(renderedImage, format);
// Write image to zip file
String imageName = filename + "_page_" + pageNum + "_" + count++ + "." + format;
synchronized (zos) {
zos.putNextEntry(new ZipEntry(imageName));
ByteArrayOutputStream imageBaos = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, format, imageBaos);
zos.write(imageBaos.toByteArray());
zos.closeEntry();
}
}
}
}
private BufferedImage convertToRGB(RenderedImage renderedImage, String format) {
int width = renderedImage.getWidth();
int height = renderedImage.getHeight();
BufferedImage rgbImage;
if ("png".equalsIgnoreCase(format)) {
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
} else if ("jpeg".equalsIgnoreCase(format) || "jpg".equalsIgnoreCase(format)) {
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
} else if ("gif".equalsIgnoreCase(format)) {
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED);
} else {
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
}
Graphics2D g = rgbImage.createGraphics();
g.drawImage((Image) renderedImage, 0, 0, null);
g.dispose();
return rgbImage;
}
} }

View File

@@ -13,8 +13,7 @@ import java.util.stream.Collectors;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@@ -27,6 +26,7 @@ import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest; import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@@ -37,10 +37,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class OCRController { public class OCRController {
private static final Logger logger = LoggerFactory.getLogger(OCRController.class); @Autowired ApplicationProperties applicationProperties;
public List<String> getAvailableTesseractLanguages() { public List<String> getAvailableTesseractLanguages() {
String tessdataDir = "/usr/share/tessdata"; String tessdataDir = applicationProperties.getSystem().getTessdataDir();
File[] files = new File(tessdataDir).listFiles(); File[] files = new File(tessdataDir).listFiles();
if (files == null) { if (files == null) {
return Collections.emptyList(); return Collections.emptyList();

View File

@@ -43,7 +43,7 @@ public class ApiDocService {
Map<String, List<String>> outputToFileTypes = new HashMap<>(); Map<String, List<String>> outputToFileTypes = new HashMap<>();
public List getExtensionTypes(boolean output, String operationName) { public List<String> getExtensionTypes(boolean output, String operationName) {
if (outputToFileTypes.size() == 0) { if (outputToFileTypes.size() == 0) {
outputToFileTypes.put("PDF", Arrays.asList("pdf")); outputToFileTypes.put("PDF", Arrays.asList("pdf"));
outputToFileTypes.put( outputToFileTypes.put(

View File

@@ -1,21 +1,14 @@
package stirling.software.SPDF.controller.api.security; package stirling.software.SPDF.controller.api.security;
import java.awt.Color; import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import org.apache.pdfbox.Loader; import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -32,6 +25,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.PDFText; import stirling.software.SPDF.model.PDFText;
import stirling.software.SPDF.model.api.security.RedactPdfRequest; import stirling.software.SPDF.model.api.security.RedactPdfRequest;
import stirling.software.SPDF.pdf.TextFinder; import stirling.software.SPDF.pdf.TextFinder;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@@ -81,22 +75,9 @@ public class RedactController {
} }
if (convertPDFToImage) { if (convertPDFToImage) {
PDDocument imageDocument = new PDDocument(); PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document);
PDFRenderer pdfRenderer = new PDFRenderer(document);
pdfRenderer.setSubsamplingAllowed(true);
for (int page = 0; page < document.getNumberOfPages(); ++page) {
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
PDPage newPage = new PDPage(new PDRectangle(bim.getWidth(), bim.getHeight()));
imageDocument.addPage(newPage);
PDImageXObject pdImage = LosslessFactory.createFromImage(imageDocument, bim);
PDPageContentStream contentStream =
new PDPageContentStream(
imageDocument, newPage, AppendMode.APPEND, true, true);
contentStream.drawImage(pdImage, 0, 0);
contentStream.close();
}
document.close(); document.close();
document = imageDocument; document = convertedPdf;
} }
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();

View File

@@ -36,6 +36,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.AddWatermarkRequest; import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@@ -60,6 +61,7 @@ public class WatermarkController {
float opacity = request.getOpacity(); float opacity = request.getOpacity();
int widthSpacer = request.getWidthSpacer(); int widthSpacer = request.getWidthSpacer();
int heightSpacer = request.getHeightSpacer(); int heightSpacer = request.getHeightSpacer();
boolean convertPdfToImage = request.isConvertPDFToImage();
// Load the input PDF // Load the input PDF
PDDocument document = Loader.loadPDF(pdfFile.getBytes()); PDDocument document = Loader.loadPDF(pdfFile.getBytes());
@@ -104,6 +106,12 @@ public class WatermarkController {
contentStream.close(); contentStream.close();
} }
if (convertPdfToImage) {
PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document);
document.close();
document = convertedPdf;
}
return WebResponseUtils.pdfDocToWebResponse( return WebResponseUtils.pdfDocToWebResponse(
document, document,
Filenames.toSimpleFileName(pdfFile.getOriginalFilename()) Filenames.toSimpleFileName(pdfFile.getOriginalFilename())

View File

@@ -1,13 +1,10 @@
package stirling.software.SPDF.controller.web; package stirling.software.SPDF.controller.web;
import java.util.HashMap; import java.time.Instant;
import java.util.Iterator; import java.time.temporal.ChronoUnit;
import java.util.List; import java.util.*;
import java.util.Map; import java.util.stream.Collectors;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@@ -23,26 +20,31 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import stirling.software.SPDF.model.ApplicationProperties; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties.GithubProvider; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApplicationProperties.GoogleProvider; import stirling.software.SPDF.model.*;
import stirling.software.SPDF.model.ApplicationProperties.KeycloakProvider;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.Authority; import stirling.software.SPDF.model.provider.GithubProvider;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.provider.KeycloakProvider;
import stirling.software.SPDF.repository.UserRepository; import stirling.software.SPDF.repository.UserRepository;
@Controller @Controller
@Slf4j
@Tag(name = "Account Security", description = "Account Security APIs") @Tag(name = "Account Security", description = "Account Security APIs")
public class AccountWebController { public class AccountWebController {
@Autowired ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
private static final Logger logger = LoggerFactory.getLogger(AccountWebController.class); @Autowired SessionPersistentRegistry sessionPersistentRegistry;
@Autowired
private UserRepository userRepository; // Assuming you have a repository for user operations
@GetMapping("/login") @GetMapping("/login")
public String login(HttpServletRequest request, Model model, Authentication authentication) { public String login(HttpServletRequest request, Model model, Authentication authentication) {
// If the user is already authenticated, redirect them to the home page.
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
return "redirect:/"; return "redirect:/";
} }
@@ -72,10 +74,17 @@ public class AccountWebController {
} }
} }
} }
// Remove any null keys/values from the providerList
providerList
.entrySet()
.removeIf(entry -> entry.getKey() == null || entry.getValue() == null);
model.addAttribute("providerlist", providerList); model.addAttribute("providerlist", providerList);
model.addAttribute("loginMethod", applicationProperties.getSecurity().getLoginMethod());
model.addAttribute( model.addAttribute(
"oAuth2Enabled", applicationProperties.getSecurity().getOAUTH2().getEnabled()); "oAuth2Enabled", applicationProperties.getSecurity().getOAUTH2().getEnabled());
model.addAttribute(
"samlEnabled", applicationProperties.getSecurity().getSAML().getEnabled());
model.addAttribute("currentPage", "login"); model.addAttribute("currentPage", "login");
@@ -131,6 +140,13 @@ public class AccountWebController {
break; break;
case "invalid_id_token": case "invalid_id_token":
erroroauth = "login.oauth2InvalidIdToken"; erroroauth = "login.oauth2InvalidIdToken";
break;
case "oauth2_admin_blocked_user":
erroroauth = "login.oauth2AdminBlockedUser";
break;
case "userIsDisabled":
erroroauth = "login.userIsDisabled";
break;
default: default:
break; break;
} }
@@ -149,9 +165,6 @@ public class AccountWebController {
return "login"; return "login";
} }
@Autowired
private UserRepository userRepository; // Assuming you have a repository for user operations
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/addUsers") @GetMapping("/addUsers")
public String showAddUserForm( public String showAddUserForm(
@@ -160,6 +173,13 @@ public class AccountWebController {
Iterator<User> iterator = allUsers.iterator(); Iterator<User> iterator = allUsers.iterator();
Map<String, String> roleDetails = Role.getAllRoleDetails(); Map<String, String> roleDetails = Role.getAllRoleDetails();
// Map to store session information and user activity status
Map<String, Boolean> userSessions = new HashMap<>();
Map<String, Date> userLastRequest = new HashMap<>();
int activeUsers = 0;
int disabledUsers = 0;
while (iterator.hasNext()) { while (iterator.hasNext()) {
User user = iterator.next(); User user = iterator.next();
if (user != null) { if (user != null) {
@@ -170,9 +190,73 @@ public class AccountWebController {
break; // Break out of the inner loop once the user is removed break; // Break out of the inner loop once the user is removed
} }
} }
// Determine the user's session status and last request time
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
boolean hasActiveSession = false;
Date lastRequest = null;
Optional<SessionEntity> latestSession =
sessionPersistentRegistry.findLatestSession(user.getUsername());
if (latestSession.isPresent()) {
SessionEntity sessionEntity = latestSession.get();
Date lastAccessedTime = sessionEntity.getLastRequest();
Instant now = Instant.now();
// Calculate session expiration and update session status accordingly
Instant expirationTime =
lastAccessedTime
.toInstant()
.plus(maxInactiveInterval, ChronoUnit.SECONDS);
if (now.isAfter(expirationTime)) {
sessionPersistentRegistry.expireSession(sessionEntity.getSessionId());
hasActiveSession = false;
} else {
hasActiveSession = !sessionEntity.isExpired();
}
lastRequest = sessionEntity.getLastRequest();
} else {
hasActiveSession = false;
lastRequest = new Date(0); // No session, set default last request time
}
userSessions.put(user.getUsername(), hasActiveSession);
userLastRequest.put(user.getUsername(), lastRequest);
if (hasActiveSession) {
activeUsers++;
}
if (!user.isEnabled()) {
disabledUsers++;
}
} }
} }
// Sort users by active status and last request date
List<User> sortedUsers =
allUsers.stream()
.sorted(
(u1, u2) -> {
boolean u1Active = userSessions.get(u1.getUsername());
boolean u2Active = userSessions.get(u2.getUsername());
if (u1Active && !u2Active) {
return -1;
} else if (!u1Active && u2Active) {
return 1;
} else {
Date u1LastRequest =
userLastRequest.getOrDefault(
u1.getUsername(), new Date(0));
Date u2LastRequest =
userLastRequest.getOrDefault(
u2.getUsername(), new Date(0));
return u2LastRequest.compareTo(u1LastRequest);
}
})
.collect(Collectors.toList());
String messageType = request.getParameter("messageType"); String messageType = request.getParameter("messageType");
String deleteMessage = null; String deleteMessage = null;
@@ -197,6 +281,9 @@ public class AccountWebController {
case "invalidUsername": case "invalidUsername":
addMessage = "invalidUsernameMessage"; addMessage = "invalidUsernameMessage";
break; break;
case "invalidPassword":
addMessage = "invalidPasswordMessage";
break;
default: default:
break; break;
} }
@@ -212,16 +299,24 @@ public class AccountWebController {
case "downgradeCurrentUser": case "downgradeCurrentUser":
changeMessage = "downgradeCurrentUserMessage"; changeMessage = "downgradeCurrentUserMessage";
break; break;
case "disabledCurrentUser":
changeMessage = "disabledCurrentUserMessage";
break;
default: default:
changeMessage = messageType;
break; break;
} }
model.addAttribute("changeMessage", changeMessage); model.addAttribute("changeMessage", changeMessage);
} }
model.addAttribute("users", allUsers); model.addAttribute("users", sortedUsers);
model.addAttribute("currentUsername", authentication.getName()); model.addAttribute("currentUsername", authentication.getName());
model.addAttribute("roleDetails", roleDetails); model.addAttribute("roleDetails", roleDetails);
model.addAttribute("userSessions", userSessions);
model.addAttribute("userLastRequest", userLastRequest);
model.addAttribute("totalUsers", allUsers.size());
model.addAttribute("activeUsers", activeUsers);
model.addAttribute("disabledUsers", disabledUsers);
return "addUsers"; return "addUsers";
} }
@@ -259,7 +354,7 @@ public class AccountWebController {
if (username != null) { if (username != null) {
// Fetch user details from the database // Fetch user details from the database
Optional<User> user = Optional<User> user =
userRepository.findByUsernameIgnoreCase( userRepository.findByUsernameIgnoreCaseWithSettings(
username); // Assuming findByUsername method exists username); // Assuming findByUsername method exists
if (!user.isPresent()) { if (!user.isPresent()) {
return "redirect:/error"; return "redirect:/error";
@@ -272,7 +367,7 @@ public class AccountWebController {
settingsJson = objectMapper.writeValueAsString(user.get().getSettings()); settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
// Handle JSON conversion error // Handle JSON conversion error
logger.error("exception", e); log.error("exception", e);
return "redirect:/error"; return "redirect:/error";
} }

View File

@@ -9,6 +9,8 @@ import org.springframework.web.servlet.ModelAndView;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.CheckProgramInstall;
@Controller @Controller
@Tag(name = "Convert", description = "Convert APIs") @Tag(name = "Convert", description = "Convert APIs")
public class ConverterWebController { public class ConverterWebController {
@@ -21,14 +23,6 @@ public class ConverterWebController {
return "convert/book-to-pdf"; return "convert/book-to-pdf";
} }
@ConditionalOnExpression("#{bookAndHtmlFormatsInstalled}")
@GetMapping("/pdf-to-book")
@Hidden
public String convertPdfToBookForm(Model model) {
model.addAttribute("currentPage", "pdf-to-book");
return "convert/pdf-to-book";
}
@GetMapping("/img-to-pdf") @GetMapping("/img-to-pdf")
@Hidden @Hidden
public String convertImgToPdfForm(Model model) { public String convertImgToPdfForm(Model model) {
@@ -57,13 +51,6 @@ public class ConverterWebController {
return "convert/url-to-pdf"; return "convert/url-to-pdf";
} }
@GetMapping("/pdf-to-img")
@Hidden
public String pdfToimgForm(Model model) {
model.addAttribute("currentPage", "pdf-to-img");
return "convert/pdf-to-img";
}
@GetMapping("/file-to-pdf") @GetMapping("/file-to-pdf")
@Hidden @Hidden
public String convertToPdfForm(Model model) { public String convertToPdfForm(Model model) {
@@ -73,6 +60,23 @@ public class ConverterWebController {
// PDF TO...... // PDF TO......
@ConditionalOnExpression("#{bookAndHtmlFormatsInstalled}")
@GetMapping("/pdf-to-book")
@Hidden
public String convertPdfToBookForm(Model model) {
model.addAttribute("currentPage", "pdf-to-book");
return "convert/pdf-to-book";
}
@GetMapping("/pdf-to-img")
@Hidden
public String pdfToimgForm(Model model) {
boolean isPython = CheckProgramInstall.isPythonAvailable();
model.addAttribute("isPython", isPython);
model.addAttribute("currentPage", "pdf-to-img");
return "convert/pdf-to-img";
}
@GetMapping("/pdf-to-html") @GetMapping("/pdf-to-html")
@Hidden @Hidden
public ModelAndView pdfToHTML() { public ModelAndView pdfToHTML() {

View File

@@ -0,0 +1,41 @@
package stirling.software.SPDF.controller.web;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import stirling.software.SPDF.config.security.database.DatabaseBackupHelper;
import stirling.software.SPDF.utils.FileInfo;
@Controller
@Tag(name = "Database Management", description = "Database management and security APIs")
public class DatabaseWebController {
@Autowired private DatabaseBackupHelper databaseBackupHelper;
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/database")
public String database(HttpServletRequest request, Model model, Authentication authentication) {
String error = request.getParameter("error");
String confirmed = request.getParameter("infoMessage");
if (error != null) {
model.addAttribute("error", error);
} else if (confirmed != null) {
model.addAttribute("infoMessage", confirmed);
}
List<FileInfo> backupList = databaseBackupHelper.getBackupList();
model.addAttribute("systemUpdate", backupList);
return "database";
}
}

View File

@@ -310,4 +310,11 @@ public class GeneralWebController {
model.addAttribute("currentPage", "auto-split-pdf"); model.addAttribute("currentPage", "auto-split-pdf");
return "auto-split-pdf"; return "auto-split-pdf";
} }
@GetMapping("/remove-image-pdf")
@Hidden
public String removeImagePdfForm(Model model) {
model.addAttribute("currentPage", "remove-image-pdf");
return "remove-image-pdf";
}
} }

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