mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12fff5b4a7 | ||
|
|
ffd1abbdb3 | ||
|
|
79bc62a5d2 | ||
|
|
1c43bd363a | ||
|
|
4f404a1ccf | ||
|
|
2ae413c5ea | ||
|
|
36358fc139 | ||
|
|
d39a7ddda7 | ||
|
|
789eaa263f | ||
|
|
4575d7178b | ||
|
|
3cdf363eab | ||
|
|
d990d4181d | ||
|
|
a46dc141d0 | ||
|
|
6fee27739c | ||
|
|
1cc562a6b1 | ||
|
|
d90e51233d | ||
|
|
1cd3e2846e | ||
|
|
03da0e7d68 | ||
|
|
685b562d3d | ||
|
|
080faf9353 | ||
|
|
d486bb4939 | ||
|
|
41f9929fd2 | ||
|
|
0b0db1793e | ||
|
|
f3cf747cfe | ||
|
|
e3982ed4c5 | ||
|
|
5c7d675960 | ||
|
|
7fc6ec5fe1 | ||
|
|
7722001463 | ||
|
|
43d4b46b31 | ||
|
|
eee17d4d19 | ||
|
|
c40144db19 | ||
|
|
c77cd73deb | ||
|
|
b76f3662e4 | ||
|
|
188408fc1e | ||
|
|
0b86dd79d3 | ||
|
|
0436460c03 | ||
|
|
4d84dcdd42 | ||
|
|
039e3b5fa8 | ||
|
|
15c5b0eb92 | ||
|
|
36c9369404 | ||
|
|
43f3261972 | ||
|
|
3d7efc5d94 | ||
|
|
ebf256fd50 | ||
|
|
f678e1d2e3 | ||
|
|
81c14351ee | ||
|
|
3711d8d6b1 | ||
|
|
a3b2a9b3e3 | ||
|
|
dd7925fbf4 | ||
|
|
1b1d17f6f5 | ||
|
|
58a00dde24 | ||
|
|
a95583b9e0 | ||
|
|
56f788957a | ||
|
|
df1295059a | ||
|
|
853161e891 | ||
|
|
64b33ea62b | ||
|
|
0ca4600371 | ||
|
|
2d254ef0f6 | ||
|
|
a28464a954 | ||
|
|
fe56831889 | ||
|
|
f5fdb870a8 | ||
|
|
2befd2f6e9 | ||
|
|
a2f6acd732 | ||
|
|
1436821a3a | ||
|
|
23f872823d | ||
|
|
80cba55459 | ||
|
|
b58efaf388 | ||
|
|
68df661204 | ||
|
|
20f984156f | ||
|
|
bd75ad042a | ||
|
|
8bb807471f | ||
|
|
59d816bfa7 | ||
|
|
473021a13c | ||
|
|
f616486bee | ||
|
|
cb5c2a5803 | ||
|
|
3e061516a5 | ||
|
|
8afbd0afed | ||
|
|
884801ea36 | ||
|
|
8fcee482f0 | ||
|
|
dbf72cf053 | ||
|
|
84e23abddc | ||
|
|
a7945da3b4 |
@@ -63,9 +63,6 @@ labels:
|
||||
files:
|
||||
- 'app/core/src/main/resources/static/.*'
|
||||
- 'app/proprietary/src/main/resources/static/.*'
|
||||
- 'app/core/src/main/java/stirling/software/SPDF/controller/web/.*'
|
||||
- 'app/core/src/main/java/stirling/software/SPDF/UI/.*'
|
||||
- 'app/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/.*'
|
||||
- 'frontend/**'
|
||||
- 'frontend/.*'
|
||||
- 'frontend/**/.*'
|
||||
|
||||
@@ -59,8 +59,8 @@ def find_duplicate_keys(file_path, keys=None, prefix=""):
|
||||
return duplicates
|
||||
|
||||
|
||||
# Maximum size for TOML files (e.g., 570 KB)
|
||||
MAX_FILE_SIZE = 570 * 1024
|
||||
# Maximum size for TOML files (e.g., 1 MB)
|
||||
MAX_FILE_SIZE = 1000 * 1024
|
||||
|
||||
|
||||
def parse_toml_file(file_path):
|
||||
|
||||
@@ -97,9 +97,9 @@ cffi==2.0.0 \
|
||||
--hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \
|
||||
--hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf
|
||||
# via weasyprint
|
||||
cfgv==3.4.0 \
|
||||
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
|
||||
--hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
|
||||
cfgv==3.5.0 \
|
||||
--hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \
|
||||
--hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132
|
||||
# via pre-commit
|
||||
cssselect2==0.8.0 \
|
||||
--hash=sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e \
|
||||
@@ -109,259 +109,269 @@ distlib==0.4.0 \
|
||||
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
||||
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
||||
# via virtualenv
|
||||
filelock==3.20.0 \
|
||||
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
|
||||
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
|
||||
filelock==3.20.3 \
|
||||
--hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \
|
||||
--hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1
|
||||
# via virtualenv
|
||||
fonttools==4.60.1 \
|
||||
--hash=sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c \
|
||||
--hash=sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc \
|
||||
--hash=sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a \
|
||||
--hash=sha256:0eae96373e4b7c9e45d099d7a523444e3554360927225c1cdae221a58a45b856 \
|
||||
--hash=sha256:122e1a8ada290423c493491d002f622b1992b1ab0b488c68e31c413390dc7eb2 \
|
||||
--hash=sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259 \
|
||||
--hash=sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c \
|
||||
--hash=sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce \
|
||||
--hash=sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003 \
|
||||
--hash=sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272 \
|
||||
--hash=sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77 \
|
||||
--hash=sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038 \
|
||||
--hash=sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea \
|
||||
--hash=sha256:2ee06fc57512144d8b0445194c2da9f190f61ad51e230f14836286470c99f854 \
|
||||
--hash=sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2 \
|
||||
--hash=sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258 \
|
||||
--hash=sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652 \
|
||||
--hash=sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08 \
|
||||
--hash=sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99 \
|
||||
--hash=sha256:596ecaca36367027d525b3b426d8a8208169d09edcf8c7506aceb3a38bfb55c7 \
|
||||
--hash=sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914 \
|
||||
--hash=sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6 \
|
||||
--hash=sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed \
|
||||
--hash=sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb \
|
||||
--hash=sha256:7473a8ed9ed09aeaa191301244a5a9dbe46fe0bf54f9d6cd21d83044c3321217 \
|
||||
--hash=sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc \
|
||||
--hash=sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f \
|
||||
--hash=sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c \
|
||||
--hash=sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877 \
|
||||
--hash=sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801 \
|
||||
--hash=sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85 \
|
||||
--hash=sha256:8b4eb332f9501cb1cd3d4d099374a1e1306783ff95489a1026bde9eb02ccc34a \
|
||||
--hash=sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb \
|
||||
--hash=sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383 \
|
||||
--hash=sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401 \
|
||||
--hash=sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28 \
|
||||
--hash=sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01 \
|
||||
--hash=sha256:a140761c4ff63d0cb9256ac752f230460ee225ccef4ad8f68affc723c88e2036 \
|
||||
--hash=sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc \
|
||||
--hash=sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac \
|
||||
--hash=sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903 \
|
||||
--hash=sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3 \
|
||||
--hash=sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6 \
|
||||
--hash=sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c \
|
||||
--hash=sha256:b42d86938e8dda1cd9a1a87a6d82f1818eaf933348429653559a458d027446da \
|
||||
--hash=sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299 \
|
||||
--hash=sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15 \
|
||||
--hash=sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199 \
|
||||
--hash=sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf \
|
||||
--hash=sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1 \
|
||||
--hash=sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537 \
|
||||
--hash=sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d \
|
||||
--hash=sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c \
|
||||
--hash=sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4 \
|
||||
--hash=sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9 \
|
||||
--hash=sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed \
|
||||
--hash=sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987 \
|
||||
--hash=sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa
|
||||
fonttools==4.61.1 \
|
||||
--hash=sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87 \
|
||||
--hash=sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796 \
|
||||
--hash=sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75 \
|
||||
--hash=sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d \
|
||||
--hash=sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371 \
|
||||
--hash=sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b \
|
||||
--hash=sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b \
|
||||
--hash=sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2 \
|
||||
--hash=sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3 \
|
||||
--hash=sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9 \
|
||||
--hash=sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd \
|
||||
--hash=sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c \
|
||||
--hash=sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c \
|
||||
--hash=sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56 \
|
||||
--hash=sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37 \
|
||||
--hash=sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0 \
|
||||
--hash=sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958 \
|
||||
--hash=sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5 \
|
||||
--hash=sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118 \
|
||||
--hash=sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69 \
|
||||
--hash=sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9 \
|
||||
--hash=sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261 \
|
||||
--hash=sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb \
|
||||
--hash=sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47 \
|
||||
--hash=sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24 \
|
||||
--hash=sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c \
|
||||
--hash=sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba \
|
||||
--hash=sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c \
|
||||
--hash=sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91 \
|
||||
--hash=sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1 \
|
||||
--hash=sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19 \
|
||||
--hash=sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6 \
|
||||
--hash=sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5 \
|
||||
--hash=sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2 \
|
||||
--hash=sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d \
|
||||
--hash=sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881 \
|
||||
--hash=sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063 \
|
||||
--hash=sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7 \
|
||||
--hash=sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09 \
|
||||
--hash=sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da \
|
||||
--hash=sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e \
|
||||
--hash=sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e \
|
||||
--hash=sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8 \
|
||||
--hash=sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa \
|
||||
--hash=sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6 \
|
||||
--hash=sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e \
|
||||
--hash=sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a \
|
||||
--hash=sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c \
|
||||
--hash=sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7 \
|
||||
--hash=sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd
|
||||
# via weasyprint
|
||||
identify==2.6.15 \
|
||||
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
|
||||
--hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf
|
||||
identify==2.6.16 \
|
||||
--hash=sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0 \
|
||||
--hash=sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980
|
||||
# via pre-commit
|
||||
nodeenv==1.9.1 \
|
||||
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
||||
nodeenv==1.10.0 \
|
||||
--hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \
|
||||
--hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb
|
||||
# via pre-commit
|
||||
numpy==2.2.6 \
|
||||
--hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \
|
||||
--hash=sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47 \
|
||||
--hash=sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84 \
|
||||
--hash=sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d \
|
||||
--hash=sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6 \
|
||||
--hash=sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f \
|
||||
--hash=sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b \
|
||||
--hash=sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49 \
|
||||
--hash=sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163 \
|
||||
--hash=sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571 \
|
||||
--hash=sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42 \
|
||||
--hash=sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff \
|
||||
--hash=sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491 \
|
||||
--hash=sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4 \
|
||||
--hash=sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566 \
|
||||
--hash=sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf \
|
||||
--hash=sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40 \
|
||||
--hash=sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd \
|
||||
--hash=sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06 \
|
||||
--hash=sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282 \
|
||||
--hash=sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680 \
|
||||
--hash=sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db \
|
||||
--hash=sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3 \
|
||||
--hash=sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90 \
|
||||
--hash=sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1 \
|
||||
--hash=sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289 \
|
||||
--hash=sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab \
|
||||
--hash=sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c \
|
||||
--hash=sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d \
|
||||
--hash=sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb \
|
||||
--hash=sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d \
|
||||
--hash=sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a \
|
||||
--hash=sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf \
|
||||
--hash=sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1 \
|
||||
--hash=sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2 \
|
||||
--hash=sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a \
|
||||
--hash=sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543 \
|
||||
--hash=sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00 \
|
||||
--hash=sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c \
|
||||
--hash=sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f \
|
||||
--hash=sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd \
|
||||
--hash=sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868 \
|
||||
--hash=sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303 \
|
||||
--hash=sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83 \
|
||||
--hash=sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3 \
|
||||
--hash=sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d \
|
||||
--hash=sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87 \
|
||||
--hash=sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa \
|
||||
--hash=sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f \
|
||||
--hash=sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae \
|
||||
--hash=sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda \
|
||||
--hash=sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 \
|
||||
--hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \
|
||||
--hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \
|
||||
--hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8
|
||||
numpy==2.4.1 \
|
||||
--hash=sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c \
|
||||
--hash=sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba \
|
||||
--hash=sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5 \
|
||||
--hash=sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8 \
|
||||
--hash=sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0 \
|
||||
--hash=sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d \
|
||||
--hash=sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574 \
|
||||
--hash=sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696 \
|
||||
--hash=sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5 \
|
||||
--hash=sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505 \
|
||||
--hash=sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0 \
|
||||
--hash=sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162 \
|
||||
--hash=sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844 \
|
||||
--hash=sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205 \
|
||||
--hash=sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4 \
|
||||
--hash=sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc \
|
||||
--hash=sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d \
|
||||
--hash=sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93 \
|
||||
--hash=sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01 \
|
||||
--hash=sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c \
|
||||
--hash=sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f \
|
||||
--hash=sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33 \
|
||||
--hash=sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82 \
|
||||
--hash=sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2 \
|
||||
--hash=sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42 \
|
||||
--hash=sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509 \
|
||||
--hash=sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a \
|
||||
--hash=sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e \
|
||||
--hash=sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556 \
|
||||
--hash=sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a \
|
||||
--hash=sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510 \
|
||||
--hash=sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295 \
|
||||
--hash=sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73 \
|
||||
--hash=sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3 \
|
||||
--hash=sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9 \
|
||||
--hash=sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8 \
|
||||
--hash=sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745 \
|
||||
--hash=sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2 \
|
||||
--hash=sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02 \
|
||||
--hash=sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d \
|
||||
--hash=sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344 \
|
||||
--hash=sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f \
|
||||
--hash=sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be \
|
||||
--hash=sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425 \
|
||||
--hash=sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1 \
|
||||
--hash=sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2 \
|
||||
--hash=sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2 \
|
||||
--hash=sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb \
|
||||
--hash=sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9 \
|
||||
--hash=sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15 \
|
||||
--hash=sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690 \
|
||||
--hash=sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0 \
|
||||
--hash=sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261 \
|
||||
--hash=sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a \
|
||||
--hash=sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc \
|
||||
--hash=sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f \
|
||||
--hash=sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5 \
|
||||
--hash=sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df \
|
||||
--hash=sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9 \
|
||||
--hash=sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2 \
|
||||
--hash=sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8 \
|
||||
--hash=sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426 \
|
||||
--hash=sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b \
|
||||
--hash=sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87 \
|
||||
--hash=sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220 \
|
||||
--hash=sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b \
|
||||
--hash=sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3 \
|
||||
--hash=sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e \
|
||||
--hash=sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501 \
|
||||
--hash=sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee \
|
||||
--hash=sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7 \
|
||||
--hash=sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c
|
||||
# via opencv-python-headless
|
||||
opencv-python-headless==4.12.0.88 \
|
||||
--hash=sha256:1e58d664809b3350c1123484dd441e1667cd7bed3086db1b9ea1b6f6cb20b50e \
|
||||
--hash=sha256:236c8df54a90f4d02076e6f9c1cc763d794542e886c576a6fee46ec8ff75a7a9 \
|
||||
--hash=sha256:365bb2e486b50feffc2d07a405b953a8f3e8eaa63865bc650034e5c71e7a5154 \
|
||||
--hash=sha256:86b413bdd6c6bf497832e346cd5371995de148e579b9774f8eba686dee3f5528 \
|
||||
--hash=sha256:aeb4b13ecb8b4a0beb2668ea07928160ea7c2cd2d9b5ef571bbee6bafe9cc8d0 \
|
||||
--hash=sha256:cfdc017ddf2e59b6c2f53bc12d74b6b0be7ded4ec59083ea70763921af2b6c09 \
|
||||
--hash=sha256:fde2cf5c51e4def5f2132d78e0c08f9c14783cd67356922182c6845b9af87dbd
|
||||
opencv-python-headless==4.13.0.90 \
|
||||
--hash=sha256:0e0c8c9f620802fddc4fa7f471a1d263c7b0dca16cd9e7e2f996bb8bd2128c0c \
|
||||
--hash=sha256:12a28674f215542c9bf93338de1b5bffd76996d32da9acb9e739fdb9c8bbd738 \
|
||||
--hash=sha256:32255203040dc98803be96362e13f9e4bce20146898222d2e5c242f80de50da5 \
|
||||
--hash=sha256:96060fc57a1abb1144b0b8129e2ff3bfcdd0ccd8e8bd05bd85256ff4ed587d3b \
|
||||
--hash=sha256:dbc1f4625e5af3a80ebdbd84380227c0f445228588f2521b11af47710caca1ba \
|
||||
--hash=sha256:e13790342591557050157713af17a7435ac1b50c65282715093c9297fa045d8f \
|
||||
--hash=sha256:eba38bc255d0b7d1969c5bcc90a060ca2b61a3403b613872c750bfa5dfe9e03b \
|
||||
--hash=sha256:f46b17ea0aa7e4124ca6ad71143f89233ae9557f61d2326bcdb34329a1ddf9bd
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
pdf2image==1.17.0 \
|
||||
--hash=sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57 \
|
||||
--hash=sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
pillow==12.0.0 \
|
||||
--hash=sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643 \
|
||||
--hash=sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e \
|
||||
--hash=sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e \
|
||||
--hash=sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc \
|
||||
--hash=sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642 \
|
||||
--hash=sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6 \
|
||||
--hash=sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1 \
|
||||
--hash=sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b \
|
||||
--hash=sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399 \
|
||||
--hash=sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba \
|
||||
--hash=sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad \
|
||||
--hash=sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47 \
|
||||
--hash=sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739 \
|
||||
--hash=sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b \
|
||||
--hash=sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f \
|
||||
--hash=sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10 \
|
||||
--hash=sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52 \
|
||||
--hash=sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d \
|
||||
--hash=sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b \
|
||||
--hash=sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a \
|
||||
--hash=sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9 \
|
||||
--hash=sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d \
|
||||
--hash=sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098 \
|
||||
--hash=sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905 \
|
||||
--hash=sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b \
|
||||
--hash=sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3 \
|
||||
--hash=sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371 \
|
||||
--hash=sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953 \
|
||||
--hash=sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01 \
|
||||
--hash=sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca \
|
||||
--hash=sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e \
|
||||
--hash=sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7 \
|
||||
--hash=sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27 \
|
||||
--hash=sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082 \
|
||||
--hash=sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e \
|
||||
--hash=sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d \
|
||||
--hash=sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8 \
|
||||
--hash=sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a \
|
||||
--hash=sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad \
|
||||
--hash=sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3 \
|
||||
--hash=sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a \
|
||||
--hash=sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d \
|
||||
--hash=sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353 \
|
||||
--hash=sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee \
|
||||
--hash=sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b \
|
||||
--hash=sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b \
|
||||
--hash=sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a \
|
||||
--hash=sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7 \
|
||||
--hash=sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef \
|
||||
--hash=sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a \
|
||||
--hash=sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a \
|
||||
--hash=sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257 \
|
||||
--hash=sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07 \
|
||||
--hash=sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4 \
|
||||
--hash=sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c \
|
||||
--hash=sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c \
|
||||
--hash=sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4 \
|
||||
--hash=sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe \
|
||||
--hash=sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8 \
|
||||
--hash=sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5 \
|
||||
--hash=sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6 \
|
||||
--hash=sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e \
|
||||
--hash=sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8 \
|
||||
--hash=sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e \
|
||||
--hash=sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275 \
|
||||
--hash=sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3 \
|
||||
--hash=sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76 \
|
||||
--hash=sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227 \
|
||||
--hash=sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9 \
|
||||
--hash=sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5 \
|
||||
--hash=sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79 \
|
||||
--hash=sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca \
|
||||
--hash=sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa \
|
||||
--hash=sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b \
|
||||
--hash=sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e \
|
||||
--hash=sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197 \
|
||||
--hash=sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab \
|
||||
--hash=sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79 \
|
||||
--hash=sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2 \
|
||||
--hash=sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363 \
|
||||
--hash=sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0 \
|
||||
--hash=sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e \
|
||||
--hash=sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782 \
|
||||
--hash=sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925 \
|
||||
--hash=sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0 \
|
||||
--hash=sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b \
|
||||
--hash=sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced \
|
||||
--hash=sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c \
|
||||
--hash=sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344 \
|
||||
--hash=sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9 \
|
||||
--hash=sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1
|
||||
pillow==12.1.0 \
|
||||
--hash=sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d \
|
||||
--hash=sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc \
|
||||
--hash=sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84 \
|
||||
--hash=sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de \
|
||||
--hash=sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0 \
|
||||
--hash=sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef \
|
||||
--hash=sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4 \
|
||||
--hash=sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82 \
|
||||
--hash=sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9 \
|
||||
--hash=sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030 \
|
||||
--hash=sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0 \
|
||||
--hash=sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18 \
|
||||
--hash=sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a \
|
||||
--hash=sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef \
|
||||
--hash=sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b \
|
||||
--hash=sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6 \
|
||||
--hash=sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179 \
|
||||
--hash=sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e \
|
||||
--hash=sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72 \
|
||||
--hash=sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64 \
|
||||
--hash=sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451 \
|
||||
--hash=sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd \
|
||||
--hash=sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924 \
|
||||
--hash=sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616 \
|
||||
--hash=sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a \
|
||||
--hash=sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94 \
|
||||
--hash=sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc \
|
||||
--hash=sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8 \
|
||||
--hash=sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9 \
|
||||
--hash=sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91 \
|
||||
--hash=sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a \
|
||||
--hash=sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c \
|
||||
--hash=sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670 \
|
||||
--hash=sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea \
|
||||
--hash=sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91 \
|
||||
--hash=sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c \
|
||||
--hash=sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc \
|
||||
--hash=sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0 \
|
||||
--hash=sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b \
|
||||
--hash=sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65 \
|
||||
--hash=sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661 \
|
||||
--hash=sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19 \
|
||||
--hash=sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1 \
|
||||
--hash=sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0 \
|
||||
--hash=sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e \
|
||||
--hash=sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75 \
|
||||
--hash=sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4 \
|
||||
--hash=sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8 \
|
||||
--hash=sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd \
|
||||
--hash=sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7 \
|
||||
--hash=sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61 \
|
||||
--hash=sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51 \
|
||||
--hash=sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551 \
|
||||
--hash=sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45 \
|
||||
--hash=sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1 \
|
||||
--hash=sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644 \
|
||||
--hash=sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796 \
|
||||
--hash=sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587 \
|
||||
--hash=sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304 \
|
||||
--hash=sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b \
|
||||
--hash=sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8 \
|
||||
--hash=sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17 \
|
||||
--hash=sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171 \
|
||||
--hash=sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3 \
|
||||
--hash=sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7 \
|
||||
--hash=sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988 \
|
||||
--hash=sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a \
|
||||
--hash=sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0 \
|
||||
--hash=sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c \
|
||||
--hash=sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2 \
|
||||
--hash=sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14 \
|
||||
--hash=sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5 \
|
||||
--hash=sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a \
|
||||
--hash=sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377 \
|
||||
--hash=sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0 \
|
||||
--hash=sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5 \
|
||||
--hash=sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b \
|
||||
--hash=sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d \
|
||||
--hash=sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac \
|
||||
--hash=sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c \
|
||||
--hash=sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554 \
|
||||
--hash=sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643 \
|
||||
--hash=sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13 \
|
||||
--hash=sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09 \
|
||||
--hash=sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208 \
|
||||
--hash=sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda \
|
||||
--hash=sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea \
|
||||
--hash=sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e \
|
||||
--hash=sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0 \
|
||||
--hash=sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831 \
|
||||
--hash=sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd
|
||||
# via
|
||||
# -r .github/scripts/requirements_dev.in
|
||||
# pdf2image
|
||||
# weasyprint
|
||||
platformdirs==4.5.0 \
|
||||
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
|
||||
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
|
||||
platformdirs==4.5.1 \
|
||||
--hash=sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda \
|
||||
--hash=sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31
|
||||
# via virtualenv
|
||||
pre-commit==4.3.0 \
|
||||
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
|
||||
--hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16
|
||||
pre-commit==4.5.1 \
|
||||
--hash=sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77 \
|
||||
--hash=sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
pycparser==2.23 \
|
||||
--hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \
|
||||
--hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934
|
||||
pycparser==3.0 \
|
||||
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
|
||||
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
|
||||
# via cffi
|
||||
pydyf==0.11.0 \
|
||||
--hash=sha256:0aaf9e2ebbe786ec7a78ec3fbffa4cdcecde53fd6f563221d53c6bc1328848a3 \
|
||||
--hash=sha256:394dddf619cca9d0c55715e3c55ea121a9bf9cbc780cdc1201a2427917b86b64
|
||||
pydyf==0.12.1 \
|
||||
--hash=sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc \
|
||||
--hash=sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095
|
||||
# via weasyprint
|
||||
pyphen==0.17.2 \
|
||||
--hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \
|
||||
@@ -442,9 +452,9 @@ pyyaml==6.0.3 \
|
||||
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
|
||||
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
||||
# via pre-commit
|
||||
tinycss2==1.4.0 \
|
||||
--hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \
|
||||
--hash=sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289
|
||||
tinycss2==1.5.1 \
|
||||
--hash=sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661 \
|
||||
--hash=sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957
|
||||
# via
|
||||
# cssselect2
|
||||
# weasyprint
|
||||
@@ -452,17 +462,17 @@ tinyhtml5==2.0.0 \
|
||||
--hash=sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc \
|
||||
--hash=sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e
|
||||
# via weasyprint
|
||||
unoserver==3.4 \
|
||||
--hash=sha256:3dcf2204013def1d1ddd3671f38b11346bdf349fef9728277462666a8a634419 \
|
||||
--hash=sha256:64c24d33d4f65d680a2d9f676518cb28e7fd6c1f9d9a745c33e4a4cb59afdfcd
|
||||
unoserver==3.6 \
|
||||
--hash=sha256:25c360fa194396a89cb79b4edd2735f8e4f0fd8531e59db3952114585bd7df05 \
|
||||
--hash=sha256:e446bcb3638c51880f002aaeecab1cf74dfa9df81035f027f7ff2e081b6d7015
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
virtualenv==20.35.4 \
|
||||
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
|
||||
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
|
||||
virtualenv==20.36.1 \
|
||||
--hash=sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f \
|
||||
--hash=sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba
|
||||
# via pre-commit
|
||||
weasyprint==66.0 \
|
||||
--hash=sha256:82b0783b726fcd318e2c977dcdddca76515b30044bc7a830cc4fbe717582a6d0 \
|
||||
--hash=sha256:da71dc87dc129ac9cffdc65e5477e90365ab9dbae45c744014ec1d06303dde40
|
||||
weasyprint==68.0 \
|
||||
--hash=sha256:447f40898b747cb44ac31a5d493d512e7441fd56e13f63744c099383bbf9cda9 \
|
||||
--hash=sha256:c2cb40c71b50837c5971f00171c9e4078e8c9912dd7c217f3e90e068f11e8aa1
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
webencodings==0.5.1 \
|
||||
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
|
||||
@@ -471,86 +481,27 @@ webencodings==0.5.1 \
|
||||
# cssselect2
|
||||
# tinycss2
|
||||
# tinyhtml5
|
||||
zopfli==0.2.3.post1 \
|
||||
--hash=sha256:0aa5f90d6298bda02a95bc8dc8c3c19004d5a4e44bda00b67ca7431d857b4b54 \
|
||||
--hash=sha256:0cc20b02a9531559945324c38302fd4ba763311632d0ec8a1a0aa9c10ea363e6 \
|
||||
--hash=sha256:1d8cc06605519e82b16df090e17cb3990d1158861b2872c3117f1168777b81e4 \
|
||||
--hash=sha256:1f990634fd5c5c8ced8edddd8bd45fab565123b4194d6841e01811292650acae \
|
||||
--hash=sha256:2345e713260a350bea0b01a816a469ea356bc2d63d009a0d777691ecbbcf7493 \
|
||||
--hash=sha256:2768c877f76c8a0e7519b1c86c93757f3c01492ddde55751e9988afb7eff64e1 \
|
||||
--hash=sha256:29ea74e72ffa6e291b8c6f2504ce6c146b4fe990c724c1450eb8e4c27fd31431 \
|
||||
--hash=sha256:34a99592f3d9eb6f737616b5bd74b48a589fdb3cb59a01a50d636ea81d6af272 \
|
||||
--hash=sha256:3654bfc927bc478b1c3f3ff5056ed7b20a1a37fa108ca503256d0a699c03bbb1 \
|
||||
--hash=sha256:3657e416ffb8f31d9d3424af12122bb251befae109f2e271d87d825c92fc5b7b \
|
||||
--hash=sha256:37d011e92f7b9622742c905fdbed9920a1d0361df84142807ea2a528419dea7f \
|
||||
--hash=sha256:3827170de28faf144992d3d4dcf8f3998fe3c8a6a6f4a08f1d42c2ec6119d2bb \
|
||||
--hash=sha256:39e576f93576c5c223b41d9c780bbb91fd6db4babf3223d2a4fe7bf568e2b5a8 \
|
||||
--hash=sha256:3a89277ed5f8c0fb2d0b46d669aa0633123aa7381f1f6118c12f15e0fb48f8ca \
|
||||
--hash=sha256:3c163911f8bad94b3e1db0a572e7c28ba681a0c91d0002ea1e4fa9264c21ef17 \
|
||||
--hash=sha256:3f0197b6aa6eb3086ae9e66d6dd86c4d502b6c68b0ec490496348ae8c05ecaef \
|
||||
--hash=sha256:48dba9251060289101343110ab47c0756f66f809bb4d1ddbb6d5c7e7752115c5 \
|
||||
--hash=sha256:4915a41375bdee4db749ecd07d985a0486eb688a6619f713b7bf6fbfd145e960 \
|
||||
--hash=sha256:4c1226a7e2c7105ac31503a9bb97454743f55d88164d6d46bc138051b77f609b \
|
||||
--hash=sha256:4e50ffac74842c1c1018b9b73875a0d0a877c066ab06bf7cccbaa84af97e754f \
|
||||
--hash=sha256:518f1f4ed35dd69ce06b552f84e6d081f07c552b4c661c5312d950a0b764a58a \
|
||||
--hash=sha256:5aad740b4d4fcbaaae4887823925166ffd062db3b248b3f432198fc287381d1a \
|
||||
--hash=sha256:5f272186e03ad55e7af09ab78055535c201b1a0bcc2944edb1768298d9c483a4 \
|
||||
--hash=sha256:5fcfc0dc2761e4fcc15ad5d273b4d58c2e8e059d3214a7390d4d3c8e2aee644e \
|
||||
--hash=sha256:60db20f06c3d4c5934b16cfa62a2cc5c3f0686bffe0071ed7804d3c31ab1a04e \
|
||||
--hash=sha256:615a8ac9dda265e9cc38b2a76c3142e4a9f30fea4a79c85f670850783bc6feb4 \
|
||||
--hash=sha256:6482db9876c68faac2d20a96b566ffbf65ddaadd97b222e4e73641f4f8722fc4 \
|
||||
--hash=sha256:6617fb10f9e4393b331941861d73afb119cd847e88e4974bdbe8068ceef3f73f \
|
||||
--hash=sha256:676919fba7311125244eb0c4393679ac5fe856e5864a15d122bd815205369fa0 \
|
||||
--hash=sha256:6c2d2bc8129707e34c51f9352c4636ca313b52350bbb7e04637c46c1818a2a70 \
|
||||
--hash=sha256:71390dbd3fbf6ebea9a5d85ffed8c26ee1453ee09248e9b88486e30e0397b775 \
|
||||
--hash=sha256:716cdbfc57bfd3d3e31a58e6246e8190e6849b7dbb7c4ce39ef8bbf0edb8f6d5 \
|
||||
--hash=sha256:75a26a2307b10745a83b660c404416e984ee6fca515ec7f0765f69af3ce08072 \
|
||||
--hash=sha256:7be5cc6732eb7b4df17305d8a7b293223f934a31783a874a01164703bc1be6cd \
|
||||
--hash=sha256:7cce242b5df12b2b172489daf19c32e5577dd2fac659eb4b17f6a6efb446fd5c \
|
||||
--hash=sha256:81c341d9bb87a6dbbb0d45d6e272aca80c7c97b4b210f9b6e233bf8b87242f29 \
|
||||
--hash=sha256:89899641d4de97dbad8e0cde690040d078b6aea04066dacaab98e0b5a23573f2 \
|
||||
--hash=sha256:8d5ab297d660b75c159190ce6d73035502310e40fd35170aed7d1a1aea7ddd65 \
|
||||
--hash=sha256:8fbe5bcf10d01aab3513550f284c09fef32f342b36f56bfae2120a9c4d12c130 \
|
||||
--hash=sha256:91a2327a4d7e77471fa4fbb26991c6de4a738c6fc6a33e09bb25f56a870a4b7b \
|
||||
--hash=sha256:95a260cafd56b8fffa679918937401c80bb38e1681c448b988022e4c3610965d \
|
||||
--hash=sha256:96484dc0f48be1c5d7ae9f38ed1ce41e3675fd506b27c11a6607f14b49101e99 \
|
||||
--hash=sha256:9a6aec38a989bad7ddd1ef53f1265699e49e294d08231b5313d61293f3cd6237 \
|
||||
--hash=sha256:9ba214f4f45bec195ee8559651154d3ac2932470b9d91c5715fc29c013349f8c \
|
||||
--hash=sha256:9f4a7ec2770e6af05f5a02733fd3900f30a9cd58e5d6d3727e14c5bcd6e7d587 \
|
||||
--hash=sha256:a1cf720896d2ce998bc8e051d4b4ce0d8bec007aab6243102e8e1d22a0b2fb3f \
|
||||
--hash=sha256:a241a68581d34d67b40c425cce3d1fd211c092f99d9250947824ccba9f491949 \
|
||||
--hash=sha256:a53b18797cdef27e019db595d66c4b077325afe2fd62145953275f53d84ce40c \
|
||||
--hash=sha256:a82fc2dbebe6eb908b9c665e71496f8525c1bc4d2e3a7a7722ef2b128b6227c8 \
|
||||
--hash=sha256:a86eb88e06bd87e1fff31dac878965c26b0c26db59ddcf78bb0379a954b120de \
|
||||
--hash=sha256:aa588b21044f8a74e423d8c8a4c7fc9988501878aacced793467010039c50734 \
|
||||
--hash=sha256:b05296e8bc88c92e2b21e0a9bae4740c1551ee613c1d93a51fd28a7a0b2b6fbb \
|
||||
--hash=sha256:b0ec13f352ea5ae0fc91f98a48540512eed0767d0ec4f7f3cb92d92797983d18 \
|
||||
--hash=sha256:b3df42f52502438ee973042cc551877d24619fa1cd38ef7b7e9ac74200daca8b \
|
||||
--hash=sha256:b78008a69300d929ca2efeffec951b64a312e9a811e265ea4a907ab546d79fa6 \
|
||||
--hash=sha256:b9026a21b6d41eb0e2e63f5bc1242c3fcc43ecb770963cda99a4307863dac12e \
|
||||
--hash=sha256:bbe429fc50686bb2a2608a30843e36fbaa123462a5284f136c7d9e0145220bfd \
|
||||
--hash=sha256:bfa1eb759e07d8b7aa7a310a2bc535e127ee70addf90dc8d4b946b593c3e51a8 \
|
||||
--hash=sha256:c1e0ed5d84ffa2d677cc9582fc01e61dab2e7ef8b8996e055f0a76167b1b94df \
|
||||
--hash=sha256:c4278d1873ce6e803e5d4f8d702fd3026bd67fca744aa98881324d1157ddf748 \
|
||||
--hash=sha256:cac2b37ab21c2b36a10b685b1893ebd6b0f83ae26004838ac817680881576567 \
|
||||
--hash=sha256:cbe6df25807227519debd1a57ab236f5f6bad441500e85b13903e51f93a43214 \
|
||||
--hash=sha256:cd2c002f160502608dcc822ed2441a0f4509c52e86fcfd1a09e937278ed1ca14 \
|
||||
--hash=sha256:e0137dd64a493ba6a4be37405cfd6febe650a98cc1e9dca8f6b8c63b1db11b41 \
|
||||
--hash=sha256:e63d558847166543c2c9789e6f985400a520b7eacc4b99181668b2c3aeadd352 \
|
||||
--hash=sha256:eb45a34f23da4f8bc712b6376ca5396914b0b7c09adbb001dad964eb7f3132f8 \
|
||||
--hash=sha256:ecb7572df5372abce8073df078207d9d1749f20b8b136089916a4a0868d56051 \
|
||||
--hash=sha256:f12000a6accdd4bf0a3fa6eaa1b1c7a7bc80af0a2edf3f89d770d3dcce1d0e22 \
|
||||
--hash=sha256:f7d69c1a7168ad0e9cb864e8663acb232986a0c9c9cb9801f56bf6214f53a54d \
|
||||
--hash=sha256:f815fcc2b2a457977724bad97fb4854022980f51ce7b136925e336b530545ae1 \
|
||||
--hash=sha256:fc39f5c27f962ec8660d8d20c24762431131b5d8c672b44b0a54cf2b5bcde9b9
|
||||
zopfli==0.4.0 \
|
||||
--hash=sha256:03181d48e719fcb6cf8340189c61e8f9883d8bbbdf76bf5212a74457f7d083c1 \
|
||||
--hash=sha256:18b5f1570f64d4988482e4466f10ef5f2a30f687c19ad62a64560f2152dc89eb \
|
||||
--hash=sha256:25e4863b8dc30e5d5309f87c106b0b7d3da4ed0e340b8a52b36d4471e797589f \
|
||||
--hash=sha256:7d66337be6d5613dec55213e9ac28f378c41e2cc04fbad4a10748e4df774ca85 \
|
||||
--hash=sha256:9097e8e1dfdb7f5aea5464e469946857e80502b6d29ba1b232450916bd4a74d1 \
|
||||
--hash=sha256:a8ee992b2549e090cd3f0178bf606dd41a29e0613a04cdf5054224662c72dce6 \
|
||||
--hash=sha256:b72a010d205d00b2855acc2302772067362f9ab5a012e3550662aec60d28e6b3 \
|
||||
--hash=sha256:b8bdb41fbfdc4738b7bdc09ed7c1e951579fae192391a5e694d59bb186cdbec7 \
|
||||
--hash=sha256:c3ba02a9a6ca90481d2b2f68bab038b310d63a1e3b5ae305e95a6599787ed941 \
|
||||
--hash=sha256:d1b98ad47c434ef213444a03ef2f826eeec100144d64f6a57504b9893d3931ce \
|
||||
--hash=sha256:f67d04280065e24cb9a4174cb6b3d1f763687f8cb2963aa135ad8f57c6995f5a \
|
||||
--hash=sha256:f94e4dd7d76b4fe9f5d9229372be20d7f786164eea5152d1af1c34298c3d5975
|
||||
# via fonttools
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==25.3 \
|
||||
--hash=sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343 \
|
||||
--hash=sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd
|
||||
pip==26.0 \
|
||||
--hash=sha256:3ce220a0a17915972fbf1ab451baae1521c4539e778b28127efa79b974aff0fa \
|
||||
--hash=sha256:98436feffb9e31bc9339cf369fd55d3331b1580b6a6f1173bacacddcf9c34754
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
setuptools==80.9.0 \
|
||||
--hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \
|
||||
--hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c
|
||||
setuptools==80.10.2 \
|
||||
--hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \
|
||||
--hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
|
||||
@@ -12,25 +12,25 @@ distlib==0.4.0 \
|
||||
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
||||
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
||||
# via virtualenv
|
||||
filelock==3.20.0 \
|
||||
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
|
||||
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
|
||||
filelock==3.20.3 \
|
||||
--hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \
|
||||
--hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1
|
||||
# via virtualenv
|
||||
identify==2.6.15 \
|
||||
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
|
||||
--hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf
|
||||
identify==2.6.16 \
|
||||
--hash=sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0 \
|
||||
--hash=sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980
|
||||
# via pre-commit
|
||||
nodeenv==1.9.1 \
|
||||
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
||||
nodeenv==1.10.0 \
|
||||
--hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \
|
||||
--hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb
|
||||
# via pre-commit
|
||||
platformdirs==4.5.0 \
|
||||
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
|
||||
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
|
||||
platformdirs==4.5.1 \
|
||||
--hash=sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda \
|
||||
--hash=sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31
|
||||
# via virtualenv
|
||||
pre-commit==4.5.0 \
|
||||
--hash=sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1 \
|
||||
--hash=sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b
|
||||
pre-commit==4.5.1 \
|
||||
--hash=sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77 \
|
||||
--hash=sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61
|
||||
# via -r .github/scripts/requirements_pre_commit.in
|
||||
pyyaml==6.0.3 \
|
||||
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
|
||||
@@ -107,7 +107,7 @@ pyyaml==6.0.3 \
|
||||
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
|
||||
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
||||
# via pre-commit
|
||||
virtualenv==20.35.4 \
|
||||
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
|
||||
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
|
||||
virtualenv==20.36.1 \
|
||||
--hash=sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f \
|
||||
--hash=sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba
|
||||
# via pre-commit
|
||||
|
||||
@@ -8,7 +8,7 @@ tomli-w==1.2.0 \
|
||||
--hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \
|
||||
--hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021
|
||||
# via -r .github/scripts/requirements_sync_readme.in
|
||||
tomlkit==0.13.3 \
|
||||
--hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \
|
||||
--hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0
|
||||
tomlkit==0.14.0 \
|
||||
--hash=sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680 \
|
||||
--hash=sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064
|
||||
# via -r .github/scripts/requirements_sync_readme.in
|
||||
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout main repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
repository: ${{ github.repository }}
|
||||
ref: main
|
||||
@@ -172,7 +172,7 @@ jobs:
|
||||
return newComment.id;
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
repository: ${{ needs.check-pr.outputs.pr_repository }}
|
||||
ref: ${{ needs.check-pr.outputs.pr_ref }}
|
||||
@@ -194,81 +194,44 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Get commit hashes for frontend and backend
|
||||
id: commit-hashes
|
||||
- name: Get commit hash for app
|
||||
id: commit-hash
|
||||
run: |
|
||||
# Get last commit that touched the frontend folder, docker/frontend, or docker/compose
|
||||
FRONTEND_HASH=$(git log -1 --format="%H" -- frontend/ docker/frontend/ docker/compose/ 2>/dev/null || echo "")
|
||||
if [ -z "$FRONTEND_HASH" ]; then
|
||||
FRONTEND_HASH="no-frontend-changes"
|
||||
# Get last commit that touched the application code
|
||||
APP_HASH=$(git log -1 --format="%H" -- . 2>/dev/null || echo "")
|
||||
if [ -z "$APP_HASH" ]; then
|
||||
APP_HASH="no-changes"
|
||||
fi
|
||||
|
||||
# Get last commit that touched backend code, docker/backend, or docker/compose
|
||||
BACKEND_HASH=$(git log -1 --format="%H" -- app/ docker/backend/ docker/compose/ 2>/dev/null || echo "")
|
||||
if [ -z "$BACKEND_HASH" ]; then
|
||||
BACKEND_HASH="no-backend-changes"
|
||||
fi
|
||||
echo "App hash: $APP_HASH"
|
||||
echo "app_hash=$APP_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "Frontend hash: $FRONTEND_HASH"
|
||||
echo "Backend hash: $BACKEND_HASH"
|
||||
|
||||
echo "frontend_hash=$FRONTEND_HASH" >> $GITHUB_OUTPUT
|
||||
echo "backend_hash=$BACKEND_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
# Short hashes for tags
|
||||
if [ "$FRONTEND_HASH" = "no-frontend-changes" ]; then
|
||||
echo "frontend_short=no-frontend" >> $GITHUB_OUTPUT
|
||||
# Short hash for tags
|
||||
if [ "$APP_HASH" = "no-changes" ]; then
|
||||
echo "app_short=no-changes" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "frontend_short=${FRONTEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
echo "app_short=${APP_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [ "$BACKEND_HASH" = "no-backend-changes" ]; then
|
||||
echo "backend_short=no-backend" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "backend_short=${BACKEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check if frontend image exists
|
||||
id: check-frontend
|
||||
- name: Check if image exists
|
||||
id: check-image
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} >/dev/null 2>&1; then
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Frontend image already exists, skipping build"
|
||||
echo "Image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Frontend image needs to be built"
|
||||
echo "Image needs to be built"
|
||||
fi
|
||||
|
||||
- name: Check if backend image exists
|
||||
id: check-backend
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Backend image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Backend image needs to be built"
|
||||
fi
|
||||
|
||||
- name: Build and push V2 frontend image
|
||||
if: steps.check-frontend.outputs.exists == 'false'
|
||||
- name: Build and push V2 image
|
||||
if: steps.check-image.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/frontend/Dockerfile
|
||||
file: ./docker/embedded/Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push V2 backend image
|
||||
if: steps.check-backend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/backend/Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }}
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
@@ -283,17 +246,16 @@ jobs:
|
||||
run: |
|
||||
# Use same port strategy as regular PRs - just the PR number
|
||||
V2_PORT=${{ needs.check-pr.outputs.pr_number }}
|
||||
BACKEND_PORT=$((V2_PORT + 10000)) # Backend on higher port to avoid conflicts
|
||||
|
||||
# Create docker-compose for V2 with separate frontend and backend
|
||||
# Create docker-compose for V2 with unified embedded image
|
||||
cat > docker-compose.yml << EOF
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf-v2-backend:
|
||||
container_name: stirling-pdf-v2-backend-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
stirling-pdf-v2:
|
||||
container_name: stirling-pdf-v2-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }}
|
||||
ports:
|
||||
- "${BACKEND_PORT}:8080" # Backend API port
|
||||
- "${V2_PORT}:8080"
|
||||
volumes:
|
||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/data:/usr/share/tessdata:rw
|
||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/config:/configs:rw
|
||||
@@ -305,7 +267,7 @@ jobs:
|
||||
SECURITY_INITIALLOGIN_PASSWORD: "${{ secrets.TEST_LOGIN_PASSWORD }}"
|
||||
SYSTEM_DEFAULTLOCALE: en-GB
|
||||
UI_APPNAME: "Stirling-PDF V2 PR#${{ needs.check-pr.outputs.pr_number }}"
|
||||
UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Frontend/Backend Split Architecture"
|
||||
UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Embedded Architecture"
|
||||
UI_APPNAMENAVBAR: "V2 PR#${{ needs.check-pr.outputs.pr_number }}"
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
@@ -313,17 +275,6 @@ jobs:
|
||||
SWAGGER_SERVER_URL: "https://${V2_PORT}.ssl.stirlingpdf.cloud"
|
||||
baseUrl: "https://${V2_PORT}.ssl.stirlingpdf.cloud"
|
||||
restart: on-failure:5
|
||||
|
||||
stirling-pdf-v2-frontend:
|
||||
container_name: stirling-pdf-v2-frontend-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
ports:
|
||||
- "${V2_PORT}:80" # Frontend port (same as regular PRs)
|
||||
environment:
|
||||
VITE_API_BASE_URL: "http://${{ secrets.NEW_VPS_HOST }}:${BACKEND_PORT}"
|
||||
depends_on:
|
||||
- stirling-pdf-v2-backend
|
||||
restart: on-failure:5
|
||||
EOF
|
||||
|
||||
# Deploy to VPS
|
||||
@@ -339,15 +290,15 @@ jobs:
|
||||
# Stop any existing container and clean up
|
||||
cd /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}
|
||||
docker-compose down --remove-orphans 2>/dev/null || true
|
||||
|
||||
|
||||
# Start the new container
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
|
||||
|
||||
# Clean up unused Docker resources to save space
|
||||
docker system prune -af --volumes || true
|
||||
|
||||
# Clean up old backend/frontend images (older than 2 weeks)
|
||||
# Clean up old images (older than 2 weeks)
|
||||
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
|
||||
ENDSSH
|
||||
|
||||
@@ -383,7 +334,7 @@ jobs:
|
||||
const httpsUrl = `https://${v2Port}.ssl.stirlingpdf.cloud`;
|
||||
|
||||
const commentBody = `## 🚀 V2 Auto-Deployment Complete!\n\n` +
|
||||
`Your V2 PR with the new frontend/backend split architecture has been deployed!\n\n` +
|
||||
`Your V2 PR with embedded architecture has been deployed!\n\n` +
|
||||
`🔗 **Direct Test URL (non-SSL)** [${deploymentUrl}](${deploymentUrl})\n\n` +
|
||||
`🔐 **Secure HTTPS URL**: [${httpsUrl}](${httpsUrl})\n\n` +
|
||||
`_This deployment will be automatically cleaned up when the PR is closed._\n\n` +
|
||||
@@ -411,7 +362,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
@@ -473,9 +424,8 @@ jobs:
|
||||
# Remove V2 PR-specific directories
|
||||
rm -rf /stirling/V2-PR-${{ github.event.pull_request.number }}
|
||||
|
||||
# Clean up V2 containers by name (in case compose cleanup missed them)
|
||||
docker rm -f stirling-pdf-v2-frontend-pr-${{ github.event.pull_request.number }} || true
|
||||
docker rm -f stirling-pdf-v2-backend-pr-${{ github.event.pull_request.number }} || true
|
||||
# Clean up V2 container by name (in case compose cleanup missed it)
|
||||
docker rm -f stirling-pdf-v2-pr-${{ github.event.pull_request.number }} || true
|
||||
|
||||
echo "V2 cleanup completed"
|
||||
else
|
||||
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
@@ -145,7 +145,7 @@ jobs:
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
@@ -153,9 +153,14 @@ jobs:
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "17"
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Run Gradle Command
|
||||
run: |
|
||||
if [ "${{ needs.check-comment.outputs.disable_security }}" == "true" ]; then
|
||||
@@ -163,8 +168,11 @@ jobs:
|
||||
else
|
||||
export DISABLE_ADDITIONAL_FEATURES=false
|
||||
fi
|
||||
./gradlew clean build
|
||||
./gradlew build
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
STIRLING_PDF_DESKTOP_UI: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
@@ -367,7 +375,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
|
||||
+52
-10
@@ -36,7 +36,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up JDK ${{ matrix.jdk-version }}
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
@@ -68,9 +68,17 @@ jobs:
|
||||
java-version: ${{ matrix.jdk-version }}
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Build with Gradle and spring security ${{ matrix.spring-security }}
|
||||
run: ./gradlew clean build -PnoSpotless
|
||||
run: ./gradlew build -PnoSpotless
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DISABLE_ADDITIONAL_FEATURES: ${{ matrix.spring-security }}
|
||||
|
||||
- name: Check Test Reports Exist
|
||||
@@ -127,7 +135,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
@@ -135,9 +143,17 @@ jobs:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Generate OpenAPI documentation
|
||||
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
|
||||
- name: Upload OpenAPI Documentation
|
||||
@@ -156,7 +172,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
@@ -191,7 +207,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
@@ -199,8 +215,17 @@ jobs:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: check the licenses for compatibility
|
||||
run: ./gradlew clean checkLicense
|
||||
run: ./gradlew checkLicense
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
|
||||
- name: FAILED - check the licenses for compatibility
|
||||
if: failure()
|
||||
@@ -236,7 +261,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
@@ -244,6 +269,11 @@ jobs:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
@@ -269,6 +299,10 @@ jobs:
|
||||
chmod +x ./testing/test.sh
|
||||
chmod +x ./testing/test_disabledEndpoints.sh
|
||||
./testing/test.sh
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
|
||||
test-build-docker-images:
|
||||
if: github.event_name == 'pull_request' && needs.files-changed.outputs.project == 'true'
|
||||
@@ -291,7 +325,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Free disk space on runner
|
||||
run: |
|
||||
@@ -306,9 +340,17 @@ jobs:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Build application
|
||||
run: ./gradlew clean build
|
||||
run: ./gradlew build
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
STIRLING_PDF_DESKTOP_UI: false
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout main branch first
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
|
||||
with:
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR head (default)
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
|
||||
- name: Checkout BASE branch (safe script)
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
path: base
|
||||
@@ -317,7 +317,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@@ -336,11 +336,19 @@ jobs:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Check licenses and generate report
|
||||
id: license-check
|
||||
run: |
|
||||
./gradlew clean checkLicense generateLicenseReport || echo "LICENSE_CHECK_FAILED=true" >> $GITHUB_ENV
|
||||
./gradlew checkLicense generateLicenseReport || echo "LICENSE_CHECK_FAILED=true" >> $GITHUB_ENV
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DISABLE_ADDITIONAL_FEATURES: false
|
||||
STIRLING_PDF_DESKTOP_UI: true
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Run Labeler
|
||||
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
@@ -50,7 +50,8 @@ jobs:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
@@ -62,6 +63,10 @@ jobs:
|
||||
VERSION=$(./gradlew printVersion --quiet | tail -1)
|
||||
echo "Extracted version: $VERSION"
|
||||
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
|
||||
- name: Determine build matrix
|
||||
id: set-matrix
|
||||
@@ -110,7 +115,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
@@ -118,21 +123,25 @@ jobs:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Setup Node.js
|
||||
if: matrix.variant.build_frontend == true
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Build JAR
|
||||
run: ./gradlew clean build ${{ matrix.variant.build_frontend && '-PbuildWithFrontend=true' || '' }} -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||
run: ./gradlew build ${{ matrix.variant.build_frontend && '-PbuildWithFrontend=true' || '' }} -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DISABLE_ADDITIONAL_FEATURES: ${{ matrix.variant.disable_security }}
|
||||
STIRLING_PDF_DESKTOP_UI: false
|
||||
|
||||
@@ -167,7 +176,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
@@ -176,14 +185,14 @@ jobs:
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
@@ -194,13 +203,18 @@ jobs:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Build Java backend with JLink
|
||||
working-directory: ./
|
||||
shell: bash
|
||||
run: |
|
||||
chmod +x ./gradlew
|
||||
echo "🔧 Building Stirling-PDF JAR..."
|
||||
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||
./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||
|
||||
# Find the built JAR
|
||||
STIRLING_JAR=$(ls app/core/build/libs/stirling-pdf-*.jar | head -n 1)
|
||||
@@ -264,6 +278,9 @@ jobs:
|
||||
RUNTIME_SIZE=$(du -sh ./frontend/src-tauri/runtime/jre | cut -f1)
|
||||
echo "📊 Custom JRE size: $RUNTIME_SIZE"
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
|
||||
- name: Install frontend dependencies
|
||||
@@ -274,7 +291,7 @@ jobs:
|
||||
- name: Setup DigiCert KeyLocker
|
||||
id: digicert-setup
|
||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') }}
|
||||
uses: digicert/ssm-code-signing@v1.1.0
|
||||
uses: digicert/ssm-code-signing@9476ceec3ea1c63298d4403b983e1ccf2556ff4c # v1.1.0
|
||||
env:
|
||||
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
|
||||
@@ -382,7 +399,7 @@ jobs:
|
||||
echo "Certificate imported successfully."
|
||||
|
||||
- name: Build Tauri app
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
uses: tauri-apps/tauri-action@51a9f1156b33df106d827c3a78f8f894946c5faa # v0.5.25
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
@@ -535,25 +552,25 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Download all Tauri artifacts
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
pattern: Stirling-PDF-*
|
||||
path: ./artifacts/tauri
|
||||
|
||||
- name: Download JAR artifact (default)
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: jar
|
||||
path: ./artifacts/jars
|
||||
|
||||
- name: Download JAR artifact (with login)
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: jar-with-login
|
||||
path: ./artifacts/jars
|
||||
|
||||
- name: Download JAR artifact (server only)
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: jar-server
|
||||
path: ./artifacts/jars
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -57,8 +57,17 @@ jobs:
|
||||
java-version: 21
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew clean build
|
||||
run: ./gradlew build
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
|
||||
- name: git add
|
||||
run: |
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
@@ -45,7 +45,8 @@ jobs:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
@@ -56,6 +57,16 @@ jobs:
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
|
||||
- name: Install cosign
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||
with:
|
||||
cosign-release: "v2.4.1"
|
||||
|
||||
- name: Install cosign
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
@@ -39,7 +39,10 @@ jobs:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Generate Swagger documentation
|
||||
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||
@@ -47,12 +50,19 @@ jobs:
|
||||
- name: Upload Swagger Documentation to SwaggerHub
|
||||
run: ./gradlew swaggerhubUpload
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }}
|
||||
SWAGGERHUB_USER: "Frooodle"
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
|
||||
- name: Set API version as published and default on SwaggerHub
|
||||
run: |
|
||||
|
||||
@@ -7,6 +7,9 @@ on:
|
||||
- main
|
||||
paths:
|
||||
- "build.gradle"
|
||||
- "app/common/build.gradle"
|
||||
- "app/core/build.gradle"
|
||||
- "app/proprietary/build.gradle"
|
||||
- "README.md"
|
||||
- "frontend/public/locales/*/translation.toml"
|
||||
- "app/core/src/main/resources/static/3rdPartyLicenses.json"
|
||||
@@ -36,7 +39,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
|
||||
@@ -33,6 +33,11 @@ jobs:
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Determine build matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
@@ -72,7 +77,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
@@ -81,14 +86,14 @@ jobs:
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
@@ -99,14 +104,19 @@ jobs:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Build Java backend with JLink
|
||||
working-directory: ./
|
||||
shell: bash
|
||||
run: |
|
||||
chmod +x ./gradlew
|
||||
echo "🔧 Building Stirling-PDF JAR..."
|
||||
# STIRLING_PDF_DESKTOP_UI=false ./gradlew clean bootJar --no-daemon
|
||||
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||
# STIRLING_PDF_DESKTOP_UI=false ./gradlew bootJar --no-daemon
|
||||
./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||
|
||||
# Find the built JAR
|
||||
STIRLING_JAR=$(ls app/core/build/libs/stirling-pdf-*.jar | head -n 1)
|
||||
@@ -170,6 +180,9 @@ jobs:
|
||||
RUNTIME_SIZE=$(du -sh ./frontend/src-tauri/runtime/jre | cut -f1)
|
||||
echo "📊 Custom JRE size: $RUNTIME_SIZE"
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
|
||||
- name: Install frontend dependencies
|
||||
@@ -180,7 +193,7 @@ jobs:
|
||||
- name: Setup DigiCert KeyLocker
|
||||
id: digicert-setup
|
||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
uses: digicert/ssm-code-signing@v1.1.0
|
||||
uses: digicert/ssm-code-signing@9476ceec3ea1c63298d4403b983e1ccf2556ff4c # v1.1.0
|
||||
env:
|
||||
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
|
||||
@@ -299,7 +312,7 @@ jobs:
|
||||
ls -la /usr/bin/hd* || echo "No hd* tools found"
|
||||
|
||||
- name: Build Tauri app
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
uses: tauri-apps/tauri-action@51a9f1156b33df106d827c3a78f8f894946c5faa # v0.5.25
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
@@ -631,6 +644,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Report build results
|
||||
run: |
|
||||
if [ "${{ needs.build.result }}" = "success" ]; then
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
@@ -44,8 +44,11 @@ jobs:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew clean build
|
||||
run: ./gradlew build
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
@@ -125,7 +128,12 @@ jobs:
|
||||
outputs:
|
||||
frontend: ${{ steps.changes.outputs.frontend }}
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
@@ -144,7 +152,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
|
||||
+1
-1
@@ -213,7 +213,7 @@ id_ed25519.pub
|
||||
.mypy_cache
|
||||
.pytest_cache
|
||||
.ipynb_checkpoints
|
||||
|
||||
.build-cache
|
||||
|
||||
|
||||
**/jcef-bundle/
|
||||
|
||||
+4
-12
@@ -1,6 +1,6 @@
|
||||
# Adding New React Tools to Stirling PDF
|
||||
|
||||
This guide covers how to add new PDF tools to the React frontend, either by migrating existing Thymeleaf templates or creating entirely new tools.
|
||||
This guide covers how to add new PDF tools to the React frontend.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -188,7 +188,7 @@ import { use[ToolName]Tips } from "../components/tooltips/use[ToolName]Tips";
|
||||
|
||||
const [ToolName] = (props: BaseToolProps) => {
|
||||
const tips = use[ToolName]Tips();
|
||||
|
||||
|
||||
// In your steps array:
|
||||
steps: [
|
||||
{
|
||||
@@ -257,22 +257,14 @@ Update translation files. **Important: Only update `en-GB` files** - other langu
|
||||
- Add `options.*` keys if your tool has settings with descriptions
|
||||
|
||||
**Tooltip Writing Guidelines:**
|
||||
- **Use simple, everyday language** - avoid technical terms like "converts interactive elements"
|
||||
- **Use simple, everyday language** - avoid technical terms like "converts interactive elements"
|
||||
- **Focus on benefits** - explain what the user gains, not how it works internally
|
||||
- **Use concrete examples** - "text boxes become regular text" vs "form fields are flattened"
|
||||
- **Answer user questions** - "What does this do?", "When should I use this?", "What's this option for?"
|
||||
- **Keep descriptions concise** - 1-2 sentences maximum per section
|
||||
- **Use bullet points** for multiple benefits or features
|
||||
|
||||
## 6. Migration from Thymeleaf
|
||||
When migrating existing Thymeleaf templates:
|
||||
|
||||
1. **Identify Form Parameters**: Look at the original `<form>` inputs to determine parameter structure
|
||||
2. **Extract Translation Keys**: Find `#{key.name}` references and add them to JSON translations (For many tools these translations will already exist but some parts will be missing)
|
||||
3. **Map API Endpoint**: Note the `th:action` URL for the operation hook
|
||||
4. **Preserve Functionality**: Ensure all original form behaviour is replicated which is applicable to V2 react UI
|
||||
|
||||
## 7. Testing Your Tool
|
||||
## 6. Testing Your Tool
|
||||
- Verify tool appears in UI with correct icon and description
|
||||
- Test with various file sizes and types
|
||||
- Confirm translations work
|
||||
|
||||
@@ -128,8 +128,8 @@ return useToolOperation({
|
||||
## Architecture Overview
|
||||
|
||||
### Project Structure
|
||||
- **Backend**: Spring Boot application with Thymeleaf templating
|
||||
- **Frontend**: React-based SPA in `/frontend` directory (Thymeleaf templates fully replaced)
|
||||
- **Backend**: Spring Boot application
|
||||
- **Frontend**: React-based SPA in `/frontend` directory
|
||||
- **File Storage**: IndexedDB for client-side file persistence and thumbnails
|
||||
- **Internationalization**: JSON-based translations (converted from backend .properties)
|
||||
- **PDF Processing**: PDFBox for core PDF operations, LibreOffice for conversions, PDF.js for client-side rendering
|
||||
@@ -140,8 +140,6 @@ return useToolOperation({
|
||||
- **API Controllers** (`src/main/java/.../controller/api/`): REST endpoints for PDF operations
|
||||
- Organized by function: converters, security, misc, pipeline
|
||||
- Follow pattern: `@RestController` + `@RequestMapping("/api/v1/...")`
|
||||
- **Web Controllers** (`src/main/java/.../controller/web/`): Serve Thymeleaf templates
|
||||
- Pattern: `@Controller` + return template names
|
||||
|
||||
### Key Components
|
||||
- **SPDFApplication.java**: Main application class with desktop UI and browser launching logic
|
||||
@@ -201,7 +199,6 @@ return useToolOperation({
|
||||
|
||||
- **Java Version**: Minimum JDK 17, supports and recommends JDK 21
|
||||
- **Lombok**: Used extensively - ensure IDE plugin is installed
|
||||
- **Desktop Mode**: Set `STIRLING_PDF_DESKTOP_UI=true` for desktop application mode
|
||||
- **File Persistence**:
|
||||
- **Backend**: Designed to be stateless - files are processed in memory/temp locations only
|
||||
- **Frontend**: Uses IndexedDB for client-side file storage and caching (with thumbnails)
|
||||
|
||||
+3
-162
@@ -2,7 +2,7 @@
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
Stirling-PDF is a robust, locally hosted, web-based PDF manipulation tool. **Stirling 2.0** represents a complete frontend rewrite, replacing the legacy Thymeleaf-based UI with a modern React SPA (Single Page Application).
|
||||
Stirling-PDF is a robust, locally hosted, web-based PDF manipulation tool. **Stirling 2.0** represents a complete frontend rewrite with a modern React SPA (Single Page Application).
|
||||
|
||||
This guide focuses on developing for Stirling 2.0, including both the React frontend and Spring Boot backend development workflows.
|
||||
|
||||
@@ -38,9 +38,6 @@ This guide focuses on developing for Stirling 2.0, including both the React fron
|
||||
- PDF file association support
|
||||
- Self-contained JRE bundling with JLink
|
||||
|
||||
**Legacy (reference only during development):**
|
||||
- Thymeleaf templates (being completely replaced in 2.0)
|
||||
|
||||
## 3. Development Environment Setup
|
||||
|
||||
### Prerequisites
|
||||
@@ -100,9 +97,6 @@ Stirling 2.0 uses client-side file storage:
|
||||
- **PDF.js**: Handles client-side PDF rendering and processing
|
||||
- **URL Parameters**: Support for deep linking and tool state persistence
|
||||
|
||||
### Legacy Code Reference
|
||||
The existing Thymeleaf templates remain in the codebase during development as reference material but will be completely removed for the 2.0 release.
|
||||
|
||||
### Tauri Desktop App Development
|
||||
Stirling-PDF can be packaged as a cross-platform desktop application using Tauri with PDF file association support and bundled JRE.
|
||||
See [the frontend README](frontend/README.md#tauri) for build instructions.
|
||||
@@ -154,7 +148,6 @@ Stirling-PDF/
|
||||
│ │ │ ├── css/
|
||||
│ │ │ ├── js/
|
||||
│ │ │ └── pdfjs/
|
||||
│ │ └── templates/ # Legacy Thymeleaf templates (reference only)
|
||||
│ └── test/
|
||||
├── testing/ # Cucumber and integration tests
|
||||
│ └── cucumber/ # Cucumber test files
|
||||
@@ -309,7 +302,6 @@ For quick iterations and development of Java backend, JavaScript, and UI compone
|
||||
- RESTful API endpoints
|
||||
- JavaScript functionality
|
||||
- User interface components and styling
|
||||
- Thymeleaf templates
|
||||
|
||||
To run Stirling-PDF locally:
|
||||
|
||||
@@ -401,7 +393,7 @@ Remember to test your changes thoroughly to ensure they don't break any existing
|
||||
|
||||
### React Component Development (Stirling 2.0)
|
||||
|
||||
For Stirling 2.0, new features are built as React components instead of Thymeleaf templates:
|
||||
For Stirling 2.0, new features are built as React components:
|
||||
|
||||
#### Creating a New Tool Component
|
||||
|
||||
@@ -448,61 +440,6 @@ For Stirling 2.0, new features are built as React components instead of Thymelea
|
||||
3. **Register in Tool Picker:**
|
||||
Update the tool picker component to include the new tool with proper routing and URL parameter support.
|
||||
|
||||
### Legacy Reference: Overview of Thymeleaf
|
||||
|
||||
Thymeleaf is a server-side Java HTML template engine. It is used in Stirling-PDF to render dynamic web pages. Thymeleaf integrates heavily with Spring Boot.
|
||||
|
||||
### Thymeleaf overview
|
||||
|
||||
In Stirling-PDF, Thymeleaf is used to create HTML templates that are rendered on the server side. These templates are located in the `stirling-pdf/src/main/resources/templates` directory. Thymeleaf templates use a combination of HTML and special Thymeleaf attributes to dynamically generate content.
|
||||
|
||||
Some examples of this are:
|
||||
|
||||
```html
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
```
|
||||
or
|
||||
```html
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
```
|
||||
|
||||
Where it uses the `th:block`, `th:` indicating it's a special Thymeleaf element to be used server-side in generating the HTML, and block being the actual element type.
|
||||
In this case, we are inserting the `navbar` entry within the `fragments/navbar.html` fragment into the `th:block` element.
|
||||
|
||||
They can be more complex, such as:
|
||||
|
||||
```html
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{pageExtracter.title}, header=#{pageExtracter.header})}"></th:block>
|
||||
```
|
||||
|
||||
Which is the same as above but passes the parameters title and header into the fragment `common.html` to be used in its HTML generation.
|
||||
|
||||
Thymeleaf can also be used to loop through objects or pass things from the Java side into the HTML side.
|
||||
|
||||
```java
|
||||
@GetMapping
|
||||
public String newFeaturePage(Model model) {
|
||||
model.addAttribute("exampleData", exampleData);
|
||||
return "new-feature";
|
||||
}
|
||||
```
|
||||
|
||||
In the above example, if exampleData is a list of plain java objects of class Person and within it, you had id, name, age, etc. You can reference it like so
|
||||
|
||||
```html
|
||||
<tbody>
|
||||
<!-- Use th:each to iterate over the list -->
|
||||
<tr th:each="person : ${exampleData}">
|
||||
<td th:text="${person.id}"></td>
|
||||
<td th:text="${person.name}"></td>
|
||||
<td th:text="${person.age}"></td>
|
||||
<td th:text="${person.email}"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
```
|
||||
|
||||
This would generate n entries of tr for each person in exampleData
|
||||
|
||||
### Adding a New Feature to the Backend (API)
|
||||
|
||||
1. **Create a New Controller:**
|
||||
@@ -527,7 +464,7 @@ This would generate n entries of tr for each person in exampleData
|
||||
@GetMapping
|
||||
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
|
||||
public String newFeature() {
|
||||
return "NewFeatureResponse"; // This refers to the NewFeatureResponse.html template presenting the user with the generated html from that file when they navigate to /api/v1/new-feature
|
||||
return "NewFeatureResponse";
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -582,91 +519,6 @@ This would generate n entries of tr for each person in exampleData
|
||||
}
|
||||
```
|
||||
|
||||
### Adding a New Feature to the Frontend (UI)
|
||||
|
||||
1. **Create a New Thymeleaf Template:**
|
||||
- Create a new HTML file in the `stirling-pdf/src/main/resources/templates` directory.
|
||||
- Use Thymeleaf attributes to dynamically generate content.
|
||||
- Use `extract-page.html` as a base example for the HTML template, which is useful to ensure importing of the general layout, navbar, and footer.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{newFeature.title}, header=#{newFeature.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon organize">upload</span>
|
||||
<span class="tool-header-text" th:text="#{newFeature.header}"></span>
|
||||
</div>
|
||||
<form th:action="@{'/api/v1/new-feature'}" method="post" enctype="multipart/form-data">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<input type="hidden" id="customMode" name="customMode" value="">
|
||||
<div class="mb-3">
|
||||
<label for="featureInput" th:text="#{newFeature.prompt}"></label>
|
||||
<input type="text" class="form-control" id="featureInput" name="featureInput" th:placeholder="#{newFeature.placeholder}" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{newFeature.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
2. **Create a New Controller for the UI:**
|
||||
- Create a new Java class in the `stirling-pdf/src/main/java/stirling/software/SPDF/controller/ui` directory.
|
||||
- Annotate the class with `@Controller` and `@RequestMapping` to define the UI endpoint.
|
||||
|
||||
```java
|
||||
package stirling.software.SPDF.controller.ui;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import stirling.software.SPDF.service.NewFeatureService;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/new-feature")
|
||||
public class NewFeatureUIController {
|
||||
|
||||
@Autowired
|
||||
private NewFeatureService newFeatureService;
|
||||
|
||||
@GetMapping
|
||||
public String newFeaturePage(Model model) {
|
||||
model.addAttribute("newFeatureData", newFeatureService.getNewFeatureData());
|
||||
return "new-feature";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Update the Navigation Bar:**
|
||||
- Add a link to the new feature page in the navigation bar.
|
||||
- Update the `stirling-pdf/src/main/resources/templates/fragments/navbar.html` file.
|
||||
|
||||
```html
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" th:href="@{'/new-feature'}">New Feature</a>
|
||||
</li>
|
||||
```
|
||||
|
||||
## Adding New Translations to Existing Language Files in Stirling-PDF
|
||||
|
||||
When adding a new feature or modifying existing ones in Stirling-PDF, you'll need to add new translation entries to the existing language files. Here's a step-by-step guide:
|
||||
@@ -696,15 +548,4 @@ pdfSplitter.input.pages=Enter page numbers to split
|
||||
|
||||
Add these entries to the default GB language file and any others you wish, translating the values as appropriate for each language.
|
||||
|
||||
### 3. Use Translations in Thymeleaf Templates
|
||||
|
||||
In your Thymeleaf templates, use the `#{key}` syntax to reference the new translations:
|
||||
|
||||
```html
|
||||
<h1 th:text="#{pdfSplitter.title}">PDF Splitter</h1>
|
||||
<p th:text="#{pdfSplitter.description}">Split your PDF into multiple documents</p>
|
||||
<input type="text" th:placeholder="#{pdfSplitter.input.pages}">
|
||||
<button th:text="#{pdfSplitter.button.split}">Split PDF</button>
|
||||
```
|
||||
|
||||
Remember, never hard-code text in your templates or Java code. Always use translation keys to ensure proper localization.
|
||||
|
||||
@@ -29,7 +29,6 @@ spotless {
|
||||
dependencies {
|
||||
api 'org.springframework.boot:spring-boot-starter-web'
|
||||
api 'org.springframework.boot:spring-boot-starter-aop'
|
||||
// api 'org.springframework.boot:spring-boot-starter-thymeleaf' // Deprecated - UI moved to React frontend
|
||||
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
||||
api 'com.fathzer:javaluator:3.0.6'
|
||||
api 'com.posthog.java:posthog:1.2.0'
|
||||
@@ -42,8 +41,8 @@ dependencies {
|
||||
api "org.apache.pdfbox:preflight:$pdfboxVersion"
|
||||
api 'com.github.junrar:junrar:7.5.7' // RAR archive support for CBR files
|
||||
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
||||
api 'org.snakeyaml:snakeyaml-engine:2.10'
|
||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.14"
|
||||
api 'org.snakeyaml:snakeyaml-engine:3.0.1'
|
||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.15"
|
||||
// Simple Java Mail for EML/MSG parsing (replaces direct Angus Mail usage)
|
||||
api 'org.simplejavamail:simple-java-mail:8.12.6'
|
||||
api 'org.simplejavamail:outlook-module:8.12.6' // MSG file support
|
||||
|
||||
@@ -303,22 +303,23 @@ public class EndpointConfiguration {
|
||||
// Adding endpoints to "PageOps" group
|
||||
addEndpointToGroup("PageOps", "remove-pages");
|
||||
addEndpointToGroup("PageOps", "merge-pdfs");
|
||||
addEndpointToGroup("PageOps", "split-pdfs");
|
||||
addEndpointToGroup("PageOps", "pdf-organizer");
|
||||
addEndpointToGroup("PageOps", "split-pages");
|
||||
addEndpointToGroup("PageOps", "rearrange-pages");
|
||||
addEndpointToGroup("PageOps", "rotate-pdf");
|
||||
addEndpointToGroup("PageOps", "multi-page-layout");
|
||||
addEndpointToGroup("PageOps", "booklet-imposition");
|
||||
addEndpointToGroup("PageOps", "scale-pages");
|
||||
addEndpointToGroup("PageOps", "crop");
|
||||
addEndpointToGroup("PageOps", "extract-page");
|
||||
addEndpointToGroup("PageOps", "pdf-to-single-page");
|
||||
addEndpointToGroup("PageOps", "auto-split-pdf");
|
||||
addEndpointToGroup("PageOps", "split-by-size-or-count");
|
||||
addEndpointToGroup("PageOps", "overlay-pdf");
|
||||
addEndpointToGroup("PageOps", "split-pdf-by-sections");
|
||||
addEndpointToGroup("PageOps", "split-pdf-by-chapters");
|
||||
addEndpointToGroup("PageOps", "add-page-numbers");
|
||||
addEndpointToGroup("PageOps", "extract-pages");
|
||||
|
||||
// Adding endpoints to "Convert" group
|
||||
// Adding endpoints to "Convert" group (Frontend has 15 convert endpoints)
|
||||
addEndpointToGroup("Convert", "pdf-to-img");
|
||||
addEndpointToGroup("Convert", "img-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-pdfa");
|
||||
@@ -334,6 +335,8 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Convert", "pdf-to-csv");
|
||||
addEndpointToGroup("Convert", "pdf-to-markdown");
|
||||
addEndpointToGroup("Convert", "eml-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-epub");
|
||||
// Backend-only endpoints (not in frontend tool registry)
|
||||
addEndpointToGroup("Convert", "pdf-to-vector");
|
||||
addEndpointToGroup("Convert", "vector-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-video");
|
||||
@@ -341,6 +344,7 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Convert", "pdf-to-cbz");
|
||||
addEndpointToGroup("Convert", "pdf-to-json");
|
||||
addEndpointToGroup("Convert", "json-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-rtf");
|
||||
|
||||
// Adding endpoints to "Security" group
|
||||
addEndpointToGroup("Security", "add-password");
|
||||
@@ -351,51 +355,49 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Security", "remove-cert-sign");
|
||||
addEndpointToGroup("Security", "sanitize-pdf");
|
||||
addEndpointToGroup("Security", "auto-redact");
|
||||
addEndpointToGroup("Security", "redact");
|
||||
addEndpointToGroup("Security", "validate-signature");
|
||||
addEndpointToGroup("Security", "add-stamp");
|
||||
addEndpointToGroup("Security", "unlock-pdf-forms");
|
||||
// Backend-only endpoints (not in frontend tool registry endpoints)
|
||||
addEndpointToGroup("Security", "redact");
|
||||
addEndpointToGroup("Security", "verify-pdf");
|
||||
addEndpointToGroup("Security", "stamp");
|
||||
addEndpointToGroup("Security", "sign");
|
||||
|
||||
// Adding endpoints to "Other" group
|
||||
addEndpointToGroup("Other", "ocr-pdf");
|
||||
addEndpointToGroup("Other", "add-image");
|
||||
addEndpointToGroup("Other", "extract-images");
|
||||
addEndpointToGroup("Other", "change-metadata");
|
||||
addEndpointToGroup("Other", "update-metadata");
|
||||
addEndpointToGroup("Other", "flatten");
|
||||
addEndpointToGroup("Other", "unlock-pdf-forms");
|
||||
addEndpointToGroup("Other", REMOVE_BLANKS);
|
||||
addEndpointToGroup("Other", "remove-annotations");
|
||||
addEndpointToGroup("Other", "compare");
|
||||
addEndpointToGroup("Other", "add-page-numbers");
|
||||
addEndpointToGroup("Other", "get-info-on-pdf");
|
||||
addEndpointToGroup("Other", "remove-image-pdf");
|
||||
addEndpointToGroup("Other", "add-attachments");
|
||||
addEndpointToGroup("Other", "replace-invert-pdf");
|
||||
addEndpointToGroup("Other", "edit-table-of-contents");
|
||||
addEndpointToGroup("Other", "text-editor-pdf");
|
||||
// Backend-only endpoints (not in frontend tool registry endpoints)
|
||||
addEndpointToGroup("Other", "add-image");
|
||||
addEndpointToGroup("Other", "compare");
|
||||
addEndpointToGroup("Other", "view-pdf");
|
||||
addEndpointToGroup("Other", "replace-and-invert-color-pdf");
|
||||
addEndpointToGroup("Other", "multi-tool");
|
||||
|
||||
// Adding form-related endpoints to "Other" group
|
||||
addEndpointToGroup("Other", "fields");
|
||||
addEndpointToGroup("Other", "modify-fields");
|
||||
addEndpointToGroup("Other", "delete-fields");
|
||||
addEndpointToGroup("Other", "fill");
|
||||
|
||||
// Adding endpoints to "Advance" group
|
||||
addEndpointToGroup("Advance", "adjust-contrast");
|
||||
addEndpointToGroup("Advance", "compress-pdf");
|
||||
addEndpointToGroup("Advance", "extract-image-scans");
|
||||
addEndpointToGroup("Advance", "repair");
|
||||
addEndpointToGroup("Advance", "auto-rename");
|
||||
addEndpointToGroup("Advance", "pipeline");
|
||||
addEndpointToGroup("Advance", "handleData");
|
||||
addEndpointToGroup("Advance", "scanner-effect");
|
||||
addEndpointToGroup("Advance", "auto-split-pdf");
|
||||
addEndpointToGroup("Advance", "show-javascript");
|
||||
addEndpointToGroup("Advance", "split-by-size-or-count");
|
||||
addEndpointToGroup("Advance", "overlay-pdf");
|
||||
addEndpointToGroup("Advance", "split-pdf-by-sections");
|
||||
addEndpointToGroup("Advance", "edit-table-of-contents");
|
||||
addEndpointToGroup("Advance", "split-pdf-by-chapters");
|
||||
// Backend-only endpoints
|
||||
addEndpointToGroup("Advance", "adjust-contrast");
|
||||
addEndpointToGroup("Advance", "pipeline");
|
||||
|
||||
// CLI
|
||||
addEndpointToGroup("CLI", "compress-pdf");
|
||||
@@ -436,8 +438,8 @@ public class EndpointConfiguration {
|
||||
// Java
|
||||
addEndpointToGroup("Java", "merge-pdfs");
|
||||
addEndpointToGroup("Java", "remove-pages");
|
||||
addEndpointToGroup("Java", "split-pdfs");
|
||||
addEndpointToGroup("Java", "pdf-organizer");
|
||||
addEndpointToGroup("Java", "split-pages");
|
||||
addEndpointToGroup("Java", "rearrange-pages");
|
||||
addEndpointToGroup("Java", "rotate-pdf");
|
||||
addEndpointToGroup("Java", "pdf-to-img");
|
||||
addEndpointToGroup("Java", "img-to-pdf");
|
||||
@@ -445,9 +447,10 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Java", "remove-password");
|
||||
addEndpointToGroup("Java", "change-permissions");
|
||||
addEndpointToGroup("Java", "add-watermark");
|
||||
addEndpointToGroup("Java", "add-stamp");
|
||||
addEndpointToGroup("Java", "add-image");
|
||||
addEndpointToGroup("Java", "extract-images");
|
||||
addEndpointToGroup("Java", "change-metadata");
|
||||
addEndpointToGroup("Java", "update-metadata");
|
||||
addEndpointToGroup("Java", "cert-sign");
|
||||
addEndpointToGroup("Java", "remove-cert-sign");
|
||||
addEndpointToGroup("Java", "multi-page-layout");
|
||||
@@ -459,7 +462,6 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Java", "sanitize-pdf");
|
||||
addEndpointToGroup("Java", "crop");
|
||||
addEndpointToGroup("Java", "get-info-on-pdf");
|
||||
addEndpointToGroup("Java", "extract-page");
|
||||
addEndpointToGroup("Java", "pdf-to-single-page");
|
||||
addEndpointToGroup("Java", "markdown-to-pdf");
|
||||
addEndpointToGroup("Java", "show-javascript");
|
||||
@@ -469,7 +471,9 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Java", "split-by-size-or-count");
|
||||
addEndpointToGroup("Java", "overlay-pdf");
|
||||
addEndpointToGroup("Java", "split-pdf-by-sections");
|
||||
addEndpointToGroup("Java", "split-pdf-by-chapters");
|
||||
addEndpointToGroup("Java", REMOVE_BLANKS);
|
||||
addEndpointToGroup("Java", "remove-annotations");
|
||||
addEndpointToGroup("Java", "pdf-to-text");
|
||||
addEndpointToGroup("Java", "remove-image-pdf");
|
||||
addEndpointToGroup("Java", "pdf-to-markdown");
|
||||
@@ -479,15 +483,24 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Java", "pdf-to-cbz");
|
||||
addEndpointToGroup("Java", "pdf-to-json");
|
||||
addEndpointToGroup("Java", "json-to-pdf");
|
||||
addEndpointToGroup("rar", "pdf-to-cbr");
|
||||
addEndpointToGroup("Java", "pdf-to-video");
|
||||
addEndpointToGroup("Java", "verify-pdf");
|
||||
addEndpointToGroup("Java", "flatten");
|
||||
addEndpointToGroup("Java", "unlock-pdf-forms");
|
||||
addEndpointToGroup("Java", "validate-signature");
|
||||
addEndpointToGroup("Java", "text-editor-pdf");
|
||||
addEndpointToGroup("Java", "edit-table-of-contents");
|
||||
addEndpointToGroup("Java", "pdf-to-epub");
|
||||
addEndpointToGroup("Java", "eml-to-pdf");
|
||||
addEndpointToGroup("Java", "handleData");
|
||||
addEndpointToGroup("rar", "pdf-to-cbr");
|
||||
|
||||
// Javascript
|
||||
addEndpointToGroup("Javascript", "pdf-organizer");
|
||||
addEndpointToGroup("Javascript", "rearrange-pages");
|
||||
addEndpointToGroup("Javascript", "sign");
|
||||
addEndpointToGroup("Javascript", "compare");
|
||||
addEndpointToGroup("Javascript", "adjust-contrast");
|
||||
addEndpointToGroup("Javascript", "text-editor-pdf");
|
||||
|
||||
/* qpdf */
|
||||
addEndpointToGroup("qpdf", "repair");
|
||||
@@ -498,6 +511,7 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Ghostscript", "compress-pdf");
|
||||
addEndpointToGroup("Ghostscript", "crop");
|
||||
addEndpointToGroup("Ghostscript", "replace-invert-pdf");
|
||||
addEndpointToGroup("Ghostscript", "scanner-effect");
|
||||
addEndpointToGroup("Ghostscript", "pdf-to-vector");
|
||||
addEndpointToGroup("Ghostscript", "vector-to-pdf");
|
||||
|
||||
@@ -516,6 +530,8 @@ public class EndpointConfiguration {
|
||||
addEndpointAlternative("compress-pdf", "qpdf");
|
||||
addEndpointAlternative("compress-pdf", "Ghostscript");
|
||||
addEndpointAlternative("compress-pdf", "Java");
|
||||
addEndpointAlternative("crop", "Ghostscript");
|
||||
addEndpointAlternative("crop", "Java");
|
||||
addEndpointAlternative("ocr-pdf", "tesseract");
|
||||
addEndpointAlternative("ocr-pdf", "OCRmyPDF");
|
||||
|
||||
@@ -544,6 +560,9 @@ public class EndpointConfiguration {
|
||||
// Pdftohtml dependent endpoints
|
||||
addEndpointToGroup("Pdftohtml", "pdf-to-html");
|
||||
addEndpointToGroup("Pdftohtml", "pdf-to-markdown");
|
||||
|
||||
// Calibre dependent endpoints
|
||||
addEndpointToGroup("Calibre", "pdf-to-epub");
|
||||
}
|
||||
|
||||
private void processEnvironmentConfigs() {
|
||||
|
||||
@@ -64,16 +64,6 @@ public class AppConfig {
|
||||
return v2Enabled;
|
||||
}
|
||||
|
||||
/* Commented out Thymeleaf template engine bean - to be removed when frontend migration is complete
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true")
|
||||
public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) {
|
||||
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
|
||||
templateEngine.addTemplateResolver(new FileFallbackTemplateResolver(resourceLoader));
|
||||
return templateEngine;
|
||||
}
|
||||
*/
|
||||
|
||||
@Bean(name = "loginEnabled")
|
||||
public boolean loginEnabled() {
|
||||
return applicationProperties.getSecurity().isEnableLogin();
|
||||
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
/* Commented out entire FileFallbackTemplateResolver class - Thymeleaf dependency removed
|
||||
* This class will be removed when frontend migration to React is complete
|
||||
|
||||
|
||||
@Slf4j
|
||||
public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateResolver {
|
||||
|
||||
private final ResourceLoader resourceLoader;
|
||||
|
||||
public FileFallbackTemplateResolver(ResourceLoader resourceLoader) {
|
||||
super();
|
||||
this.resourceLoader = resourceLoader;
|
||||
setSuffix(".html");
|
||||
}
|
||||
|
||||
// Note this does not work in local IDE, Prod jar only.
|
||||
@Override
|
||||
protected ITemplateResource computeTemplateResource(
|
||||
IEngineConfiguration configuration,
|
||||
String ownerTemplate,
|
||||
String template,
|
||||
String resourceName,
|
||||
String characterEncoding,
|
||||
Map<String, Object> templateResolutionAttributes) {
|
||||
Resource resource =
|
||||
resourceLoader.getResource(
|
||||
"file:" + InstallationPathConfig.getTemplatesPath() + resourceName);
|
||||
try {
|
||||
if (resource.exists() && resource.isReadable()) {
|
||||
return new FileTemplateResource(resource.getFile().getPath(), characterEncoding);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Log the exception to help with debugging issues loading external templates
|
||||
log.warn("Unable to read template '{}' from file system", resourceName, e);
|
||||
}
|
||||
|
||||
InputStream inputStream =
|
||||
Thread.currentThread()
|
||||
.getContextClassLoader()
|
||||
.getResourceAsStream("templates/" + resourceName);
|
||||
if (inputStream != null) {
|
||||
return new InputStreamTemplateResource(inputStream, "UTF-8");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
*/
|
||||
-24
@@ -1,8 +1,6 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Locale;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -61,28 +59,6 @@ public class InstallationPathConfig {
|
||||
}
|
||||
|
||||
private static String initializeBasePath() {
|
||||
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
||||
String os = System.getProperty("os.name").toLowerCase(Locale.ROOT);
|
||||
if (os.contains("win")) {
|
||||
return Paths.get(
|
||||
System.getenv("APPDATA"), // parent path
|
||||
"Stirling-PDF")
|
||||
+ File.separator;
|
||||
} else if (os.contains("mac")) {
|
||||
return Paths.get(
|
||||
System.getProperty("user.home"),
|
||||
"Library",
|
||||
"Application Support",
|
||||
"Stirling-PDF")
|
||||
+ File.separator;
|
||||
} else {
|
||||
return Paths.get(
|
||||
System.getProperty("user.home"), // parent path
|
||||
".config",
|
||||
"Stirling-PDF")
|
||||
+ File.separator;
|
||||
}
|
||||
}
|
||||
return "." + File.separator;
|
||||
}
|
||||
|
||||
|
||||
+235
-6
@@ -1,7 +1,14 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -14,6 +21,8 @@ import stirling.software.common.model.ApplicationProperties.CustomPaths;
|
||||
import stirling.software.common.model.ApplicationProperties.CustomPaths.Operations;
|
||||
import stirling.software.common.model.ApplicationProperties.CustomPaths.Pipeline;
|
||||
import stirling.software.common.model.ApplicationProperties.System;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
import stirling.software.common.util.UnoServerPool;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@@ -32,8 +41,11 @@ public class RuntimePathConfig {
|
||||
// Tesseract data path
|
||||
private final String tessDataPath;
|
||||
|
||||
private final List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> unoServerEndpoints;
|
||||
|
||||
// Pipeline paths
|
||||
private final String pipelineWatchedFoldersPath;
|
||||
private final List<String> pipelineWatchedFoldersPaths;
|
||||
private final String pipelineFinishedFoldersPath;
|
||||
private final String pipelineDefaultWebUiConfigs;
|
||||
private final String pipelinePath;
|
||||
@@ -42,20 +54,27 @@ public class RuntimePathConfig {
|
||||
this.properties = properties;
|
||||
this.basePath = InstallationPathConfig.getPath();
|
||||
|
||||
this.pipelinePath = Path.of(basePath, "pipeline").toString();
|
||||
String defaultWatchedFolders = Path.of(this.pipelinePath, "watchedFolders").toString();
|
||||
String defaultFinishedFolders = Path.of(this.pipelinePath, "finishedFolders").toString();
|
||||
String defaultWebUIConfigs = Path.of(this.pipelinePath, "defaultWebUIConfigs").toString();
|
||||
|
||||
System system = properties.getSystem();
|
||||
CustomPaths customPaths = system.getCustomPaths();
|
||||
|
||||
Pipeline pipeline = customPaths.getPipeline();
|
||||
|
||||
this.pipelineWatchedFoldersPath =
|
||||
this.pipelinePath =
|
||||
resolvePath(
|
||||
Path.of(basePath, "pipeline").toString(),
|
||||
pipeline != null ? pipeline.getPipelineDir() : null);
|
||||
String defaultWatchedFolders = Path.of(this.pipelinePath, "watchedFolders").toString();
|
||||
String defaultFinishedFolders = Path.of(this.pipelinePath, "finishedFolders").toString();
|
||||
String defaultWebUIConfigs = Path.of(this.pipelinePath, "defaultWebUIConfigs").toString();
|
||||
|
||||
List<String> watchedFoldersDirs =
|
||||
sanitizePathList(pipeline != null ? pipeline.getWatchedFoldersDirs() : null);
|
||||
this.pipelineWatchedFoldersPaths =
|
||||
resolveWatchedFolderPaths(
|
||||
defaultWatchedFolders,
|
||||
watchedFoldersDirs,
|
||||
pipeline != null ? pipeline.getWatchedFoldersDir() : null);
|
||||
this.pipelineWatchedFoldersPath = this.pipelineWatchedFoldersPaths.get(0);
|
||||
this.pipelineFinishedFoldersPath =
|
||||
resolvePath(
|
||||
defaultFinishedFolders,
|
||||
@@ -65,6 +84,9 @@ public class RuntimePathConfig {
|
||||
defaultWebUIConfigs,
|
||||
pipeline != null ? pipeline.getWebUIConfigsDir() : null);
|
||||
|
||||
// Validate path conflicts after all paths are resolved
|
||||
validatePipelinePaths();
|
||||
|
||||
boolean isDocker = isRunningInDocker();
|
||||
|
||||
// Initialize Operation paths
|
||||
@@ -108,13 +130,220 @@ public class RuntimePathConfig {
|
||||
}
|
||||
|
||||
log.info("Using Tesseract data path: {}", this.tessDataPath);
|
||||
|
||||
ApplicationProperties.ProcessExecutor processExecutor = properties.getProcessExecutor();
|
||||
int libreOfficeLimit = 1;
|
||||
if (processExecutor != null && processExecutor.getSessionLimit() != null) {
|
||||
libreOfficeLimit = processExecutor.getSessionLimit().getLibreOfficeSessionLimit();
|
||||
}
|
||||
this.unoServerEndpoints = buildUnoServerEndpoints(processExecutor, libreOfficeLimit);
|
||||
ProcessExecutor.setUnoServerPool(new UnoServerPool(this.unoServerEndpoints));
|
||||
}
|
||||
|
||||
private String resolvePath(String defaultPath, String customPath) {
|
||||
return StringUtils.isNotBlank(customPath) ? customPath : defaultPath;
|
||||
}
|
||||
|
||||
private List<String> resolveWatchedFolderPaths(
|
||||
String defaultPath, List<String> watchedFoldersDirs, String legacyWatchedFolder) {
|
||||
List<String> rawPaths = new ArrayList<>();
|
||||
|
||||
// Collect paths from new config
|
||||
if (watchedFoldersDirs != null && !watchedFoldersDirs.isEmpty()) {
|
||||
rawPaths.addAll(watchedFoldersDirs);
|
||||
}
|
||||
// Fall back to legacy config
|
||||
else if (StringUtils.isNotBlank(legacyWatchedFolder)) {
|
||||
rawPaths.add(legacyWatchedFolder);
|
||||
}
|
||||
// Fall back to default
|
||||
else {
|
||||
rawPaths.add(defaultPath);
|
||||
}
|
||||
|
||||
// Validate, normalize, and deduplicate paths
|
||||
List<String> validatedPaths = validateAndNormalizePaths(rawPaths);
|
||||
|
||||
// Ensure we have at least one valid path (critical for system to function)
|
||||
if (validatedPaths.isEmpty()) {
|
||||
log.warn(
|
||||
"No valid watched folder paths configured, falling back to default: {}",
|
||||
defaultPath);
|
||||
validatedPaths.add(defaultPath);
|
||||
}
|
||||
|
||||
// Detect overlapping paths (warning only, not blocking)
|
||||
detectOverlappingPaths(validatedPaths);
|
||||
|
||||
return validatedPaths;
|
||||
}
|
||||
|
||||
private List<String> sanitizePathList(List<String> paths) {
|
||||
if (paths == null || paths.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> sanitized = new ArrayList<>();
|
||||
for (String path : paths) {
|
||||
if (StringUtils.isNotBlank(path)) {
|
||||
sanitized.add(path.trim());
|
||||
}
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
private List<String> validateAndNormalizePaths(List<String> paths) {
|
||||
Set<String> normalizedPaths = new LinkedHashSet<>(); // Preserves order, prevents duplicates
|
||||
|
||||
for (String pathStr : paths) {
|
||||
if (StringUtils.isBlank(pathStr)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Normalize to absolute path
|
||||
Path path = Paths.get(pathStr.trim()).toAbsolutePath().normalize();
|
||||
String normalizedPath = path.toString();
|
||||
|
||||
// Check for duplicates
|
||||
if (normalizedPaths.contains(normalizedPath)) {
|
||||
log.debug("Skipping duplicate watched folder path: {}", pathStr);
|
||||
continue;
|
||||
}
|
||||
|
||||
normalizedPaths.add(normalizedPath);
|
||||
log.info("Registered watched folder path: {}", normalizedPath);
|
||||
|
||||
} catch (InvalidPathException e) {
|
||||
log.error(
|
||||
"Invalid watched folder path '{}' - skipping: {}", pathStr, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return new ArrayList<>(normalizedPaths);
|
||||
}
|
||||
|
||||
private void detectOverlappingPaths(List<String> paths) {
|
||||
for (int i = 0; i < paths.size(); i++) {
|
||||
Path path1 = Paths.get(paths.get(i));
|
||||
for (int j = i + 1; j < paths.size(); j++) {
|
||||
Path path2 = Paths.get(paths.get(j));
|
||||
|
||||
// Check if one path is a parent of the other
|
||||
if (path1.startsWith(path2)) {
|
||||
log.warn(
|
||||
"Watched folder path '{}' is nested inside '{}' - this may cause duplicate processing",
|
||||
path1,
|
||||
path2);
|
||||
} else if (path2.startsWith(path1)) {
|
||||
log.warn(
|
||||
"Watched folder path '{}' is nested inside '{}' - this may cause duplicate processing",
|
||||
path2,
|
||||
path1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validatePipelinePaths() {
|
||||
try {
|
||||
Path finishedPath = Paths.get(pipelineFinishedFoldersPath).toAbsolutePath().normalize();
|
||||
|
||||
for (String watchedPathStr : pipelineWatchedFoldersPaths) {
|
||||
Path watchedPath = Paths.get(watchedPathStr).toAbsolutePath().normalize();
|
||||
|
||||
// Check if watched folder is same as finished folder
|
||||
if (watchedPath.equals(finishedPath)) {
|
||||
log.error(
|
||||
"CRITICAL: Watched folder '{}' is the same as finished folder '{}' - this will cause processing loops!",
|
||||
watchedPath,
|
||||
finishedPath);
|
||||
}
|
||||
// Check if watched folder contains finished folder
|
||||
else if (finishedPath.startsWith(watchedPath)) {
|
||||
log.warn(
|
||||
"Finished folder '{}' is nested inside watched folder '{}' - this may cause issues",
|
||||
finishedPath,
|
||||
watchedPath);
|
||||
}
|
||||
// Check if finished folder contains watched folder
|
||||
else if (watchedPath.startsWith(finishedPath)) {
|
||||
log.error(
|
||||
"CRITICAL: Watched folder '{}' is nested inside finished folder '{}' - this will cause processing loops!",
|
||||
watchedPath,
|
||||
finishedPath);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error validating pipeline paths: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRunningInDocker() {
|
||||
return Files.exists(Path.of("/.dockerenv"));
|
||||
}
|
||||
|
||||
private List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> buildUnoServerEndpoints(
|
||||
ApplicationProperties.ProcessExecutor processExecutor, int sessionLimit) {
|
||||
if (processExecutor == null) {
|
||||
log.warn("ProcessExecutor config missing; defaulting to a single UNO endpoint.");
|
||||
return Collections.singletonList(
|
||||
new ApplicationProperties.ProcessExecutor.UnoServerEndpoint());
|
||||
}
|
||||
if (!processExecutor.isAutoUnoServer()) {
|
||||
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> configured =
|
||||
sanitizeUnoServerEndpoints(processExecutor.getUnoServerEndpoints());
|
||||
if (!configured.isEmpty()) {
|
||||
// Warn if manual endpoint count doesn't match sessionLimit
|
||||
if (configured.size() != sessionLimit) {
|
||||
log.warn(
|
||||
"Manual UNO endpoint count ({}) differs from libreOfficeSessionLimit ({}). "
|
||||
+ "Concurrency will be limited by endpoint count, not sessionLimit.",
|
||||
configured.size(),
|
||||
sessionLimit);
|
||||
}
|
||||
return configured;
|
||||
}
|
||||
log.warn(
|
||||
"autoUnoServer disabled but no unoServerEndpoints configured; defaulting to 127.0.0.1:2003.");
|
||||
return Collections.singletonList(
|
||||
new ApplicationProperties.ProcessExecutor.UnoServerEndpoint());
|
||||
}
|
||||
int count = sessionLimit > 0 ? sessionLimit : 1;
|
||||
return buildAutoUnoServerEndpoints(count);
|
||||
}
|
||||
|
||||
private List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint>
|
||||
buildAutoUnoServerEndpoints(int count) {
|
||||
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints = new ArrayList<>();
|
||||
int basePort = 2003;
|
||||
for (int i = 0; i < count; i++) {
|
||||
ApplicationProperties.ProcessExecutor.UnoServerEndpoint endpoint =
|
||||
new ApplicationProperties.ProcessExecutor.UnoServerEndpoint();
|
||||
endpoint.setHost("127.0.0.1");
|
||||
endpoint.setPort(basePort + (i * 2));
|
||||
endpoints.add(endpoint);
|
||||
}
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
private List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint>
|
||||
sanitizeUnoServerEndpoints(
|
||||
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints) {
|
||||
if (endpoints == null || endpoints.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> sanitized = new ArrayList<>();
|
||||
for (ApplicationProperties.ProcessExecutor.UnoServerEndpoint endpoint : endpoints) {
|
||||
if (endpoint == null) {
|
||||
continue;
|
||||
}
|
||||
String host = endpoint.getHost();
|
||||
int port = endpoint.getPort();
|
||||
if (host == null || host.isBlank() || port <= 0) {
|
||||
continue;
|
||||
}
|
||||
sanitized.add(endpoint);
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +164,7 @@ public class ApplicationProperties {
|
||||
private String customGlobalAPIKey;
|
||||
private Jwt jwt = new Jwt();
|
||||
private Validation validation = new Validation();
|
||||
private String xFrameOptions = "DENY";
|
||||
|
||||
public Boolean isAltLogin() {
|
||||
return saml2.getEnabled() || oauth2.getEnabled();
|
||||
@@ -458,7 +459,9 @@ public class ApplicationProperties {
|
||||
|
||||
@Data
|
||||
public static class Pipeline {
|
||||
private String pipelineDir;
|
||||
private String watchedFoldersDir;
|
||||
private List<String> watchedFoldersDirs = new ArrayList<>();
|
||||
private String finishedFoldersDir;
|
||||
private String webUIConfigsDir;
|
||||
}
|
||||
@@ -827,6 +830,16 @@ public class ApplicationProperties {
|
||||
public static class ProcessExecutor {
|
||||
private SessionLimit sessionLimit = new SessionLimit();
|
||||
private TimeoutMinutes timeoutMinutes = new TimeoutMinutes();
|
||||
private boolean autoUnoServer = true;
|
||||
private List<UnoServerEndpoint> unoServerEndpoints = new ArrayList<>();
|
||||
|
||||
@Data
|
||||
public static class UnoServerEndpoint {
|
||||
private String host = "127.0.0.1";
|
||||
private int port = 2003;
|
||||
private String hostLocation = "auto"; // auto|local|remote
|
||||
private String protocol = "http"; // http|https
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class SessionLimit {
|
||||
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
package stirling.software.common.model;
|
||||
|
||||
/* Commented out entire InputStreamTemplateResource class - Thymeleaf dependency removed
|
||||
* This class will be removed when frontend migration to React is complete
|
||||
|
||||
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
public class InputStreamTemplateResource implements ITemplateResource {
|
||||
private final InputStream inputStream;
|
||||
private final String characterEncoding;
|
||||
|
||||
@Override
|
||||
public Reader reader() throws IOException {
|
||||
return new InputStreamReader(inputStream, characterEncoding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ITemplateResource relative(String relativeLocation) {
|
||||
// Implement logic for relative resources, if needed
|
||||
throw new UnsupportedOperationException("Relative resources not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "InputStream resource [Stream]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseName() {
|
||||
return "streamResource";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return inputStream != null;
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -29,7 +29,7 @@ public class FileMonitor {
|
||||
private final ConcurrentHashMap.KeySetView<Path, Boolean> readyForProcessingFiles;
|
||||
private final WatchService watchService;
|
||||
private final Predicate<Path> pathFilter;
|
||||
private final Path rootDir;
|
||||
private final List<Path> rootDirs;
|
||||
private Set<Path> stagingFiles;
|
||||
|
||||
/**
|
||||
@@ -47,8 +47,28 @@ public class FileMonitor {
|
||||
this.pathFilter = pathFilter;
|
||||
this.readyForProcessingFiles = ConcurrentHashMap.newKeySet();
|
||||
this.watchService = FileSystems.getDefault().newWatchService();
|
||||
log.info("Monitoring directory: {}", runtimePathConfig.getPipelineWatchedFoldersPath());
|
||||
this.rootDir = Path.of(runtimePathConfig.getPipelineWatchedFoldersPath());
|
||||
|
||||
List<String> watchedFoldersDirs = runtimePathConfig.getPipelineWatchedFoldersPaths();
|
||||
List<Path> validRootDirs = new ArrayList<>();
|
||||
|
||||
for (String pathStr : watchedFoldersDirs) {
|
||||
try {
|
||||
Path path = Path.of(pathStr);
|
||||
validRootDirs.add(path);
|
||||
log.info("Monitoring directory: {}", path);
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Failed to initialize monitoring for path '{}': {}",
|
||||
pathStr,
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
this.rootDirs = Collections.unmodifiableList(validRootDirs);
|
||||
|
||||
if (this.rootDirs.isEmpty()) {
|
||||
log.error("No valid directories to monitor - FileMonitor will not function");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldNotProcess(Path path) {
|
||||
@@ -85,13 +105,15 @@ public class FileMonitor {
|
||||
readyForProcessingFiles.clear();
|
||||
|
||||
if (path2KeyMapping.isEmpty()) {
|
||||
log.warn("not monitoring any directory, even the root directory itself: {}", rootDir);
|
||||
if (Files.exists(
|
||||
rootDir)) { // if the root directory exists, re-register the root directory
|
||||
try {
|
||||
recursivelyRegisterEntry(rootDir);
|
||||
} catch (IOException e) {
|
||||
log.error("unable to register monitoring", e);
|
||||
log.warn("Not monitoring any directories; attempting to re-register root paths.");
|
||||
for (Path rootDir : rootDirs) {
|
||||
if (Files.exists(
|
||||
rootDir)) { // if the root directory exists, re-register the root directory
|
||||
try {
|
||||
recursivelyRegisterEntry(rootDir);
|
||||
} catch (IOException e) {
|
||||
log.error("unable to register monitoring for {}", rootDir, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.net.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
@@ -38,6 +40,10 @@ public class GeneralUtils {
|
||||
*/
|
||||
private static final int MAX_DNS_ADDRESSES = 20;
|
||||
|
||||
// Constants for size conversion
|
||||
private static final BigDecimal KIB = BigDecimal.valueOf(1024L);
|
||||
private static final BigDecimal LONG_MAX_DECIMAL = BigDecimal.valueOf(Long.MAX_VALUE);
|
||||
|
||||
private final Set<String> DEFAULT_VALID_SCRIPTS = Set.of("png_to_webp.py", "split_photos.py");
|
||||
private final Set<String> DEFAULT_VALID_PIPELINE =
|
||||
Set.of(
|
||||
@@ -539,39 +545,26 @@ public class GeneralUtils {
|
||||
|
||||
try {
|
||||
if (sizeStr.endsWith("TB")) {
|
||||
return (long)
|
||||
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
|
||||
* 1024L
|
||||
* 1024L
|
||||
* 1024L
|
||||
* 1024L);
|
||||
return toBytes(parseSizeValue(sizeStr.substring(0, sizeStr.length() - 2)), 4);
|
||||
} else if (sizeStr.endsWith("GB")) {
|
||||
return (long)
|
||||
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
|
||||
* 1024L
|
||||
* 1024L
|
||||
* 1024L);
|
||||
return toBytes(parseSizeValue(sizeStr.substring(0, sizeStr.length() - 2)), 3);
|
||||
} else if (sizeStr.endsWith("MB")) {
|
||||
return (long)
|
||||
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
|
||||
* 1024L
|
||||
* 1024L);
|
||||
return toBytes(parseSizeValue(sizeStr.substring(0, sizeStr.length() - 2)), 2);
|
||||
} else if (sizeStr.endsWith("KB")) {
|
||||
return (long)
|
||||
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024L);
|
||||
return toBytes(parseSizeValue(sizeStr.substring(0, sizeStr.length() - 2)), 1);
|
||||
} else if (!sizeStr.isEmpty() && sizeStr.charAt(sizeStr.length() - 1) == 'B') {
|
||||
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
|
||||
return toBytes(parseSizeValue(sizeStr.substring(0, sizeStr.length() - 1)), 0);
|
||||
} else {
|
||||
// Use provided default unit or fall back to MB
|
||||
String unit = defaultUnit != null ? defaultUnit.toUpperCase(Locale.ROOT) : "MB";
|
||||
double value = Double.parseDouble(sizeStr);
|
||||
BigDecimal value = parseSizeValue(sizeStr);
|
||||
return switch (unit) {
|
||||
case "TB" -> (long) (value * 1024L * 1024L * 1024L * 1024L);
|
||||
case "GB" -> (long) (value * 1024L * 1024L * 1024L);
|
||||
case "MB" -> (long) (value * 1024L * 1024L);
|
||||
case "KB" -> (long) (value * 1024L);
|
||||
case "B" -> (long) value;
|
||||
default -> (long) (value * 1024L * 1024L); // Default to MB
|
||||
case "TB" -> toBytes(value, 4);
|
||||
case "GB" -> toBytes(value, 3);
|
||||
case "MB" -> toBytes(value, 2);
|
||||
case "KB" -> toBytes(value, 1);
|
||||
case "B" -> toBytes(value, 0);
|
||||
default -> toBytes(value, 2); // Default to MB
|
||||
};
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
@@ -590,6 +583,30 @@ public class GeneralUtils {
|
||||
return convertSizeToBytes(sizeStr, "MB");
|
||||
}
|
||||
|
||||
private Long toBytes(BigDecimal value, int powerOf1024) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value.compareTo(BigDecimal.ZERO) < 0) {
|
||||
log.warn("Size value cannot be negative: {}", value);
|
||||
return null;
|
||||
}
|
||||
if (powerOf1024 < 0 || powerOf1024 > 4) {
|
||||
throw new IllegalArgumentException("Invalid power for size conversion: " + powerOf1024);
|
||||
}
|
||||
BigDecimal multiplier = powerOf1024 == 0 ? BigDecimal.ONE : KIB.pow(powerOf1024);
|
||||
BigDecimal bytes = value.multiply(multiplier).setScale(0, RoundingMode.DOWN);
|
||||
if (bytes.compareTo(LONG_MAX_DECIMAL) > 0) {
|
||||
log.warn("Size value too large to fit in long: {}", bytes);
|
||||
return null;
|
||||
}
|
||||
return bytes.longValue();
|
||||
}
|
||||
|
||||
private BigDecimal parseSizeValue(String value) {
|
||||
return new BigDecimal(value);
|
||||
}
|
||||
|
||||
/* Validates if a string represents a valid size unit. */
|
||||
private boolean isValidSizeUnit(String unit) {
|
||||
// Use a precomputed Set for O(1) lookup, normalize using a locale-safe toUpperCase
|
||||
|
||||
@@ -360,8 +360,6 @@ public class PDFToFile {
|
||||
Path inputFile, Path outputFile, String outputFormat, String libreOfficeFilter) {
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(runtimePathConfig.getUnoConvertPath());
|
||||
command.add("--port");
|
||||
command.add("2003");
|
||||
command.add("--convert-to");
|
||||
command.add(outputFormat);
|
||||
if (libreOfficeFilter != null && !libreOfficeFilter.isBlank()) {
|
||||
|
||||
@@ -6,9 +6,12 @@ import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -26,11 +29,15 @@ public class ProcessExecutor {
|
||||
|
||||
private static final Map<Processes, ProcessExecutor> instances = new ConcurrentHashMap<>();
|
||||
private static ApplicationProperties applicationProperties = new ApplicationProperties();
|
||||
private static volatile UnoServerPool unoServerPool;
|
||||
private final Semaphore semaphore;
|
||||
private final boolean liveUpdates;
|
||||
private long timeoutDuration;
|
||||
private final Processes processType;
|
||||
|
||||
private ProcessExecutor(int semaphoreLimit, boolean liveUpdates, long timeout) {
|
||||
private ProcessExecutor(
|
||||
Processes processType, int semaphoreLimit, boolean liveUpdates, long timeout) {
|
||||
this.processType = processType;
|
||||
this.semaphore = new Semaphore(semaphoreLimit);
|
||||
this.liveUpdates = liveUpdates;
|
||||
this.timeoutDuration = timeout;
|
||||
@@ -173,10 +180,15 @@ public class ProcessExecutor {
|
||||
.getTimeoutMinutes()
|
||||
.getFfmpegTimeoutMinutes();
|
||||
};
|
||||
return new ProcessExecutor(semaphoreLimit, liveUpdates, timeoutMinutes);
|
||||
return new ProcessExecutor(
|
||||
processType, semaphoreLimit, liveUpdates, timeoutMinutes);
|
||||
});
|
||||
}
|
||||
|
||||
public static void setUnoServerPool(UnoServerPool pool) {
|
||||
unoServerPool = pool;
|
||||
}
|
||||
|
||||
public ProcessExecutorResult runCommandWithOutputHandling(List<String> command)
|
||||
throws IOException, InterruptedException {
|
||||
return runCommandWithOutputHandling(command, null);
|
||||
@@ -186,11 +198,22 @@ public class ProcessExecutor {
|
||||
List<String> command, File workingDirectory) throws IOException, InterruptedException {
|
||||
String messages = "";
|
||||
int exitCode = 1;
|
||||
semaphore.acquire();
|
||||
UnoServerPool.UnoServerLease unoLease = null;
|
||||
boolean useSemaphore = true;
|
||||
List<String> commandToRun = command;
|
||||
if (shouldUseUnoServerPool(command)) {
|
||||
unoLease = unoServerPool.acquireEndpoint();
|
||||
commandToRun = applyUnoServerEndpoint(command, unoLease.getEndpoint());
|
||||
useSemaphore = false;
|
||||
}
|
||||
if (useSemaphore) {
|
||||
semaphore.acquire();
|
||||
}
|
||||
try {
|
||||
|
||||
log.info("Running command: {}", String.join(" ", command));
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
||||
validateCommand(commandToRun);
|
||||
log.info("Running command: {}", String.join(" ", commandToRun));
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(commandToRun);
|
||||
|
||||
// Use the working directory if it's set
|
||||
if (workingDirectory != null) {
|
||||
@@ -268,7 +291,9 @@ public class ProcessExecutor {
|
||||
outputReaderThread.join();
|
||||
|
||||
boolean isQpdf =
|
||||
command != null && !command.isEmpty() && command.get(0).contains("qpdf");
|
||||
commandToRun != null
|
||||
&& !commandToRun.isEmpty()
|
||||
&& commandToRun.get(0).contains("qpdf");
|
||||
|
||||
if (!outputLines.isEmpty()) {
|
||||
String outputMessage = String.join("\n", outputLines);
|
||||
@@ -309,11 +334,195 @@ public class ProcessExecutor {
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
semaphore.release();
|
||||
if (useSemaphore) {
|
||||
semaphore.release();
|
||||
}
|
||||
if (unoLease != null) {
|
||||
unoLease.close();
|
||||
}
|
||||
}
|
||||
return new ProcessExecutorResult(exitCode, messages);
|
||||
}
|
||||
|
||||
private boolean shouldUseUnoServerPool(List<String> command) {
|
||||
if (processType != Processes.LIBRE_OFFICE || unoServerPool == null) {
|
||||
return false;
|
||||
}
|
||||
if (unoServerPool.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (command == null || command.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if this is a UNO conversion by looking for unoconvert executable
|
||||
String executable = command.get(0);
|
||||
if (executable != null) {
|
||||
// Extract basename from path for matching
|
||||
String basename = executable;
|
||||
int lastSlash = Math.max(executable.lastIndexOf('/'), executable.lastIndexOf('\\'));
|
||||
if (lastSlash >= 0) {
|
||||
basename = executable.substring(lastSlash + 1);
|
||||
}
|
||||
// Strip .exe extension on Windows
|
||||
if (basename.toLowerCase(java.util.Locale.ROOT).endsWith(".exe")) {
|
||||
basename = basename.substring(0, basename.length() - 4);
|
||||
}
|
||||
// Match common unoconvert variants (but NOT soffice)
|
||||
String lowerBasename = basename.toLowerCase(java.util.Locale.ROOT);
|
||||
if (lowerBasename.contains("unoconvert") || lowerBasename.equals("unoconv")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<String> applyUnoServerEndpoint(
|
||||
List<String> command,
|
||||
ApplicationProperties.ProcessExecutor.UnoServerEndpoint endpoint) {
|
||||
if (endpoint == null || command == null || command.isEmpty()) {
|
||||
return command;
|
||||
}
|
||||
List<String> updated = stripUnoEndpointArgs(command);
|
||||
String host = endpoint.getHost();
|
||||
int port = endpoint.getPort();
|
||||
String hostLocation = endpoint.getHostLocation();
|
||||
String protocol = endpoint.getProtocol();
|
||||
|
||||
// Normalize and validate host
|
||||
if (host == null || host.isBlank()) {
|
||||
host = "127.0.0.1";
|
||||
}
|
||||
|
||||
// Normalize and validate port
|
||||
if (port <= 0) {
|
||||
port = 2003;
|
||||
}
|
||||
|
||||
// Normalize and validate hostLocation (only auto|local|remote allowed)
|
||||
if (hostLocation == null) {
|
||||
hostLocation = "auto";
|
||||
} else {
|
||||
hostLocation = hostLocation.trim().toLowerCase(java.util.Locale.ROOT);
|
||||
if (!Set.of("auto", "local", "remote").contains(hostLocation)) {
|
||||
log.warn(
|
||||
"Invalid hostLocation '{}' for endpoint {}:{}, defaulting to 'auto'",
|
||||
hostLocation,
|
||||
host,
|
||||
port);
|
||||
hostLocation = "auto";
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize and validate protocol (only http|https allowed)
|
||||
if (protocol == null) {
|
||||
protocol = "http";
|
||||
} else {
|
||||
protocol = protocol.trim().toLowerCase(java.util.Locale.ROOT);
|
||||
if (!Set.of("http", "https").contains(protocol)) {
|
||||
log.warn(
|
||||
"Invalid protocol '{}' for endpoint {}:{}, defaulting to 'http'",
|
||||
protocol,
|
||||
host,
|
||||
port);
|
||||
protocol = "http";
|
||||
}
|
||||
}
|
||||
|
||||
int insertIndex = Math.min(1, updated.size());
|
||||
updated.add(insertIndex++, "--host");
|
||||
updated.add(insertIndex++, host);
|
||||
updated.add(insertIndex++, "--port");
|
||||
updated.add(insertIndex++, String.valueOf(port));
|
||||
|
||||
// Only inject --host-location if non-default (for compatibility with older unoconvert)
|
||||
if (!"auto".equals(hostLocation)) {
|
||||
updated.add(insertIndex++, "--host-location");
|
||||
updated.add(insertIndex++, hostLocation);
|
||||
}
|
||||
|
||||
// Only inject --protocol if non-default (for compatibility with older unoconvert)
|
||||
if (!"http".equals(protocol)) {
|
||||
updated.add(insertIndex++, "--protocol");
|
||||
updated.add(insertIndex, protocol);
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
private List<String> stripUnoEndpointArgs(List<String> command) {
|
||||
List<String> stripped = new ArrayList<>(command.size());
|
||||
for (int i = 0; i < command.size(); i++) {
|
||||
String arg = command.get(i);
|
||||
if ("--host".equals(arg)
|
||||
|| "--port".equals(arg)
|
||||
|| "--host-location".equals(arg)
|
||||
|| "--protocol".equals(arg)) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (arg != null
|
||||
&& (arg.startsWith("--host=")
|
||||
|| arg.startsWith("--port=")
|
||||
|| arg.startsWith("--host-location=")
|
||||
|| arg.startsWith("--protocol="))) {
|
||||
continue;
|
||||
}
|
||||
stripped.add(arg);
|
||||
}
|
||||
return stripped;
|
||||
}
|
||||
|
||||
private void validateCommand(List<String> command) {
|
||||
if (command == null || command.isEmpty()) {
|
||||
throw new IllegalArgumentException("Command must not be empty");
|
||||
}
|
||||
|
||||
// Validate all arguments for null bytes and newlines (actual security concerns)
|
||||
for (String arg : command) {
|
||||
if (arg == null) {
|
||||
throw new IllegalArgumentException("Command contains null argument");
|
||||
}
|
||||
if (arg.indexOf('\0') >= 0 || arg.indexOf('\n') >= 0 || arg.indexOf('\r') >= 0) {
|
||||
throw new IllegalArgumentException("Command contains invalid characters");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate executable (first argument)
|
||||
String executable = command.get(0);
|
||||
if (executable == null || executable.isBlank()) {
|
||||
throw new IllegalArgumentException("Command executable must not be empty");
|
||||
}
|
||||
|
||||
// Check for path traversal in executable
|
||||
if (executable.contains("..")) {
|
||||
throw new IllegalArgumentException(
|
||||
"Command executable contains path traversal: " + executable);
|
||||
}
|
||||
|
||||
// For absolute paths, verify the file exists and is executable
|
||||
if (executable.contains("/") || executable.contains("\\")) {
|
||||
Path execPath;
|
||||
try {
|
||||
execPath = Path.of(executable);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("Invalid executable path: " + executable, e);
|
||||
}
|
||||
|
||||
if (!Files.exists(execPath)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Command executable does not exist: " + executable);
|
||||
}
|
||||
|
||||
if (!Files.isRegularFile(execPath)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Command executable is not a regular file: " + executable);
|
||||
}
|
||||
}
|
||||
// For relative paths, trust that PATH resolution will work or fail appropriately
|
||||
}
|
||||
|
||||
public enum Processes {
|
||||
LIBRE_OFFICE,
|
||||
PDFTOHTML,
|
||||
|
||||
@@ -38,12 +38,12 @@ public class RequestUriUtils {
|
||||
}
|
||||
|
||||
// Specific static files bundled with the frontend
|
||||
if (normalizedUri.equals("/robots.txt")
|
||||
|| normalizedUri.equals("/favicon.ico")
|
||||
|| normalizedUri.equals("/manifest.json")
|
||||
|| normalizedUri.equals("/site.webmanifest")
|
||||
|| normalizedUri.equals("/manifest-classic.json")
|
||||
|| normalizedUri.equals("/index.html")) {
|
||||
if ("/robots.txt".equals(normalizedUri)
|
||||
|| "/favicon.ico".equals(normalizedUri)
|
||||
|| "/manifest.json".equals(normalizedUri)
|
||||
|| "/site.webmanifest".equals(normalizedUri)
|
||||
|| "/manifest-classic.json".equals(normalizedUri)
|
||||
|| "/index.html".equals(normalizedUri)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -173,6 +173,12 @@ public class RequestUriUtils {
|
||||
"/api/v1/ui-data/footer-info") // Public footer configuration
|
||||
|| trimmedUri.startsWith("/api/v1/invite/validate")
|
||||
|| trimmedUri.startsWith("/api/v1/invite/accept")
|
||||
// Health Endoints
|
||||
|| trimmedUri.startsWith("/actuator/health")
|
||||
|| trimmedUri.startsWith("/health")
|
||||
|| trimmedUri.startsWith("/healthz")
|
||||
|| trimmedUri.startsWith("/liveness")
|
||||
|| trimmedUri.startsWith("/readiness")
|
||||
|| trimmedUri.startsWith(
|
||||
"/api/v1/mobile-scanner/") // Mobile scanner endpoints (no auth)
|
||||
|| trimmedUri.startsWith("/v1/api-docs");
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.xml.XMLConstants;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.transform.OutputKeys;
|
||||
import javax.xml.transform.Transformer;
|
||||
import javax.xml.transform.TransformerException;
|
||||
import javax.xml.transform.TransformerFactory;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NamedNodeMap;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.SsrfProtectionService;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class SvgSanitizer {
|
||||
|
||||
private static final Set<String> DANGEROUS_ELEMENTS =
|
||||
Set.of("script", "foreignobject", "iframe", "object", "embed", "handler", "listener");
|
||||
private static final Set<String> URL_ATTRIBUTES = Set.of("href", "xlink:href", "src", "data");
|
||||
private static final Pattern JAVASCRIPT_URL_PATTERN =
|
||||
Pattern.compile("^\\s*javascript\\s*:", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern DATA_SCRIPT_PATTERN =
|
||||
Pattern.compile(
|
||||
"^\\s*data\\s*:[^,]*(?:script|javascript|vbscript)", Pattern.CASE_INSENSITIVE);
|
||||
private final SsrfProtectionService ssrfProtectionService;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
public byte[] sanitize(byte[] svgBytes) throws IOException {
|
||||
if (svgBytes == null || svgBytes.length == 0) {
|
||||
throw new IOException("SVG input is empty or null");
|
||||
}
|
||||
|
||||
if (applicationProperties.getSystem().isDisableSanitize()) {
|
||||
log.debug("SVG sanitization disabled by configuration");
|
||||
return svgBytes;
|
||||
}
|
||||
|
||||
try {
|
||||
Document doc = parseSecurely(svgBytes);
|
||||
Element root = doc.getDocumentElement();
|
||||
if (root == null) {
|
||||
throw new IOException("SVG document has no root element");
|
||||
}
|
||||
|
||||
sanitizeNode(root);
|
||||
|
||||
byte[] result = serializeDocument(doc);
|
||||
if (result == null || result.length == 0) {
|
||||
throw new IOException("SVG sanitization produced empty output");
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (ParserConfigurationException | SAXException | TransformerException e) {
|
||||
throw new IOException("Failed to sanitize SVG content", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Document parseSecurely(byte[] svgBytes)
|
||||
throws ParserConfigurationException, SAXException, IOException {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
|
||||
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
|
||||
|
||||
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
|
||||
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
|
||||
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
|
||||
|
||||
factory.setXIncludeAware(false);
|
||||
factory.setExpandEntityReferences(false);
|
||||
factory.setNamespaceAware(true);
|
||||
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
return builder.parse(new ByteArrayInputStream(svgBytes));
|
||||
}
|
||||
|
||||
private void sanitizeNode(Node node) {
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
NodeList children = node.getChildNodes();
|
||||
Set<Node> nodesToRemove = new HashSet<>();
|
||||
|
||||
for (int i = 0; i < children.getLength(); i++) {
|
||||
Node child = children.item(i);
|
||||
if (child.getNodeType() == Node.ELEMENT_NODE) {
|
||||
String localName = child.getLocalName();
|
||||
if (localName == null) {
|
||||
localName = child.getNodeName();
|
||||
}
|
||||
|
||||
if (isDangerousElement(localName)) {
|
||||
log.warn("Removing dangerous SVG element: {}", localName);
|
||||
nodesToRemove.add(child);
|
||||
} else {
|
||||
sanitizeNode(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (Node toRemove : nodesToRemove) {
|
||||
node.removeChild(toRemove);
|
||||
}
|
||||
|
||||
if (node.getNodeType() == Node.ELEMENT_NODE) {
|
||||
sanitizeAttributes((Element) node);
|
||||
}
|
||||
}
|
||||
|
||||
private void sanitizeAttributes(Element element) {
|
||||
NamedNodeMap attributes = element.getAttributes();
|
||||
Set<String> attributesToRemove = new HashSet<>();
|
||||
|
||||
for (int i = 0; i < attributes.getLength(); i++) {
|
||||
Node attr = attributes.item(i);
|
||||
String attrName = attr.getNodeName().toLowerCase();
|
||||
String attrValue = attr.getNodeValue();
|
||||
|
||||
if (isEventHandler(attrName)) {
|
||||
log.warn("Removing event handler attribute: {}", attrName);
|
||||
attributesToRemove.add(attr.getNodeName());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isUrlAttribute(attrName)) {
|
||||
if (isDangerousUrl(attrValue)) {
|
||||
log.warn(
|
||||
"Removing dangerous URL in attribute {}: {}",
|
||||
attrName,
|
||||
truncateForLog(attrValue));
|
||||
attributesToRemove.add(attr.getNodeName());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isExternalUrl(attrValue) && !isUrlAllowed(attrValue)) {
|
||||
log.warn(
|
||||
"Removing SSRF-blocked URL in attribute {}: {}",
|
||||
attrName,
|
||||
truncateForLog(attrValue));
|
||||
attributesToRemove.add(attr.getNodeName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (String attrName : attributesToRemove) {
|
||||
element.removeAttribute(attrName);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDangerousElement(String localName) {
|
||||
return DANGEROUS_ELEMENTS.contains(localName.toLowerCase());
|
||||
}
|
||||
|
||||
private boolean isEventHandler(String attrName) {
|
||||
return attrName.startsWith("on");
|
||||
}
|
||||
|
||||
private boolean isUrlAttribute(String attrName) {
|
||||
return URL_ATTRIBUTES.contains(attrName.toLowerCase())
|
||||
|| attrName.toLowerCase().endsWith(":href");
|
||||
}
|
||||
|
||||
private boolean isDangerousUrl(String url) {
|
||||
if (url == null || url.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String normalized = normalizeUrl(url);
|
||||
|
||||
if (JAVASCRIPT_URL_PATTERN.matcher(normalized).find()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (DATA_SCRIPT_PATTERN.matcher(normalized).find()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private String normalizeUrl(String url) {
|
||||
if (url == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String result = url.trim();
|
||||
|
||||
result = result.replaceAll("\u0000", "");
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
try {
|
||||
String decoded = URLDecoder.decode(result, StandardCharsets.UTF_8);
|
||||
if (decoded.equals(result)) {
|
||||
break; // No more decoding needed
|
||||
}
|
||||
result = decoded;
|
||||
} catch (Exception e) {
|
||||
log.debug("Failed to decode URL, continuing with current value", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result.toLowerCase();
|
||||
}
|
||||
|
||||
private boolean isExternalUrl(String url) {
|
||||
if (url == null || url.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String normalized = normalizeUrl(url);
|
||||
|
||||
if (normalized.startsWith("#")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (normalized.startsWith("data:")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return normalized.startsWith("http://")
|
||||
|| normalized.startsWith("https://")
|
||||
|| normalized.startsWith("//")
|
||||
|| normalized.startsWith("file:");
|
||||
}
|
||||
|
||||
private boolean isUrlAllowed(String url) {
|
||||
if (ssrfProtectionService == null) {
|
||||
return true;
|
||||
}
|
||||
return ssrfProtectionService.isUrlAllowed(url);
|
||||
}
|
||||
|
||||
private byte[] serializeDocument(Document doc) throws TransformerException {
|
||||
TransformerFactory transformerFactory = TransformerFactory.newInstance();
|
||||
transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
|
||||
|
||||
Transformer transformer = transformerFactory.newTransformer();
|
||||
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
|
||||
transformer.setOutputProperty(OutputKeys.INDENT, "no");
|
||||
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
transformer.transform(new DOMSource(doc), new StreamResult(outputStream));
|
||||
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
private String truncateForLog(String value) {
|
||||
if (value == null) {
|
||||
return "null";
|
||||
}
|
||||
return value.length() > 50 ? value.substring(0, 50) + "..." : value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
public class UnoServerPool {
|
||||
|
||||
private final List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints;
|
||||
private final BlockingQueue<Integer> availableIndices;
|
||||
|
||||
public UnoServerPool(List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints) {
|
||||
if (endpoints == null || endpoints.isEmpty()) {
|
||||
this.endpoints = Collections.emptyList();
|
||||
this.availableIndices = new LinkedBlockingQueue<>();
|
||||
} else {
|
||||
this.endpoints = new ArrayList<>(endpoints);
|
||||
this.availableIndices = new LinkedBlockingQueue<>();
|
||||
// Initialize queue with all endpoint indices
|
||||
for (int i = 0; i < this.endpoints.size(); i++) {
|
||||
this.availableIndices.offer(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return endpoints.isEmpty();
|
||||
}
|
||||
|
||||
public UnoServerLease acquireEndpoint() throws InterruptedException {
|
||||
if (endpoints.isEmpty()) {
|
||||
return new UnoServerLease(defaultEndpoint(), null, this);
|
||||
}
|
||||
|
||||
// Block until an endpoint index becomes available
|
||||
Integer index = availableIndices.take();
|
||||
return new UnoServerLease(endpoints.get(index), index, this);
|
||||
}
|
||||
|
||||
private void releaseEndpoint(Integer index) {
|
||||
if (index != null) {
|
||||
availableIndices.offer(index);
|
||||
}
|
||||
}
|
||||
|
||||
private static ApplicationProperties.ProcessExecutor.UnoServerEndpoint defaultEndpoint() {
|
||||
return new ApplicationProperties.ProcessExecutor.UnoServerEndpoint();
|
||||
}
|
||||
|
||||
public static class UnoServerLease implements AutoCloseable {
|
||||
private final ApplicationProperties.ProcessExecutor.UnoServerEndpoint endpoint;
|
||||
private final Integer index;
|
||||
private final UnoServerPool pool;
|
||||
private final AtomicBoolean closed = new AtomicBoolean(false);
|
||||
|
||||
public UnoServerLease(
|
||||
ApplicationProperties.ProcessExecutor.UnoServerEndpoint endpoint,
|
||||
Integer index,
|
||||
UnoServerPool pool) {
|
||||
this.endpoint = endpoint;
|
||||
this.index = index;
|
||||
this.pool = pool;
|
||||
}
|
||||
|
||||
public ApplicationProperties.ProcessExecutor.UnoServerEndpoint getEndpoint() {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// Idempotent close: only release once even if close() called multiple times
|
||||
if (!closed.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
if (pool != null && index != null) {
|
||||
pool.releaseEndpoint(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-88
@@ -1,88 +0,0 @@
|
||||
package stirling.software.common.model;
|
||||
|
||||
/* Commented out - InputStreamTemplateResource class removed with Thymeleaf migration
|
||||
* This test will be removed when frontend migration to React is complete
|
||||
|
||||
|
||||
public class InputStreamTemplateResourceTest {
|
||||
|
||||
@Test
|
||||
void gettersReturnProvidedFields() {
|
||||
byte[] data = {1, 2, 3};
|
||||
InputStream is = new ByteArrayInputStream(data);
|
||||
String encoding = "UTF-8";
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, encoding);
|
||||
|
||||
assertSame(is, resource.getInputStream());
|
||||
assertEquals(encoding, resource.getCharacterEncoding());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldsAreFinal() throws NoSuchFieldException {
|
||||
Field inputStreamField = InputStreamTemplateResource.class.getDeclaredField("inputStream");
|
||||
Field characterEncodingField =
|
||||
InputStreamTemplateResource.class.getDeclaredField("characterEncoding");
|
||||
|
||||
assertTrue(Modifier.isFinal(inputStreamField.getModifiers()));
|
||||
assertTrue(Modifier.isFinal(characterEncodingField.getModifiers()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void noSetterMethodsPresent() {
|
||||
long setterCount =
|
||||
Arrays.stream(InputStreamTemplateResource.class.getDeclaredMethods())
|
||||
.filter(method -> method.getName().startsWith("set"))
|
||||
.count();
|
||||
|
||||
assertEquals(0, setterCount, "InputStreamTemplateResource should not have setter methods");
|
||||
}
|
||||
|
||||
@Test
|
||||
void readerReturnsCorrectContent() throws Exception {
|
||||
String content = "Hello, world!";
|
||||
InputStream is = new ByteArrayInputStream(content.getBytes("UTF-8"));
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
|
||||
|
||||
try (Reader reader = resource.reader()) {
|
||||
char[] buffer = new char[content.length()];
|
||||
int read = reader.read(buffer);
|
||||
assertEquals(content.length(), read);
|
||||
assertEquals(content, new String(buffer));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void relativeThrowsUnsupportedOperationException() {
|
||||
InputStream is = new ByteArrayInputStream(new byte[0]);
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
|
||||
assertThrows(UnsupportedOperationException.class, () -> resource.relative("other"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getDescriptionReturnsExpectedString() {
|
||||
InputStream is = new ByteArrayInputStream(new byte[0]);
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
|
||||
assertEquals("InputStream resource [Stream]", resource.getDescription());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBaseNameReturnsExpectedString() {
|
||||
InputStream is = new ByteArrayInputStream(new byte[0]);
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
|
||||
assertEquals("streamResource", resource.getBaseName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void existsReturnsTrueWhenInputStreamNotNull() {
|
||||
InputStream is = new ByteArrayInputStream(new byte[0]);
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
|
||||
assertTrue(resource.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
void existsReturnsFalseWhenInputStreamIsNull() {
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(null, "UTF-8");
|
||||
assertFalse(resource.exists());
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -9,6 +9,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -34,7 +35,8 @@ class FileMonitorTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
when(runtimePathConfig.getPipelineWatchedFoldersPath()).thenReturn(tempDir.toString());
|
||||
when(runtimePathConfig.getPipelineWatchedFoldersPaths())
|
||||
.thenReturn(List.of(tempDir.toString()));
|
||||
|
||||
// This mock is used in all tests except testPathFilter
|
||||
// We use lenient to avoid UnnecessaryStubbingException in that test
|
||||
|
||||
@@ -16,6 +16,14 @@ class GeneralUtilsAdditionalTest {
|
||||
assertNull(GeneralUtils.convertSizeToBytes(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertSizeToBytesEdgeCases() {
|
||||
assertNull(GeneralUtils.convertSizeToBytes("-10MB"));
|
||||
assertNull(GeneralUtils.convertSizeToBytes("10000000TB")); // overflow beyond long
|
||||
assertEquals(1099511627776L, GeneralUtils.convertSizeToBytes("1TB"));
|
||||
assertEquals(2684354560L, GeneralUtils.convertSizeToBytes("2.5GB"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFormatBytes() {
|
||||
assertEquals("512 B", GeneralUtils.formatBytes(512));
|
||||
|
||||
@@ -37,21 +37,50 @@ public class ProcessExecutorTest {
|
||||
|
||||
@Test
|
||||
public void testRunCommandWithOutputHandling_Error() {
|
||||
// Mock the command to execute
|
||||
// Test with a command that will fail to execute (non-existent command)
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("nonexistent-command");
|
||||
command.add("nonexistent-command-that-does-not-exist");
|
||||
|
||||
// Execute the command and expect an IOException
|
||||
IOException thrown =
|
||||
// Execute the command and expect an IOException (command not found)
|
||||
assertThrows(
|
||||
IOException.class, () -> processExecutor.runCommandWithOutputHandling(command));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRunCommandWithOutputHandling_PathTraversal() {
|
||||
// Test that path traversal is blocked
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("../../../etc/passwd");
|
||||
|
||||
// Execute the command and expect an IllegalArgumentException
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IOException.class,
|
||||
IllegalArgumentException.class,
|
||||
() -> processExecutor.runCommandWithOutputHandling(command));
|
||||
|
||||
// Check the exception message to ensure it indicates the command was not found
|
||||
// Check the exception message
|
||||
String errorMessage = thrown.getMessage();
|
||||
assertTrue(
|
||||
errorMessage.contains("error=2")
|
||||
|| errorMessage.contains("No such file or directory"),
|
||||
errorMessage.contains("path traversal"),
|
||||
"Unexpected error message: " + errorMessage);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRunCommandWithOutputHandling_NullByte() {
|
||||
// Test that null bytes are blocked
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("test\0command");
|
||||
|
||||
// Execute the command and expect an IllegalArgumentException
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> processExecutor.runCommandWithOutputHandling(command));
|
||||
|
||||
// Check the exception message
|
||||
String errorMessage = thrown.getMessage();
|
||||
assertTrue(
|
||||
errorMessage.contains("invalid characters"),
|
||||
"Unexpected error message: " + errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
public class UnoServerPoolTest {
|
||||
|
||||
@Test
|
||||
void testEmptyPool() throws InterruptedException {
|
||||
UnoServerPool pool = new UnoServerPool(Collections.emptyList());
|
||||
assertTrue(pool.isEmpty(), "Pool with empty list should be empty");
|
||||
|
||||
UnoServerPool.UnoServerLease lease = pool.acquireEndpoint();
|
||||
assertNotNull(lease, "Should return a default lease for empty pool");
|
||||
assertNotNull(lease.getEndpoint(), "Default lease should have an endpoint");
|
||||
lease.close(); // Should not throw
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSingleEndpointAcquireRelease() throws InterruptedException {
|
||||
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints =
|
||||
createEndpoints(1);
|
||||
UnoServerPool pool = new UnoServerPool(endpoints);
|
||||
assertFalse(pool.isEmpty(), "Pool should not be empty");
|
||||
|
||||
UnoServerPool.UnoServerLease lease = pool.acquireEndpoint();
|
||||
assertNotNull(lease, "Should acquire endpoint");
|
||||
assertEquals("127.0.0.1", lease.getEndpoint().getHost());
|
||||
assertEquals(2003, lease.getEndpoint().getPort());
|
||||
|
||||
lease.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleEndpointsDistribution() throws InterruptedException {
|
||||
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints =
|
||||
createEndpoints(3);
|
||||
UnoServerPool pool = new UnoServerPool(endpoints);
|
||||
|
||||
List<Integer> portsUsed = new ArrayList<>();
|
||||
|
||||
// Acquire all endpoints
|
||||
try (UnoServerPool.UnoServerLease lease1 = pool.acquireEndpoint();
|
||||
UnoServerPool.UnoServerLease lease2 = pool.acquireEndpoint();
|
||||
UnoServerPool.UnoServerLease lease3 = pool.acquireEndpoint()) {
|
||||
|
||||
portsUsed.add(lease1.getEndpoint().getPort());
|
||||
portsUsed.add(lease2.getEndpoint().getPort());
|
||||
portsUsed.add(lease3.getEndpoint().getPort());
|
||||
|
||||
// All three endpoints should be in use (different ports)
|
||||
assertEquals(3, portsUsed.stream().distinct().count(), "Should use all 3 endpoints");
|
||||
}
|
||||
// All released after try-with-resources
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConcurrentAccess() throws InterruptedException {
|
||||
int endpointCount = 3;
|
||||
int threadCount = 10;
|
||||
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints =
|
||||
createEndpoints(endpointCount);
|
||||
UnoServerPool pool = new UnoServerPool(endpoints);
|
||||
|
||||
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
||||
CountDownLatch startLatch = new CountDownLatch(1);
|
||||
CountDownLatch doneLatch = new CountDownLatch(threadCount);
|
||||
AtomicInteger successCount = new AtomicInteger(0);
|
||||
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
executor.submit(
|
||||
() -> {
|
||||
try {
|
||||
startLatch.await(); // Wait for all threads to be ready
|
||||
UnoServerPool.UnoServerLease lease = pool.acquireEndpoint();
|
||||
assertNotNull(lease, "Should acquire endpoint");
|
||||
Thread.sleep(10); // Simulate work
|
||||
lease.close();
|
||||
successCount.incrementAndGet();
|
||||
} catch (Exception e) {
|
||||
fail("Thread failed: " + e.getMessage());
|
||||
} finally {
|
||||
doneLatch.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
startLatch.countDown(); // Start all threads
|
||||
boolean finished = doneLatch.await(5, TimeUnit.SECONDS);
|
||||
executor.shutdown();
|
||||
|
||||
assertTrue(finished, "All threads should complete within timeout");
|
||||
assertEquals(
|
||||
threadCount, successCount.get(), "All threads should successfully acquire/release");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBlockingBehavior() throws InterruptedException {
|
||||
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints =
|
||||
createEndpoints(2);
|
||||
UnoServerPool pool = new UnoServerPool(endpoints);
|
||||
|
||||
// Acquire both endpoints
|
||||
UnoServerPool.UnoServerLease lease1 = pool.acquireEndpoint();
|
||||
UnoServerPool.UnoServerLease lease2 = pool.acquireEndpoint();
|
||||
|
||||
AtomicInteger acquired = new AtomicInteger(0);
|
||||
CountDownLatch acquireLatch = new CountDownLatch(1);
|
||||
|
||||
// Try to acquire a third endpoint in separate thread (should block)
|
||||
Thread blockingThread =
|
||||
new Thread(
|
||||
() -> {
|
||||
try {
|
||||
acquireLatch.countDown(); // Signal we're about to block
|
||||
UnoServerPool.UnoServerLease lease3 = pool.acquireEndpoint();
|
||||
acquired.incrementAndGet();
|
||||
lease3.close();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
});
|
||||
|
||||
blockingThread.start();
|
||||
acquireLatch.await(); // Wait for thread to start
|
||||
Thread.sleep(100); // Give it time to block
|
||||
|
||||
// Should still be 0 because thread is blocked
|
||||
assertEquals(0, acquired.get(), "Third acquire should be blocked");
|
||||
|
||||
// Release one endpoint
|
||||
lease1.close();
|
||||
Thread.sleep(100); // Give blocked thread time to acquire
|
||||
|
||||
// Now the third acquire should succeed
|
||||
assertEquals(1, acquired.get(), "Third acquire should succeed after release");
|
||||
|
||||
lease2.close();
|
||||
blockingThread.join(1000);
|
||||
assertFalse(blockingThread.isAlive(), "Thread should complete");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEndpointReuse() throws InterruptedException {
|
||||
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints =
|
||||
createEndpoints(1);
|
||||
UnoServerPool pool = new UnoServerPool(endpoints);
|
||||
|
||||
int port1, port2;
|
||||
|
||||
try (UnoServerPool.UnoServerLease lease1 = pool.acquireEndpoint()) {
|
||||
port1 = lease1.getEndpoint().getPort();
|
||||
}
|
||||
|
||||
try (UnoServerPool.UnoServerLease lease2 = pool.acquireEndpoint()) {
|
||||
port2 = lease2.getEndpoint().getPort();
|
||||
}
|
||||
|
||||
assertEquals(port1, port2, "Should reuse the same endpoint after release");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHostLocationAndProtocol() throws InterruptedException {
|
||||
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints = new ArrayList<>();
|
||||
ApplicationProperties.ProcessExecutor.UnoServerEndpoint endpoint =
|
||||
new ApplicationProperties.ProcessExecutor.UnoServerEndpoint();
|
||||
endpoint.setHost("remote.server");
|
||||
endpoint.setPort(8080);
|
||||
endpoint.setHostLocation("remote");
|
||||
endpoint.setProtocol("https");
|
||||
endpoints.add(endpoint);
|
||||
|
||||
UnoServerPool pool = new UnoServerPool(endpoints);
|
||||
|
||||
try (UnoServerPool.UnoServerLease lease = pool.acquireEndpoint()) {
|
||||
assertEquals("remote.server", lease.getEndpoint().getHost());
|
||||
assertEquals(8080, lease.getEndpoint().getPort());
|
||||
assertEquals("remote", lease.getEndpoint().getHostLocation());
|
||||
assertEquals("https", lease.getEndpoint().getProtocol());
|
||||
}
|
||||
}
|
||||
|
||||
private List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> createEndpoints(
|
||||
int count) {
|
||||
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints = new ArrayList<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
ApplicationProperties.ProcessExecutor.UnoServerEndpoint endpoint =
|
||||
new ApplicationProperties.ProcessExecutor.UnoServerEndpoint();
|
||||
endpoint.setHost("127.0.0.1");
|
||||
endpoint.setPort(2003 + (i * 2));
|
||||
endpoints.add(endpoint);
|
||||
}
|
||||
return endpoints;
|
||||
}
|
||||
}
|
||||
+5
-24
@@ -2,11 +2,6 @@ apply plugin: 'org.springframework.boot'
|
||||
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
repositories {
|
||||
maven { url = 'https://build.shibboleth.net/maven/releases' }
|
||||
maven { url = 'https://maven.pkg.github.com/jcefmaven/jcefmaven' }
|
||||
}
|
||||
|
||||
configurations {
|
||||
developmentOnly
|
||||
runtimeClasspath {
|
||||
@@ -43,13 +38,6 @@ spotless {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
if (System.getenv('STIRLING_PDF_DESKTOP_UI') != 'false'
|
||||
|| (project.hasProperty('STIRLING_PDF_DESKTOP_UI')
|
||||
&& project.getProperty('STIRLING_PDF_DESKTOP_UI') != 'false')) {
|
||||
implementation 'org.openjfx:javafx-controls:21'
|
||||
implementation 'org.openjfx:javafx-swing:21'
|
||||
}
|
||||
|
||||
if (System.getenv('DISABLE_ADDITIONAL_FEATURES') != 'true'
|
||||
|| (project.hasProperty('DISABLE_ADDITIONAL_FEATURES')
|
||||
&& System.getProperty('DISABLE_ADDITIONAL_FEATURES') != 'true')) {
|
||||
@@ -63,7 +51,7 @@ dependencies {
|
||||
implementation 'commons-io:commons-io:2.21.0'
|
||||
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
||||
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
||||
implementation 'io.micrometer:micrometer-core:1.16.1'
|
||||
implementation 'io.micrometer:micrometer-core:1.16.2'
|
||||
implementation 'com.google.zxing:core:3.5.4'
|
||||
implementation "org.commonmark:commonmark:$commonmarkVersion" // https://mvnrepository.com/artifact/org.commonmark/commonmark
|
||||
implementation "org.commonmark:commonmark-ext-gfm-tables:$commonmarkVersion"
|
||||
@@ -91,6 +79,9 @@ dependencies {
|
||||
// Batik
|
||||
implementation 'org.apache.xmlgraphics:batik-all:1.19'
|
||||
|
||||
// PDFBox Graphics2D bridge for Batik SVG to PDF conversion
|
||||
implementation 'de.rototor.pdfbox:graphics2d:3.0.5'
|
||||
|
||||
// TwelveMonkeys
|
||||
runtimeOnly "com.twelvemonkeys.imageio:imageio-batik:$imageioVersion"
|
||||
runtimeOnly "com.twelvemonkeys.imageio:imageio-bmp:$imageioVersion"
|
||||
@@ -117,19 +108,8 @@ sourceSets {
|
||||
resources {
|
||||
srcDirs += ['../configs']
|
||||
}
|
||||
java {
|
||||
if (System.getenv('STIRLING_PDF_DESKTOP_UI') == 'false') {
|
||||
exclude 'stirling/software/SPDF/UI/impl/**'
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
test {
|
||||
java {
|
||||
if (System.getenv('STIRLING_PDF_DESKTOP_UI') == 'false') {
|
||||
exclude 'stirling/software/SPDF/UI/impl/**'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -241,6 +221,7 @@ tasks.register('npmBuild', Exec) {
|
||||
commandLine = Os.isFamily(Os.FAMILY_WINDOWS) ? ['cmd', '/c', 'npm', 'run', 'build'] : ['npm', 'run', 'build']
|
||||
dependsOn npmInstall
|
||||
inputs.dir(new File(frontendDir, 'src'))
|
||||
inputs.dir(new File(frontendDir, 'public'))
|
||||
inputs.file(new File(frontendDir, 'package.json'))
|
||||
outputs.dir(frontendDistDir)
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import io.github.pixee.security.SystemCommand;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -28,7 +27,6 @@ import stirling.software.common.configuration.AppConfig;
|
||||
import stirling.software.common.configuration.ConfigInitializer;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.UrlUtils;
|
||||
|
||||
@Slf4j
|
||||
@EnableScheduling
|
||||
@@ -60,19 +58,6 @@ public class SPDFApplication {
|
||||
|
||||
Properties props = new Properties();
|
||||
|
||||
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
||||
System.setProperty("java.awt.headless", "false");
|
||||
app.setHeadless(false);
|
||||
props.put("java.awt.headless", "false");
|
||||
props.put("spring.main.web-application-type", "servlet");
|
||||
|
||||
int desiredPort = 8080;
|
||||
String port = UrlUtils.findAvailablePort(desiredPort);
|
||||
props.put("server.port", port);
|
||||
System.setProperty("server.port", port);
|
||||
log.info("Desktop UI mode: Using port {}", port);
|
||||
}
|
||||
|
||||
app.setAdditionalProfiles(getActiveProfile(args));
|
||||
|
||||
ConfigInitializer initializer = new ConfigInitializer();
|
||||
@@ -153,13 +138,6 @@ public class SPDFApplication {
|
||||
"Running in Tauri mode. Parent process PID: {}",
|
||||
parentPid != null ? parentPid : "not set");
|
||||
}
|
||||
// Desktop UI initialization removed - webBrowser dependency eliminated
|
||||
// Keep backwards compatibility for STIRLING_PDF_DESKTOP_UI system property
|
||||
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
||||
log.info("Desktop UI mode enabled, but WebBrowser functionality has been removed");
|
||||
// webBrowser.initWebUI(url); // Removed - desktop UI eliminated
|
||||
}
|
||||
|
||||
// Standard browser opening logic
|
||||
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
|
||||
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
|
||||
@@ -192,14 +170,6 @@ public class SPDFApplication {
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void cleanup() {
|
||||
// webBrowser cleanup removed - desktop UI eliminated
|
||||
// if (webBrowser != null) {
|
||||
// webBrowser.cleanup();
|
||||
// }
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onWebServerInitialized(WebServerInitializedEvent event) {
|
||||
int actualPort = event.getWebServer().getPort();
|
||||
|
||||
@@ -118,7 +118,8 @@ public class ExternalAppDepConfig {
|
||||
|
||||
for (String group : affectedGroups) {
|
||||
List<String> affectedFeatures = getAffectedFeatures(group);
|
||||
endpointConfiguration.disableGroup(group);
|
||||
endpointConfiguration.disableGroup(
|
||||
group, EndpointConfiguration.DisableReason.DEPENDENCY);
|
||||
log.warn(
|
||||
"Missing dependency: {} - Disabling group: {} (Affected features: {})",
|
||||
command,
|
||||
@@ -143,7 +144,8 @@ public class ExternalAppDepConfig {
|
||||
commandToGroupMapping.getOrDefault(
|
||||
command, List.of("Weasyprint"));
|
||||
for (String group : affectedGroups) {
|
||||
endpointConfiguration.disableGroup(group);
|
||||
endpointConfiguration.disableGroup(
|
||||
group, EndpointConfiguration.DisableReason.DEPENDENCY);
|
||||
}
|
||||
log.warn(
|
||||
"WeasyPrint version {} is below required {} - disabling"
|
||||
@@ -172,7 +174,8 @@ public class ExternalAppDepConfig {
|
||||
List<String> affectedGroups =
|
||||
commandToGroupMapping.getOrDefault(command, List.of("qpdf"));
|
||||
for (String group : affectedGroups) {
|
||||
endpointConfiguration.disableGroup(group);
|
||||
endpointConfiguration.disableGroup(
|
||||
group, EndpointConfiguration.DisableReason.DEPENDENCY);
|
||||
}
|
||||
log.warn(
|
||||
"qpdf version {} is below required {} - disabling group(s): {}",
|
||||
@@ -226,7 +229,8 @@ public class ExternalAppDepConfig {
|
||||
int ec = runAndWait(List.of(python, "-c", "import cv2"), DEFAULT_TIMEOUT).exitCode();
|
||||
if (ec != 0) {
|
||||
List<String> openCVFeatures = getAffectedFeatures("OpenCV");
|
||||
endpointConfiguration.disableGroup("OpenCV");
|
||||
endpointConfiguration.disableGroup(
|
||||
"OpenCV", EndpointConfiguration.DisableReason.DEPENDENCY);
|
||||
log.warn(
|
||||
"OpenCV not available in Python - Disabling OpenCV features: {}",
|
||||
String.join(", ", openCVFeatures));
|
||||
@@ -236,8 +240,10 @@ public class ExternalAppDepConfig {
|
||||
private void disablePythonAndOpenCV(String reason) {
|
||||
List<String> pythonFeatures = getAffectedFeatures("Python");
|
||||
List<String> openCVFeatures = getAffectedFeatures("OpenCV");
|
||||
endpointConfiguration.disableGroup("Python");
|
||||
endpointConfiguration.disableGroup("OpenCV");
|
||||
endpointConfiguration.disableGroup(
|
||||
"Python", EndpointConfiguration.DisableReason.DEPENDENCY);
|
||||
endpointConfiguration.disableGroup(
|
||||
"OpenCV", EndpointConfiguration.DisableReason.DEPENDENCY);
|
||||
log.warn(
|
||||
"Missing dependency: Python (reason: {}) - Disabling Python features: {} and OpenCV"
|
||||
+ " features: {}",
|
||||
|
||||
@@ -106,16 +106,29 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
} else if (hasConfiguredOrigins) {
|
||||
// Use user-configured origins
|
||||
// Use user-configured origins + always include Tauri origins for desktop app support
|
||||
logger.info(
|
||||
"Configuring CORS with allowed origins: {}",
|
||||
applicationProperties.getSystem().getCorsAllowedOrigins());
|
||||
|
||||
String[] allowedOrigins =
|
||||
applicationProperties
|
||||
.getSystem()
|
||||
.getCorsAllowedOrigins()
|
||||
.toArray(new String[0]);
|
||||
// Combine user-configured origins with Tauri origins
|
||||
java.util.List<String> allOrigins =
|
||||
new java.util.ArrayList<>(
|
||||
applicationProperties.getSystem().getCorsAllowedOrigins());
|
||||
|
||||
// Always include Tauri origins for desktop app compatibility
|
||||
// Tauri v1 uses tauri://localhost, v2 uses http(s)://tauri.localhost
|
||||
if (!allOrigins.contains("tauri://localhost")) {
|
||||
allOrigins.add("tauri://localhost");
|
||||
}
|
||||
if (!allOrigins.contains("http://tauri.localhost")) {
|
||||
allOrigins.add("http://tauri.localhost");
|
||||
}
|
||||
if (!allOrigins.contains("https://tauri.localhost")) {
|
||||
allOrigins.add("https://tauri.localhost");
|
||||
}
|
||||
|
||||
String[] allowedOrigins = allOrigins.toArray(new String[0]);
|
||||
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns(allowedOrigins)
|
||||
|
||||
@@ -18,26 +18,22 @@ import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.CropPdfForm;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@GeneralApi
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class CropController {
|
||||
@@ -122,7 +118,7 @@ public class CropController {
|
||||
return r >= threshold && g >= threshold && b >= threshold;
|
||||
}
|
||||
|
||||
@PostMapping(value = "/crop", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/crop", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(
|
||||
summary = "Crops a PDF document",
|
||||
description =
|
||||
|
||||
+13
-10
@@ -19,7 +19,6 @@ import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -27,36 +26,38 @@ import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.EditTableOfContentsRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@GeneralApi
|
||||
@Slf4j
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@RequiredArgsConstructor
|
||||
public class EditTableOfContentsController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@PostMapping(value = "/extract-bookmarks", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(
|
||||
value = "/extract-bookmarks",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(
|
||||
summary = "Extract PDF Bookmarks",
|
||||
description = "Extracts bookmarks/table of contents from a PDF document as JSON.")
|
||||
@ResponseBody
|
||||
public List<Map<String, Object>> extractBookmarks(@RequestParam("file") MultipartFile file)
|
||||
public ResponseEntity<List<Map<String, Object>>> extractBookmarks(@RequestParam("file") MultipartFile file)
|
||||
throws Exception {
|
||||
try (PDDocument document = pdfDocumentFactory.load(file)) {
|
||||
PDDocumentOutline outline = document.getDocumentCatalog().getDocumentOutline();
|
||||
|
||||
if (outline == null) {
|
||||
log.info("No outline/bookmarks found in PDF");
|
||||
return new ArrayList<>();
|
||||
return ResponseEntity.ok(new ArrayList<>());
|
||||
}
|
||||
|
||||
return extractBookmarkItems(document, outline);
|
||||
List<Map<String, Object>> bookmarks = extractBookmarkItems(document, outline);
|
||||
return ResponseEntity.ok(bookmarks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +143,9 @@ public class EditTableOfContentsController {
|
||||
return bookmark;
|
||||
}
|
||||
|
||||
@PostMapping(value = "/edit-table-of-contents", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(
|
||||
value = "/edit-table-of-contents",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(
|
||||
summary = "Edit Table of Contents",
|
||||
description = "Add or edit bookmarks/table of contents in a PDF document.")
|
||||
|
||||
+6
-8
@@ -14,34 +14,32 @@ import org.apache.pdfbox.util.Matrix;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralFormCopyUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@GeneralApi
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class MultiPageLayoutController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(value = "/multi-page-layout", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(
|
||||
value = "/multi-page-layout",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(
|
||||
summary = "Merge multiple pages of a PDF document into a single page",
|
||||
description =
|
||||
|
||||
+4
-8
@@ -15,26 +15,22 @@ import org.apache.pdfbox.util.Matrix;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.ScalePagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@GeneralApi
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class ScalePagesController {
|
||||
@@ -116,7 +112,7 @@ public class ScalePagesController {
|
||||
return sizeMap;
|
||||
}
|
||||
|
||||
@PostMapping(value = "/scale-pages", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/scale-pages", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(
|
||||
summary = "Change the size of a PDF page/document",
|
||||
description =
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
@@ -52,385 +49,4 @@ public class SettingsController {
|
||||
public ResponseEntity<Map<String, Boolean>> getDisabledEndpoints() {
|
||||
return ResponseEntity.ok(endpointConfiguration.getEndpointStatuses());
|
||||
}
|
||||
|
||||
// ========== GENERAL SETTINGS ==========
|
||||
|
||||
@GetMapping("/admin/settings/general")
|
||||
@Hidden
|
||||
public ResponseEntity<Map<String, Object>> getGeneralSettings() {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
settings.put("ui", applicationProperties.getUi());
|
||||
settings.put(
|
||||
"system",
|
||||
Map.of(
|
||||
"defaultLocale", applicationProperties.getSystem().getDefaultLocale(),
|
||||
"showUpdate", applicationProperties.getSystem().isShowUpdate(),
|
||||
"showUpdateOnlyAdmin",
|
||||
applicationProperties.getSystem().isShowUpdateOnlyAdmin(),
|
||||
"customHTMLFiles", applicationProperties.getSystem().isCustomHTMLFiles(),
|
||||
"fileUploadLimit", applicationProperties.getSystem().getFileUploadLimit()));
|
||||
return ResponseEntity.ok(settings);
|
||||
}
|
||||
|
||||
@PostMapping("/admin/settings/general")
|
||||
@Hidden
|
||||
public ResponseEntity<String> updateGeneralSettings(@RequestBody Map<String, Object> settings)
|
||||
throws IOException {
|
||||
// Update UI settings
|
||||
if (settings.containsKey("ui")) {
|
||||
Map<String, String> ui = (Map<String, String>) settings.get("ui");
|
||||
if (ui.containsKey("appNameNavbar")) {
|
||||
GeneralUtils.saveKeyToSettings("ui.appNameNavbar", ui.get("appNameNavbar"));
|
||||
applicationProperties.getUi().setAppNameNavbar(ui.get("appNameNavbar"));
|
||||
}
|
||||
}
|
||||
|
||||
// Update System settings
|
||||
if (settings.containsKey("system")) {
|
||||
Map<String, Object> system = (Map<String, Object>) settings.get("system");
|
||||
if (system.containsKey("defaultLocale")) {
|
||||
GeneralUtils.saveKeyToSettings("system.defaultLocale", system.get("defaultLocale"));
|
||||
applicationProperties
|
||||
.getSystem()
|
||||
.setDefaultLocale((String) system.get("defaultLocale"));
|
||||
}
|
||||
if (system.containsKey("showUpdate")) {
|
||||
GeneralUtils.saveKeyToSettings("system.showUpdate", system.get("showUpdate"));
|
||||
applicationProperties.getSystem().setShowUpdate((Boolean) system.get("showUpdate"));
|
||||
}
|
||||
if (system.containsKey("showUpdateOnlyAdmin")) {
|
||||
GeneralUtils.saveKeyToSettings(
|
||||
"system.showUpdateOnlyAdmin", system.get("showUpdateOnlyAdmin"));
|
||||
applicationProperties
|
||||
.getSystem()
|
||||
.setShowUpdateOnlyAdmin((Boolean) system.get("showUpdateOnlyAdmin"));
|
||||
}
|
||||
if (system.containsKey("fileUploadLimit")) {
|
||||
GeneralUtils.saveKeyToSettings(
|
||||
"system.fileUploadLimit", system.get("fileUploadLimit"));
|
||||
applicationProperties
|
||||
.getSystem()
|
||||
.setFileUploadLimit((String) system.get("fileUploadLimit"));
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(
|
||||
"General settings updated. Restart required for changes to take effect.");
|
||||
}
|
||||
|
||||
// ========== SECURITY SETTINGS ==========
|
||||
|
||||
@GetMapping("/admin/settings/security")
|
||||
@Hidden
|
||||
public ResponseEntity<Map<String, Object>> getSecuritySettings() {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
ApplicationProperties.Security security = applicationProperties.getSecurity();
|
||||
|
||||
settings.put("enableLogin", security.isEnableLogin());
|
||||
settings.put("loginMethod", security.getLoginMethod());
|
||||
settings.put("loginAttemptCount", security.getLoginAttemptCount());
|
||||
settings.put("loginResetTimeMinutes", security.getLoginResetTimeMinutes());
|
||||
settings.put(
|
||||
"initialLogin",
|
||||
Map.of(
|
||||
"username",
|
||||
security.getInitialLogin().getUsername() != null
|
||||
? security.getInitialLogin().getUsername()
|
||||
: ""));
|
||||
|
||||
// JWT settings
|
||||
ApplicationProperties.Security.Jwt jwt = security.getJwt();
|
||||
settings.put(
|
||||
"jwt",
|
||||
Map.of(
|
||||
"enableKeystore", jwt.isEnableKeystore(),
|
||||
"enableKeyRotation", jwt.isEnableKeyRotation(),
|
||||
"enableKeyCleanup", jwt.isEnableKeyCleanup(),
|
||||
"keyRetentionDays", jwt.getKeyRetentionDays()));
|
||||
|
||||
return ResponseEntity.ok(settings);
|
||||
}
|
||||
|
||||
@PostMapping("/admin/settings/security")
|
||||
@Hidden
|
||||
public ResponseEntity<String> updateSecuritySettings(@RequestBody Map<String, Object> settings)
|
||||
throws IOException {
|
||||
if (settings.containsKey("enableLogin")) {
|
||||
GeneralUtils.saveKeyToSettings("security.enableLogin", settings.get("enableLogin"));
|
||||
applicationProperties
|
||||
.getSecurity()
|
||||
.setEnableLogin((Boolean) settings.get("enableLogin"));
|
||||
}
|
||||
if (settings.containsKey("loginMethod")) {
|
||||
GeneralUtils.saveKeyToSettings("security.loginMethod", settings.get("loginMethod"));
|
||||
applicationProperties
|
||||
.getSecurity()
|
||||
.setLoginMethod((String) settings.get("loginMethod"));
|
||||
}
|
||||
if (settings.containsKey("loginAttemptCount")) {
|
||||
GeneralUtils.saveKeyToSettings(
|
||||
"security.loginAttemptCount", settings.get("loginAttemptCount"));
|
||||
applicationProperties
|
||||
.getSecurity()
|
||||
.setLoginAttemptCount((Integer) settings.get("loginAttemptCount"));
|
||||
}
|
||||
if (settings.containsKey("loginResetTimeMinutes")) {
|
||||
GeneralUtils.saveKeyToSettings(
|
||||
"security.loginResetTimeMinutes", settings.get("loginResetTimeMinutes"));
|
||||
applicationProperties
|
||||
.getSecurity()
|
||||
.setLoginResetTimeMinutes(
|
||||
((Number) settings.get("loginResetTimeMinutes")).longValue());
|
||||
}
|
||||
|
||||
// JWT settings
|
||||
if (settings.containsKey("jwt")) {
|
||||
Map<String, Object> jwt = (Map<String, Object>) settings.get("jwt");
|
||||
if (jwt.containsKey("keyRetentionDays")) {
|
||||
GeneralUtils.saveKeyToSettings(
|
||||
"security.jwt.keyRetentionDays", jwt.get("keyRetentionDays"));
|
||||
applicationProperties
|
||||
.getSecurity()
|
||||
.getJwt()
|
||||
.setKeyRetentionDays((Integer) jwt.get("keyRetentionDays"));
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(
|
||||
"Security settings updated. Restart required for changes to take effect.");
|
||||
}
|
||||
|
||||
// ========== CONNECTIONS SETTINGS (OAuth/SAML) ==========
|
||||
|
||||
@GetMapping("/admin/settings/connections")
|
||||
@Hidden
|
||||
public ResponseEntity<Map<String, Object>> getConnectionsSettings() {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
ApplicationProperties.Security security = applicationProperties.getSecurity();
|
||||
|
||||
// OAuth2 settings
|
||||
ApplicationProperties.Security.OAUTH2 oauth2 = security.getOauth2();
|
||||
settings.put(
|
||||
"oauth2",
|
||||
Map.of(
|
||||
"enabled", oauth2.getEnabled(),
|
||||
"issuer", oauth2.getIssuer() != null ? oauth2.getIssuer() : "",
|
||||
"clientId", oauth2.getClientId() != null ? oauth2.getClientId() : "",
|
||||
"provider", oauth2.getProvider() != null ? oauth2.getProvider() : "",
|
||||
"autoCreateUser", oauth2.getAutoCreateUser(),
|
||||
"blockRegistration", oauth2.getBlockRegistration(),
|
||||
"useAsUsername",
|
||||
oauth2.getUseAsUsername() != null
|
||||
? oauth2.getUseAsUsername()
|
||||
: ""));
|
||||
|
||||
// SAML2 settings
|
||||
ApplicationProperties.Security.SAML2 saml2 = security.getSaml2();
|
||||
settings.put(
|
||||
"saml2",
|
||||
Map.of(
|
||||
"enabled", saml2.getEnabled(),
|
||||
"provider", saml2.getProvider() != null ? saml2.getProvider() : "",
|
||||
"autoCreateUser", saml2.getAutoCreateUser(),
|
||||
"blockRegistration", saml2.getBlockRegistration(),
|
||||
"registrationId", saml2.getRegistrationId()));
|
||||
|
||||
return ResponseEntity.ok(settings);
|
||||
}
|
||||
|
||||
@PostMapping("/admin/settings/connections")
|
||||
@Hidden
|
||||
public ResponseEntity<String> updateConnectionsSettings(
|
||||
@RequestBody Map<String, Object> settings) throws IOException {
|
||||
// OAuth2 settings
|
||||
if (settings.containsKey("oauth2")) {
|
||||
Map<String, Object> oauth2 = (Map<String, Object>) settings.get("oauth2");
|
||||
if (oauth2.containsKey("enabled")) {
|
||||
GeneralUtils.saveKeyToSettings("security.oauth2.enabled", oauth2.get("enabled"));
|
||||
applicationProperties
|
||||
.getSecurity()
|
||||
.getOauth2()
|
||||
.setEnabled((Boolean) oauth2.get("enabled"));
|
||||
}
|
||||
if (oauth2.containsKey("issuer")) {
|
||||
GeneralUtils.saveKeyToSettings("security.oauth2.issuer", oauth2.get("issuer"));
|
||||
applicationProperties
|
||||
.getSecurity()
|
||||
.getOauth2()
|
||||
.setIssuer((String) oauth2.get("issuer"));
|
||||
}
|
||||
if (oauth2.containsKey("clientId")) {
|
||||
GeneralUtils.saveKeyToSettings("security.oauth2.clientId", oauth2.get("clientId"));
|
||||
applicationProperties
|
||||
.getSecurity()
|
||||
.getOauth2()
|
||||
.setClientId((String) oauth2.get("clientId"));
|
||||
}
|
||||
if (oauth2.containsKey("clientSecret")) {
|
||||
GeneralUtils.saveKeyToSettings(
|
||||
"security.oauth2.clientSecret", oauth2.get("clientSecret"));
|
||||
applicationProperties
|
||||
.getSecurity()
|
||||
.getOauth2()
|
||||
.setClientSecret((String) oauth2.get("clientSecret"));
|
||||
}
|
||||
if (oauth2.containsKey("provider")) {
|
||||
GeneralUtils.saveKeyToSettings("security.oauth2.provider", oauth2.get("provider"));
|
||||
applicationProperties
|
||||
.getSecurity()
|
||||
.getOauth2()
|
||||
.setProvider((String) oauth2.get("provider"));
|
||||
}
|
||||
if (oauth2.containsKey("autoCreateUser")) {
|
||||
GeneralUtils.saveKeyToSettings(
|
||||
"security.oauth2.autoCreateUser", oauth2.get("autoCreateUser"));
|
||||
applicationProperties
|
||||
.getSecurity()
|
||||
.getOauth2()
|
||||
.setAutoCreateUser((Boolean) oauth2.get("autoCreateUser"));
|
||||
}
|
||||
if (oauth2.containsKey("blockRegistration")) {
|
||||
GeneralUtils.saveKeyToSettings(
|
||||
"security.oauth2.blockRegistration", oauth2.get("blockRegistration"));
|
||||
applicationProperties
|
||||
.getSecurity()
|
||||
.getOauth2()
|
||||
.setBlockRegistration((Boolean) oauth2.get("blockRegistration"));
|
||||
}
|
||||
if (oauth2.containsKey("useAsUsername")) {
|
||||
GeneralUtils.saveKeyToSettings(
|
||||
"security.oauth2.useAsUsername", oauth2.get("useAsUsername"));
|
||||
applicationProperties
|
||||
.getSecurity()
|
||||
.getOauth2()
|
||||
.setUseAsUsername((String) oauth2.get("useAsUsername"));
|
||||
}
|
||||
}
|
||||
|
||||
// SAML2 settings
|
||||
if (settings.containsKey("saml2")) {
|
||||
Map<String, Object> saml2 = (Map<String, Object>) settings.get("saml2");
|
||||
if (saml2.containsKey("enabled")) {
|
||||
GeneralUtils.saveKeyToSettings("security.saml2.enabled", saml2.get("enabled"));
|
||||
applicationProperties
|
||||
.getSecurity()
|
||||
.getSaml2()
|
||||
.setEnabled((Boolean) saml2.get("enabled"));
|
||||
}
|
||||
if (saml2.containsKey("provider")) {
|
||||
GeneralUtils.saveKeyToSettings("security.saml2.provider", saml2.get("provider"));
|
||||
applicationProperties
|
||||
.getSecurity()
|
||||
.getSaml2()
|
||||
.setProvider((String) saml2.get("provider"));
|
||||
}
|
||||
if (saml2.containsKey("autoCreateUser")) {
|
||||
GeneralUtils.saveKeyToSettings(
|
||||
"security.saml2.autoCreateUser", saml2.get("autoCreateUser"));
|
||||
applicationProperties
|
||||
.getSecurity()
|
||||
.getSaml2()
|
||||
.setAutoCreateUser((Boolean) saml2.get("autoCreateUser"));
|
||||
}
|
||||
if (saml2.containsKey("blockRegistration")) {
|
||||
GeneralUtils.saveKeyToSettings(
|
||||
"security.saml2.blockRegistration", saml2.get("blockRegistration"));
|
||||
applicationProperties
|
||||
.getSecurity()
|
||||
.getSaml2()
|
||||
.setBlockRegistration((Boolean) saml2.get("blockRegistration"));
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(
|
||||
"Connection settings updated. Restart required for changes to take effect.");
|
||||
}
|
||||
|
||||
// ========== PRIVACY SETTINGS ==========
|
||||
|
||||
@GetMapping("/admin/settings/privacy")
|
||||
@Hidden
|
||||
public ResponseEntity<Map<String, Object>> getPrivacySettings() {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
|
||||
settings.put("enableAnalytics", applicationProperties.getSystem().getEnableAnalytics());
|
||||
settings.put("googleVisibility", applicationProperties.getSystem().isGooglevisibility());
|
||||
settings.put("metricsEnabled", applicationProperties.getMetrics().isEnabled());
|
||||
|
||||
return ResponseEntity.ok(settings);
|
||||
}
|
||||
|
||||
@PostMapping("/admin/settings/privacy")
|
||||
@Hidden
|
||||
public ResponseEntity<String> updatePrivacySettings(@RequestBody Map<String, Object> settings)
|
||||
throws IOException {
|
||||
if (settings.containsKey("enableAnalytics")) {
|
||||
GeneralUtils.saveKeyToSettings(
|
||||
"system.enableAnalytics", settings.get("enableAnalytics"));
|
||||
applicationProperties
|
||||
.getSystem()
|
||||
.setEnableAnalytics((Boolean) settings.get("enableAnalytics"));
|
||||
}
|
||||
if (settings.containsKey("googleVisibility")) {
|
||||
GeneralUtils.saveKeyToSettings(
|
||||
"system.googlevisibility", settings.get("googleVisibility"));
|
||||
applicationProperties
|
||||
.getSystem()
|
||||
.setGooglevisibility((Boolean) settings.get("googleVisibility"));
|
||||
}
|
||||
if (settings.containsKey("metricsEnabled")) {
|
||||
GeneralUtils.saveKeyToSettings("metrics.enabled", settings.get("metricsEnabled"));
|
||||
applicationProperties.getMetrics().setEnabled((Boolean) settings.get("metricsEnabled"));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(
|
||||
"Privacy settings updated. Restart required for changes to take effect.");
|
||||
}
|
||||
|
||||
// ========== ADVANCED SETTINGS ==========
|
||||
|
||||
@GetMapping("/admin/settings/advanced")
|
||||
@Hidden
|
||||
public ResponseEntity<Map<String, Object>> getAdvancedSettings() {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
|
||||
settings.put("endpoints", applicationProperties.getEndpoints());
|
||||
settings.put(
|
||||
"enableAlphaFunctionality",
|
||||
applicationProperties.getSystem().isEnableAlphaFunctionality());
|
||||
settings.put("maxDPI", applicationProperties.getSystem().getMaxDPI());
|
||||
settings.put("enableUrlToPDF", applicationProperties.getSystem().isEnableUrlToPDF());
|
||||
settings.put("customPaths", applicationProperties.getSystem().getCustomPaths());
|
||||
settings.put(
|
||||
"tempFileManagement", applicationProperties.getSystem().getTempFileManagement());
|
||||
|
||||
return ResponseEntity.ok(settings);
|
||||
}
|
||||
|
||||
@PostMapping("/admin/settings/advanced")
|
||||
@Hidden
|
||||
public ResponseEntity<String> updateAdvancedSettings(@RequestBody Map<String, Object> settings)
|
||||
throws IOException {
|
||||
if (settings.containsKey("enableAlphaFunctionality")) {
|
||||
GeneralUtils.saveKeyToSettings(
|
||||
"system.enableAlphaFunctionality", settings.get("enableAlphaFunctionality"));
|
||||
applicationProperties
|
||||
.getSystem()
|
||||
.setEnableAlphaFunctionality(
|
||||
(Boolean) settings.get("enableAlphaFunctionality"));
|
||||
}
|
||||
if (settings.containsKey("maxDPI")) {
|
||||
GeneralUtils.saveKeyToSettings("system.maxDPI", settings.get("maxDPI"));
|
||||
applicationProperties.getSystem().setMaxDPI((Integer) settings.get("maxDPI"));
|
||||
}
|
||||
if (settings.containsKey("enableUrlToPDF")) {
|
||||
GeneralUtils.saveKeyToSettings("system.enableUrlToPDF", settings.get("enableUrlToPDF"));
|
||||
applicationProperties
|
||||
.getSystem()
|
||||
.setEnableUrlToPDF((Boolean) settings.get("enableUrlToPDF"));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(
|
||||
"Advanced settings updated. Restart required for changes to take effect.");
|
||||
}
|
||||
}
|
||||
|
||||
+4
-8
@@ -15,20 +15,18 @@ import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertEbookToPdfRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
@@ -36,9 +34,7 @@ import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
@ConvertApi
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ConvertEbookToPDFController {
|
||||
@@ -58,7 +54,7 @@ public class ConvertEbookToPDFController {
|
||||
return endpointConfiguration.isGroupEnabled("Ghostscript");
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/ebook/pdf")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/ebook/pdf")
|
||||
@Operation(
|
||||
summary = "Convert an eBook file to PDF",
|
||||
description =
|
||||
|
||||
-2
@@ -97,8 +97,6 @@ public class ConvertOfficeController {
|
||||
// Unoconvert: schreibe direkt in outputPath innerhalb des workDir
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(runtimePathConfig.getUnoConvertPath());
|
||||
command.add("--port");
|
||||
command.add("2003");
|
||||
command.add("--convert-to");
|
||||
command.add("pdf");
|
||||
command.add(inputPath.toString());
|
||||
|
||||
+4
-8
@@ -12,14 +12,10 @@ import org.apache.commons.io.FilenameUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -28,15 +24,15 @@ import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest.OutputFormat;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest.TargetDevice;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
@ConvertApi
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ConvertPDFToEpubController {
|
||||
@@ -77,7 +73,7 @@ public class ConvertPDFToEpubController {
|
||||
return command;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/epub")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/epub")
|
||||
@Operation(
|
||||
summary = "Convert PDF to EPUB/AZW3",
|
||||
description =
|
||||
|
||||
+4
-8
@@ -3,31 +3,27 @@ package stirling.software.SPDF.controller.api.converters;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.util.PDFToFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@ConvertApi
|
||||
@RequiredArgsConstructor
|
||||
public class ConvertPDFToHtml {
|
||||
|
||||
private final TempFileManager tempFileManager;
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/html")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/html")
|
||||
@Operation(
|
||||
summary = "Convert PDF to HTML",
|
||||
description =
|
||||
|
||||
+7
-11
@@ -7,19 +7,17 @@ import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest;
|
||||
import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest;
|
||||
import stirling.software.SPDF.model.api.converters.PdfToWordRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
@@ -28,9 +26,7 @@ import stirling.software.common.util.PDFToFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
@ConvertApi
|
||||
@RequiredArgsConstructor
|
||||
public class ConvertPDFToOffice {
|
||||
|
||||
@@ -38,7 +34,7 @@ public class ConvertPDFToOffice {
|
||||
private final TempFileManager tempFileManager;
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/presentation")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/presentation")
|
||||
@Operation(
|
||||
summary = "Convert PDF to Presentation format",
|
||||
description =
|
||||
@@ -53,7 +49,7 @@ public class ConvertPDFToOffice {
|
||||
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import");
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/text")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/text")
|
||||
@Operation(
|
||||
summary = "Convert PDF to Text or RTF format",
|
||||
description =
|
||||
@@ -79,7 +75,7 @@ public class ConvertPDFToOffice {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/word")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/word")
|
||||
@Operation(
|
||||
summary = "Convert PDF to Word document",
|
||||
description =
|
||||
@@ -93,7 +89,7 @@ public class ConvertPDFToOffice {
|
||||
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/xml")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/xml")
|
||||
@Operation(
|
||||
summary = "Convert PDF to XML",
|
||||
description =
|
||||
|
||||
+17
-16
@@ -74,30 +74,26 @@ import org.apache.xmpbox.xml.XmpSerializer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.converters.PdfToPdfARequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@ConvertApi
|
||||
@Slf4j
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
@RequiredArgsConstructor
|
||||
public class ConvertPDFToPDFA {
|
||||
|
||||
@@ -483,15 +479,22 @@ public class ConvertPDFToPDFA {
|
||||
command.add("-dCompatibilityLevel=" + profile.getCompatibilityLevel());
|
||||
command.add("-sDEVICE=pdfwrite");
|
||||
command.add("-sColorConversionStrategy=RGB");
|
||||
command.add("-dProcessColorModel=/DeviceRGB");
|
||||
command.add("-sOutputICCProfile=" + colorProfiles.rgb().toAbsolutePath());
|
||||
command.add("-sDefaultRGBProfile=" + colorProfiles.rgb().toAbsolutePath());
|
||||
command.add("-sDefaultGrayProfile=" + colorProfiles.gray().toAbsolutePath());
|
||||
command.add("-dEmbedAllFonts=true");
|
||||
command.add("-dSubsetFonts=false"); // Embed complete fonts to avoid incomplete glyphs
|
||||
command.add("-dSubsetFonts=true");
|
||||
command.add("-dCompressFonts=true");
|
||||
command.add("-dNOSUBSTFONTS=false"); // Allow font substitution for problematic fonts
|
||||
command.add("-dPDFSETTINGS=/prepress");
|
||||
|
||||
// Explicitly tune downsampling/compression for high-quality print
|
||||
command.add("-dColorImageDownsampleType=/Bicubic");
|
||||
command.add("-dColorImageResolution=300");
|
||||
command.add("-dGrayImageDownsampleType=/Bicubic");
|
||||
command.add("-dGrayImageResolution=300");
|
||||
command.add("-dMonoImageDownsampleType=/Bicubic");
|
||||
command.add("-dMonoImageResolution=1200");
|
||||
|
||||
command.add("-dNOPAUSE");
|
||||
command.add("-dBATCH");
|
||||
command.add("-dNOOUTERSAVE");
|
||||
@@ -562,7 +565,7 @@ public class ConvertPDFToPDFA {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/pdfa")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/pdfa")
|
||||
@Operation(
|
||||
summary = "Convert a PDF to a PDF/A or PDF/X",
|
||||
description =
|
||||
@@ -2449,9 +2452,7 @@ public class ConvertPDFToPDFA {
|
||||
|
||||
@Getter
|
||||
private enum PdfXProfile {
|
||||
PDF_X_1("PDF/X-1", "_PDFX-1.pdf", "1.3", "2001", "pdfx-1", "pdfx"),
|
||||
PDF_X_3("PDF/X-3", "_PDFX-3.pdf", "1.3", "2003", "pdfx-3"),
|
||||
PDF_X_4("PDF/X-4", "_PDFX-4.pdf", "1.4", "2008", "pdfx-4");
|
||||
PDF_X("PDF/X", "_PDFX.pdf", "1.6", "2008", "pdfx");
|
||||
|
||||
private final String displayName;
|
||||
private final String suffix;
|
||||
@@ -2477,7 +2478,7 @@ public class ConvertPDFToPDFA {
|
||||
|
||||
static PdfXProfile fromRequest(String requestToken) {
|
||||
if (requestToken == null) {
|
||||
return PDF_X_4;
|
||||
return PDF_X;
|
||||
}
|
||||
String normalized = requestToken.trim().toLowerCase(Locale.ROOT);
|
||||
Optional<PdfXProfile> match =
|
||||
@@ -2485,7 +2486,7 @@ public class ConvertPDFToPDFA {
|
||||
.filter(profile -> profile.requestTokens.contains(normalized))
|
||||
.findFirst();
|
||||
|
||||
return match.orElse(PDF_X_4);
|
||||
return match.orElse(PDF_X);
|
||||
}
|
||||
|
||||
String outputSuffix() {
|
||||
|
||||
+3
-4
@@ -9,7 +9,6 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -89,7 +88,7 @@ public class ConvertPdfJsonController {
|
||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, docName);
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/text-editor/metadata")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/text-editor/metadata")
|
||||
@Operation(
|
||||
summary = "Extract PDF metadata for text editor lazy loading",
|
||||
description =
|
||||
@@ -127,7 +126,7 @@ public class ConvertPdfJsonController {
|
||||
.body(jsonBytes);
|
||||
}
|
||||
|
||||
@PostMapping(
|
||||
@AutoJobPostMapping(
|
||||
value = "/pdf/text-editor/partial/{jobId}",
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
@StandardPdfResponse
|
||||
@@ -180,7 +179,7 @@ public class ConvertPdfJsonController {
|
||||
return WebResponseUtils.bytesToWebResponse(jsonBytes, docName, MediaType.APPLICATION_JSON);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/pdf/text-editor/clear-cache/{jobId}")
|
||||
@AutoJobPostMapping(value = "/pdf/text-editor/clear-cache/{jobId}")
|
||||
@Operation(
|
||||
summary = "Clear cached PDF document for text editor",
|
||||
description =
|
||||
|
||||
+4
-8
@@ -27,18 +27,16 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.converters.PdfToVideoRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ApplicationContextProvider;
|
||||
@@ -51,9 +49,7 @@ import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
@ConvertApi
|
||||
@RequiredArgsConstructor
|
||||
public class ConvertPdfToVideoController {
|
||||
|
||||
@@ -68,7 +64,7 @@ public class ConvertPdfToVideoController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/video")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/video")
|
||||
@Operation(
|
||||
summary = "Convert PDF to Video Slideshow",
|
||||
description =
|
||||
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
package stirling.software.SPDF.controller.api.converters;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.MultiFileResponse;
|
||||
import stirling.software.SPDF.model.api.converters.SvgToPdfRequest;
|
||||
import stirling.software.SPDF.utils.SvgToPdf;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.SvgSanitizer;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@ConvertApi
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class ConvertSvgToPDF {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final SvgSanitizer svgSanitizer;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/svg/pdf")
|
||||
@MultiFileResponse
|
||||
@Operation(
|
||||
summary = "Convert SVG to PDF",
|
||||
description =
|
||||
"This endpoint converts one or more SVG (Scalable Vector Graphics) files to PDF format. "
|
||||
+ "Each SVG is converted to a separate PDF file. "
|
||||
+ "The conversion preserves vector graphics for crisp output at any resolution - no rasterization occurs. "
|
||||
+ "SVG dimensions (width/height) determine the PDF page size; defaults to A4 if not specified. "
|
||||
+ "SVG content is sanitized to prevent XSS attacks. "
|
||||
+ "Input: SVG file(s), Output: PDF file(s) or ZIP. Type: MIMO")
|
||||
public ResponseEntity<byte[]> convertSvgToPdf(@ModelAttribute SvgToPdfRequest request) {
|
||||
|
||||
MultipartFile[] inputFiles = request.getFileInput();
|
||||
boolean combineIntoSinglePdf = Boolean.TRUE.equals(request.getCombineIntoSinglePdf());
|
||||
|
||||
// Validate input
|
||||
if (inputFiles == null || inputFiles.length == 0) {
|
||||
log.error("No files provided for SVG to PDF conversion.");
|
||||
return ResponseEntity.badRequest()
|
||||
.body("No files provided".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
try {
|
||||
List<byte[]> sanitizedSvgs = new ArrayList<>();
|
||||
List<String> filenames = new ArrayList<>();
|
||||
|
||||
for (MultipartFile inputFile : inputFiles) {
|
||||
if (inputFile == null || inputFile.isEmpty()) {
|
||||
log.warn("Skipping empty file in batch conversion");
|
||||
continue;
|
||||
}
|
||||
|
||||
String originalFilename = inputFile.getOriginalFilename();
|
||||
if (originalFilename == null || originalFilename.trim().isEmpty()) {
|
||||
log.warn("Skipping file with null or empty filename");
|
||||
continue;
|
||||
}
|
||||
|
||||
String lowerFilename = originalFilename.toLowerCase(Locale.ROOT);
|
||||
if (!lowerFilename.endsWith(".svg")) {
|
||||
log.warn("Skipping non-SVG file: {}", originalFilename);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] fileBytes = inputFile.getBytes();
|
||||
byte[] sanitizedBytes = svgSanitizer.sanitize(fileBytes);
|
||||
sanitizedSvgs.add(sanitizedBytes);
|
||||
filenames.add(Filenames.toSimpleFileName(originalFilename));
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error(
|
||||
"SVG sanitization/reading failed for {}: {}",
|
||||
originalFilename,
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (sanitizedSvgs.isEmpty()) {
|
||||
log.error("No valid SVG files were found");
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body("No valid SVG files were found".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
if (combineIntoSinglePdf) {
|
||||
return handleCombinedConversion(sanitizedSvgs, filenames);
|
||||
} else {
|
||||
return handleSeparateConversion(sanitizedSvgs, filenames);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Unexpected error during SVG to PDF conversion", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(
|
||||
"An unexpected error occurred during conversion"
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseEntity<byte[]> handleCombinedConversion(
|
||||
List<byte[]> sanitizedSvgs, List<String> filenames) {
|
||||
try {
|
||||
log.info("Combining {} SVG files into single PDF", sanitizedSvgs.size());
|
||||
|
||||
byte[] pdfBytes = SvgToPdf.combineIntoPdf(sanitizedSvgs);
|
||||
|
||||
if (pdfBytes == null || pdfBytes.length == 0) {
|
||||
log.error("PDF conversion failed - empty output");
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(
|
||||
"PDF conversion failed - empty output"
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
|
||||
|
||||
String outputFilename =
|
||||
filenames.isEmpty()
|
||||
? "combined_svgs.pdf"
|
||||
: GeneralUtils.generateFilename(filenames.get(0), "_combined.pdf");
|
||||
|
||||
log.info("Successfully combined {} SVGs into single PDF", sanitizedSvgs.size());
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
pdfBytes, outputFilename, MediaType.APPLICATION_PDF);
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("Error combining SVGs into PDF", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(
|
||||
("Conversion failed: " + e.getMessage())
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseEntity<byte[]> handleSeparateConversion(
|
||||
List<byte[]> sanitizedSvgs, List<String> filenames) {
|
||||
List<ConvertedPdf> convertedPdfs = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < sanitizedSvgs.size(); i++) {
|
||||
byte[] sanitizedBytes = sanitizedSvgs.get(i);
|
||||
String baseFilename = filenames.get(i);
|
||||
|
||||
try {
|
||||
byte[] pdfBytes = SvgToPdf.convert(sanitizedBytes);
|
||||
|
||||
if (pdfBytes == null || pdfBytes.length == 0) {
|
||||
log.error("PDF conversion failed - empty output for {}", baseFilename);
|
||||
continue;
|
||||
}
|
||||
|
||||
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
|
||||
|
||||
String outputFilename = GeneralUtils.generateFilename(baseFilename, ".pdf");
|
||||
convertedPdfs.add(new ConvertedPdf(outputFilename, pdfBytes));
|
||||
|
||||
log.info("Successfully converted SVG to PDF: {}", baseFilename);
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("File processing error for SVG to PDF: {}", baseFilename, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (convertedPdfs.isEmpty()) {
|
||||
log.error("No files were successfully converted");
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body("No files were successfully converted".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
try {
|
||||
if (convertedPdfs.size() == 1) {
|
||||
ConvertedPdf pdf = convertedPdfs.get(0);
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
pdf.content, pdf.filename, MediaType.APPLICATION_PDF);
|
||||
}
|
||||
|
||||
String zipFilename =
|
||||
filenames.isEmpty()
|
||||
? "converted_svgs.zip"
|
||||
: GeneralUtils.generateFilename(
|
||||
filenames.get(0), "_converted_svgs.zip");
|
||||
byte[] zipBytes = createZipFromPdfs(convertedPdfs);
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
zipBytes, zipFilename, MediaType.APPLICATION_OCTET_STREAM);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to create response", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body("Failed to create response".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] createZipFromPdfs(List<ConvertedPdf> pdfs) throws IOException {
|
||||
try (TempFile tempZipFile = new TempFile(tempFileManager, ".zip");
|
||||
ZipOutputStream zipOut =
|
||||
new ZipOutputStream(Files.newOutputStream(tempZipFile.getPath()))) {
|
||||
|
||||
for (ConvertedPdf pdf : pdfs) {
|
||||
ZipEntry pdfEntry = new ZipEntry(pdf.filename);
|
||||
zipOut.putNextEntry(pdfEntry);
|
||||
zipOut.write(pdf.content);
|
||||
zipOut.closeEntry();
|
||||
log.debug("Added {} to ZIP", pdf.filename);
|
||||
}
|
||||
|
||||
return Files.readAllBytes(tempZipFile.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
private static class ConvertedPdf {
|
||||
final String filename;
|
||||
final byte[] content;
|
||||
|
||||
ConvertedPdf(String filename, byte[] content) {
|
||||
this.filename = filename;
|
||||
this.content = content;
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-8
@@ -21,19 +21,17 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
@@ -43,10 +41,8 @@ import stirling.software.common.util.ProcessExecutor;
|
||||
import stirling.software.common.util.RegexPatternUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
@ConvertApi
|
||||
@Slf4j
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@RequiredArgsConstructor
|
||||
public class ConvertWebsiteToPDF {
|
||||
|
||||
@@ -59,7 +55,7 @@ public class ConvertWebsiteToPDF {
|
||||
|
||||
private static final Pattern NUMERIC_HTML_ENTITY_PATTERN = Pattern.compile("&#(x?[0-9a-f]+);");
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/url/pdf")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/url/pdf")
|
||||
@Operation(
|
||||
summary = "Convert a URL to a PDF",
|
||||
description =
|
||||
|
||||
+5
-9
@@ -13,12 +13,8 @@ import org.apache.commons.io.FilenameUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
@@ -27,6 +23,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.SPDF.model.api.converters.PdfVectorExportRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
@@ -35,10 +33,8 @@ import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@ConvertApi
|
||||
@Slf4j
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
@RequiredArgsConstructor
|
||||
public class PdfVectorExportController {
|
||||
|
||||
@@ -49,7 +45,7 @@ public class PdfVectorExportController {
|
||||
private final TempFileManager tempFileManager;
|
||||
private final EndpointConfiguration endpointConfiguration;
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/vector/pdf")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/vector/pdf")
|
||||
@Operation(
|
||||
summary = "Convert PostScript formats to PDF",
|
||||
description =
|
||||
@@ -94,7 +90,7 @@ public class PdfVectorExportController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/vector")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/vector")
|
||||
@Operation(
|
||||
summary = "Convert PDF to vector format",
|
||||
description =
|
||||
|
||||
+17
-13
@@ -8,9 +8,6 @@ import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
@@ -18,7 +15,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@@ -28,20 +24,22 @@ import stirling.software.SPDF.model.api.filter.ContainsTextRequest;
|
||||
import stirling.software.SPDF.model.api.filter.FileSizeRequest;
|
||||
import stirling.software.SPDF.model.api.filter.PageRotationRequest;
|
||||
import stirling.software.SPDF.model.api.filter.PageSizeRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.FilterApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.PdfUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/filter")
|
||||
@Tag(name = "Filter", description = "Filter APIs")
|
||||
@FilterApi
|
||||
@RequiredArgsConstructor
|
||||
public class FilterController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-contains-text")
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/filter-contains-text")
|
||||
@Operation(
|
||||
summary = "Checks if a PDF contains set text, returns true if does",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
@@ -70,7 +68,9 @@ public class FilterController {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-contains-image")
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/filter-contains-image")
|
||||
@Operation(
|
||||
summary = "Checks if a PDF contains an image",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
@@ -98,7 +98,9 @@ public class FilterController {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-count")
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/filter-page-count")
|
||||
@Operation(
|
||||
summary = "Checks if a PDF is greater, less or equal to a setPageCount",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
@@ -129,7 +131,7 @@ public class FilterController {
|
||||
: ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-size")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-size")
|
||||
@Operation(
|
||||
summary = "Checks if a PDF is of a certain size",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
@@ -166,7 +168,7 @@ public class FilterController {
|
||||
: ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-file-size")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-file-size")
|
||||
@Operation(
|
||||
summary = "Checks if a PDF is a set file size",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
@@ -194,7 +196,9 @@ public class FilterController {
|
||||
: ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-rotation")
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/filter-page-rotation")
|
||||
@Operation(
|
||||
summary = "Checks if a PDF is of a certain rotation",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
|
||||
+4
-8
@@ -36,20 +36,18 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.service.LineArtConversionService;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
@@ -60,10 +58,8 @@ import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@MiscApi
|
||||
@Slf4j
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
@RequiredArgsConstructor
|
||||
public class CompressController {
|
||||
|
||||
@@ -922,7 +918,7 @@ public class CompressController {
|
||||
return Math.min(9, currentLevel + 1);
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/compress-pdf")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/compress-pdf")
|
||||
@Operation(
|
||||
summary = "Optimize PDF file",
|
||||
description =
|
||||
|
||||
+10
@@ -112,6 +112,16 @@ public class ConfigController {
|
||||
"showSettingsWhenNoLogin",
|
||||
applicationProperties.getSystem().isShowSettingsWhenNoLogin());
|
||||
|
||||
// SSO Provider settings
|
||||
boolean enableOAuth =
|
||||
applicationProperties.getSecurity().getOauth2() != null
|
||||
&& applicationProperties.getSecurity().getOauth2().getEnabled();
|
||||
boolean enableSaml =
|
||||
applicationProperties.getSecurity().getSaml2() != null
|
||||
&& applicationProperties.getSecurity().getSaml2().getEnabled();
|
||||
configData.put("enableOAuth", enableOAuth);
|
||||
configData.put("enableSaml", enableSaml);
|
||||
|
||||
// Mail settings - check both SMTP enabled AND invites enabled
|
||||
boolean smtpEnabled = applicationProperties.getMail().isEnabled();
|
||||
boolean invitesEnabled = applicationProperties.getMail().isEnableInvites();
|
||||
|
||||
+4
-8
@@ -24,20 +24,18 @@ import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
@@ -50,9 +48,7 @@ import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
@MiscApi
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class OCRController {
|
||||
@@ -85,7 +81,7 @@ public class OCRController {
|
||||
.toList();
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/ocr-pdf")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/ocr-pdf")
|
||||
@Operation(
|
||||
summary = "Process a PDF file with OCR",
|
||||
description =
|
||||
|
||||
+50
-7
@@ -1,7 +1,12 @@
|
||||
package stirling.software.SPDF.controller.api.misc;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -14,11 +19,11 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.OverlayImageRequest;
|
||||
import stirling.software.SPDF.utils.SvgOverlayUtil;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.PdfUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@MiscApi
|
||||
@@ -32,25 +37,63 @@ public class OverlayImageController {
|
||||
@Operation(
|
||||
summary = "Overlay image onto a PDF file",
|
||||
description =
|
||||
"This endpoint overlays an image onto a PDF file at the specified coordinates."
|
||||
+ " The image can be overlaid on every page of the PDF if specified. "
|
||||
+ " Input:PDF/IMAGE Output:PDF Type:SISO")
|
||||
"This endpoint overlays an image onto a PDF file at the specified coordinates. "
|
||||
+ "Supports both raster formats (PNG, JPEG, etc.) and vector format (SVG). "
|
||||
+ "SVG files are rendered as vector graphics for crisp output at any resolution. "
|
||||
+ "The image can be overlaid on every page of the PDF if specified. "
|
||||
+ "Input:PDF/IMAGE/SVG Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> overlayImage(@ModelAttribute OverlayImageRequest request) {
|
||||
MultipartFile pdfFile = request.getFileInput();
|
||||
MultipartFile imageFile = request.getImageFile();
|
||||
float x = request.getX();
|
||||
float y = request.getY();
|
||||
boolean everyPage = Boolean.TRUE.equals(request.getEveryPage());
|
||||
|
||||
try {
|
||||
byte[] pdfBytes = pdfFile.getBytes();
|
||||
byte[] imageBytes = imageFile.getBytes();
|
||||
byte[] result =
|
||||
PdfUtils.overlayImage(
|
||||
pdfDocumentFactory, pdfBytes, imageBytes, x, y, everyPage);
|
||||
|
||||
boolean isSvg = SvgOverlayUtil.isSvgImage(imageBytes);
|
||||
|
||||
PDDocument document = pdfDocumentFactory.load(pdfBytes);
|
||||
|
||||
int pages = document.getNumberOfPages();
|
||||
for (int i = 0; i < pages; i++) {
|
||||
PDPage page = document.getPage(i);
|
||||
|
||||
if (isSvg) {
|
||||
SvgOverlayUtil.overlaySvgOnPage(document, page, imageBytes, x, y);
|
||||
} else {
|
||||
try (PDPageContentStream contentStream =
|
||||
new PDPageContentStream(
|
||||
document,
|
||||
page,
|
||||
PDPageContentStream.AppendMode.APPEND,
|
||||
true,
|
||||
true)) {
|
||||
PDImageXObject image =
|
||||
PDImageXObject.createFromByteArray(document, imageBytes, "");
|
||||
contentStream.drawImage(image, x, y);
|
||||
log.info("Image successfully overlaid onto PDF page {}", i);
|
||||
}
|
||||
}
|
||||
|
||||
if (!everyPage && i == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
document.save(baos);
|
||||
document.close();
|
||||
|
||||
byte[] result = baos.toByteArray();
|
||||
log.info("PDF with overlaid image successfully created");
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
result,
|
||||
GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_overlayed.pdf"));
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to add image to PDF", e);
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
|
||||
+10
-1
@@ -52,9 +52,17 @@ public class PageNumbersController {
|
||||
int pageNumber = request.getStartingNumber();
|
||||
String pagesToNumber = request.getPagesToNumber();
|
||||
String customText = request.getCustomText();
|
||||
int zeroPad = request.getZeroPad();
|
||||
float fontSize = request.getFontSize();
|
||||
String fontType = request.getFontType();
|
||||
String fontColor = request.getFontColor();
|
||||
// compute padded number string where requested
|
||||
String formatN;
|
||||
if (zeroPad > 0) {
|
||||
formatN = String.format("%%0%dd", Math.max(0, zeroPad));
|
||||
} else {
|
||||
formatN = "%d";
|
||||
}
|
||||
|
||||
Color color = Color.BLACK;
|
||||
if (fontColor != null && !fontColor.trim().isEmpty()) {
|
||||
@@ -93,9 +101,10 @@ public class PageNumbersController {
|
||||
PDPage page = document.getPage(i);
|
||||
PDRectangle pageSize = page.getMediaBox();
|
||||
|
||||
String nFormatted = String.format(formatN, pageNumber);
|
||||
String text =
|
||||
customText
|
||||
.replace("{n}", String.valueOf(pageNumber))
|
||||
.replace("{n}", nFormatted)
|
||||
.replace("{total}", String.valueOf(document.getNumberOfPages()))
|
||||
.replace(
|
||||
"{filename}",
|
||||
|
||||
+202
-65
@@ -7,11 +7,13 @@ import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
@@ -34,17 +36,15 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.InitBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.AddStampRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -53,15 +53,20 @@ import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
@MiscApi
|
||||
@RequiredArgsConstructor
|
||||
public class StampController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
private static final int MAX_DATE_FORMAT_LENGTH = 50;
|
||||
private static final Pattern SAFE_DATE_FORMAT_PATTERN =
|
||||
Pattern.compile("^[yMdHhmsS/\\-:\\s.,'+EGuwWDFzZXa]+$");
|
||||
private static final Pattern CUSTOM_DATE_PATTERN = Pattern.compile("@date\\{([^}]{1,50})\\}");
|
||||
// Placeholder for escaped @ symbol (using Unicode private use area)
|
||||
private static final String ESCAPED_AT_PLACEHOLDER = "\uE000ESCAPED_AT\uE000";
|
||||
|
||||
/**
|
||||
* Initialize data binder for multipart file uploads. This method registers a custom editor for
|
||||
* MultipartFile to handle file uploads. It sets the MultipartFile to null if the uploaded file
|
||||
@@ -79,7 +84,7 @@ public class StampController {
|
||||
});
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-stamp")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-stamp")
|
||||
@Operation(
|
||||
summary = "Add stamp to a PDF file",
|
||||
description =
|
||||
@@ -170,7 +175,9 @@ public class StampController {
|
||||
overrideX,
|
||||
overrideY,
|
||||
margin,
|
||||
customColor);
|
||||
customColor,
|
||||
pageIndex,
|
||||
pdfFileName);
|
||||
} else if ("image".equalsIgnoreCase(stampType)) {
|
||||
addImageStamp(
|
||||
contentStream,
|
||||
@@ -205,9 +212,11 @@ public class StampController {
|
||||
float fontSize,
|
||||
String alphabet,
|
||||
float overrideX, // X override
|
||||
float overrideY,
|
||||
float overrideY, // Y override
|
||||
float margin,
|
||||
String colorString) // Y override
|
||||
String colorString,
|
||||
int currentPageNumber,
|
||||
String filename)
|
||||
throws IOException {
|
||||
String resourceDir;
|
||||
PDFont font = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
|
||||
@@ -235,8 +244,6 @@ public class StampController {
|
||||
}
|
||||
}
|
||||
|
||||
contentStream.setFont(font, fontSize);
|
||||
|
||||
Color redactColor;
|
||||
try {
|
||||
if (!colorString.startsWith("#")) {
|
||||
@@ -244,45 +251,54 @@ public class StampController {
|
||||
}
|
||||
redactColor = Color.decode(colorString);
|
||||
} catch (NumberFormatException e) {
|
||||
|
||||
redactColor = Color.LIGHT_GRAY;
|
||||
}
|
||||
|
||||
contentStream.setNonStrokingColor(redactColor);
|
||||
|
||||
PDRectangle pageSize = page.getMediaBox();
|
||||
float x, y;
|
||||
|
||||
if (overrideX >= 0 && overrideY >= 0) {
|
||||
// Use override values if provided
|
||||
x = overrideX;
|
||||
y = overrideY;
|
||||
} else {
|
||||
x = calculatePositionX(pageSize, position, fontSize, font, fontSize, stampText, margin);
|
||||
y =
|
||||
calculatePositionY(
|
||||
pageSize, position, calculateTextCapHeight(font, fontSize), margin);
|
||||
}
|
||||
|
||||
String currentDate = LocalDate.now().toString();
|
||||
String currentTime = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
|
||||
|
||||
int pageCount = document.getNumberOfPages();
|
||||
|
||||
String processedStampText =
|
||||
stampText
|
||||
.replace("@date", currentDate)
|
||||
.replace("@time", currentTime)
|
||||
.replace("@page_count", String.valueOf(pageCount));
|
||||
processStampText(stampText, currentPageNumber, pageCount, filename, document);
|
||||
|
||||
// Split the stampText into multiple lines
|
||||
String[] lines =
|
||||
RegexPatternUtils.getInstance().getEscapedNewlinePattern().split(stampText);
|
||||
String normalizedText =
|
||||
RegexPatternUtils.getInstance()
|
||||
.getEscapedNewlinePattern()
|
||||
.matcher(processedStampText)
|
||||
.replaceAll("\n");
|
||||
String[] lines = normalizedText.split("\\r?\\n");
|
||||
|
||||
PDRectangle pageSize = page.getMediaBox();
|
||||
|
||||
// Use fontSize directly (default 40 if not specified)
|
||||
float effectiveFontSize = fontSize > 0 ? fontSize : 40f;
|
||||
|
||||
contentStream.setFont(font, effectiveFontSize);
|
||||
|
||||
// Calculate dynamic line height based on font ascent and descent
|
||||
float ascent = font.getFontDescriptor().getAscent();
|
||||
float descent = font.getFontDescriptor().getDescent();
|
||||
float lineHeight = ((ascent - descent) / 1000) * fontSize;
|
||||
float lineHeight = ((ascent - descent) / 1000) * effectiveFontSize;
|
||||
|
||||
float maxLineWidth = 0;
|
||||
for (String line : lines) {
|
||||
float lineWidth = calculateTextWidth(line, font, effectiveFontSize);
|
||||
if (lineWidth > maxLineWidth) {
|
||||
maxLineWidth = lineWidth;
|
||||
}
|
||||
}
|
||||
|
||||
float totalTextHeight = lines.length * lineHeight;
|
||||
|
||||
float x, y;
|
||||
|
||||
if (overrideX >= 0 && overrideY >= 0) {
|
||||
x = overrideX;
|
||||
y = overrideY;
|
||||
} else {
|
||||
x = calculatePositionX(pageSize, position, maxLineWidth, margin);
|
||||
y = calculatePositionY(pageSize, position, totalTextHeight, margin);
|
||||
}
|
||||
|
||||
contentStream.beginText();
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
@@ -295,6 +311,140 @@ public class StampController {
|
||||
contentStream.endText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process stamp text by replacing all @commands with their actual values. Supported commands:
|
||||
*
|
||||
* <p>Date & Time:
|
||||
*
|
||||
* <ul>
|
||||
* <li>@date - Current date (YYYY-MM-DD)
|
||||
* <li>@time - Current time (HH:mm:ss)
|
||||
* <li>@datetime - Current date and time (YYYY-MM-DD HH:mm:ss)
|
||||
* <li>@date{format} - Custom date/time format (e.g., @date{dd/MM/yyyy})
|
||||
* <li>@year - Current year (4 digits)
|
||||
* <li>@month - Current month (01-12)
|
||||
* <li>@day - Current day of month (01-31)
|
||||
* </ul>
|
||||
*
|
||||
* <p>Page Information:
|
||||
*
|
||||
* <ul>
|
||||
* <li>@page_number or @page - Current page number
|
||||
* <li>@total_pages or @page_count - Total number of pages
|
||||
* </ul>
|
||||
*
|
||||
* <p>File Information:
|
||||
*
|
||||
* <ul>
|
||||
* <li>@filename - Original filename (without extension)
|
||||
* <li>@filename_full - Original filename (with extension)
|
||||
* </ul>
|
||||
*
|
||||
* <p>Document Metadata:
|
||||
*
|
||||
* <ul>
|
||||
* <li>@author - Document author (from PDF metadata)
|
||||
* <li>@title - Document title (from PDF metadata)
|
||||
* <li>@subject - Document subject (from PDF metadata)
|
||||
* </ul>
|
||||
*
|
||||
* <p>Other:
|
||||
*
|
||||
* <ul>
|
||||
* <li>@uuid - Short unique identifier (8 characters)
|
||||
* </ul>
|
||||
*/
|
||||
private String processStampText(
|
||||
String stampText,
|
||||
int currentPageNumber,
|
||||
int totalPages,
|
||||
String filename,
|
||||
PDDocument document) {
|
||||
if (stampText == null || stampText.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Handle escaped @@ sequences first - replace with placeholder to preserve literal @
|
||||
String result = stampText.replace("@@", ESCAPED_AT_PLACEHOLDER);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String currentDate = now.toLocalDate().toString();
|
||||
String currentTime = now.toLocalTime().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
|
||||
String currentDateTime = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
|
||||
String filenameWithoutExt = filename != null ? filename : "";
|
||||
if (filename != null && filename.contains(".")) {
|
||||
int lastDot = filename.lastIndexOf('.');
|
||||
if (lastDot > 0) { // Ensure there's actually a name before the dot
|
||||
filenameWithoutExt = filename.substring(0, lastDot);
|
||||
}
|
||||
}
|
||||
|
||||
String author = "";
|
||||
String title = "";
|
||||
String subject = "";
|
||||
if (document != null && document.getDocumentInformation() != null) {
|
||||
var info = document.getDocumentInformation();
|
||||
author = info.getAuthor() != null ? info.getAuthor() : "";
|
||||
title = info.getTitle() != null ? info.getTitle() : "";
|
||||
subject = info.getSubject() != null ? info.getSubject() : "";
|
||||
}
|
||||
|
||||
String uuid = UUID.randomUUID().toString().substring(0, 8);
|
||||
|
||||
// Process @date{format} with custom format first (must be before simple @date)
|
||||
Matcher matcher = CUSTOM_DATE_PATTERN.matcher(result);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (matcher.find()) {
|
||||
String format = matcher.group(1);
|
||||
String replacement = processCustomDateFormat(format, now);
|
||||
matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
|
||||
}
|
||||
matcher.appendTail(sb);
|
||||
result = sb.toString();
|
||||
|
||||
result =
|
||||
result.replace("@datetime", currentDateTime)
|
||||
.replace("@date", currentDate)
|
||||
.replace("@time", currentTime)
|
||||
.replace("@year", String.valueOf(now.getYear()))
|
||||
.replace("@month", String.format("%02d", now.getMonthValue()))
|
||||
.replace("@day", String.format("%02d", now.getDayOfMonth()))
|
||||
.replace("@page_number", String.valueOf(currentPageNumber))
|
||||
.replace(
|
||||
"@page_count", String.valueOf(totalPages)) // Must come before @page
|
||||
.replace("@total_pages", String.valueOf(totalPages))
|
||||
.replace(
|
||||
"@page",
|
||||
String.valueOf(currentPageNumber)) // Must come after @page_count
|
||||
.replace("@filename_full", filename != null ? filename : "")
|
||||
.replace("@filename", filenameWithoutExt)
|
||||
.replace("@author", author)
|
||||
.replace("@title", title)
|
||||
.replace("@subject", subject)
|
||||
.replace("@uuid", uuid);
|
||||
|
||||
result = result.replace(ESCAPED_AT_PLACEHOLDER, "@");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private String processCustomDateFormat(String format, LocalDateTime now) {
|
||||
if (format == null || format.length() > MAX_DATE_FORMAT_LENGTH) {
|
||||
return "[invalid format: too long]";
|
||||
}
|
||||
|
||||
if (!SAFE_DATE_FORMAT_PATTERN.matcher(format).matches()) {
|
||||
return "[invalid format]";
|
||||
}
|
||||
|
||||
try {
|
||||
return now.format(DateTimeFormatter.ofPattern(format));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return "[invalid format: " + format + "]";
|
||||
}
|
||||
}
|
||||
|
||||
private void addImageStamp(
|
||||
PDPageContentStream contentStream,
|
||||
MultipartFile stampImage,
|
||||
@@ -331,8 +481,8 @@ public class StampController {
|
||||
x = overrideX;
|
||||
y = overrideY;
|
||||
} else {
|
||||
x = calculatePositionX(pageSize, position, desiredPhysicalWidth, null, 0, null, margin);
|
||||
y = calculatePositionY(pageSize, position, fontSize, margin);
|
||||
x = calculatePositionX(pageSize, position, desiredPhysicalWidth, margin);
|
||||
y = calculatePositionY(pageSize, position, desiredPhysicalHeight, margin);
|
||||
}
|
||||
|
||||
contentStream.saveGraphicsState();
|
||||
@@ -343,23 +493,14 @@ public class StampController {
|
||||
}
|
||||
|
||||
private float calculatePositionX(
|
||||
PDRectangle pageSize,
|
||||
int position,
|
||||
float contentWidth,
|
||||
PDFont font,
|
||||
float fontSize,
|
||||
String text,
|
||||
float margin)
|
||||
throws IOException {
|
||||
float actualWidth =
|
||||
(text != null) ? calculateTextWidth(text, font, fontSize) : contentWidth;
|
||||
PDRectangle pageSize, int position, float contentWidth, float margin) {
|
||||
return switch (position % 3) {
|
||||
case 1: // Left
|
||||
yield pageSize.getLowerLeftX() + margin;
|
||||
case 2: // Center
|
||||
yield (pageSize.getWidth() - actualWidth) / 2;
|
||||
yield (pageSize.getWidth() - contentWidth) / 2;
|
||||
case 0: // Right
|
||||
yield pageSize.getUpperRightX() - actualWidth - margin;
|
||||
yield pageSize.getUpperRightX() - contentWidth - margin;
|
||||
default:
|
||||
yield 0;
|
||||
};
|
||||
@@ -368,12 +509,12 @@ public class StampController {
|
||||
private float calculatePositionY(
|
||||
PDRectangle pageSize, int position, float height, float margin) {
|
||||
return switch ((position - 1) / 3) {
|
||||
case 0: // Top
|
||||
yield pageSize.getUpperRightY() - height - margin;
|
||||
case 1: // Middle
|
||||
yield (pageSize.getHeight() - height) / 2;
|
||||
case 2: // Bottom
|
||||
yield pageSize.getLowerLeftY() + margin;
|
||||
case 0: // Top - first line near the top
|
||||
yield pageSize.getUpperRightY() - margin;
|
||||
case 1: // Middle - center of text block at page center
|
||||
yield (pageSize.getHeight() + height) / 2;
|
||||
case 2: // Bottom - first line positioned so last line is at bottom margin
|
||||
yield pageSize.getLowerLeftY() + margin + height;
|
||||
default:
|
||||
yield 0;
|
||||
};
|
||||
@@ -382,8 +523,4 @@ public class StampController {
|
||||
private float calculateTextWidth(String text, PDFont font, float fontSize) throws IOException {
|
||||
return font.getStringWidth(text) / 1000 * fontSize;
|
||||
}
|
||||
|
||||
private float calculateTextCapHeight(PDFont font, float fontSize) {
|
||||
return font.getFontDescriptor().getCapHeight() / 1000 * fontSize;
|
||||
}
|
||||
}
|
||||
|
||||
+69
-16
@@ -1,11 +1,9 @@
|
||||
package stirling.software.SPDF.controller.api.pipeline;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystemException;
|
||||
import java.nio.file.FileVisitOption;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -17,6 +15,7 @@ import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -24,7 +23,6 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -45,14 +43,20 @@ import stirling.software.common.util.FileMonitor;
|
||||
@Slf4j
|
||||
public class PipelineDirectoryProcessor {
|
||||
|
||||
private static final int MAX_DIRECTORY_DEPTH = 50; // Prevent excessive recursion
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ApiDocService apiDocService;
|
||||
private final PipelineProcessor processor;
|
||||
private final FileMonitor fileMonitor;
|
||||
private final PostHogService postHogService;
|
||||
private final String watchedFoldersDir;
|
||||
private final List<String> watchedFoldersDirs;
|
||||
private final String finishedFoldersDir;
|
||||
|
||||
// Track processed directories in current scan to prevent duplicates
|
||||
private final ThreadLocal<java.util.Set<Path>> processedDirsInScan =
|
||||
ThreadLocal.withInitial(java.util.HashSet::new);
|
||||
|
||||
public PipelineDirectoryProcessor(
|
||||
ObjectMapper objectMapper,
|
||||
ApiDocService apiDocService,
|
||||
@@ -65,13 +69,26 @@ public class PipelineDirectoryProcessor {
|
||||
this.processor = processor;
|
||||
this.fileMonitor = fileMonitor;
|
||||
this.postHogService = postHogService;
|
||||
this.watchedFoldersDir = runtimePathConfig.getPipelineWatchedFoldersPath();
|
||||
this.watchedFoldersDirs = runtimePathConfig.getPipelineWatchedFoldersPaths();
|
||||
this.finishedFoldersDir = runtimePathConfig.getPipelineFinishedFoldersPath();
|
||||
}
|
||||
|
||||
@Scheduled(fixedRate = 60000)
|
||||
public void scanFolders() {
|
||||
Path watchedFolderPath = Paths.get(watchedFoldersDir).toAbsolutePath();
|
||||
// Clear the processed directories set for this scan cycle
|
||||
processedDirsInScan.get().clear();
|
||||
|
||||
try {
|
||||
for (String watchedFoldersDir : watchedFoldersDirs) {
|
||||
scanWatchedFolder(Paths.get(watchedFoldersDir).toAbsolutePath());
|
||||
}
|
||||
} finally {
|
||||
// Clean up ThreadLocal to prevent memory leaks
|
||||
processedDirsInScan.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private void scanWatchedFolder(Path watchedFolderPath) {
|
||||
if (!Files.exists(watchedFolderPath)) {
|
||||
try {
|
||||
Files.createDirectories(watchedFolderPath);
|
||||
@@ -82,16 +99,34 @@ public class PipelineDirectoryProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the path is a directory and readable
|
||||
if (!Files.isDirectory(watchedFolderPath)) {
|
||||
log.error("Path is not a directory: {}", watchedFolderPath);
|
||||
return;
|
||||
}
|
||||
if (!Files.isReadable(watchedFolderPath)) {
|
||||
log.error("Directory is not readable: {}", watchedFolderPath);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Use FOLLOW_LINKS to follow symlinks, with max depth to prevent infinite loops
|
||||
Files.walkFileTree(
|
||||
watchedFolderPath,
|
||||
EnumSet.of(FileVisitOption.FOLLOW_LINKS),
|
||||
MAX_DIRECTORY_DEPTH,
|
||||
new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(
|
||||
Path dir, BasicFileAttributes attrs) {
|
||||
try {
|
||||
String dirName =
|
||||
dir.getFileName() != null
|
||||
? dir.getFileName().toString()
|
||||
: "";
|
||||
// Skip root directory and "processing" subdirectories
|
||||
if (!dir.equals(watchedFolderPath) && !dir.endsWith("processing")) {
|
||||
if (!dir.equals(watchedFolderPath)
|
||||
&& !"processing".equals(dirName)) {
|
||||
handleDirectory(dir);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -102,8 +137,11 @@ public class PipelineDirectoryProcessor {
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFileFailed(Path path, IOException exc) {
|
||||
// Handle broken symlinks or inaccessible directories
|
||||
log.error("Error accessing path: {}", path, exc);
|
||||
// Handle broken symlinks, permission issues, or inaccessible
|
||||
// directories
|
||||
if (exc != null) {
|
||||
log.debug("Cannot access path '{}': {}", path, exc.getMessage());
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
@@ -113,6 +151,17 @@ public class PipelineDirectoryProcessor {
|
||||
}
|
||||
|
||||
public void handleDirectory(Path dir) throws IOException {
|
||||
// Normalize path to absolute to prevent duplicate processing from different path
|
||||
// representations
|
||||
Path normalizedDir = dir.toAbsolutePath().normalize();
|
||||
|
||||
// Check if we've already processed this directory in this scan cycle
|
||||
java.util.Set<Path> processedDirs = processedDirsInScan.get();
|
||||
if (!processedDirs.add(normalizedDir)) {
|
||||
log.debug("Directory already processed in this scan cycle: {}", normalizedDir);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Handling directory: {}", dir);
|
||||
Path processingDir = createProcessingDirectory(dir);
|
||||
Optional<Path> jsonFileOptional = findJsonFile(dir);
|
||||
@@ -164,7 +213,8 @@ public class PipelineDirectoryProcessor {
|
||||
postHogService.captureEvent("pipeline_directory_event", properties);
|
||||
|
||||
List<File> filesToProcess = prepareFilesForProcessing(files, processingDir);
|
||||
runPipelineAgainstFiles(filesToProcess, config, dir, processingDir);
|
||||
try (PipelineResult result =
|
||||
runPipelineAgainstFiles(filesToProcess, config, dir, processingDir)) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,14 +353,14 @@ public class PipelineDirectoryProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
private void runPipelineAgainstFiles(
|
||||
private PipelineResult runPipelineAgainstFiles(
|
||||
List<File> filesToProcess, PipelineConfig config, Path dir, Path processingDir)
|
||||
throws IOException {
|
||||
try {
|
||||
List<Resource> inputFiles =
|
||||
processor.generateInputFiles(filesToProcess.toArray(new File[0]));
|
||||
if (inputFiles == null || inputFiles.isEmpty()) {
|
||||
return;
|
||||
return new PipelineResult();
|
||||
}
|
||||
PipelineResult result = processor.runPipelineAgainstFiles(inputFiles, config);
|
||||
|
||||
@@ -321,9 +371,11 @@ public class PipelineDirectoryProcessor {
|
||||
moveAndRenameFiles(result.getOutputFiles(), config, dir);
|
||||
deleteOriginalFiles(filesToProcess, processingDir);
|
||||
}
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("Error during processing", e);
|
||||
moveFilesBack(filesToProcess, processingDir);
|
||||
return new PipelineResult();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,8 +402,9 @@ public class PipelineDirectoryProcessor {
|
||||
log.info("Created directory: {}", outputPath);
|
||||
}
|
||||
Path outputFile = outputPath.resolve(outputFileName);
|
||||
try (OutputStream os = new FileOutputStream(outputFile.toFile())) {
|
||||
os.write(((ByteArrayResource) resource).getByteArray());
|
||||
try (OutputStream os = new FileOutputStream(outputFile.toFile());
|
||||
InputStream is = resource.getInputStream()) {
|
||||
is.transferTo(os);
|
||||
}
|
||||
log.info("File moved and renamed to {}", outputFile);
|
||||
}
|
||||
|
||||
+105
-41
@@ -15,12 +15,13 @@ import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RequestCallback;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@@ -38,6 +39,8 @@ import stirling.software.SPDF.model.PipelineResult;
|
||||
import stirling.software.SPDF.service.ApiDocService;
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
import stirling.software.common.service.UserServiceInterface;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@@ -49,13 +52,17 @@ public class PipelineProcessor {
|
||||
|
||||
private final ServletContext servletContext;
|
||||
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
public PipelineProcessor(
|
||||
ApiDocService apiDocService,
|
||||
@Autowired(required = false) UserServiceInterface userService,
|
||||
ServletContext servletContext) {
|
||||
ServletContext servletContext,
|
||||
TempFileManager tempFileManager) {
|
||||
this.apiDocService = apiDocService;
|
||||
this.userService = userService;
|
||||
this.servletContext = servletContext;
|
||||
this.tempFileManager = tempFileManager;
|
||||
}
|
||||
|
||||
public static String removeTrailingNaming(String filename) {
|
||||
@@ -137,14 +144,18 @@ public class PipelineProcessor {
|
||||
body.add(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
||||
ResponseEntity<Resource> response = sendWebRequest(url, body);
|
||||
// If the operation is filter and the response body is null or empty,
|
||||
// skip
|
||||
// this
|
||||
// file
|
||||
if (response.getBody() instanceof TempFileResource tempFileResource) {
|
||||
result.addTempFile(tempFileResource.getTempFile());
|
||||
}
|
||||
|
||||
if (operation.startsWith("/api/v1/filter/filter-")
|
||||
&& (response.getBody() == null
|
||||
|| response.getBody().length == 0)) {
|
||||
|| response.getBody().contentLength() == 0)) {
|
||||
filtersApplied = true;
|
||||
log.info("Skipping file due to filtering {}", operation);
|
||||
continue;
|
||||
@@ -154,7 +165,7 @@ public class PipelineProcessor {
|
||||
hasErrors = true;
|
||||
continue;
|
||||
}
|
||||
processOutputFiles(operation, response, newOutputFiles);
|
||||
processOutputFiles(operation, response, newOutputFiles, result);
|
||||
}
|
||||
}
|
||||
if (!hasInputFileType) {
|
||||
@@ -215,10 +226,13 @@ public class PipelineProcessor {
|
||||
body.add(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
||||
ResponseEntity<Resource> response = sendWebRequest(url, body);
|
||||
if (response.getBody() instanceof TempFileResource tempFileResource) {
|
||||
result.addTempFile(tempFileResource.getTempFile());
|
||||
}
|
||||
// Handle the response
|
||||
if (HttpStatus.OK.equals(response.getStatusCode())) {
|
||||
processOutputFiles(operation, response, newOutputFiles);
|
||||
processOutputFiles(operation, response, newOutputFiles, result);
|
||||
} else {
|
||||
// Log error if the response status is not OK
|
||||
logPrintStream.println(
|
||||
@@ -267,22 +281,47 @@ public class PipelineProcessor {
|
||||
return result;
|
||||
}
|
||||
|
||||
/* package */ ResponseEntity<byte[]> sendWebRequest(
|
||||
/* package */ ResponseEntity<Resource> sendWebRequest(
|
||||
String url, MultiValueMap<String, Object> body) {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
// Set up headers, including API key
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
String apiKey = getApiKeyForUser();
|
||||
headers.add("X-API-KEY", apiKey);
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
// Create HttpEntity with the body and headers
|
||||
if (apiKey != null && !apiKey.isEmpty()) {
|
||||
headers.add("X-API-KEY", apiKey);
|
||||
}
|
||||
|
||||
// Let the message converter set the multipart boundary/content type
|
||||
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
|
||||
// Make the request to the REST endpoint
|
||||
return restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class);
|
||||
|
||||
RequestCallback requestCallback =
|
||||
restTemplate.httpEntityCallback(entity, Resource.class /* response type hint */);
|
||||
return restTemplate.execute(
|
||||
url,
|
||||
HttpMethod.POST,
|
||||
requestCallback,
|
||||
response -> {
|
||||
try {
|
||||
TempFile tempFile = tempFileManager.createManagedTempFile("pipeline");
|
||||
Files.copy(
|
||||
response.getBody(),
|
||||
tempFile.getPath(),
|
||||
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
TempFileResource resource = new TempFileResource(tempFile);
|
||||
return ResponseEntity.status(response.getStatusCode())
|
||||
.headers(response.getHeaders())
|
||||
.body(resource);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private List<Resource> processOutputFiles(
|
||||
String operation, ResponseEntity<byte[]> response, List<Resource> newOutputFiles)
|
||||
String operation,
|
||||
ResponseEntity<Resource> response,
|
||||
List<Resource> newOutputFiles,
|
||||
PipelineResult result)
|
||||
throws IOException {
|
||||
// Define filename
|
||||
String newFilename;
|
||||
@@ -298,10 +337,14 @@ public class PipelineProcessor {
|
||||
// Check if the response body is a zip file
|
||||
if (isZip(response.getBody(), newFilename)) {
|
||||
// Unzip the file and add all the files to the new output files
|
||||
newOutputFiles.addAll(unzip(response.getBody()));
|
||||
newOutputFiles.addAll(unzip(response.getBody(), result));
|
||||
} else {
|
||||
final Resource tempResource = response.getBody();
|
||||
if (tempResource instanceof TempFileResource) {
|
||||
result.addTempFile(((TempFileResource) tempResource).getTempFile());
|
||||
}
|
||||
Resource outputResource =
|
||||
new ByteArrayResource(response.getBody()) {
|
||||
new FileSystemResource(tempResource.getFile()) {
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
@@ -313,7 +356,7 @@ public class PipelineProcessor {
|
||||
return newOutputFiles;
|
||||
}
|
||||
|
||||
public String extractFilename(ResponseEntity<byte[]> response) {
|
||||
public String extractFilename(ResponseEntity<Resource> response) {
|
||||
// Default filename if not found
|
||||
String filename = "default-filename.ext";
|
||||
HttpHeaders headers = response.getHeaders();
|
||||
@@ -348,14 +391,7 @@ public class PipelineProcessor {
|
||||
// debug statement
|
||||
log.info("Reading file: {}", path);
|
||||
if (Files.exists(path)) {
|
||||
Resource fileResource =
|
||||
new ByteArrayResource(Files.readAllBytes(path)) {
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return file.getName();
|
||||
}
|
||||
};
|
||||
Resource fileResource = new FileSystemResource(file);
|
||||
outputFiles.add(fileResource);
|
||||
} else {
|
||||
log.info("File not found: {}", path);
|
||||
@@ -372,8 +408,11 @@ public class PipelineProcessor {
|
||||
}
|
||||
List<Resource> outputFiles = new ArrayList<>();
|
||||
for (MultipartFile file : files) {
|
||||
Path tempFile = Files.createTempFile("SPDF-upload-", ".tmp");
|
||||
file.transferTo(tempFile);
|
||||
|
||||
Resource fileResource =
|
||||
new ByteArrayResource(file.getBytes()) {
|
||||
new FileSystemResource(tempFile.toFile()) {
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
@@ -386,8 +425,8 @@ public class PipelineProcessor {
|
||||
return outputFiles;
|
||||
}
|
||||
|
||||
private boolean isZip(byte[] data, String filename) {
|
||||
if (data == null || data.length < 4) {
|
||||
private boolean isZip(Resource data, String filename) throws IOException {
|
||||
if (data == null || data.contentLength() < 4) {
|
||||
return false;
|
||||
}
|
||||
if (filename != null) {
|
||||
@@ -398,29 +437,41 @@ public class PipelineProcessor {
|
||||
}
|
||||
}
|
||||
// Check the first four bytes of the data against the standard zip magic number
|
||||
return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04;
|
||||
try (InputStream is = data.getInputStream()) {
|
||||
byte[] header = new byte[4];
|
||||
if (is.read(header) < 4) {
|
||||
return false;
|
||||
}
|
||||
return header[0] == 0x50 && header[1] == 0x4B && header[2] == 0x03 && header[3] == 0x04;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isZip(byte[] data) {
|
||||
private boolean isZip(Resource data) throws IOException {
|
||||
return isZip(data, null);
|
||||
}
|
||||
|
||||
private List<Resource> unzip(byte[] data) throws IOException {
|
||||
log.info("Unzipping data of length: {}", data.length);
|
||||
private List<Resource> unzip(Resource data, PipelineResult result) throws IOException {
|
||||
log.info("Unzipping data of length: {}", data.contentLength());
|
||||
List<Resource> unzippedFiles = new ArrayList<>();
|
||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
|
||||
try (InputStream bais = data.getInputStream();
|
||||
ZipInputStream zis = ZipSecurity.createHardenedInputStream(bais)) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zis.getNextEntry()) != null) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[1024];
|
||||
int count;
|
||||
while ((count = zis.read(buffer)) != -1) {
|
||||
baos.write(buffer, 0, count);
|
||||
if (entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
TempFile tempFile = tempFileManager.createManagedTempFile("unzip");
|
||||
result.addTempFile(tempFile);
|
||||
try (OutputStream os = Files.newOutputStream(tempFile.getPath())) {
|
||||
byte[] buffer = new byte[4096];
|
||||
int count;
|
||||
while ((count = zis.read(buffer)) != -1) {
|
||||
os.write(buffer, 0, count);
|
||||
}
|
||||
}
|
||||
final String filename = entry.getName();
|
||||
Resource fileResource =
|
||||
new ByteArrayResource(baos.toByteArray()) {
|
||||
new FileSystemResource(tempFile.getFile()) {
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
@@ -428,9 +479,9 @@ public class PipelineProcessor {
|
||||
}
|
||||
};
|
||||
// If the unzipped file is a zip file, unzip it
|
||||
if (isZip(baos.toByteArray(), filename)) {
|
||||
if (isZip(fileResource, filename)) {
|
||||
log.info("File {} is a zip file. Unzipping...", filename);
|
||||
unzippedFiles.addAll(unzip(baos.toByteArray()));
|
||||
unzippedFiles.addAll(unzip(fileResource, result));
|
||||
} else {
|
||||
unzippedFiles.add(fileResource);
|
||||
}
|
||||
@@ -439,4 +490,17 @@ public class PipelineProcessor {
|
||||
log.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size());
|
||||
return unzippedFiles;
|
||||
}
|
||||
|
||||
private static class TempFileResource extends FileSystemResource {
|
||||
private final TempFile tempFile;
|
||||
|
||||
public TempFileResource(TempFile tempFile) {
|
||||
super(tempFile.getFile());
|
||||
this.tempFile = tempFile;
|
||||
}
|
||||
|
||||
public TempFile getTempFile() {
|
||||
return tempFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+124
-279
@@ -3,21 +3,15 @@ package stirling.software.SPDF.controller.api.security;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.pdfbox.cos.COSInputStream;
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.cos.COSString;
|
||||
import org.apache.pdfbox.io.RandomAccessRead;
|
||||
import org.apache.pdfbox.io.RandomAccessReadBufferedFile;
|
||||
import org.apache.pdfbox.pdmodel.*;
|
||||
import org.apache.pdfbox.pdmodel.common.PDMetadata;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
@@ -35,8 +29,7 @@ import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup;
|
||||
import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentProperties;
|
||||
import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript;
|
||||
import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI;
|
||||
import org.apache.pdfbox.pdmodel.interactive.action.*;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
|
||||
@@ -44,23 +37,14 @@ import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlin
|
||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineNode;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
||||
import org.apache.pdfbox.preflight.PreflightDocument;
|
||||
import org.apache.pdfbox.preflight.ValidationResult;
|
||||
import org.apache.pdfbox.preflight.exception.SyntaxValidationException;
|
||||
import org.apache.pdfbox.preflight.exception.ValidationException;
|
||||
import org.apache.pdfbox.preflight.parser.PreflightParser;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.apache.xmpbox.XMPMetadata;
|
||||
import org.apache.xmpbox.schema.PDFAIdentificationSchema;
|
||||
import org.apache.xmpbox.xml.DomXmpParser;
|
||||
import org.apache.xmpbox.xml.XmpParsingException;
|
||||
import org.apache.xmpbox.xml.XmpSerializer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -68,21 +52,22 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.security.PDFVerificationResult;
|
||||
import stirling.software.SPDF.service.VeraPDFService;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.SecurityApi;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.RegexPatternUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/security")
|
||||
@SecurityApi
|
||||
@Slf4j
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
@RequiredArgsConstructor
|
||||
public class GetInfoOnPDF {
|
||||
|
||||
@@ -95,6 +80,7 @@ public class GetInfoOnPDF {
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final VeraPDFService veraPDFService;
|
||||
|
||||
private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) {
|
||||
if (outline == null) return;
|
||||
@@ -111,214 +97,8 @@ public class GetInfoOnPDF {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean checkForStandard(PDDocument document, String standardKeyword) {
|
||||
if ("PDF/A".equalsIgnoreCase(standardKeyword)) {
|
||||
return getPdfAConformanceLevel(document) != null;
|
||||
}
|
||||
|
||||
return checkStandardInMetadata(document, standardKeyword);
|
||||
}
|
||||
|
||||
public static String getPdfAConformanceLevel(PDDocument document) {
|
||||
if (document == null || document.isEncrypted()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getPdfAVersionFromMetadata(document);
|
||||
}
|
||||
|
||||
private static String getPdfAVersionFromMetadata(PDDocument document) {
|
||||
try {
|
||||
PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata();
|
||||
if (pdMetadata != null) {
|
||||
try (COSInputStream metaStream = pdMetadata.createInputStream()) {
|
||||
DomXmpParser domXmpParser = new DomXmpParser();
|
||||
XMPMetadata xmpMeta = domXmpParser.parse(metaStream);
|
||||
|
||||
PDFAIdentificationSchema pdfId = xmpMeta.getPDFAIdentificationSchema();
|
||||
if (pdfId != null) {
|
||||
Integer part = pdfId.getPart();
|
||||
String conformance = pdfId.getConformance();
|
||||
|
||||
if (part != null && conformance != null) {
|
||||
return part + conformance.toUpperCase(Locale.ROOT);
|
||||
}
|
||||
} else {
|
||||
try (COSInputStream rawStream = pdMetadata.createInputStream()) {
|
||||
byte[] metadataBytes = rawStream.readAllBytes();
|
||||
String rawMetadata = new String(metadataBytes, StandardCharsets.UTF_8);
|
||||
String extracted = extractPdfAVersionFromRawXml(rawMetadata);
|
||||
if (extracted != null) {
|
||||
return extracted;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (XmpParsingException e) {
|
||||
log.debug("XMP parsing failed, trying raw metadata search: {}", e.getMessage());
|
||||
try (COSInputStream metaStream = pdMetadata.createInputStream()) {
|
||||
byte[] metadataBytes = metaStream.readAllBytes();
|
||||
String rawMetadata = new String(metadataBytes, StandardCharsets.UTF_8);
|
||||
String extracted = extractPdfAVersionFromRawXml(rawMetadata);
|
||||
if (extracted != null) {
|
||||
return extracted;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Error reading PDF/A metadata: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String extractPdfAVersionFromRawXml(String rawXml) {
|
||||
if (rawXml == null || rawXml.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Pattern partPattern = RegexPatternUtils.getInstance().getPdfAidPartPattern();
|
||||
Pattern confPattern = RegexPatternUtils.getInstance().getPdfAidConformancePattern();
|
||||
|
||||
Matcher partMatcher = partPattern.matcher(rawXml);
|
||||
Matcher confMatcher = confPattern.matcher(rawXml);
|
||||
|
||||
if (partMatcher.find() && confMatcher.find()) {
|
||||
String part = partMatcher.group(1);
|
||||
String conformance = confMatcher.group(1).toUpperCase(Locale.ROOT);
|
||||
return part + conformance;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Error parsing raw XMP for PDF/A version: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean validatePdfAWithPreflight(PDDocument document, String version) {
|
||||
if (document == null || document.isEncrypted()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use Stream-to-File pattern: save to temp file instead of loading into memory
|
||||
// This prevents OutOfMemoryError on large PDFs
|
||||
Path tempFile = null;
|
||||
try {
|
||||
tempFile = Files.createTempFile("preflight-", ".pdf");
|
||||
|
||||
// Save document to temp file (avoids loading entire document into memory)
|
||||
try (var outputStream = Files.newOutputStream(tempFile)) {
|
||||
document.save(outputStream);
|
||||
}
|
||||
|
||||
// Use RandomAccessReadBufferedFile for efficient file-based reading
|
||||
// This avoids Windows file locking issues that occur with memory-mapped files
|
||||
try (RandomAccessRead source = new RandomAccessReadBufferedFile(tempFile.toFile())) {
|
||||
PreflightParser parser = new PreflightParser(source);
|
||||
|
||||
try (PDDocument parsedDocument = parser.parse()) {
|
||||
if (!(parsedDocument instanceof PreflightDocument preflightDocument)) {
|
||||
log.debug(
|
||||
"Parsed document is not a PreflightDocument; unable to validate claimed PDF/A {}",
|
||||
version);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
ValidationResult result = preflightDocument.validate();
|
||||
if (!result.isValid() && log.isDebugEnabled()) {
|
||||
log.debug(
|
||||
"PDF/A validation found {} errors for claimed version {}",
|
||||
result.getErrorsList().size(),
|
||||
version);
|
||||
int logged = 0;
|
||||
for (ValidationResult.ValidationError error : result.getErrorsList()) {
|
||||
log.debug(
|
||||
" Error {}: {}", error.getErrorCode(), error.getDetails());
|
||||
if (++logged >= MAX_LOGGED_ERRORS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.isValid();
|
||||
} catch (ValidationException e) {
|
||||
log.debug(
|
||||
"Validation exception during PDF/A validation: {}", e.getMessage());
|
||||
}
|
||||
} catch (SyntaxValidationException e) {
|
||||
log.debug(
|
||||
"Syntax validation failed during PDF/A validation: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.debug("IOException during PDF/A validation: {}", e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.debug("Unexpected error during PDF/A validation: {}", e.getMessage());
|
||||
} finally {
|
||||
// Explicitly clean up temp file to prevent disk exhaustion
|
||||
// This must be in finally block to ensure cleanup even on exceptions
|
||||
if (tempFile != null) {
|
||||
try {
|
||||
Files.deleteIfExists(tempFile);
|
||||
} catch (IOException e) {
|
||||
log.warn(
|
||||
"Failed to delete temp file during PDF/A validation cleanup: {}",
|
||||
tempFile,
|
||||
e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean checkStandardInMetadata(PDDocument document, String standardKeyword) {
|
||||
// Check XMP Metadata
|
||||
try {
|
||||
PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata();
|
||||
if (pdMetadata != null) {
|
||||
try (COSInputStream metaStream = pdMetadata.createInputStream()) {
|
||||
// First try to read raw metadata as string to check for standard keywords
|
||||
byte[] metadataBytes = metaStream.readAllBytes();
|
||||
String rawMetadata = new String(metadataBytes, StandardCharsets.UTF_8);
|
||||
|
||||
if (rawMetadata.contains(standardKeyword)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// If raw check doesn't find it, try parsing with XMP parser
|
||||
try (COSInputStream metaStream = pdMetadata.createInputStream()) {
|
||||
try {
|
||||
DomXmpParser domXmpParser = new DomXmpParser();
|
||||
XMPMetadata xmpMeta = domXmpParser.parse(metaStream);
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
new XmpSerializer().serialize(xmpMeta, baos, true);
|
||||
String xmpString = baos.toString(StandardCharsets.UTF_8);
|
||||
|
||||
if (xmpString.contains(standardKeyword)) {
|
||||
return true;
|
||||
}
|
||||
} catch (XmpParsingException e) {
|
||||
// XMP parsing failed, but we already checked raw metadata above
|
||||
log.debug(
|
||||
"XMP parsing failed for standard check, but raw metadata was already checked: {}",
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ExceptionUtils.logException("PDF standard checking", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ObjectNode generatePDFSummaryData(
|
||||
PDDocument document, String pdfaConformanceLevel, Boolean pdfaValidationPassed) {
|
||||
PDDocument document, List<PDFVerificationResult> verificationResults) {
|
||||
ObjectNode summaryData = objectMapper.createObjectNode();
|
||||
|
||||
// Check if encrypted
|
||||
@@ -346,24 +126,16 @@ public class GetInfoOnPDF {
|
||||
}
|
||||
|
||||
// Check standard compliance
|
||||
if (pdfaConformanceLevel != null) {
|
||||
summaryData.put("standardCompliance", "PDF/A-" + pdfaConformanceLevel);
|
||||
summaryData.put("standardPurpose", "long-term archiving");
|
||||
if (pdfaValidationPassed != null) {
|
||||
summaryData.put("standardValidationPassed", pdfaValidationPassed);
|
||||
if (verificationResults != null && !verificationResults.isEmpty()) {
|
||||
ArrayNode complianceArray = objectMapper.createArrayNode();
|
||||
for (PDFVerificationResult result : verificationResults) {
|
||||
ObjectNode complianceNode = objectMapper.createObjectNode();
|
||||
complianceNode.put("Standard", result.getStandard());
|
||||
complianceNode.put("Compliant", result.isCompliant());
|
||||
complianceNode.put("Summary", result.getComplianceSummary());
|
||||
complianceArray.add(complianceNode);
|
||||
}
|
||||
} else if (checkForStandard(document, "PDF/X")) {
|
||||
summaryData.put("standardCompliance", "PDF/X");
|
||||
summaryData.put("standardPurpose", "graphic exchange");
|
||||
} else if (checkForStandard(document, "PDF/UA")) {
|
||||
summaryData.put("standardCompliance", "PDF/UA");
|
||||
summaryData.put("standardPurpose", "universal accessibility");
|
||||
} else if (checkForStandard(document, "PDF/E")) {
|
||||
summaryData.put("standardCompliance", "PDF/E");
|
||||
summaryData.put("standardPurpose", "engineering workflows");
|
||||
} else if (checkForStandard(document, "PDF/VT")) {
|
||||
summaryData.put("standardCompliance", "PDF/VT");
|
||||
summaryData.put("standardPurpose", "variable and transactional printing");
|
||||
summaryData.set("Compliance", complianceArray);
|
||||
}
|
||||
|
||||
return summaryData;
|
||||
@@ -591,40 +363,113 @@ public class GetInfoOnPDF {
|
||||
return docInfoNode;
|
||||
}
|
||||
|
||||
private static ObjectNode extractComplianceInfo(PDDocument document) {
|
||||
private static ObjectNode extractComplianceInfo(
|
||||
PDDocument doc, List<PDFVerificationResult> verificationResults) {
|
||||
ObjectNode compliancy = objectMapper.createObjectNode();
|
||||
|
||||
try {
|
||||
String pdfaConformanceLevel = getPdfAConformanceLevel(document);
|
||||
boolean isPdfACompliant = pdfaConformanceLevel != null;
|
||||
boolean isPdfXCompliant = checkForStandard(document, "PDF/X");
|
||||
boolean isPdfECompliant = checkForStandard(document, "PDF/E");
|
||||
boolean isPdfVTCompliant = checkForStandard(document, "PDF/VT");
|
||||
boolean isPdfUACompliant = checkForStandard(document, "PDF/UA");
|
||||
boolean isPdfBCompliant = checkForStandard(document, "PDF/B");
|
||||
boolean isPdfSECCompliant = checkForStandard(document, "PDF/SEC");
|
||||
boolean isPdfA = false;
|
||||
boolean isPdfUA = false;
|
||||
boolean isPdfX = false;
|
||||
boolean isPdfE = false;
|
||||
boolean isPdfB = false;
|
||||
String pdfAConformanceLevel = null;
|
||||
|
||||
compliancy.put("IsPDF/ACompliant", isPdfACompliant);
|
||||
if (pdfaConformanceLevel != null) {
|
||||
compliancy.put("PDF/AConformanceLevel", pdfaConformanceLevel);
|
||||
Boolean pdfaValidationPassed =
|
||||
validatePdfAWithPreflight(document, pdfaConformanceLevel);
|
||||
compliancy.put("IsPDF/AValidated", pdfaValidationPassed);
|
||||
if (verificationResults != null) {
|
||||
for (PDFVerificationResult result : verificationResults) {
|
||||
if (result == null) continue;
|
||||
if (result.isCompliant()) {
|
||||
String std = result.getStandard().toLowerCase();
|
||||
if (std.contains("pdf_a") || std.contains("pdfa")) {
|
||||
isPdfA = true;
|
||||
String profile = result.getValidationProfile();
|
||||
if (profile != null) {
|
||||
if (profile.contains("1b")
|
||||
|| profile.contains("2b")
|
||||
|| profile.contains("3b")) {
|
||||
isPdfB = true;
|
||||
}
|
||||
// Simple extraction: remove "pdfa-" prefix
|
||||
pdfAConformanceLevel = profile.replace("pdfa-", "");
|
||||
}
|
||||
}
|
||||
if (std.contains("pdf_ua") || std.contains("pdfua")) isPdfUA = true;
|
||||
if (std.contains("pdf_x") || std.contains("pdfx")) isPdfX = true;
|
||||
if (std.contains("pdf_e") || std.contains("pdfe")) isPdfE = true;
|
||||
}
|
||||
}
|
||||
compliancy.put("IsPDF/XCompliant", isPdfXCompliant);
|
||||
compliancy.put("IsPDF/ECompliant", isPdfECompliant);
|
||||
compliancy.put("IsPDF/VTCompliant", isPdfVTCompliant);
|
||||
compliancy.put("IsPDF/UACompliant", isPdfUACompliant);
|
||||
compliancy.put("IsPDF/BCompliant", isPdfBCompliant);
|
||||
compliancy.put("IsPDF/SECCompliant", isPdfSECCompliant);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error extracting compliance info: {}", e.getMessage());
|
||||
compliancy.put("IsPDF/ACompliant", isPdfA);
|
||||
compliancy.put("IsPDF/UACompliant", isPdfUA);
|
||||
compliancy.put("IsPDF/ECompliant", isPdfE);
|
||||
compliancy.put("IsPDF/VTCompliant", false); // Not currently implemented
|
||||
compliancy.put("IsPDF/BCompliant", isPdfB);
|
||||
if (pdfAConformanceLevel != null) {
|
||||
compliancy.put("PDF/AConformanceLevel", pdfAConformanceLevel);
|
||||
}
|
||||
|
||||
compliancy.put("IsPDF/SECCompliant", isSECCompliant(doc));
|
||||
|
||||
if (verificationResults != null && !verificationResults.isEmpty()) {
|
||||
// Keep original simple structure as backup or extra info
|
||||
for (PDFVerificationResult result : verificationResults) {
|
||||
if (result == null) continue;
|
||||
String standard = result.getStandard();
|
||||
boolean isCompliant = result.isCompliant();
|
||||
|
||||
if (standard != null) {
|
||||
// Avoid overwriting specific keys if collision, but here keys are distinct
|
||||
// enough
|
||||
compliancy.put(standard, isCompliant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return compliancy;
|
||||
}
|
||||
|
||||
private static boolean isSECCompliant(PDDocument doc) {
|
||||
try {
|
||||
// 1. Check Encryption
|
||||
if (doc.isEncrypted()) return false;
|
||||
|
||||
PDDocumentCatalog catalog = doc.getDocumentCatalog();
|
||||
|
||||
// 2. Check for JavaScript (Active Content)
|
||||
if (catalog.getOpenAction() instanceof PDActionJavaScript) return false;
|
||||
if (catalog.getNames() != null && catalog.getNames().getJavaScript() != null)
|
||||
return false;
|
||||
|
||||
// Check for AcroForm
|
||||
if (catalog.getAcroForm() != null) return false;
|
||||
|
||||
// 3. Check for Embedded Files
|
||||
if (catalog.getNames() != null && catalog.getNames().getEmbeddedFiles() != null)
|
||||
return false;
|
||||
|
||||
// 4. Check for External Links or Navigation Actions
|
||||
for (PDPage page : doc.getPages()) {
|
||||
for (PDAnnotation annotation : page.getAnnotations()) {
|
||||
if (annotation instanceof PDAnnotationLink) {
|
||||
PDAnnotationLink link = (PDAnnotationLink) annotation;
|
||||
PDAction action = link.getAction();
|
||||
if (action instanceof PDActionURI
|
||||
|| action instanceof PDActionLaunch
|
||||
|| action instanceof PDActionRemoteGoTo
|
||||
|| action instanceof PDActionSubmitForm) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("Error checking SEC compliance: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static ObjectNode extractEncryptionInfo(PDDocument document) {
|
||||
ObjectNode encryption = objectMapper.createObjectNode();
|
||||
|
||||
@@ -1215,7 +1060,7 @@ public class GetInfoOnPDF {
|
||||
return stats;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/get-info-on-pdf")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/get-info-on-pdf")
|
||||
@Operation(
|
||||
summary = "Get comprehensive PDF information",
|
||||
description =
|
||||
@@ -1231,6 +1076,13 @@ public class GetInfoOnPDF {
|
||||
return createErrorResponse("Invalid PDF file: " + e.getMessage());
|
||||
}
|
||||
|
||||
List<PDFVerificationResult> verificationResults = null;
|
||||
try {
|
||||
verificationResults = veraPDFService.validatePDF(inputFile.getInputStream());
|
||||
} catch (Exception e) {
|
||||
log.error("VeraPDF validation failed", e);
|
||||
}
|
||||
|
||||
boolean readonly = true;
|
||||
|
||||
try (PDDocument pdfBoxDoc = pdfDocumentFactory.load(inputFile, readonly)) {
|
||||
@@ -1239,20 +1091,13 @@ public class GetInfoOnPDF {
|
||||
ObjectNode metadata = extractMetadata(pdfBoxDoc);
|
||||
ObjectNode basicInfo = extractBasicInfo(pdfBoxDoc, inputFile.getSize());
|
||||
ObjectNode docInfoNode = extractDocumentInfo(pdfBoxDoc);
|
||||
ObjectNode compliancy = extractComplianceInfo(pdfBoxDoc);
|
||||
ObjectNode compliancy = extractComplianceInfo(pdfBoxDoc, verificationResults);
|
||||
ObjectNode encryption = extractEncryptionInfo(pdfBoxDoc);
|
||||
ObjectNode permissionsNode = extractPermissions(pdfBoxDoc);
|
||||
ObjectNode other = extractOtherInfo(pdfBoxDoc);
|
||||
ObjectNode formFieldsNode = extractFormFields(pdfBoxDoc);
|
||||
|
||||
// Generate summary data
|
||||
String pdfaConformanceLevel = getPdfAConformanceLevel(pdfBoxDoc);
|
||||
Boolean pdfaValidationPassed = null;
|
||||
if (pdfaConformanceLevel != null) {
|
||||
pdfaValidationPassed = validatePdfAWithPreflight(pdfBoxDoc, pdfaConformanceLevel);
|
||||
}
|
||||
ObjectNode summaryData =
|
||||
generatePDFSummaryData(pdfBoxDoc, pdfaConformanceLevel, pdfaValidationPassed);
|
||||
ObjectNode summaryData = generatePDFSummaryData(pdfBoxDoc, verificationResults);
|
||||
|
||||
// Extract per-page information
|
||||
ObjectNode pageInfoParent = extractPerPageInfo(pdfBoxDoc);
|
||||
|
||||
+4
-8
@@ -6,16 +6,12 @@ import java.util.List;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.verapdf.core.EncryptedPdfException;
|
||||
import org.verapdf.core.ModelParsingException;
|
||||
import org.verapdf.core.ValidationException;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -23,11 +19,11 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.security.PDFVerificationRequest;
|
||||
import stirling.software.SPDF.model.api.security.PDFVerificationResult;
|
||||
import stirling.software.SPDF.service.VeraPDFService;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.SecurityApi;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/security")
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
@SecurityApi
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class VerifyPDFController {
|
||||
@@ -41,7 +37,7 @@ public class VerifyPDFController {
|
||||
+ "Automatically detects PDF/A, PDF/UA-1, PDF/UA-2, and WTPDF standards "
|
||||
+ "from the document's XMP metadata and validates compliance. "
|
||||
+ "Input:PDF Output:JSON Type:SISO")
|
||||
@PostMapping(value = "/verify-pdf", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/verify-pdf", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public ResponseEntity<List<PDFVerificationResult>> verifyPDF(
|
||||
@ModelAttribute PDFVerificationRequest request) {
|
||||
|
||||
|
||||
-221
@@ -1,221 +0,0 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.ApplicationContextProvider;
|
||||
import stirling.software.common.util.CheckProgramInstall;
|
||||
|
||||
@Controller
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
public class ConverterWebController {
|
||||
|
||||
@GetMapping("/img-to-pdf")
|
||||
@Hidden
|
||||
public String convertImgToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "img-to-pdf");
|
||||
return "convert/img-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/cbz-to-pdf")
|
||||
@Hidden
|
||||
public String convertCbzToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "cbz-to-pdf");
|
||||
return "convert/cbz-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-cbz")
|
||||
@Hidden
|
||||
public String convertPdfToCbzForm(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-to-cbz");
|
||||
return "convert/pdf-to-cbz";
|
||||
}
|
||||
|
||||
@GetMapping("/cbr-to-pdf")
|
||||
@Hidden
|
||||
public String convertCbrToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "cbr-to-pdf");
|
||||
return "convert/cbr-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/ebook-to-pdf")
|
||||
@Hidden
|
||||
public String convertEbookToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "ebook-to-pdf");
|
||||
return "convert/ebook-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-epub")
|
||||
@Hidden
|
||||
public String convertPdfToEpubForm(Model model) {
|
||||
if (!ApplicationContextProvider.getBean(EndpointConfiguration.class)
|
||||
.isEndpointEnabled("pdf-to-epub")) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
model.addAttribute("currentPage", "pdf-to-epub");
|
||||
return "convert/pdf-to-epub";
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-cbr")
|
||||
@Hidden
|
||||
public String convertPdfToCbrForm(Model model) {
|
||||
if (!ApplicationContextProvider.getBean(EndpointConfiguration.class)
|
||||
.isEndpointEnabled("pdf-to-cbr")) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
model.addAttribute("currentPage", "pdf-to-cbr");
|
||||
return "convert/pdf-to-cbr";
|
||||
}
|
||||
|
||||
@GetMapping("/html-to-pdf")
|
||||
@Hidden
|
||||
public String convertHTMLToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "html-to-pdf");
|
||||
return "convert/html-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/markdown-to-pdf")
|
||||
@Hidden
|
||||
public String convertMarkdownToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "markdown-to-pdf");
|
||||
return "convert/markdown-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-markdown")
|
||||
@Hidden
|
||||
public String convertPdfToMarkdownForm(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-to-markdown");
|
||||
return "convert/pdf-to-markdown";
|
||||
}
|
||||
|
||||
@GetMapping("/url-to-pdf")
|
||||
@Hidden
|
||||
public String convertURLToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "url-to-pdf");
|
||||
return "convert/url-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/file-to-pdf")
|
||||
@Hidden
|
||||
public String convertToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "file-to-pdf");
|
||||
return "convert/file-to-pdf";
|
||||
}
|
||||
|
||||
// PDF TO......
|
||||
|
||||
@GetMapping("/pdf-to-img")
|
||||
@Hidden
|
||||
public String pdfToimgForm(Model model) {
|
||||
boolean isPython = CheckProgramInstall.isPythonAvailable();
|
||||
ApplicationProperties properties =
|
||||
ApplicationContextProvider.getBean(ApplicationProperties.class);
|
||||
if (properties != null && properties.getSystem() != null) {
|
||||
model.addAttribute("maxDPI", properties.getSystem().getMaxDPI());
|
||||
} else {
|
||||
model.addAttribute("maxDPI", 500); // Default value if not set
|
||||
}
|
||||
model.addAttribute("isPython", isPython);
|
||||
model.addAttribute("currentPage", "pdf-to-img");
|
||||
return "convert/pdf-to-img";
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-html")
|
||||
@Hidden
|
||||
public ModelAndView pdfToHTML() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-html");
|
||||
modelAndView.addObject("currentPage", "pdf-to-html");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-presentation")
|
||||
@Hidden
|
||||
public ModelAndView pdfToPresentation() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-presentation");
|
||||
modelAndView.addObject("currentPage", "pdf-to-presentation");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-text")
|
||||
@Hidden
|
||||
public ModelAndView pdfToText() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-text");
|
||||
modelAndView.addObject("currentPage", "pdf-to-text");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-word")
|
||||
@Hidden
|
||||
public ModelAndView pdfToWord() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-word");
|
||||
modelAndView.addObject("currentPage", "pdf-to-word");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-xml")
|
||||
@Hidden
|
||||
public ModelAndView pdfToXML() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-xml");
|
||||
modelAndView.addObject("currentPage", "pdf-to-xml");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-csv")
|
||||
@Hidden
|
||||
public ModelAndView pdfToCSV() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-csv");
|
||||
modelAndView.addObject("currentPage", "pdf-to-csv");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-pdfa")
|
||||
@Hidden
|
||||
public String pdfToPdfAForm(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-to-pdfa");
|
||||
return "convert/pdf-to-pdfa";
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-vector")
|
||||
@Hidden
|
||||
public String pdfToVectorForm(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-to-vector");
|
||||
return "convert/pdf-to-vector";
|
||||
}
|
||||
|
||||
@GetMapping("/vector-to-pdf")
|
||||
@Hidden
|
||||
public String vectorToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "vector-to-pdf");
|
||||
return "convert/vector-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/eml-to-pdf")
|
||||
@Hidden
|
||||
public String convertEmlToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "eml-to-pdf");
|
||||
return "convert/eml-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-video")
|
||||
@Hidden
|
||||
public String pdfToVideo(Model model) {
|
||||
ApplicationProperties properties =
|
||||
ApplicationContextProvider.getBean(ApplicationProperties.class);
|
||||
if (properties != null && properties.getSystem() != null) {
|
||||
model.addAttribute("maxDPI", properties.getSystem().getMaxDPI());
|
||||
} else {
|
||||
model.addAttribute("maxDPI", 500);
|
||||
}
|
||||
model.addAttribute("currentPage", "pdf-to-video");
|
||||
return "convert/pdf-to-video";
|
||||
}
|
||||
}
|
||||
-352
@@ -1,352 +0,0 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.ui.Model;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.SignatureFile;
|
||||
import stirling.software.SPDF.service.SharedSignatureService;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.service.UserServiceInterface;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
|
||||
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@Slf4j
|
||||
public class GeneralWebController {
|
||||
|
||||
private final SharedSignatureService signatureService;
|
||||
private final UserServiceInterface userService;
|
||||
private final ResourceLoader resourceLoader;
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
|
||||
public GeneralWebController(
|
||||
SharedSignatureService signatureService,
|
||||
@Autowired(required = false) UserServiceInterface userService,
|
||||
ResourceLoader resourceLoader,
|
||||
RuntimePathConfig runtimePathConfig) {
|
||||
this.signatureService = signatureService;
|
||||
this.userService = userService;
|
||||
this.resourceLoader = resourceLoader;
|
||||
this.runtimePathConfig = runtimePathConfig;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/pipeline")
|
||||
@Hidden
|
||||
public String pipelineForm(Model model) {
|
||||
model.addAttribute("currentPage", "pipeline");
|
||||
List<String> pipelineConfigs = new ArrayList<>();
|
||||
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
|
||||
if (new File(runtimePathConfig.getPipelineDefaultWebUiConfigs()).exists()) {
|
||||
try (Stream<Path> paths =
|
||||
Files.walk(Paths.get(runtimePathConfig.getPipelineDefaultWebUiConfigs()))) {
|
||||
List<Path> jsonFiles =
|
||||
paths.filter(Files::isRegularFile)
|
||||
.filter(p -> p.toString().endsWith(".json"))
|
||||
.toList();
|
||||
for (Path jsonFile : jsonFiles) {
|
||||
String content = Files.readString(jsonFile, StandardCharsets.UTF_8);
|
||||
pipelineConfigs.add(content);
|
||||
}
|
||||
for (String config : pipelineConfigs) {
|
||||
Map<String, Object> jsonContent =
|
||||
new ObjectMapper()
|
||||
.readValue(config, new TypeReference<Map<String, Object>>() {});
|
||||
String name = (String) jsonContent.get("name");
|
||||
if (name == null || name.isEmpty()) {
|
||||
String filename =
|
||||
jsonFiles
|
||||
.get(pipelineConfigs.indexOf(config))
|
||||
.getFileName()
|
||||
.toString();
|
||||
name = filename.substring(0, filename.lastIndexOf('.'));
|
||||
}
|
||||
Map<String, String> configWithName = new HashMap<>();
|
||||
configWithName.put("json", config);
|
||||
configWithName.put("name", name);
|
||||
pipelineConfigsWithNames.add(configWithName);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("exception", e);
|
||||
}
|
||||
}
|
||||
if (pipelineConfigsWithNames.isEmpty()) {
|
||||
Map<String, String> configWithName = new HashMap<>();
|
||||
configWithName.put("json", "");
|
||||
configWithName.put("name", "No preloaded configs found");
|
||||
pipelineConfigsWithNames.add(configWithName);
|
||||
}
|
||||
model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames);
|
||||
model.addAttribute("pipelineConfigs", pipelineConfigs);
|
||||
return "pipeline";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/merge-pdfs")
|
||||
@Hidden
|
||||
public String mergePdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "merge-pdfs");
|
||||
return "merge-pdfs";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/split-pdf-by-sections")
|
||||
@Hidden
|
||||
public String splitPdfBySections(Model model) {
|
||||
model.addAttribute("currentPage", "split-pdf-by-sections");
|
||||
return "split-pdf-by-sections";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/split-pdf-by-chapters")
|
||||
@Hidden
|
||||
public String splitPdfByChapters(Model model) {
|
||||
model.addAttribute("currentPage", "split-pdf-by-chapters");
|
||||
return "split-pdf-by-chapters";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/view-pdf")
|
||||
@Hidden
|
||||
public String ViewPdfForm2(Model model) {
|
||||
model.addAttribute("currentPage", "view-pdf");
|
||||
return "view-pdf";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/edit-table-of-contents")
|
||||
@Hidden
|
||||
public String editTableOfContents(Model model) {
|
||||
model.addAttribute("currentPage", "edit-table-of-contents");
|
||||
return "edit-table-of-contents";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/multi-tool")
|
||||
@Hidden
|
||||
public String multiToolForm(Model model) {
|
||||
model.addAttribute("currentPage", "multi-tool");
|
||||
return "multi-tool";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/remove-pages")
|
||||
@Hidden
|
||||
public String pageDeleter(Model model) {
|
||||
model.addAttribute("currentPage", "remove-pages");
|
||||
return "remove-pages";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/pdf-organizer")
|
||||
@Hidden
|
||||
public String pageOrganizer(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-organizer");
|
||||
return "pdf-organizer";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/extract-page")
|
||||
@Hidden
|
||||
public String extractPages(Model model) {
|
||||
model.addAttribute("currentPage", "extract-page");
|
||||
return "extract-page";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/pdf-to-single-page")
|
||||
@Hidden
|
||||
public String pdfToSinglePage(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-to-single-page");
|
||||
return "pdf-to-single-page";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/rotate-pdf")
|
||||
@Hidden
|
||||
public String rotatePdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "rotate-pdf");
|
||||
return "rotate-pdf";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/split-pdfs")
|
||||
@Hidden
|
||||
public String splitPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "split-pdfs");
|
||||
return "split-pdfs";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/sign")
|
||||
@Hidden
|
||||
public String signForm(Model model) {
|
||||
String username = "";
|
||||
if (userService != null) {
|
||||
username = userService.getCurrentUsername();
|
||||
}
|
||||
// Get signatures from both personal and ALL_USERS folders
|
||||
List<SignatureFile> signatures = signatureService.getAvailableSignatures(username);
|
||||
model.addAttribute("currentPage", "sign");
|
||||
model.addAttribute("fonts", getFontNames());
|
||||
model.addAttribute("signatures", signatures);
|
||||
return "sign";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/multi-page-layout")
|
||||
@Hidden
|
||||
public String multiPageLayoutForm(Model model) {
|
||||
model.addAttribute("currentPage", "multi-page-layout");
|
||||
return "multi-page-layout";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/scale-pages")
|
||||
@Hidden
|
||||
public String scalePagesFrom(Model model) {
|
||||
model.addAttribute("currentPage", "scale-pages");
|
||||
return "scale-pages";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/split-by-size-or-count")
|
||||
@Hidden
|
||||
public String splitBySizeOrCount(Model model) {
|
||||
model.addAttribute("currentPage", "split-by-size-or-count");
|
||||
return "split-by-size-or-count";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/overlay-pdf")
|
||||
@Hidden
|
||||
public String overlayPdf(Model model) {
|
||||
model.addAttribute("currentPage", "overlay-pdf");
|
||||
return "overlay-pdf";
|
||||
}
|
||||
|
||||
private List<FontResource> getFontNames() {
|
||||
List<FontResource> fontNames = new ArrayList<>();
|
||||
// Extract font names from classpath
|
||||
fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2"));
|
||||
// Extract font names from external directory
|
||||
fontNames.addAll(
|
||||
getFontNamesFromLocation(
|
||||
"file:"
|
||||
+ InstallationPathConfig.getStaticPath()
|
||||
+ "fonts"
|
||||
+ File.separator
|
||||
+ "*"));
|
||||
return fontNames;
|
||||
}
|
||||
|
||||
private List<FontResource> getFontNamesFromLocation(String locationPattern) {
|
||||
try {
|
||||
Resource[] resources =
|
||||
GeneralUtils.getResourcesFromLocationPattern(locationPattern, resourceLoader);
|
||||
return Arrays.stream(resources)
|
||||
.map(
|
||||
resource -> {
|
||||
try {
|
||||
String filename = resource.getFilename();
|
||||
if (filename != null) {
|
||||
int lastDotIndex = filename.lastIndexOf('.');
|
||||
if (lastDotIndex != -1) {
|
||||
String name = filename.substring(0, lastDotIndex);
|
||||
String extension = filename.substring(lastDotIndex + 1);
|
||||
return new FontResource(name, extension);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
throw ExceptionUtils.createRuntimeException(
|
||||
"error.fontLoadingFailed",
|
||||
"Error processing font file",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
} catch (Exception e) {
|
||||
throw ExceptionUtils.createRuntimeException(
|
||||
"error.fontDirectoryReadFailed", "Failed to read font directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getFormatFromExtension(String extension) {
|
||||
return switch (extension) {
|
||||
case "ttf" -> "truetype";
|
||||
case "woff" -> "woff";
|
||||
case "woff2" -> "woff2";
|
||||
case "eot" -> "embedded-opentype";
|
||||
case "svg" -> "svg";
|
||||
default ->
|
||||
// or throw an exception if an unexpected extension is encountered
|
||||
"";
|
||||
};
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/crop")
|
||||
@Hidden
|
||||
public String cropForm(Model model) {
|
||||
model.addAttribute("currentPage", "crop");
|
||||
return "crop";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/auto-split-pdf")
|
||||
@Hidden
|
||||
public String autoSPlitPDFForm(Model model) {
|
||||
model.addAttribute("currentPage", "auto-split-pdf");
|
||||
return "auto-split-pdf";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/remove-image-pdf")
|
||||
@Hidden
|
||||
public String removeImagePdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "remove-image-pdf");
|
||||
return "remove-image-pdf";
|
||||
}
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
public class FontResource {
|
||||
|
||||
private String name;
|
||||
|
||||
private String extension;
|
||||
|
||||
private String type;
|
||||
|
||||
public FontResource(String name, String extension) {
|
||||
this.name = name;
|
||||
this.extension = extension;
|
||||
this.type = getFormatFromExtension(extension);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.Dependency;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
@Slf4j
|
||||
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
|
||||
@RequiredArgsConstructor
|
||||
public class HomeWebController {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/about")
|
||||
@Hidden
|
||||
public String gameForm(Model model) {
|
||||
model.addAttribute("currentPage", "about");
|
||||
return "about";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/licenses")
|
||||
@Hidden
|
||||
public String licensesForm(Model model) {
|
||||
model.addAttribute("currentPage", "licenses");
|
||||
Resource resource = new ClassPathResource("static/3rdPartyLicenses.json");
|
||||
try (InputStream is = resource.getInputStream()) {
|
||||
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
Map<String, List<Dependency>> data = mapper.readValue(json, new TypeReference<>() {});
|
||||
model.addAttribute("dependencies", data.get("dependencies"));
|
||||
} catch (IOException e) {
|
||||
log.error("exception", e);
|
||||
}
|
||||
return "licenses";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/releases")
|
||||
public String getReleaseNotes(Model model) {
|
||||
return "releases";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/")
|
||||
public String home(Model model) {
|
||||
model.addAttribute("currentPage", "home");
|
||||
String showSurvey = System.getenv("SHOW_SURVEY");
|
||||
boolean showSurveyValue = showSurvey == null || "true".equalsIgnoreCase(showSurvey);
|
||||
model.addAttribute("showSurveyFromDocker", showSurveyValue);
|
||||
return "home";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/home")
|
||||
public String root(Model model) {
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/home-legacy")
|
||||
public String redirectHomeLegacy() {
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@ResponseBody
|
||||
@Hidden
|
||||
public String getRobotsTxt() {
|
||||
boolean allowGoogle = applicationProperties.getSystem().isGooglevisibility();
|
||||
if (allowGoogle) {
|
||||
return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /";
|
||||
} else {
|
||||
return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.CheckProgramInstall;
|
||||
|
||||
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class OtherWebController {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/compress-pdf")
|
||||
@Hidden
|
||||
public String compressPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "compress-pdf");
|
||||
return "misc/compress-pdf";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/replace-and-invert-color-pdf")
|
||||
@Hidden
|
||||
public String replaceAndInvertColorPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "replace-invert-color-pdf");
|
||||
return "misc/replace-color";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/extract-image-scans")
|
||||
@Hidden
|
||||
public ModelAndView extractImageScansForm() {
|
||||
ModelAndView modelAndView = new ModelAndView("misc/extract-image-scans");
|
||||
boolean isPython = CheckProgramInstall.isPythonAvailable();
|
||||
modelAndView.addObject("isPython", isPython);
|
||||
modelAndView.addObject("currentPage", "extract-image-scans");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/show-javascript")
|
||||
@Hidden
|
||||
public String extractJavascriptForm(Model model) {
|
||||
model.addAttribute("currentPage", "show-javascript");
|
||||
return "misc/show-javascript";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/stamp")
|
||||
@Hidden
|
||||
public String stampForm(Model model) {
|
||||
model.addAttribute("currentPage", "stamp");
|
||||
return "misc/stamp";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/add-page-numbers")
|
||||
@Hidden
|
||||
public String addPageNumbersForm(Model model) {
|
||||
model.addAttribute("currentPage", "add-page-numbers");
|
||||
return "misc/add-page-numbers";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/scanner-effect")
|
||||
@Hidden
|
||||
public String scannerEffectForm(Model model) {
|
||||
model.addAttribute("currentPage", "scanner-effect");
|
||||
return "misc/scanner-effect";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/extract-images")
|
||||
@Hidden
|
||||
public String extractImagesForm(Model model) {
|
||||
model.addAttribute("currentPage", "extract-images");
|
||||
return "misc/extract-images";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/flatten")
|
||||
@Hidden
|
||||
public String flattenForm(Model model) {
|
||||
model.addAttribute("currentPage", "flatten");
|
||||
return "misc/flatten";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/change-metadata")
|
||||
@Hidden
|
||||
public String addWatermarkForm(Model model) {
|
||||
model.addAttribute("currentPage", "change-metadata");
|
||||
return "misc/change-metadata";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/unlock-pdf-forms")
|
||||
@Hidden
|
||||
public String unlockPDFForms(Model model) {
|
||||
model.addAttribute("currentPage", "unlock-pdf-forms");
|
||||
return "misc/unlock-pdf-forms";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/compare")
|
||||
@Hidden
|
||||
public String compareForm(Model model) {
|
||||
model.addAttribute("currentPage", "compare");
|
||||
return "misc/compare";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/print-file")
|
||||
@Hidden
|
||||
public String printFileForm(Model model) {
|
||||
model.addAttribute("currentPage", "print-file");
|
||||
return "misc/print-file";
|
||||
}
|
||||
|
||||
public List<String> getAvailableTesseractLanguages() {
|
||||
String tessdataDir = runtimePathConfig.getTessDataPath();
|
||||
File[] files = new File(tessdataDir).listFiles();
|
||||
if (files == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Arrays.stream(files)
|
||||
.filter(file -> file.getName().endsWith(".traineddata"))
|
||||
.map(file -> file.getName().replace(".traineddata", ""))
|
||||
.filter(lang -> !"osd".equalsIgnoreCase(lang))
|
||||
.sorted()
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/ocr-pdf")
|
||||
@Hidden
|
||||
public ModelAndView ocrPdfPage() {
|
||||
ModelAndView modelAndView = new ModelAndView("misc/ocr-pdf");
|
||||
List<String> languages = getAvailableTesseractLanguages();
|
||||
modelAndView.addObject("languages", languages);
|
||||
modelAndView.addObject("currentPage", "ocr-pdf");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/add-image")
|
||||
@Hidden
|
||||
public String overlayImage(Model model) {
|
||||
model.addAttribute("currentPage", "add-image");
|
||||
return "misc/add-image";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/adjust-contrast")
|
||||
@Hidden
|
||||
public String contrast(Model model) {
|
||||
model.addAttribute("currentPage", "adjust-contrast");
|
||||
return "misc/adjust-contrast";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/repair")
|
||||
@Hidden
|
||||
public String repairForm(Model model) {
|
||||
model.addAttribute("currentPage", "repair");
|
||||
return "misc/repair";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/remove-blanks")
|
||||
@Hidden
|
||||
public String removeBlanksForm(Model model) {
|
||||
model.addAttribute("currentPage", "remove-blanks");
|
||||
return "misc/remove-blanks";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/remove-annotations")
|
||||
@Hidden
|
||||
public String removeAnnotationsForm(Model model) {
|
||||
model.addAttribute("currentPage", "remove-annotations");
|
||||
return "misc/remove-annotations";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/auto-crop")
|
||||
@Hidden
|
||||
public String autoCropForm(Model model) {
|
||||
model.addAttribute("currentPage", "auto-crop");
|
||||
return "misc/auto-crop";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/auto-rename")
|
||||
@Hidden
|
||||
public String autoRenameForm(Model model) {
|
||||
model.addAttribute("currentPage", "auto-rename");
|
||||
return "misc/auto-rename";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/add-attachments")
|
||||
@Hidden
|
||||
public String attachmentsForm(Model model) {
|
||||
model.addAttribute("currentPage", "add-attachments");
|
||||
return "misc/add-attachments";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/extract-attachments")
|
||||
@Hidden
|
||||
public String extractAttachmentsForm(Model model) {
|
||||
model.addAttribute("currentPage", "extract-attachments");
|
||||
return "misc/extract-attachments";
|
||||
}
|
||||
}
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import org.springframework.ui.Model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
public class SecurityWebController {
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/auto-redact")
|
||||
@Hidden
|
||||
public String autoRedactForm(Model model) {
|
||||
model.addAttribute("currentPage", "auto-redact");
|
||||
return "security/auto-redact";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/redact")
|
||||
public String redactForm(Model model) {
|
||||
model.addAttribute("currentPage", "redact");
|
||||
return "security/redact";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/add-password")
|
||||
@Hidden
|
||||
public String addPasswordForm(Model model) {
|
||||
model.addAttribute("currentPage", "add-password");
|
||||
return "security/add-password";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/change-permissions")
|
||||
@Hidden
|
||||
public String permissionsForm(Model model) {
|
||||
model.addAttribute("currentPage", "change-permissions");
|
||||
return "security/change-permissions";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/remove-password")
|
||||
@Hidden
|
||||
public String removePasswordForm(Model model) {
|
||||
model.addAttribute("currentPage", "remove-password");
|
||||
return "security/remove-password";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/add-watermark")
|
||||
@Hidden
|
||||
public String addWatermarkForm(Model model) {
|
||||
model.addAttribute("currentPage", "add-watermark");
|
||||
return "security/add-watermark";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/cert-sign")
|
||||
@Hidden
|
||||
public String certSignForm(Model model) {
|
||||
model.addAttribute("currentPage", "cert-sign");
|
||||
return "security/cert-sign";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/validate-signature")
|
||||
@Hidden
|
||||
public String certSignVerifyForm(Model model) {
|
||||
model.addAttribute("currentPage", "validate-signature");
|
||||
return "security/validate-signature";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/remove-cert-sign")
|
||||
@Hidden
|
||||
public String certUnSignForm(Model model) {
|
||||
model.addAttribute("currentPage", "remove-cert-sign");
|
||||
return "security/remove-cert-sign";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/sanitize-pdf")
|
||||
@Hidden
|
||||
public String sanitizeForm(Model model) {
|
||||
model.addAttribute("currentPage", "sanitize-pdf");
|
||||
return "security/sanitize-pdf";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/get-info-on-pdf")
|
||||
@Hidden
|
||||
public String getInfo(Model model) {
|
||||
model.addAttribute("currentPage", "get-info-on-pdf");
|
||||
return "security/get-info-on-pdf";
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,37 @@
|
||||
package stirling.software.SPDF.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.util.TempFile;
|
||||
|
||||
@Data
|
||||
public class PipelineResult {
|
||||
@Slf4j
|
||||
public class PipelineResult implements AutoCloseable {
|
||||
private List<Resource> outputFiles;
|
||||
private boolean hasErrors;
|
||||
private boolean filtersApplied;
|
||||
private List<TempFile> tempFiles = new ArrayList<>();
|
||||
|
||||
public void addTempFile(TempFile tempFile) {
|
||||
tempFiles.add(tempFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
for (TempFile file : tempFiles) {
|
||||
file.close();
|
||||
log.debug("Deleted temp file: {}", file.getAbsolutePath());
|
||||
}
|
||||
tempFiles.clear();
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-4
@@ -14,9 +14,6 @@ public class PdfToPdfARequest extends PDFFile {
|
||||
@Schema(
|
||||
description = "The output format type (PDF/A or PDF/X)",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
allowableValues = {
|
||||
"pdfa", "pdfa-1", "pdfa-2", "pdfa-2b", "pdfa-3", "pdfa-3b", "pdfx", "pdfx-1",
|
||||
"pdfx-3", "pdfx-4"
|
||||
})
|
||||
allowableValues = {"pdfa", "pdfa-1", "pdfa-2", "pdfa-2b", "pdfa-3", "pdfa-3b", "pdfx"})
|
||||
private String outputFormat;
|
||||
}
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package stirling.software.SPDF.model.api.converters;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode
|
||||
public class SvgToPdfRequest {
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"The SVG file(s) to be converted to PDF. "
|
||||
+ "SVGs are scalable and have inherent dimensions - the conversion uses these dimensions "
|
||||
+ "to determine the PDF page size. If dimensions are not specified in the SVG, A4 size is used.",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private MultipartFile[] fileInput;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"Whether to combine all SVG files into a single PDF (each SVG as a separate page) "
|
||||
+ "or create separate PDF files for each SVG.",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
defaultValue = "false")
|
||||
private Boolean combineIntoSinglePdf;
|
||||
}
|
||||
+7
@@ -39,6 +39,13 @@ public class AddPageNumbersRequest extends PDFWithPageNums {
|
||||
requiredMode = RequiredMode.NOT_REQUIRED)
|
||||
private String fontColor;
|
||||
|
||||
@Schema(
|
||||
description = "Zero-padding width for page numbers (Bates Stamping). Set to 0 to disable padding",
|
||||
minimum = "0",
|
||||
defaultValue = "0",
|
||||
requiredMode = RequiredMode.NOT_REQUIRED)
|
||||
private int zeroPad = 0;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"Position: 1-9 representing positions on the page (1=top-left, 2=top-center,"
|
||||
|
||||
@@ -32,8 +32,8 @@ public class AddStampRequest extends PDFWithPageNums {
|
||||
private String alphabet = "roman";
|
||||
|
||||
@Schema(
|
||||
description = "The font size of the stamp text and image",
|
||||
defaultValue = "30",
|
||||
description = "The font size of the stamp text and image in points.",
|
||||
defaultValue = "40",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private float fontSize;
|
||||
|
||||
|
||||
+4
-1
@@ -14,7 +14,10 @@ import stirling.software.common.model.api.PDFFile;
|
||||
public class OverlayImageRequest extends PDFFile {
|
||||
|
||||
@Schema(
|
||||
description = "The image file to be overlaid onto the PDF.",
|
||||
description =
|
||||
"The image file to be overlaid onto the PDF. "
|
||||
+ "Supports raster formats (PNG, JPEG, etc.) and vector format (SVG). "
|
||||
+ "SVG files are rendered as vector graphics for crisp output at any resolution.",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
format = "binary")
|
||||
private MultipartFile imageFile;
|
||||
|
||||
+3
-2
@@ -2,6 +2,7 @@ package stirling.software.SPDF.service;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyStore;
|
||||
@@ -510,7 +511,7 @@ public class CertificateValidationService {
|
||||
private byte[] downloadTrustList(String urlStr) {
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
URL url = new URL(urlStr);
|
||||
URL url = URI.create(urlStr).toURL();
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setConnectTimeout(10_000);
|
||||
@@ -700,7 +701,7 @@ public class CertificateValidationService {
|
||||
private byte[] downloadXml(String urlStr) {
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
URL url = new URL(urlStr);
|
||||
URL url = URI.create(urlStr).toURL();
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setConnectTimeout(10_000);
|
||||
|
||||
@@ -3424,7 +3424,7 @@ public class PdfJsonConversionService {
|
||||
}
|
||||
break;
|
||||
case "Tj":
|
||||
if (i == 0 || !(tokens.get(i - 1) instanceof COSString cosString)) {
|
||||
if (i == 0 || !(tokens.get(i - 1) instanceof COSString)) {
|
||||
log.debug(
|
||||
"Encountered Tj without preceding string operand; aborting rewrite");
|
||||
return false;
|
||||
@@ -3435,7 +3435,8 @@ public class PdfJsonConversionService {
|
||||
i,
|
||||
cursor.remaining());
|
||||
if (!rewriteShowText(
|
||||
cosString,
|
||||
tokens,
|
||||
i - 1,
|
||||
currentFont,
|
||||
currentFontModel,
|
||||
currentFontName,
|
||||
@@ -3496,7 +3497,8 @@ public class PdfJsonConversionService {
|
||||
}
|
||||
|
||||
private boolean rewriteShowText(
|
||||
COSString cosString,
|
||||
List<Object> tokens,
|
||||
int tokenIndex,
|
||||
PDFont font,
|
||||
PdfJsonFont fontModel,
|
||||
String expectedFontName,
|
||||
@@ -3509,6 +3511,7 @@ public class PdfJsonConversionService {
|
||||
expectedFontName);
|
||||
return false;
|
||||
}
|
||||
COSString cosString = (COSString) tokens.get(tokenIndex);
|
||||
int glyphCount = countGlyphs(cosString, font);
|
||||
log.trace(
|
||||
"rewriteShowText consuming {} glyphs at cursor index {} for font {}",
|
||||
@@ -3525,7 +3528,7 @@ public class PdfJsonConversionService {
|
||||
return false;
|
||||
}
|
||||
if (removeOnly) {
|
||||
cosString.setValue(new byte[0]);
|
||||
tokens.set(tokenIndex, new COSString(new byte[0]));
|
||||
return true;
|
||||
}
|
||||
MergedText replacement = mergeText(consumed);
|
||||
@@ -3540,7 +3543,7 @@ public class PdfJsonConversionService {
|
||||
replacement.text());
|
||||
return false;
|
||||
}
|
||||
cosString.setValue(encoded);
|
||||
tokens.set(tokenIndex, new COSString(encoded));
|
||||
return true;
|
||||
} catch (IOException | IllegalArgumentException | UnsupportedOperationException ex) {
|
||||
log.debug(
|
||||
|
||||
+2
-7
@@ -82,7 +82,7 @@ public class TelegramPipelineBot extends TelegramLongPollingBot {
|
||||
|
||||
@PostConstruct
|
||||
public void register() {
|
||||
if (StringUtils.isAnyBlank(getBotUsername(), getBotToken())) {
|
||||
if (StringUtils.isAnyBlank(getBotUsername(), this.telegramProperties.getBotToken())) {
|
||||
log.warn("Telegram bot disabled because botToken or botUsername is not configured");
|
||||
return;
|
||||
}
|
||||
@@ -392,7 +392,7 @@ public class TelegramPipelineBot extends TelegramLongPollingBot {
|
||||
new URI(
|
||||
"https",
|
||||
"api.telegram.org",
|
||||
"/file/bot" + getBotToken() + "/" + filePath,
|
||||
"/file/bot" + this.telegramProperties.getBotToken() + "/" + filePath,
|
||||
null);
|
||||
return uri.toURL();
|
||||
} catch (URISyntaxException e) {
|
||||
@@ -516,9 +516,4 @@ public class TelegramPipelineBot extends TelegramLongPollingBot {
|
||||
public String getBotUsername() {
|
||||
return telegramProperties.getBotUsername();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBotToken() {
|
||||
return telegramProperties.getBotToken();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package stirling.software.SPDF.utils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
|
||||
import org.apache.batik.bridge.BridgeContext;
|
||||
import org.apache.batik.bridge.DocumentLoader;
|
||||
import org.apache.batik.bridge.GVTBuilder;
|
||||
import org.apache.batik.bridge.UserAgent;
|
||||
import org.apache.batik.bridge.UserAgentAdapter;
|
||||
import org.apache.batik.gvt.GraphicsNode;
|
||||
import org.apache.batik.util.XMLResourceDescriptor;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.apache.pdfbox.util.Matrix;
|
||||
import org.w3c.dom.svg.SVGDocument;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import de.rototor.pdfbox.graphics2d.PdfBoxGraphics2D;
|
||||
|
||||
@UtilityClass
|
||||
@Slf4j
|
||||
public class SvgOverlayUtil {
|
||||
|
||||
public void overlaySvgOnPage(
|
||||
PDDocument document, PDPage page, byte[] svgBytes, float x, float y)
|
||||
throws IOException {
|
||||
try {
|
||||
String parser = XMLResourceDescriptor.getXMLParserClassName();
|
||||
SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(parser);
|
||||
|
||||
SVGDocument svgDoc;
|
||||
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(svgBytes)) {
|
||||
svgDoc = factory.createSVGDocument("file:///overlay.svg", inputStream);
|
||||
}
|
||||
|
||||
UserAgent userAgent = new UserAgentAdapter();
|
||||
DocumentLoader loader = new DocumentLoader(userAgent);
|
||||
BridgeContext ctx = new BridgeContext(userAgent, loader);
|
||||
ctx.setDynamicState(BridgeContext.DYNAMIC);
|
||||
|
||||
GVTBuilder builder = new GVTBuilder();
|
||||
GraphicsNode rootNode = builder.build(ctx, svgDoc);
|
||||
|
||||
float svgWidth = (float) ctx.getDocumentSize().getWidth();
|
||||
float svgHeight = (float) ctx.getDocumentSize().getHeight();
|
||||
|
||||
PdfBoxGraphics2D pdfGraphics = new PdfBoxGraphics2D(document, svgWidth, svgHeight);
|
||||
|
||||
try {
|
||||
rootNode.paint(pdfGraphics);
|
||||
} finally {
|
||||
pdfGraphics.dispose();
|
||||
}
|
||||
|
||||
PDFormXObject xform = pdfGraphics.getXFormObject();
|
||||
|
||||
try (PDPageContentStream newContentStream =
|
||||
new PDPageContentStream(
|
||||
document, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
|
||||
newContentStream.saveGraphicsState();
|
||||
|
||||
newContentStream.transform(new Matrix(1, 0, 0, 1, x, y));
|
||||
|
||||
newContentStream.drawForm(xform);
|
||||
|
||||
newContentStream.restoreGraphicsState();
|
||||
}
|
||||
|
||||
log.info("SVG successfully overlaid as vector graphic at ({}, {})", x, y);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to overlay SVG as vector graphic", e);
|
||||
throw new IOException("SVG overlay failed: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSvgImage(byte[] bytes) {
|
||||
if (bytes == null || bytes.length < 5) {
|
||||
return false;
|
||||
}
|
||||
// Check for SVG markers: <?xml or <svg
|
||||
String start =
|
||||
new String(
|
||||
bytes,
|
||||
0,
|
||||
Math.min(200, bytes.length),
|
||||
java.nio.charset.StandardCharsets.UTF_8)
|
||||
.toLowerCase();
|
||||
return start.contains("<svg") || (start.contains("<?xml") && start.contains("svg"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
package stirling.software.SPDF.utils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
|
||||
import org.apache.batik.bridge.BridgeContext;
|
||||
import org.apache.batik.bridge.DocumentLoader;
|
||||
import org.apache.batik.bridge.GVTBuilder;
|
||||
import org.apache.batik.bridge.UserAgent;
|
||||
import org.apache.batik.bridge.UserAgentAdapter;
|
||||
import org.apache.batik.gvt.GraphicsNode;
|
||||
import org.apache.batik.util.XMLResourceDescriptor;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.w3c.dom.svg.SVGDocument;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import de.rototor.pdfbox.graphics2d.PdfBoxGraphics2D;
|
||||
|
||||
@UtilityClass
|
||||
@Slf4j
|
||||
public class SvgToPdf {
|
||||
|
||||
/** Default page width in points (A4) */
|
||||
private static final float DEFAULT_PAGE_WIDTH = 595f;
|
||||
|
||||
/** Default page height in points (A4) */
|
||||
private static final float DEFAULT_PAGE_HEIGHT = 842f;
|
||||
|
||||
/** Timeout for SVG rendering in seconds (prevents DoS via complex SVGs) */
|
||||
private static final int RENDERING_TIMEOUT_SECONDS = 30;
|
||||
|
||||
public byte[] convert(byte[] svgBytes) throws IOException {
|
||||
if (svgBytes == null || svgBytes.length == 0) {
|
||||
throw new IOException("SVG input is empty or null");
|
||||
}
|
||||
|
||||
log.debug("Starting SVG to PDF conversion, input size: {} bytes", svgBytes.length);
|
||||
|
||||
try {
|
||||
// 1. Load SVG using Batik
|
||||
String parser = XMLResourceDescriptor.getXMLParserClassName();
|
||||
SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(parser);
|
||||
|
||||
SVGDocument svgDoc;
|
||||
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(svgBytes)) {
|
||||
svgDoc = factory.createSVGDocument("file:///input.svg", inputStream);
|
||||
}
|
||||
|
||||
// 2. Build the GVT (Graphics Vector Tree) with timeout protection
|
||||
UserAgent userAgent = new UserAgentAdapter();
|
||||
DocumentLoader loader = new DocumentLoader(userAgent);
|
||||
BridgeContext ctx = new BridgeContext(userAgent, loader);
|
||||
ctx.setDynamicState(BridgeContext.DYNAMIC);
|
||||
|
||||
GraphicsNode rootNode = buildGvtWithTimeout(ctx, svgDoc);
|
||||
|
||||
// 3. Get SVG dimensions
|
||||
float width = (float) ctx.getDocumentSize().getWidth();
|
||||
float height = (float) ctx.getDocumentSize().getHeight();
|
||||
|
||||
if (width <= 0) {
|
||||
width = DEFAULT_PAGE_WIDTH;
|
||||
log.warn("SVG width not specified, using default: {}", width);
|
||||
}
|
||||
if (height <= 0) {
|
||||
height = DEFAULT_PAGE_HEIGHT;
|
||||
log.warn("SVG height not specified, using default: {}", height);
|
||||
}
|
||||
|
||||
log.debug("SVG dimensions: {}x{} points", width, height);
|
||||
|
||||
// 4. Create PDF document and render
|
||||
return renderToPdf(rootNode, width, height);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to convert SVG to PDF", e);
|
||||
throw new IOException("SVG to PDF conversion failed: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private GraphicsNode buildGvtWithTimeout(BridgeContext ctx, SVGDocument svgDoc)
|
||||
throws IOException {
|
||||
GVTBuilder builder = new GVTBuilder();
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
Callable<GraphicsNode> buildTask = () -> builder.build(ctx, svgDoc);
|
||||
Future<GraphicsNode> future = executor.submit(buildTask);
|
||||
|
||||
try {
|
||||
return future.get(RENDERING_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
future.cancel(true);
|
||||
throw new IOException(
|
||||
"SVG rendering timed out after "
|
||||
+ RENDERING_TIMEOUT_SECONDS
|
||||
+ " seconds. The SVG may be too complex.");
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("SVG rendering was interrupted", e);
|
||||
} catch (ExecutionException e) {
|
||||
Throwable cause = e.getCause();
|
||||
throw new IOException(
|
||||
"SVG rendering failed: "
|
||||
+ (cause != null ? cause.getMessage() : e.getMessage()),
|
||||
cause);
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] renderToPdf(GraphicsNode rootNode, float width, float height)
|
||||
throws IOException {
|
||||
try (PDDocument document = new PDDocument();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
|
||||
PDPage page = new PDPage(new PDRectangle(width, height));
|
||||
document.addPage(page);
|
||||
|
||||
// Create and use PdfBoxGraphics2D with proper resource management
|
||||
PdfBoxGraphics2D pdfGraphics = new PdfBoxGraphics2D(document, width, height);
|
||||
try {
|
||||
rootNode.paint(pdfGraphics);
|
||||
} finally {
|
||||
pdfGraphics.dispose();
|
||||
}
|
||||
|
||||
PDFormXObject xform = pdfGraphics.getXFormObject();
|
||||
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
|
||||
contentStream.drawForm(xform);
|
||||
}
|
||||
|
||||
document.save(outputStream);
|
||||
|
||||
byte[] result = outputStream.toByteArray();
|
||||
log.debug("SVG to PDF conversion complete, output size: {} bytes", result.length);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] combineIntoPdf(List<byte[]> svgBytesList) throws IOException {
|
||||
if (svgBytesList == null || svgBytesList.isEmpty()) {
|
||||
throw new IOException("SVG list is empty or null");
|
||||
}
|
||||
|
||||
log.debug("Combining {} SVG files into single PDF", svgBytesList.size());
|
||||
|
||||
try (PDDocument document = new PDDocument();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
|
||||
for (int i = 0; i < svgBytesList.size(); i++) {
|
||||
byte[] svgBytes = svgBytesList.get(i);
|
||||
if (svgBytes == null || svgBytes.length == 0) {
|
||||
log.warn("Skipping empty SVG at index {}", i);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
addSvgAsPage(document, svgBytes);
|
||||
log.debug("Added SVG {} of {} to combined PDF", i + 1, svgBytesList.size());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to add SVG {} to combined PDF: {}", i, e.getMessage());
|
||||
// Continue with other SVGs
|
||||
}
|
||||
}
|
||||
|
||||
if (document.getNumberOfPages() == 0) {
|
||||
throw new IOException("No SVG files were successfully added to the PDF");
|
||||
}
|
||||
|
||||
document.save(outputStream);
|
||||
byte[] result = outputStream.toByteArray();
|
||||
log.debug(
|
||||
"Combined SVG to PDF conversion complete, output size: {} bytes",
|
||||
result.length);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private void addSvgAsPage(PDDocument document, byte[] svgBytes) throws IOException {
|
||||
String parser = XMLResourceDescriptor.getXMLParserClassName();
|
||||
SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(parser);
|
||||
|
||||
SVGDocument svgDoc;
|
||||
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(svgBytes)) {
|
||||
svgDoc = factory.createSVGDocument("file:///input.svg", inputStream);
|
||||
}
|
||||
|
||||
UserAgent userAgent = new UserAgentAdapter();
|
||||
DocumentLoader loader = new DocumentLoader(userAgent);
|
||||
BridgeContext ctx = new BridgeContext(userAgent, loader);
|
||||
ctx.setDynamicState(BridgeContext.DYNAMIC);
|
||||
|
||||
GraphicsNode rootNode = buildGvtWithTimeout(ctx, svgDoc);
|
||||
|
||||
float svgWidth = (float) ctx.getDocumentSize().getWidth();
|
||||
float svgHeight = (float) ctx.getDocumentSize().getHeight();
|
||||
|
||||
if (svgWidth <= 0) svgWidth = DEFAULT_PAGE_WIDTH;
|
||||
if (svgHeight <= 0) svgHeight = DEFAULT_PAGE_HEIGHT;
|
||||
|
||||
// Use SVG dimensions directly for the PDF page
|
||||
PDPage page = new PDPage(new PDRectangle(svgWidth, svgHeight));
|
||||
document.addPage(page);
|
||||
|
||||
PdfBoxGraphics2D pdfGraphics = new PdfBoxGraphics2D(document, svgWidth, svgHeight);
|
||||
try {
|
||||
rootNode.paint(pdfGraphics);
|
||||
} finally {
|
||||
pdfGraphics.dispose();
|
||||
}
|
||||
|
||||
PDFormXObject xform = pdfGraphics.getXFormObject();
|
||||
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
|
||||
contentStream.drawForm(xform);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ server.servlet.context-path=${SYSTEM_ROOTURIPATH:/}
|
||||
spring.devtools.restart.enabled=true
|
||||
spring.devtools.livereload.enabled=true
|
||||
spring.devtools.restart.exclude=stirling.software.proprietary.security/**
|
||||
# spring.thymeleaf.encoding=UTF-8 # Disabled - React frontend replaces Thymeleaf
|
||||
spring.web.resources.mime-mappings.webmanifest=application/manifest+json
|
||||
spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000}
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ security:
|
||||
revocation:
|
||||
mode: none # Revocation checking mode: 'none' (disabled), 'ocsp' (OCSP only), 'crl' (CRL only), 'ocsp+crl' (OCSP with CRL fallback)
|
||||
hardFail: false # Fail validation if revocation status cannot be determined (true=strict, false=soft-fail)
|
||||
xFrameOptions: DENY # X-Frame-Options header value. Options: 'DENY' (default, prevents all framing), 'SAMEORIGIN' (allows framing from same domain), 'DISABLED' (no X-Frame-Options header sent). Note: automatically set to DISABLED when login is disabled
|
||||
|
||||
premium:
|
||||
key: 00000000-0000-0000-0000-000000000000
|
||||
@@ -202,7 +203,9 @@ system:
|
||||
name: postgres # set the name of your database. Should match the name of the database you create
|
||||
customPaths:
|
||||
pipeline:
|
||||
pipelineDir: "" # Defaults to /pipeline
|
||||
watchedFoldersDir: "" # Defaults to /pipeline/watchedFolders
|
||||
watchedFoldersDirs: [] # List of watched folder directories. Defaults to watchedFoldersDir or /pipeline/watchedFolders.
|
||||
finishedFoldersDir: "" # Defaults to /pipeline/finishedFolders
|
||||
operations:
|
||||
weasyprint: "" # Defaults to /opt/venv/bin/weasyprint
|
||||
@@ -242,8 +245,20 @@ AutomaticallyGenerated:
|
||||
appVersion: 0.35.0
|
||||
|
||||
processExecutor:
|
||||
autoUnoServer: true # true: use local pool based on libreOfficeSessionLimit; false: use unoServerEndpoints
|
||||
unoServerEndpoints: [] # Used when autoUnoServer is false
|
||||
# Example manual endpoints (uncomment to use):
|
||||
# unoServerEndpoints:
|
||||
# - host: "127.0.0.1"
|
||||
# port: 2003
|
||||
# hostLocation: "auto" # auto|local|remote (use "remote" for port-forwarded servers)
|
||||
# protocol: "http" # http|https
|
||||
# - host: "remote-server.local"
|
||||
# port: 8080
|
||||
# hostLocation: "remote"
|
||||
# protocol: "https"
|
||||
sessionLimit: # Process executor instances limits
|
||||
libreOfficeSessionLimit: 1
|
||||
libreOfficeSessionLimit: 1 # Each additional uno server adds ~50MB idle RAM
|
||||
pdfToHtmlSessionLimit: 1
|
||||
qpdfSessionLimit: 4
|
||||
tesseractSessionLimit: 1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title='<3')}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,479 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{account.title})}"></th:block>
|
||||
<link rel="stylesheet" th:href="@{/css/modern-tables.css}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
|
||||
<div class="data-container">
|
||||
<div class="data-panel">
|
||||
<div class="data-header">
|
||||
<h1 class="data-title">
|
||||
<span class="data-icon">
|
||||
<span class="material-symbols-rounded">settings_account_box</span>
|
||||
</span>
|
||||
<span th:text="#{account.accountSettings}">User Settings</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="data-body">
|
||||
<div th:if="${messageType}" class="alert alert-danger data-mb-3">
|
||||
<span th:text="#{${messageType}}">Default message if not found</span>
|
||||
</div>
|
||||
|
||||
<div th:if="${error}" class="alert alert-danger data-mb-3" role="alert">
|
||||
<span th:text="${error}">Error Message</span>
|
||||
</div>
|
||||
|
||||
<!-- Admin Settings Banner (for admins only) -->
|
||||
<div th:if="${role == 'ROLE_ADMIN'}" class="data-panel data-mb-3" style="background-color: var(--md-sys-color-secondary-container);">
|
||||
<div class="data-body" style="display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.5rem; background-color: var(--md-sys-color-secondary-container);">
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<span class="material-symbols-rounded" style="font-size: 2rem; color: var(--md-sys-color-secondary);">
|
||||
admin_panel_settings
|
||||
</span>
|
||||
<div>
|
||||
<h4 style="margin: 0; color: var(--md-sys-color-secondary);" th:text="#{account.adminTitle}">Administrator Tools</h4>
|
||||
<p style="margin: 0.25rem 0 0 0; color: var(--md-sys-color-secondary);" th:text="#{account.adminNotif}">You have admin privileges. Access system settings and user management.</p>
|
||||
</div>
|
||||
</div>
|
||||
<a class="data-btn" th:href="@{'/adminSettings'}" role="button" target="_blank"
|
||||
style="background-color: var(--md-sys-color-secondary); color: var(--md-sys-color-on-secondary); display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.625rem 1.25rem; border-radius: 0.5rem; font-weight: 500; border: none; cursor: pointer; text-decoration: none;">
|
||||
<span class="material-symbols-rounded">admin_panel_settings</span>
|
||||
<span th:text="#{account.adminSettings}">Admin Settings</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Account Management Buttons -->
|
||||
<th:block th:if="not ${oAuth2Login} or not ${saml2Login}">
|
||||
<div class="data-section-title">Account Management</div>
|
||||
<div class="data-actions data-actions-start data-mb-3">
|
||||
<button class="data-btn data-btn-primary" data-bs-toggle="modal" data-bs-target="#changeUsernameModal">
|
||||
<span class="material-symbols-rounded">edit</span>
|
||||
<span th:text="#{account.changeUsername}">Change Username</span>
|
||||
</button>
|
||||
<button class="data-btn data-btn-primary" data-bs-toggle="modal" data-bs-target="#changePasswordModal">
|
||||
<span class="material-symbols-rounded">key</span>
|
||||
<span th:text="#{account.changePassword}">Change Password</span>
|
||||
</button>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<!-- API Key Section -->
|
||||
<div class="data-section-title" th:text="#{account.yourApiKey}">API Key</div>
|
||||
<div class="data-panel data-mb-3">
|
||||
<div class="data-header">
|
||||
<h5 class="data-title">
|
||||
<span class="data-icon">
|
||||
<span class="material-symbols-rounded">key</span>
|
||||
</span>
|
||||
<span th:text="#{account.yourApiKey}">API Key</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="data-body">
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<input class="data-form-control" id="apiKey" th:placeholder="#{account.yourApiKey}" readonly style="flex: 1;">
|
||||
<button class="data-btn data-btn-secondary" id="copyBtn" type="button" onclick="copyToClipboard()" title="Copy to clipboard">
|
||||
<span class="material-symbols-rounded">content_copy</span>
|
||||
</button>
|
||||
<button class="data-btn data-btn-secondary" id="showBtn" type="button" onclick="showApiKey()" title="Show/hide API key">
|
||||
<span class="material-symbols-rounded" id="eyeIcon">visibility</span>
|
||||
</button>
|
||||
<button class="data-btn data-btn-secondary" id="refreshBtn" type="button" onclick="refreshApiKey()" title="Refresh API key">
|
||||
<span class="material-symbols-rounded">refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Sync Section -->
|
||||
<div class="data-section-title" th:text="#{account.syncTitle}">Sync browser settings with Account</div>
|
||||
<div class="data-panel data-mb-3">
|
||||
<div class="data-header">
|
||||
<h5 class="data-title">
|
||||
<span class="data-icon">
|
||||
<span class="material-symbols-rounded">sync</span>
|
||||
</span>
|
||||
<span th:text="#{account.settingsCompare}">Settings Comparison</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="data-body">
|
||||
<div class="table-responsive">
|
||||
<table id="settingsTable" class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" th:text="#{account.property}">Property</th>
|
||||
<th scope="col" th:text="#{account.accountSettings}">Account Setting</th>
|
||||
<th scope="col" th:text="#{account.webBrowserSettings}">Web Browser Setting</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- This will be dynamically populated by JavaScript -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="data-actions data-mt-3">
|
||||
<button id="syncToBrowser" class="data-btn data-btn-primary">
|
||||
<span class="material-symbols-rounded">cloud_download</span>
|
||||
<span th:text="#{account.syncToBrowser}">Sync Account -> Browser</span>
|
||||
</button>
|
||||
<button id="syncToAccount" class="data-btn data-btn-secondary">
|
||||
<span class="material-symbols-rounded">cloud_upload</span>
|
||||
<span th:text="#{account.syncToAccount}">Sync Account <- Browser</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change Username Modal -->
|
||||
<div class="modal fade" id="changeUsernameModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<form id="formsavechangeusername" th:action="@{'/api/v1/user/change-username'}" method="post" class="modal-content data-modal">
|
||||
<div class="data-modal-header">
|
||||
<h5 class="data-modal-title">
|
||||
<span class="data-icon">
|
||||
<span class="material-symbols-rounded">edit</span>
|
||||
</span>
|
||||
<span th:text="#{account.changeUsername}">Change Username</span>
|
||||
</h5>
|
||||
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="data-modal-body">
|
||||
<div class="data-form-group">
|
||||
<label for="newUsername" class="data-form-label" th:text="#{account.newUsername}">New Username</label>
|
||||
<input type="text" class="data-form-control" name="newUsername" id="newUsername" th:placeholder="#{account.newUsername}">
|
||||
<span id="usernameError" style="display: none; color: var(--md-sys-color-error);" th:text="#{invalidUsernameMessage}">Invalid username!</span>
|
||||
</div>
|
||||
<div class="data-form-group">
|
||||
<label for="currentPasswordChangeUsername" class="data-form-label" th:text="#{password}">Password</label>
|
||||
<input type="password" class="data-form-control" name="currentPasswordChangeUsername" id="currentPasswordChangeUsername" th:placeholder="#{password}">
|
||||
</div>
|
||||
<div class="data-form-actions">
|
||||
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
<span th:text="#{cancel}">Cancel</span>
|
||||
</button>
|
||||
<button type="submit" class="data-btn data-btn-primary">
|
||||
<span class="material-symbols-rounded">check</span>
|
||||
<span th:text="#{account.changeUsername}">Change Username</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change Password Modal -->
|
||||
<div class="modal fade" id="changePasswordModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<form id="formsavechangepassword" th:action="@{'/api/v1/user/change-password'}" method="post" class="modal-content data-modal">
|
||||
<div class="data-modal-header">
|
||||
<h5 class="data-modal-title">
|
||||
<span class="data-icon">
|
||||
<span class="material-symbols-rounded">key</span>
|
||||
</span>
|
||||
<span th:text="#{account.changePassword}">Change Password</span>
|
||||
</h5>
|
||||
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="data-modal-body">
|
||||
<div class="data-form-group">
|
||||
<label for="currentPassword" class="data-form-label" th:text="#{account.oldPassword}">Old Password</label>
|
||||
<input type="password" class="data-form-control" name="currentPassword" id="currentPassword" th:placeholder="#{account.oldPassword}">
|
||||
</div>
|
||||
<div class="data-form-group">
|
||||
<label for="newPassword" class="data-form-label" th:text="#{account.newPassword}">New Password</label>
|
||||
<input type="password" class="data-form-control" name="newPassword" id="newPassword" th:placeholder="#{account.newPassword}">
|
||||
</div>
|
||||
<div class="data-form-group">
|
||||
<label for="confirmNewPassword" class="data-form-label" th:text="#{account.confirmNewPassword}">Confirm New Password</label>
|
||||
<input type="password" class="data-form-control" name="confirmNewPassword" id="confirmNewPassword" th:placeholder="#{account.confirmNewPassword}">
|
||||
<span id="confirmPasswordError" style="display: none; color: var(--md-sys-color-error);" th:text="#{confirmPasswordErrorMessage}">New Password and Confirm New Password must match.</span>
|
||||
</div>
|
||||
<div class="data-form-actions">
|
||||
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
<span th:text="#{cancel}">Cancel</span>
|
||||
</button>
|
||||
<button type="submit" class="data-btn data-btn-primary">
|
||||
<span class="material-symbols-rounded">check</span>
|
||||
<span th:text="#{account.changePassword}">Change Password</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript for validation -->
|
||||
<script th:inline="javascript">
|
||||
jQuery.validator.addMethod("usernamePattern", function(value, element) {
|
||||
// Regular expression for user name: Min. 3 characters, max. 50 characters
|
||||
const regexUsername = /^[a-zA-Z0-9](?!.*[-@._+]{2,})([a-zA-Z0-9@._+-]{1,48})[a-zA-Z0-9]$/;
|
||||
|
||||
// Regular expression for email addresses: Max. 320 characters, with RFC-like validation
|
||||
const regexEmail = /^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*(?:\.[A-Za-z]{2,})$/;
|
||||
|
||||
// Check if the field is optional or meets the requirements
|
||||
return this.optional(element) || regexUsername.test(value) || regexEmail.test(value);
|
||||
}, /*[[#{invalidUsernameMessage}]]*/ "Invalid username format");
|
||||
|
||||
$(document).ready(function() {
|
||||
$.validator.addMethod("passwordMatch", function(value, element) {
|
||||
return $('#newPassword').val() === $('#confirmNewPassword').val();
|
||||
}, /*[[#{confirmPasswordErrorMessage}]]*/ "New Password and Confirm New Password must match.");
|
||||
|
||||
$('#formsavechangepassword').validate({
|
||||
rules: {
|
||||
currentPassword: {
|
||||
required: true
|
||||
},
|
||||
newPassword: {
|
||||
required: true
|
||||
},
|
||||
confirmNewPassword: {
|
||||
required: true,
|
||||
passwordMatch: true
|
||||
}
|
||||
},
|
||||
errorPlacement: function(error, element) {
|
||||
if (element.attr("name") === "newPassword" || element.attr("name") === "confirmNewPassword") {
|
||||
$("#confirmPasswordError").text(error.text()).show();
|
||||
} else {
|
||||
error.insertAfter(element);
|
||||
}
|
||||
},
|
||||
success: function(label, element) {
|
||||
if ($(element).attr("name") === "newPassword" || $(element).attr("name") === "confirmNewPassword") {
|
||||
$("#confirmPasswordError").hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$('#formsavechangeusername').validate({
|
||||
rules: {
|
||||
newUsername: {
|
||||
required: true,
|
||||
usernamePattern: true
|
||||
},
|
||||
currentPasswordChangeUsername: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
messages: {
|
||||
newUsername: {
|
||||
usernamePattern: /*[[#{invalidUsernameMessage}]]*/ "Invalid username format"
|
||||
},
|
||||
},
|
||||
errorPlacement: function(error, element) {
|
||||
if (element.attr("name") === "newUsername") {
|
||||
$("#usernameError").text(error.text()).show();
|
||||
} else {
|
||||
error.insertAfter(element);
|
||||
}
|
||||
},
|
||||
success: function(label, element) {
|
||||
if ($(element).attr("name") === "newUsername") {
|
||||
$("#usernameError").hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- JavaScript for API Key -->
|
||||
<script th:inline="javascript">
|
||||
function copyToClipboard() {
|
||||
const apiKeyElement = document.getElementById("apiKey");
|
||||
apiKeyElement.select();
|
||||
document.execCommand("copy");
|
||||
}
|
||||
|
||||
function showApiKey() {
|
||||
const apiKeyElement = document.getElementById("apiKey");
|
||||
const copyBtn = document.getElementById("copyBtn");
|
||||
const eyeIcon = document.getElementById("eyeIcon");
|
||||
if (apiKeyElement.type === "password") {
|
||||
apiKeyElement.type = "text";
|
||||
eyeIcon.textContent = "visibility_off";
|
||||
copyBtn.disabled = false; // Enable copy button when API key is visible
|
||||
} else {
|
||||
apiKeyElement.type = "password";
|
||||
eyeIcon.textContent = "visibility";
|
||||
copyBtn.disabled = true; // Disable copy button when API key is hidden
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async function() {
|
||||
try {
|
||||
/*<![CDATA[*/
|
||||
const urlGetApiKey = /*[[@{/api/v1/user/get-api-key}]]*/ "/api/v1/user/get-api-key";
|
||||
/*]]>*/
|
||||
let response = await window.fetchWithCsrf(urlGetApiKey, { method: 'POST' });
|
||||
if (response.status === 200) {
|
||||
let apiKey = await response.text();
|
||||
manageUIState(apiKey);
|
||||
} else {
|
||||
manageUIState(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('There was an error:', error);
|
||||
}
|
||||
finally {
|
||||
showApiKey();
|
||||
}
|
||||
});
|
||||
|
||||
async function refreshApiKey() {
|
||||
try {
|
||||
/*<![CDATA[*/
|
||||
const urlUpdateApiKey = /*[[@{/api/v1/user/update-api-key}]]*/ "/api/v1/user/update-api-key";
|
||||
/*]]>*/
|
||||
let response = await window.fetchWithCsrf(urlUpdateApiKey, { method: 'POST' });
|
||||
if (response.status === 200) {
|
||||
let apiKey = await response.text();
|
||||
manageUIState(apiKey);
|
||||
document.getElementById("apiKey").type = 'text';
|
||||
document.getElementById("copyBtn").disabled = false;
|
||||
} else {
|
||||
alert('Error refreshing API key.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('There was an error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function manageUIState(apiKey) {
|
||||
const apiKeyElement = document.getElementById("apiKey");
|
||||
const showBtn = document.getElementById("showBtn");
|
||||
const copyBtn = document.getElementById("copyBtn");
|
||||
|
||||
if (apiKey && apiKey.trim().length > 0) {
|
||||
apiKeyElement.value = apiKey;
|
||||
showBtn.disabled = false;
|
||||
copyBtn.disabled = false;
|
||||
} else {
|
||||
apiKeyElement.value = "";
|
||||
showBtn.disabled = true;
|
||||
copyBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- JavaScript for Settings Sync -->
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener("DOMContentLoaded", async function() {
|
||||
const settingsTableBody = document.querySelector("#settingsTable tbody");
|
||||
|
||||
// Helper function to check if a key should be ignored
|
||||
function shouldIgnoreKey(key) {
|
||||
return key === 'debug' ||
|
||||
key === '0' ||
|
||||
key === '1' ||
|
||||
key.includes('pdfjs') ||
|
||||
key.includes('clientSubmissionOrder') ||
|
||||
key.includes('lastSubmitTime') ||
|
||||
key.includes('lastClientId') ||
|
||||
key.includes('stirling_jwt') ||
|
||||
key.includes('JSESSIONID') ||
|
||||
key.includes('XSRF-TOKEN') ||
|
||||
key.includes('remember-me') ||
|
||||
key.includes('auth') ||
|
||||
key.includes('token') ||
|
||||
key.includes('session') ||
|
||||
key.includes('posthog') || key.includes('ssoRedirectAttempts') || key.includes('lastRedirectAttempt') || key.includes('surveyVersion') ||
|
||||
key.includes('pageViews');
|
||||
}
|
||||
|
||||
/*<![CDATA[*/
|
||||
var accountSettingsString = /*[[${settings}]]*/ {};
|
||||
/*]]>*/
|
||||
var accountSettings = JSON.parse(accountSettingsString);
|
||||
|
||||
let allKeys = new Set([...Object.keys(accountSettings), ...Object.keys(localStorage)]);
|
||||
|
||||
allKeys.forEach(key => {
|
||||
if(shouldIgnoreKey(key)) return; // Using our helper function
|
||||
|
||||
const accountValue = accountSettings[key] || '-';
|
||||
const browserValue = localStorage.getItem(key) || '-';
|
||||
|
||||
const row = settingsTableBody.insertRow();
|
||||
const propertyCell = row.insertCell(0);
|
||||
const accountCell = row.insertCell(1);
|
||||
const browserCell = row.insertCell(2);
|
||||
|
||||
propertyCell.textContent = key;
|
||||
accountCell.textContent = accountValue;
|
||||
browserCell.textContent = browserValue;
|
||||
});
|
||||
|
||||
document.getElementById('syncToBrowser').addEventListener('click', function() {
|
||||
// First, clear the local storage
|
||||
localStorage.clear();
|
||||
|
||||
// Then, set the account settings to local storage
|
||||
for (let key in accountSettings) {
|
||||
if(!shouldIgnoreKey(key)) { // Using our helper function
|
||||
localStorage.setItem(key, accountSettings[key]);
|
||||
}
|
||||
}
|
||||
location.reload(); // Refresh the page after sync
|
||||
});
|
||||
|
||||
document.getElementById('syncToAccount').addEventListener('click', async function() {
|
||||
/*<![CDATA[*/
|
||||
const urlUpdateUserSettings = /*[[@{/api/v1/user/updateUserSettings}]]*/ "/api/v1/user/updateUserSettings";
|
||||
/*]]>*/
|
||||
|
||||
let settings = {};
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if(!shouldIgnoreKey(key)) { // Using our helper function
|
||||
settings[key] = localStorage.getItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await window.fetchWithCsrf(urlUpdateUserSettings, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(settings)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error syncing settings to account');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Error syncing settings to account');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,458 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{adminUserSettings.title}, header=#{adminUserSettings.header})}"></th:block>
|
||||
<link rel="stylesheet" th:href="@{/css/modern-tables.css}">
|
||||
<style>
|
||||
.active-user {
|
||||
color: var(--md-sys-color-tertiary);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
|
||||
<div class="data-container">
|
||||
<div class="data-panel">
|
||||
<div class="data-header">
|
||||
<h1 class="data-title">
|
||||
<span class="data-icon">
|
||||
<span class="material-symbols-rounded">manage_accounts</span>
|
||||
</span>
|
||||
<span th:text="#{adminUserSettings.header}">Admin User Control Settings</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="data-body">
|
||||
<!-- User Stats Banner -->
|
||||
<div class="data-panel data-mb-3" style="background-color: var(--md-sys-color-primary-container);">
|
||||
<div class="data-body" style="padding: 1.25rem;">
|
||||
<div style="display: flex; flex-wrap: wrap; justify-content: space-around; align-items: center; gap: 1.5rem;">
|
||||
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
||||
<span class="material-symbols-rounded" style="font-size: 2.25rem; color: var(--md-sys-color-primary);">
|
||||
group
|
||||
</span>
|
||||
<div>
|
||||
<div style="color: var(--md-sys-color-primary); font-size: 0.875rem; font-weight: 500;" th:text="#{adminUserSettings.totalUsers}">Total Users</div>
|
||||
<div style="color: var(--md-sys-color-primary); font-size: 1.5rem; font-weight: 700;">
|
||||
<span th:text="${totalUsers}"></span>
|
||||
<span th:if="${@runningProOrHigher}" th:text="'/' + ${maxPaidUsers}" style="font-size: 1rem;"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
||||
<span class="material-symbols-rounded" style="font-size: 2.25rem; color: var(--md-sys-color-primary);">
|
||||
check_circle
|
||||
</span>
|
||||
<div>
|
||||
<div style="color: var(--md-sys-color-primary); font-size: 0.875rem; font-weight: 500;" th:text="#{adminUserSettings.activeUsers}">Active Users</div>
|
||||
<div style="color: var(--md-sys-color-primary); font-size: 1.5rem; font-weight: 700;" th:text="${activeUsers}"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
||||
<span class="material-symbols-rounded" style="font-size: 2.25rem; color: var(--md-sys-color-primary);">
|
||||
person_off
|
||||
</span>
|
||||
<div>
|
||||
<div style="color: var(--md-sys-color-primary); font-size: 0.875rem; font-weight: 500;" th:text="#{adminUserSettings.disabledUsers}">Disabled Users</div>
|
||||
<div style="color: var(--md-sys-color-primary); font-size: 1.5rem; font-weight: 700;" th:text="${disabledUsers}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert Messages -->
|
||||
<div th:if="${addMessage}" th:class="${#strings.contains(addMessage, 'Successfully') or #strings.contains(addMessage, 'Created') ? 'alert alert-success data-mb-3' : 'alert alert-danger data-mb-3'}">
|
||||
<span th:text="#{${addMessage}}">Default message if not found</span>
|
||||
</div>
|
||||
|
||||
<div th:if="${changeMessage}" th:class="${#strings.contains(changeMessage, 'Successfully') or #strings.contains(changeMessage, 'Created') ? 'alert alert-success data-mb-3' : 'alert alert-danger data-mb-3'}">
|
||||
<span th:text="#{${changeMessage}}">Default message if not found</span>
|
||||
</div>
|
||||
|
||||
<div th:if="${deleteMessage}" th:class="${#strings.contains(deleteMessage, 'Successfully') or #strings.contains(deleteMessage, 'Created') ? 'alert alert-success data-mb-3' : 'alert alert-danger data-mb-3'}">
|
||||
<span th:text="#{${deleteMessage}}">Default message if not found</span>
|
||||
</div>
|
||||
|
||||
<!-- Admin Actions -->
|
||||
<div class="data-section-title">User Management</div>
|
||||
<div class="data-actions data-mb-3">
|
||||
<button
|
||||
th:data-bs-toggle="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? null : 'modal'"
|
||||
th:data-bs-target="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? null : '#addUserModal'"
|
||||
th:class="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? 'data-btn data-btn-danger' : 'data-btn data-btn-primary'"
|
||||
th:title="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? #{adminUserSettings.maxUsersReached} : #{adminUserSettings.addUser}">
|
||||
<span class="material-symbols-rounded">person_add</span>
|
||||
<span th:text="#{adminUserSettings.addUser}">Add New User</span>
|
||||
</button>
|
||||
|
||||
<a th:href="@{'/teams'}" class="data-btn data-btn-secondary" th:title="#{adminUserSettings.teams}">
|
||||
<span class="material-symbols-rounded">group</span>
|
||||
<span th:text="#{adminUserSettings.teams}">Manage Teams</span>
|
||||
</a>
|
||||
|
||||
<button
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#changeUserRoleModal"
|
||||
class="data-btn data-btn-secondary"
|
||||
th:title="#{adminUserSettings.changeUserRole}">
|
||||
<span class="material-symbols-rounded">edit</span>
|
||||
<span th:text="#{adminUserSettings.changeUserRole}">Change User's Role</span>
|
||||
</button>
|
||||
|
||||
<a th:href="@{'/usage'}" th:if="${@runningEE}" class="data-btn data-btn-secondary" th:title="#{adminUserSettings.usage}">
|
||||
<span class="material-symbols-rounded">analytics</span>
|
||||
<span th:text="#{adminUserSettings.usage}">Usage Statistics</span>
|
||||
</a>
|
||||
|
||||
<a href="/audit" th:if="${@runningEE}" class="data-btn data-btn-secondary" title="Audit Dashboard">
|
||||
<span class="material-symbols-rounded">security</span>
|
||||
<span>Audit Dashboard</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Users Table -->
|
||||
<div class="table-responsive">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col" th:title="#{username}" class="text-overflow" th:text="#{username}">Username</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.team}" class="text-overflow" th:text="#{adminUserSettings.team}">Team</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.role}" class="text-overflow" th:text="#{adminUserSettings.role}">Roles</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.authenticated}" class="text-overflow" th:text="#{adminUserSettings.authenticated}">Authenticated</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.lastRequest}" class="text-overflow" th:text="#{adminUserSettings.lastRequest}">Last Request</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.actions}" class="text-overflow" th:text="#{adminUserSettings.actions}">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="user : ${users}">
|
||||
<td th:text="${user.id}" th:title="${user.id}" class="text-overflow"></td>
|
||||
<td th:text="${user.username}" th:title="${user.username}" class="text-overflow" th:classappend="${userSessions[user.username] ? 'active-user' : ''}"></td>
|
||||
<td th:text="${user.team != null ? user.team.name : '—'}" th:title="${user.team != null ? user.team.name : '—'}" class="text-overflow"></td>
|
||||
<td>
|
||||
<span class="data-badge" style="background-color: var(--md-sys-color-secondary-container); color: var(--md-sys-color-secondary); padding: 0.25rem 0.5rem; border-radius: 1rem; font-size: 0.875rem; display: inline-flex; align-items: center; gap: 0.25rem;">
|
||||
<span class="material-symbols-rounded" style="font-size: 1rem;">shield</span>
|
||||
<span th:text="#{${user.roleName}}" th:title="#{${user.roleName}}" class="text-overflow">Role</span>
|
||||
</span>
|
||||
</td>
|
||||
<td th:text="${user.authenticationType}" th:title="${user.authenticationType}"></td>
|
||||
<td th:text="${userLastRequest[user.username] != null ? #dates.format(userLastRequest[user.username], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}" th:title="${userLastRequest[user.username] != null ? #dates.format(userLastRequest[user.username], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}"></td>
|
||||
<td>
|
||||
<div class="data-action-cell">
|
||||
<form th:if="${user.username != currentUsername}" th:action="@{'/api/v1/user/admin/deleteUser/' + ${user.username}}" method="post" onsubmit="return confirmDeleteUser()" style="display: inline;">
|
||||
<button type="submit" th:title="#{adminUserSettings.deleteUser}" class="data-icon-btn data-icon-btn-danger">
|
||||
<span class="material-symbols-rounded">person_remove</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<a th:if="${user.username == currentUsername}" th:title="#{adminUserSettings.editOwnProfil}" th:href="@{'/account'}" class="data-icon-btn data-icon-btn-primary">
|
||||
<span class="material-symbols-rounded">edit</span>
|
||||
</a>
|
||||
|
||||
<form th:action="@{'/api/v1/user/admin/changeUserEnabled/' + ${user.username}}" method="post" onsubmit="return confirmChangeUserStatus()" style="display: inline;">
|
||||
<input type="hidden" name="enabled" th:value="!${user.enabled}" />
|
||||
<button type="submit" th:if="${user.enabled}" th:title="#{adminUserSettings.enabledUser}" class="data-icon-btn data-icon-btn-primary">
|
||||
<span class="material-symbols-rounded">person</span>
|
||||
</button>
|
||||
<button type="submit" th:unless="${user.enabled}" th:title="#{adminUserSettings.disabledUser}" class="data-icon-btn data-icon-btn-danger">
|
||||
<span class="material-symbols-rounded">person_off</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p th:if="${!@runningProOrHigher}" class="data-mt-3" th:text="#{enterpriseEdition.ssoAdvert}"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change User Role Modal -->
|
||||
<div class="modal fade" id="changeUserRoleModal" tabindex="-1" aria-labelledby="changeUserRoleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<form th:action="@{'/api/v1/user/admin/changeRole'}" method="post" class="modal-content data-modal">
|
||||
<div class="data-modal-header">
|
||||
<h5 class="data-modal-title">
|
||||
<span class="data-icon">
|
||||
<span class="material-symbols-rounded">edit</span>
|
||||
</span>
|
||||
<span th:text="#{adminUserSettings.changeUserRole}">Change User's Role</span>
|
||||
</h5>
|
||||
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="data-modal-body">
|
||||
<div class="data-mb-2">
|
||||
<button class="data-btn data-btn-secondary" data-toggle="tooltip" data-placement="auto" th:title="#{downgradeCurrentUserLongMessage}" style="padding: 0.25rem 0.5rem;">
|
||||
<span class="material-symbols-rounded">help</span>
|
||||
<span th:text="#{help}">Help</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="data-form-group">
|
||||
<label for="username" class="data-form-label" th:text="#{username}">Username</label>
|
||||
<select name="username" id="username" class="data-form-control" required>
|
||||
<option value="" disabled selected th:text="#{selectFilter}">-- Select --</option>
|
||||
<option th:each="user : ${users}" th:if="${user.username != currentUsername}" th:value="${user.username}" th:text="${user.username}">Username</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="data-form-group">
|
||||
<label for="role" class="data-form-label" th:text="#{adminUserSettings.role}">Role</label>
|
||||
<select name="role" id="role" class="data-form-control" required>
|
||||
<option value="" disabled selected th:text="#{selectFilter}">-- Select --</option>
|
||||
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}" th:text="#{${roleDetail.value}}">Role</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="data-form-group">
|
||||
<label for="team" class="data-form-label" th:text="#{adminUserSettings.team}">Team</label>
|
||||
<select name="teamId" id="team" class="data-form-control" required>
|
||||
<option value="" th:text="#{selectFilter}">-- Select --</option>
|
||||
<option th:each="team : ${teams}" th:value="${team.id}" th:text="${team.name}"></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="data-form-actions">
|
||||
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
<span th:text="#{cancel}">Cancel</span>
|
||||
</button>
|
||||
<button type="submit" class="data-btn data-btn-primary">
|
||||
<span class="material-symbols-rounded">check</span>
|
||||
<span th:text="#{adminUserSettings.submit}">Save User</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add User Modal -->
|
||||
<div class="modal fade" id="addUserModal" tabindex="-1" style="z-index: 10000;" aria-labelledby="addUserModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<form id="formsaveuser" th:action="@{'/api/v1/user/admin/saveUser'}" method="post" class="modal-content data-modal">
|
||||
<div class="data-modal-header">
|
||||
<h5 class="data-modal-title">
|
||||
<span class="data-icon">
|
||||
<span class="material-symbols-rounded">person_add</span>
|
||||
</span>
|
||||
<span th:text="#{adminUserSettings.addUser}">Add New User</span>
|
||||
</h5>
|
||||
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="data-modal-body">
|
||||
<div class="data-mb-2">
|
||||
<button class="data-btn data-btn-secondary" data-toggle="tooltip" data-placement="auto" th:title="#{adminUserSettings.usernameInfo}" style="padding: 0.25rem 0.5rem;">
|
||||
<span class="material-symbols-rounded">help</span>
|
||||
<span th:text="#{help}">Help</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="data-form-group">
|
||||
<label for="username" class="data-form-label" th:text="#{username}">Username</label>
|
||||
<input type="text" class="data-form-control" name="username" id="username" th:title="#{adminUserSettings.usernameInfo}" required>
|
||||
<span id="usernameError" style="display: none; color: var(--md-sys-color-error);" th:text="#{invalidUsernameMessage}">Invalid username!</span>
|
||||
</div>
|
||||
|
||||
<div class="data-form-group" id="passwordContainer">
|
||||
<label for="password" class="data-form-label" th:text="#{password}">Password</label>
|
||||
<input type="password" class="data-form-control" name="password" id="password" required>
|
||||
</div>
|
||||
|
||||
<div class="data-form-group">
|
||||
<label for="role" class="data-form-label" th:text="#{adminUserSettings.role}">Role</label>
|
||||
<select name="role" class="data-form-control" id="role" required>
|
||||
<option value="" disabled selected th:text="#{selectFilter}">-- Select --</option>
|
||||
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}" th:text="#{${roleDetail.value}}">Role</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="data-form-group">
|
||||
<label for="team" class="data-form-label" th:text="#{adminUserSettings.team}">Team</label>
|
||||
<select name="teamId" class="data-form-control" required>
|
||||
<option value="" th:text="#{selectFilter}">-- Select --</option>
|
||||
<option th:each="team : ${teams}" th:value="${team.id}" th:text="${team.name}"></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="data-form-group">
|
||||
<label for="authType" class="data-form-label">Authentication Type</label>
|
||||
<select id="authType" name="authType" class="data-form-control" required>
|
||||
<option value="web" selected>WEB</option>
|
||||
<option value="sso">SSO</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="data-form-group" id="checkboxContainer">
|
||||
<div class="form-check">
|
||||
<input id="forceChange" name="forceChange" type="checkbox">
|
||||
<label for="forceChange" th:text="#{adminUserSettings.forceChange}">Force user to change username/password on login</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-form-actions">
|
||||
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
<span th:text="#{cancel}">Cancel</span>
|
||||
</button>
|
||||
<button type="submit" class="data-btn data-btn-primary">
|
||||
<span class="material-symbols-rounded">check</span>
|
||||
<span th:text="#{adminUserSettings.submit}">Save User</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Team Modal -->
|
||||
<div class="modal fade" id="addTeamModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<form th:action="@{'/api/v1/team/create'}" method="post" class="modal-content data-modal">
|
||||
<div class="data-modal-header">
|
||||
<h5 class="data-modal-title">
|
||||
<span class="data-icon">
|
||||
<span class="material-symbols-rounded">group_add</span>
|
||||
</span>
|
||||
<span th:text="#{adminUserSettings.createTeam}">Create Team</span>
|
||||
</h5>
|
||||
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="data-modal-body">
|
||||
<div class="data-form-group">
|
||||
<label for="teamName" class="data-form-label" th:text="#{adminUserSettings.teamName}">Team Name</label>
|
||||
<input type="text" name="name" id="teamName" class="data-form-control" required />
|
||||
</div>
|
||||
<div class="data-form-actions">
|
||||
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
<span th:text="#{cancel}">Cancel</span>
|
||||
</button>
|
||||
<button type="submit" class="data-btn data-btn-primary">
|
||||
<span class="material-symbols-rounded">check</span>
|
||||
<span th:text="#{adminUserSettings.submit}">Create</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script th:inline="javascript">
|
||||
const delete_confirm_text = /*[[#{adminUserSettings.confirmDeleteUser}]]*/ 'Should the user be deleted?';
|
||||
const change_confirm_text = /*[[#{adminUserSettings.confirmChangeUserStatus}]]*/ 'Should the user be disabled/enabled?';
|
||||
|
||||
function confirmDeleteUser() {
|
||||
return confirm(delete_confirm_text);
|
||||
}
|
||||
|
||||
function confirmChangeUserStatus() {
|
||||
return confirm(change_confirm_text);
|
||||
}
|
||||
|
||||
jQuery.validator.addMethod("usernamePattern", function(value, element) {
|
||||
// Regular expression for user name: Min. 3 characters, max. 50 characters
|
||||
const regexUsername = /^[a-zA-Z0-9](?!.*[-@._+]{2,})([a-zA-Z0-9@._+-]{1,48})[a-zA-Z0-9]$/;
|
||||
|
||||
// Regular expression for email addresses: Max. 320 characters, with RFC-like validation
|
||||
const regexEmail = /^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*(?:\.[A-Za-z]{2,})$/;
|
||||
|
||||
// Check if the field is optional or meets the requirements
|
||||
return this.optional(element) || regexUsername.test(value) || regexEmail.test(value);
|
||||
}, /*[[#{invalidUsernameMessage}]]*/ "Invalid username format");
|
||||
|
||||
$(document).ready(function() {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
|
||||
$('#formsaveuser').validate({
|
||||
rules: {
|
||||
username: {
|
||||
required: true,
|
||||
usernamePattern: true
|
||||
},
|
||||
password: {
|
||||
required: true
|
||||
},
|
||||
role: {
|
||||
required: true
|
||||
},
|
||||
authType: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
messages: {
|
||||
username: {
|
||||
usernamePattern: /*[[#{invalidUsernameMessage}]]*/ "Invalid username format"
|
||||
},
|
||||
},
|
||||
errorPlacement: function(error, element) {
|
||||
if (element.attr("name") === "username") {
|
||||
$("#usernameError").text(error.text()).show();
|
||||
} else if (element.attr("name") !== "role" && element.attr("name") !== "authType") {
|
||||
error.insertAfter(element);
|
||||
}
|
||||
},
|
||||
success: function(label, element) {
|
||||
if ($(element).attr("name") === "username") {
|
||||
$("#usernameError").hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$('#username').on('input', function() {
|
||||
var usernameInput = $(this);
|
||||
var isValid = usernameInput[0].checkValidity();
|
||||
var errorSpan = $('#usernameError');
|
||||
|
||||
if (isValid) {
|
||||
usernameInput.removeClass('invalid').addClass('valid');
|
||||
errorSpan.hide();
|
||||
} else {
|
||||
usernameInput.removeClass('valid').addClass('invalid');
|
||||
errorSpan.show();
|
||||
}
|
||||
});
|
||||
|
||||
$('#authType').on('change', function() {
|
||||
var authType = $(this).val();
|
||||
var passwordField = $('#password');
|
||||
var passwordFieldContainer = $('#passwordContainer');
|
||||
var checkboxContainer = $('#checkboxContainer');
|
||||
|
||||
if (authType === 'sso') {
|
||||
passwordField.removeAttr('required');
|
||||
passwordField.prop('disabled', true).val('');
|
||||
passwordFieldContainer.slideUp('fast');
|
||||
checkboxContainer.slideUp('fast');
|
||||
} else {
|
||||
passwordField.prop('disabled', false);
|
||||
passwordField.attr('required', 'required');
|
||||
passwordFieldContainer.slideDown('fast');
|
||||
checkboxContainer.slideDown('fast');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user