Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eaafa21842 | ||
|
|
1a62680dcc | ||
|
|
789642a824 | ||
|
|
e1199a1fd3 | ||
|
|
c0f9c9570e | ||
|
|
a8c4a0bfb1 | ||
|
|
99d728d815 | ||
|
|
3317babe5c | ||
|
|
5ad5d851b1 | ||
|
|
6cedc8c994 | ||
|
|
eba24b4d6c | ||
|
|
68e95c2652 | ||
|
|
de23752b44 | ||
|
|
b44bb7c50c | ||
|
|
537aee3ab6 | ||
|
|
a6ae74e794 | ||
|
|
50ec2d82d4 | ||
|
|
f95e2fdaa3 | ||
|
|
70c9cb36c3 | ||
|
|
1b737cbce5 | ||
|
|
4492fa39be |
@@ -59,8 +59,8 @@ def find_duplicate_keys(file_path, keys=None, prefix=""):
|
||||
return duplicates
|
||||
|
||||
|
||||
# Maximum size for TOML files (e.g., 1 MB)
|
||||
MAX_FILE_SIZE = 1000 * 1024
|
||||
# Maximum size for TOML files (e.g., 570 KB)
|
||||
MAX_FILE_SIZE = 570 * 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.5.0 \
|
||||
--hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \
|
||||
--hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132
|
||||
cfgv==3.4.0 \
|
||||
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
|
||||
--hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
|
||||
# via pre-commit
|
||||
cssselect2==0.8.0 \
|
||||
--hash=sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e \
|
||||
@@ -109,269 +109,259 @@ distlib==0.4.0 \
|
||||
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
||||
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
||||
# via virtualenv
|
||||
filelock==3.20.3 \
|
||||
--hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \
|
||||
--hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1
|
||||
filelock==3.20.0 \
|
||||
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
|
||||
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
|
||||
# via virtualenv
|
||||
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
|
||||
fonttools==4.60.1 \
|
||||
--hash=sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c \
|
||||
--hash=sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc \
|
||||
--hash=sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a \
|
||||
--hash=sha256:0eae96373e4b7c9e45d099d7a523444e3554360927225c1cdae221a58a45b856 \
|
||||
--hash=sha256:122e1a8ada290423c493491d002f622b1992b1ab0b488c68e31c413390dc7eb2 \
|
||||
--hash=sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259 \
|
||||
--hash=sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c \
|
||||
--hash=sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce \
|
||||
--hash=sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003 \
|
||||
--hash=sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272 \
|
||||
--hash=sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77 \
|
||||
--hash=sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038 \
|
||||
--hash=sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea \
|
||||
--hash=sha256:2ee06fc57512144d8b0445194c2da9f190f61ad51e230f14836286470c99f854 \
|
||||
--hash=sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2 \
|
||||
--hash=sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258 \
|
||||
--hash=sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652 \
|
||||
--hash=sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08 \
|
||||
--hash=sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99 \
|
||||
--hash=sha256:596ecaca36367027d525b3b426d8a8208169d09edcf8c7506aceb3a38bfb55c7 \
|
||||
--hash=sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914 \
|
||||
--hash=sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6 \
|
||||
--hash=sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed \
|
||||
--hash=sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb \
|
||||
--hash=sha256:7473a8ed9ed09aeaa191301244a5a9dbe46fe0bf54f9d6cd21d83044c3321217 \
|
||||
--hash=sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc \
|
||||
--hash=sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f \
|
||||
--hash=sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c \
|
||||
--hash=sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877 \
|
||||
--hash=sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801 \
|
||||
--hash=sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85 \
|
||||
--hash=sha256:8b4eb332f9501cb1cd3d4d099374a1e1306783ff95489a1026bde9eb02ccc34a \
|
||||
--hash=sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb \
|
||||
--hash=sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383 \
|
||||
--hash=sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401 \
|
||||
--hash=sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28 \
|
||||
--hash=sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01 \
|
||||
--hash=sha256:a140761c4ff63d0cb9256ac752f230460ee225ccef4ad8f68affc723c88e2036 \
|
||||
--hash=sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc \
|
||||
--hash=sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac \
|
||||
--hash=sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903 \
|
||||
--hash=sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3 \
|
||||
--hash=sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6 \
|
||||
--hash=sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c \
|
||||
--hash=sha256:b42d86938e8dda1cd9a1a87a6d82f1818eaf933348429653559a458d027446da \
|
||||
--hash=sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299 \
|
||||
--hash=sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15 \
|
||||
--hash=sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199 \
|
||||
--hash=sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf \
|
||||
--hash=sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1 \
|
||||
--hash=sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537 \
|
||||
--hash=sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d \
|
||||
--hash=sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c \
|
||||
--hash=sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4 \
|
||||
--hash=sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9 \
|
||||
--hash=sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed \
|
||||
--hash=sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987 \
|
||||
--hash=sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa
|
||||
# via weasyprint
|
||||
identify==2.6.16 \
|
||||
--hash=sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0 \
|
||||
--hash=sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980
|
||||
identify==2.6.15 \
|
||||
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
|
||||
--hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf
|
||||
# via pre-commit
|
||||
nodeenv==1.10.0 \
|
||||
--hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \
|
||||
--hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb
|
||||
nodeenv==1.9.1 \
|
||||
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
||||
# via pre-commit
|
||||
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
|
||||
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
|
||||
# via opencv-python-headless
|
||||
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
|
||||
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
|
||||
# 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.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
|
||||
pillow==12.0.0 \
|
||||
--hash=sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643 \
|
||||
--hash=sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e \
|
||||
--hash=sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e \
|
||||
--hash=sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc \
|
||||
--hash=sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642 \
|
||||
--hash=sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6 \
|
||||
--hash=sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1 \
|
||||
--hash=sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b \
|
||||
--hash=sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399 \
|
||||
--hash=sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba \
|
||||
--hash=sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad \
|
||||
--hash=sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47 \
|
||||
--hash=sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739 \
|
||||
--hash=sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b \
|
||||
--hash=sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f \
|
||||
--hash=sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10 \
|
||||
--hash=sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52 \
|
||||
--hash=sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d \
|
||||
--hash=sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b \
|
||||
--hash=sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a \
|
||||
--hash=sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9 \
|
||||
--hash=sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d \
|
||||
--hash=sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098 \
|
||||
--hash=sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905 \
|
||||
--hash=sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b \
|
||||
--hash=sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3 \
|
||||
--hash=sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371 \
|
||||
--hash=sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953 \
|
||||
--hash=sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01 \
|
||||
--hash=sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca \
|
||||
--hash=sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e \
|
||||
--hash=sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7 \
|
||||
--hash=sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27 \
|
||||
--hash=sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082 \
|
||||
--hash=sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e \
|
||||
--hash=sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d \
|
||||
--hash=sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8 \
|
||||
--hash=sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a \
|
||||
--hash=sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad \
|
||||
--hash=sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3 \
|
||||
--hash=sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a \
|
||||
--hash=sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d \
|
||||
--hash=sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353 \
|
||||
--hash=sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee \
|
||||
--hash=sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b \
|
||||
--hash=sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b \
|
||||
--hash=sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a \
|
||||
--hash=sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7 \
|
||||
--hash=sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef \
|
||||
--hash=sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a \
|
||||
--hash=sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a \
|
||||
--hash=sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257 \
|
||||
--hash=sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07 \
|
||||
--hash=sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4 \
|
||||
--hash=sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c \
|
||||
--hash=sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c \
|
||||
--hash=sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4 \
|
||||
--hash=sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe \
|
||||
--hash=sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8 \
|
||||
--hash=sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5 \
|
||||
--hash=sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6 \
|
||||
--hash=sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e \
|
||||
--hash=sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8 \
|
||||
--hash=sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e \
|
||||
--hash=sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275 \
|
||||
--hash=sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3 \
|
||||
--hash=sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76 \
|
||||
--hash=sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227 \
|
||||
--hash=sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9 \
|
||||
--hash=sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5 \
|
||||
--hash=sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79 \
|
||||
--hash=sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca \
|
||||
--hash=sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa \
|
||||
--hash=sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b \
|
||||
--hash=sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e \
|
||||
--hash=sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197 \
|
||||
--hash=sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab \
|
||||
--hash=sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79 \
|
||||
--hash=sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2 \
|
||||
--hash=sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363 \
|
||||
--hash=sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0 \
|
||||
--hash=sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e \
|
||||
--hash=sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782 \
|
||||
--hash=sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925 \
|
||||
--hash=sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0 \
|
||||
--hash=sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b \
|
||||
--hash=sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced \
|
||||
--hash=sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c \
|
||||
--hash=sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344 \
|
||||
--hash=sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9 \
|
||||
--hash=sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1
|
||||
# via
|
||||
# -r .github/scripts/requirements_dev.in
|
||||
# pdf2image
|
||||
# weasyprint
|
||||
platformdirs==4.5.1 \
|
||||
--hash=sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda \
|
||||
--hash=sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31
|
||||
platformdirs==4.5.0 \
|
||||
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
|
||||
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
|
||||
# via virtualenv
|
||||
pre-commit==4.5.1 \
|
||||
--hash=sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77 \
|
||||
--hash=sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61
|
||||
pre-commit==4.3.0 \
|
||||
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
|
||||
--hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
pycparser==3.0 \
|
||||
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
|
||||
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
|
||||
pycparser==2.23 \
|
||||
--hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \
|
||||
--hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934
|
||||
# via cffi
|
||||
pydyf==0.12.1 \
|
||||
--hash=sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc \
|
||||
--hash=sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095
|
||||
pydyf==0.11.0 \
|
||||
--hash=sha256:0aaf9e2ebbe786ec7a78ec3fbffa4cdcecde53fd6f563221d53c6bc1328848a3 \
|
||||
--hash=sha256:394dddf619cca9d0c55715e3c55ea121a9bf9cbc780cdc1201a2427917b86b64
|
||||
# via weasyprint
|
||||
pyphen==0.17.2 \
|
||||
--hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \
|
||||
@@ -452,9 +442,9 @@ pyyaml==6.0.3 \
|
||||
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
|
||||
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
||||
# via pre-commit
|
||||
tinycss2==1.5.1 \
|
||||
--hash=sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661 \
|
||||
--hash=sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957
|
||||
tinycss2==1.4.0 \
|
||||
--hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \
|
||||
--hash=sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289
|
||||
# via
|
||||
# cssselect2
|
||||
# weasyprint
|
||||
@@ -462,17 +452,17 @@ tinyhtml5==2.0.0 \
|
||||
--hash=sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc \
|
||||
--hash=sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e
|
||||
# via weasyprint
|
||||
unoserver==3.6 \
|
||||
--hash=sha256:25c360fa194396a89cb79b4edd2735f8e4f0fd8531e59db3952114585bd7df05 \
|
||||
--hash=sha256:e446bcb3638c51880f002aaeecab1cf74dfa9df81035f027f7ff2e081b6d7015
|
||||
unoserver==3.4 \
|
||||
--hash=sha256:3dcf2204013def1d1ddd3671f38b11346bdf349fef9728277462666a8a634419 \
|
||||
--hash=sha256:64c24d33d4f65d680a2d9f676518cb28e7fd6c1f9d9a745c33e4a4cb59afdfcd
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
virtualenv==20.36.1 \
|
||||
--hash=sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f \
|
||||
--hash=sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba
|
||||
virtualenv==20.35.4 \
|
||||
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
|
||||
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
|
||||
# via pre-commit
|
||||
weasyprint==68.0 \
|
||||
--hash=sha256:447f40898b747cb44ac31a5d493d512e7441fd56e13f63744c099383bbf9cda9 \
|
||||
--hash=sha256:c2cb40c71b50837c5971f00171c9e4078e8c9912dd7c217f3e90e068f11e8aa1
|
||||
weasyprint==66.0 \
|
||||
--hash=sha256:82b0783b726fcd318e2c977dcdddca76515b30044bc7a830cc4fbe717582a6d0 \
|
||||
--hash=sha256:da71dc87dc129ac9cffdc65e5477e90365ab9dbae45c744014ec1d06303dde40
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
webencodings==0.5.1 \
|
||||
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
|
||||
@@ -481,27 +471,86 @@ webencodings==0.5.1 \
|
||||
# cssselect2
|
||||
# tinycss2
|
||||
# tinyhtml5
|
||||
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
|
||||
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
|
||||
# via fonttools
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==26.0 \
|
||||
--hash=sha256:3ce220a0a17915972fbf1ab451baae1521c4539e778b28127efa79b974aff0fa \
|
||||
--hash=sha256:98436feffb9e31bc9339cf369fd55d3331b1580b6a6f1173bacacddcf9c34754
|
||||
pip==25.3 \
|
||||
--hash=sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343 \
|
||||
--hash=sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
setuptools==80.10.2 \
|
||||
--hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \
|
||||
--hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173
|
||||
setuptools==80.9.0 \
|
||||
--hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \
|
||||
--hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c
|
||||
# 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.3 \
|
||||
--hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \
|
||||
--hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1
|
||||
filelock==3.20.0 \
|
||||
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
|
||||
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
|
||||
# via virtualenv
|
||||
identify==2.6.16 \
|
||||
--hash=sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0 \
|
||||
--hash=sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980
|
||||
identify==2.6.15 \
|
||||
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
|
||||
--hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf
|
||||
# via pre-commit
|
||||
nodeenv==1.10.0 \
|
||||
--hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \
|
||||
--hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb
|
||||
nodeenv==1.9.1 \
|
||||
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
||||
# via pre-commit
|
||||
platformdirs==4.5.1 \
|
||||
--hash=sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda \
|
||||
--hash=sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31
|
||||
platformdirs==4.5.0 \
|
||||
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
|
||||
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
|
||||
# via virtualenv
|
||||
pre-commit==4.5.1 \
|
||||
--hash=sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77 \
|
||||
--hash=sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61
|
||||
pre-commit==4.5.0 \
|
||||
--hash=sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1 \
|
||||
--hash=sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b
|
||||
# via -r .github/scripts/requirements_pre_commit.in
|
||||
pyyaml==6.0.3 \
|
||||
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
|
||||
@@ -107,7 +107,7 @@ pyyaml==6.0.3 \
|
||||
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
|
||||
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
||||
# via pre-commit
|
||||
virtualenv==20.36.1 \
|
||||
--hash=sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f \
|
||||
--hash=sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba
|
||||
virtualenv==20.35.4 \
|
||||
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
|
||||
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
|
||||
# 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.14.0 \
|
||||
--hash=sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680 \
|
||||
--hash=sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064
|
||||
tomlkit==0.13.3 \
|
||||
--hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \
|
||||
--hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0
|
||||
# via -r .github/scripts/requirements_sync_readme.in
|
||||
|
||||
@@ -130,7 +130,7 @@ jobs:
|
||||
|
||||
- name: Setup Node.js
|
||||
if: matrix.variant.build_frontend == true
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
@@ -185,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@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
@@ -291,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@9476ceec3ea1c63298d4403b983e1ccf2556ff4c # v1.1.0
|
||||
uses: digicert/ssm-code-signing@v1.1.0
|
||||
env:
|
||||
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
|
||||
@@ -399,7 +399,7 @@ jobs:
|
||||
echo "Certificate imported successfully."
|
||||
|
||||
- name: Build Tauri app
|
||||
uses: tauri-apps/tauri-action@51a9f1156b33df106d827c3a78f8f894946c5faa # v0.5.25
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
|
||||
@@ -33,11 +33,6 @@ 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: |
|
||||
@@ -86,14 +81,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@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
@@ -193,7 +188,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@9476ceec3ea1c63298d4403b983e1ccf2556ff4c # v1.1.0
|
||||
uses: digicert/ssm-code-signing@v1.1.0
|
||||
env:
|
||||
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
|
||||
@@ -312,7 +307,7 @@ jobs:
|
||||
ls -la /usr/bin/hd* || echo "No hd* tools found"
|
||||
|
||||
- name: Build Tauri app
|
||||
uses: tauri-apps/tauri-action@51a9f1156b33df106d827c3a78f8f894946c5faa # v0.5.25
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
@@ -644,11 +639,6 @@ 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
|
||||
|
||||
@@ -128,11 +128,6 @@ jobs:
|
||||
outputs:
|
||||
frontend: ${{ steps.changes.outputs.frontend }}
|
||||
steps:
|
||||
- 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
|
||||
|
||||
+6
-155
@@ -1,14 +1,10 @@
|
||||
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;
|
||||
@@ -45,7 +41,6 @@ public class RuntimePathConfig {
|
||||
|
||||
// Pipeline paths
|
||||
private final String pipelineWatchedFoldersPath;
|
||||
private final List<String> pipelineWatchedFoldersPaths;
|
||||
private final String pipelineFinishedFoldersPath;
|
||||
private final String pipelineDefaultWebUiConfigs;
|
||||
private final String pipelinePath;
|
||||
@@ -54,27 +49,20 @@ 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.pipelinePath =
|
||||
this.pipelineWatchedFoldersPath =
|
||||
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,
|
||||
@@ -84,9 +72,6 @@ public class RuntimePathConfig {
|
||||
defaultWebUIConfigs,
|
||||
pipeline != null ? pipeline.getWebUIConfigsDir() : null);
|
||||
|
||||
// Validate path conflicts after all paths are resolved
|
||||
validatePipelinePaths();
|
||||
|
||||
boolean isDocker = isRunningInDocker();
|
||||
|
||||
// Initialize Operation paths
|
||||
@@ -144,140 +129,6 @@ public class RuntimePathConfig {
|
||||
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"));
|
||||
}
|
||||
|
||||
+143
-38
@@ -215,75 +215,182 @@ public class ApplicationProperties {
|
||||
@Setter
|
||||
@ToString
|
||||
public static class SAML2 {
|
||||
private String provider;
|
||||
private Boolean enabled = false;
|
||||
private Boolean autoCreateUser = false;
|
||||
private Boolean blockRegistration = false;
|
||||
private String registrationId = "stirling";
|
||||
private Boolean enableSingleLogout = false;
|
||||
|
||||
@ToString.Exclude
|
||||
@JsonProperty("idpMetadataUri")
|
||||
@ToString.Exclude private String metadataUri;
|
||||
|
||||
private Provider provider = new Provider();
|
||||
private SP sp = new SP();
|
||||
|
||||
// Legacy field mappings for backward compatibility
|
||||
@Deprecated(since = "2.3.1", forRemoval = true)
|
||||
@JsonIgnore
|
||||
private String idpMetadataUri;
|
||||
|
||||
@Deprecated(since = "2.3.1", forRemoval = true)
|
||||
@JsonIgnore
|
||||
private String idpSingleLogoutUrl;
|
||||
|
||||
@Deprecated(since = "2.3.1", forRemoval = true)
|
||||
@JsonIgnore
|
||||
private String idpSingleLoginUrl;
|
||||
|
||||
@Deprecated(since = "2.3.1", forRemoval = true)
|
||||
@JsonIgnore
|
||||
private String idpIssuer;
|
||||
|
||||
@JsonProperty("idpCert")
|
||||
@Deprecated(since = "2.3.1", forRemoval = true)
|
||||
@JsonIgnore
|
||||
private String idpEntityId;
|
||||
|
||||
@Deprecated(since = "2.3.1", forRemoval = true)
|
||||
@JsonIgnore
|
||||
private String idpCert;
|
||||
|
||||
@ToString.Exclude
|
||||
@JsonProperty("privateKey")
|
||||
@Deprecated(since = "2.3.1", forRemoval = true)
|
||||
@JsonIgnore
|
||||
private String privateKey;
|
||||
|
||||
@ToString.Exclude
|
||||
@JsonProperty("spCert")
|
||||
@Deprecated(since = "2.3.1", forRemoval = true)
|
||||
@JsonIgnore
|
||||
private String spCert;
|
||||
|
||||
/** Migrate legacy flat properties to new nested structure on set. */
|
||||
public void setIdpMetadataUri(String value) {
|
||||
this.idpMetadataUri = value;
|
||||
if (value != null
|
||||
&& !value.isBlank()
|
||||
&& (metadataUri == null || metadataUri.isBlank())) {
|
||||
this.metadataUri = value;
|
||||
}
|
||||
}
|
||||
|
||||
public void setIdpSingleLoginUrl(String value) {
|
||||
this.idpSingleLoginUrl = value;
|
||||
if (value != null && !value.isBlank()) {
|
||||
this.provider.setSingleLoginUrl(value);
|
||||
}
|
||||
}
|
||||
|
||||
public void setIdpSingleLogoutUrl(String value) {
|
||||
this.idpSingleLogoutUrl = value;
|
||||
if (value != null && !value.isBlank()) {
|
||||
this.provider.setSingleLogoutUrl(value);
|
||||
}
|
||||
}
|
||||
|
||||
public void setIdpIssuer(String value) {
|
||||
this.idpIssuer = value;
|
||||
if (value != null && !value.isBlank()) {
|
||||
this.provider.setEntityId(value);
|
||||
}
|
||||
}
|
||||
|
||||
public void setIdpEntityId(String value) {
|
||||
this.idpEntityId = value;
|
||||
if (value != null && !value.isBlank()) {
|
||||
this.provider.setEntityId(value);
|
||||
}
|
||||
}
|
||||
|
||||
public void setIdpCert(String value) {
|
||||
this.idpCert = value;
|
||||
if (value != null && !value.isBlank()) {
|
||||
this.provider.setCert(value);
|
||||
}
|
||||
}
|
||||
|
||||
public void setPrivateKey(String value) {
|
||||
this.privateKey = value;
|
||||
if (value != null && !value.isBlank()) {
|
||||
this.sp.setPrivateKey(value);
|
||||
}
|
||||
}
|
||||
|
||||
public void setSpCert(String value) {
|
||||
this.spCert = value;
|
||||
if (value != null && !value.isBlank()) {
|
||||
this.sp.setCert(value);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public InputStream getIdpMetadataUri() throws IOException {
|
||||
if (idpMetadataUri.startsWith("classpath:")) {
|
||||
return new ClassPathResource(idpMetadataUri.substring("classpath:".length()))
|
||||
public InputStream getMetadataUriAsStream() throws IOException {
|
||||
String uri = getEffectiveMetadataUri();
|
||||
if (uri == null || uri.isBlank()) {
|
||||
throw new IOException("security.saml2.metadataUri is not configured");
|
||||
}
|
||||
if (uri.startsWith("classpath:")) {
|
||||
return new ClassPathResource(uri.substring("classpath:".length()))
|
||||
.getInputStream();
|
||||
}
|
||||
try {
|
||||
URI uri = new URI(idpMetadataUri);
|
||||
URL url = uri.toURL();
|
||||
URI parsedUri = new URI(uri);
|
||||
URL url = parsedUri.toURL();
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("GET");
|
||||
return connection.getInputStream();
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IOException("Invalid URI format: " + idpMetadataUri, e);
|
||||
throw new IOException("Invalid URI format: " + uri, e);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Resource getSpCert() {
|
||||
if (spCert == null) return null;
|
||||
if (spCert.startsWith("classpath:")) {
|
||||
return new ClassPathResource(spCert.substring("classpath:".length()));
|
||||
} else {
|
||||
return new FileSystemResource(spCert);
|
||||
public String getEffectiveMetadataUri() {
|
||||
if (metadataUri != null && !metadataUri.isBlank()) {
|
||||
return metadataUri;
|
||||
}
|
||||
return idpMetadataUri; // Legacy fallback
|
||||
}
|
||||
|
||||
/** IdP configuration - manual fallback when metadata is unavailable. */
|
||||
@Data
|
||||
public static class Provider {
|
||||
private String name = ""; // Display name only
|
||||
private String singleLoginUrl;
|
||||
private String singleLogoutUrl;
|
||||
private String entityId;
|
||||
@ToString.Exclude private String cert;
|
||||
|
||||
@JsonIgnore
|
||||
public Resource getCertResource() {
|
||||
if (cert == null) return null;
|
||||
if (cert.startsWith("classpath:")) {
|
||||
return new ClassPathResource(cert.substring("classpath:".length()));
|
||||
} else {
|
||||
return new FileSystemResource(cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Resource getIdpCert() {
|
||||
if (idpCert == null) return null;
|
||||
if (idpCert.startsWith("classpath:")) {
|
||||
return new ClassPathResource(idpCert.substring("classpath:".length()));
|
||||
} else {
|
||||
return new FileSystemResource(idpCert);
|
||||
}
|
||||
}
|
||||
/** Service Provider (SP) credentials for signing SAML requests. */
|
||||
@Data
|
||||
public static class SP {
|
||||
@ToString.Exclude private String privateKey;
|
||||
@ToString.Exclude private String cert;
|
||||
|
||||
@JsonIgnore
|
||||
public Resource getPrivateKey() {
|
||||
if (privateKey == null) return null;
|
||||
if (privateKey.startsWith("classpath:")) {
|
||||
return new ClassPathResource(privateKey.substring("classpath:".length()));
|
||||
} else {
|
||||
return new FileSystemResource(privateKey);
|
||||
@JsonIgnore
|
||||
public Resource getPrivateKeyResource() {
|
||||
if (privateKey == null) return null;
|
||||
if (privateKey.startsWith("classpath:")) {
|
||||
return new ClassPathResource(privateKey.substring("classpath:".length()));
|
||||
} else {
|
||||
return new FileSystemResource(privateKey);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Resource getCertResource() {
|
||||
if (cert == null) return null;
|
||||
if (cert.startsWith("classpath:")) {
|
||||
return new ClassPathResource(cert.substring("classpath:".length()));
|
||||
} else {
|
||||
return new FileSystemResource(cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -459,9 +566,7 @@ 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;
|
||||
}
|
||||
|
||||
@@ -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 List<Path> rootDirs;
|
||||
private final Path rootDir;
|
||||
private Set<Path> stagingFiles;
|
||||
|
||||
/**
|
||||
@@ -47,28 +47,8 @@ public class FileMonitor {
|
||||
this.pathFilter = pathFilter;
|
||||
this.readyForProcessingFiles = ConcurrentHashMap.newKeySet();
|
||||
this.watchService = FileSystems.getDefault().newWatchService();
|
||||
|
||||
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");
|
||||
}
|
||||
log.info("Monitoring directory: {}", runtimePathConfig.getPipelineWatchedFoldersPath());
|
||||
this.rootDir = Path.of(runtimePathConfig.getPipelineWatchedFoldersPath());
|
||||
}
|
||||
|
||||
private boolean shouldNotProcess(Path path) {
|
||||
@@ -105,15 +85,13 @@ public class FileMonitor {
|
||||
readyForProcessingFiles.clear();
|
||||
|
||||
if (path2KeyMapping.isEmpty()) {
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ public class RequestUriUtils {
|
||||
|| normalizedUri.startsWith("/pdfium/")
|
||||
|| normalizedUri.startsWith("/assets/")
|
||||
|| normalizedUri.startsWith("/locales/")
|
||||
|| normalizedUri.startsWith("/Login/")
|
||||
|| normalizedUri.startsWith("/login/")
|
||||
|| normalizedUri.startsWith("/samples/")
|
||||
|| normalizedUri.startsWith("/classic-logo/")
|
||||
|| normalizedUri.startsWith("/modern-logo/")
|
||||
@@ -165,9 +165,9 @@ public class RequestUriUtils {
|
||||
|| trimmedUri.contains("/oauth2/authorization/") // OAuth2 authorization endpoint
|
||||
|| trimmedUri.startsWith("/api/v1/auth/login")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/refresh")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/logout")
|
||||
|| trimmedUri.startsWith("/logout")
|
||||
|| trimmedUri.startsWith(
|
||||
"/api/v1/proprietary/ui-data/login") // Login page config (SSO providers +
|
||||
"/api/v1/proprietary/ui-data/login") // Login page config (SSO providers
|
||||
// enableLogin)
|
||||
|| trimmedUri.startsWith(
|
||||
"/api/v1/ui-data/footer-info") // Public footer configuration
|
||||
|
||||
+8
-8
@@ -31,9 +31,9 @@ class ApplicationPropertiesSaml2HttpTest {
|
||||
String url = server.url("/meta").toString();
|
||||
|
||||
var s = new ApplicationProperties.Security.SAML2();
|
||||
s.setIdpMetadataUri(url);
|
||||
s.setMetadataUri(url);
|
||||
|
||||
try (InputStream in = s.getIdpMetadataUri()) {
|
||||
try (InputStream in = s.getMetadataUriAsStream()) {
|
||||
String body = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
assertTrue(body.contains("EntityDescriptor"));
|
||||
}
|
||||
@@ -44,9 +44,9 @@ class ApplicationPropertiesSaml2HttpTest {
|
||||
void idpMetadataUri_invalidUri_triggers_catch_and_throwsIOException() {
|
||||
// Ungültige URI -> new URI(...) wirft URISyntaxException -> catch -> IOException
|
||||
var s = new ApplicationProperties.Security.SAML2();
|
||||
s.setIdpMetadataUri("http:##invalid uri"); // absichtlich kaputt (Leerzeichen + ##)
|
||||
s.setMetadataUri("http:##invalid uri"); // absichtlich kaputt (Leerzeichen + ##)
|
||||
|
||||
assertThrows(IOException.class, s::getIdpMetadataUri);
|
||||
assertThrows(IOException.class, s::getMetadataUriAsStream);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -57,8 +57,8 @@ class ApplicationPropertiesSaml2HttpTest {
|
||||
Path tmp = Files.createTempFile("spdf-spcert-", ".crt");
|
||||
Files.writeString(tmp, "CERT");
|
||||
|
||||
s.setSpCert(tmp.toString());
|
||||
Resource r = s.getSpCert();
|
||||
s.getSp().setCert(tmp.toString());
|
||||
Resource r = s.getSp().getCertResource();
|
||||
|
||||
assertNotNull(r);
|
||||
assertInstanceOf(FileSystemResource.class, r, "Expected FileSystemResource for FS path");
|
||||
@@ -71,8 +71,8 @@ class ApplicationPropertiesSaml2HttpTest {
|
||||
|
||||
// bewusst nicht existierender Pfad -> else-Zweig wird trotzdem genommen
|
||||
String missing = "/this/path/does/not/exist/idp.crt";
|
||||
s.setIdpCert(missing);
|
||||
Resource r = s.getIdpCert();
|
||||
s.getProvider().setCert(missing);
|
||||
Resource r = s.getProvider().getCertResource();
|
||||
|
||||
assertNotNull(r);
|
||||
assertInstanceOf(FileSystemResource.class, r, "Expected FileSystemResource for FS path");
|
||||
|
||||
+15
-16
@@ -15,9 +15,9 @@ class ApplicationPropertiesSaml2ResourceTest {
|
||||
@Test
|
||||
void idpMetadataUri_classpath_is_resolved() throws Exception {
|
||||
var s = new ApplicationProperties.Security.SAML2();
|
||||
s.setIdpMetadataUri("classpath:saml/dummy.txt");
|
||||
s.setMetadataUri("classpath:saml/dummy.txt");
|
||||
|
||||
try (InputStream in = s.getIdpMetadataUri()) {
|
||||
try (InputStream in = s.getMetadataUriAsStream()) {
|
||||
assertNotNull(in, "Classpath InputStream should not be null");
|
||||
String txt = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
assertTrue(txt.contains("ok"));
|
||||
@@ -28,27 +28,26 @@ class ApplicationPropertiesSaml2ResourceTest {
|
||||
void spCert_idpCert_privateKey_null_classpath_and_filesystem() throws Exception {
|
||||
var s = new ApplicationProperties.Security.SAML2();
|
||||
|
||||
s.setSpCert(null);
|
||||
s.setIdpCert(null);
|
||||
s.setPrivateKey(null);
|
||||
assertNull(s.getSpCert());
|
||||
assertNull(s.getIdpCert());
|
||||
assertNull(s.getPrivateKey());
|
||||
// Test null values via the nested structure
|
||||
assertNull(s.getSp().getCertResource());
|
||||
assertNull(s.getProvider().getCertResource());
|
||||
assertNull(s.getSp().getPrivateKeyResource());
|
||||
|
||||
s.setSpCert("classpath:saml/dummy.txt");
|
||||
s.setIdpCert("classpath:saml/dummy.txt");
|
||||
s.setPrivateKey("classpath:saml/dummy.txt");
|
||||
Resource sp = s.getSpCert();
|
||||
Resource idp = s.getIdpCert();
|
||||
Resource pk = s.getPrivateKey();
|
||||
// Set classpath resources via the nested structure
|
||||
s.getSp().setCert("classpath:saml/dummy.txt");
|
||||
s.getProvider().setCert("classpath:saml/dummy.txt");
|
||||
s.getSp().setPrivateKey("classpath:saml/dummy.txt");
|
||||
Resource sp = s.getSp().getCertResource();
|
||||
Resource idp = s.getProvider().getCertResource();
|
||||
Resource pk = s.getSp().getPrivateKeyResource();
|
||||
assertTrue(sp.exists());
|
||||
assertTrue(idp.exists());
|
||||
assertTrue(pk.exists());
|
||||
|
||||
Path tmp = Files.createTempFile("spdf-key-", ".pem");
|
||||
Files.writeString(tmp, "KEY");
|
||||
s.setPrivateKey(tmp.toString());
|
||||
Resource pkFs = s.getPrivateKey();
|
||||
s.getSp().setPrivateKey(tmp.toString());
|
||||
Resource pkFs = s.getSp().getPrivateKeyResource();
|
||||
assertNotNull(pkFs);
|
||||
assertTrue(pkFs.exists());
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ 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;
|
||||
@@ -35,8 +34,7 @@ class FileMonitorTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
when(runtimePathConfig.getPipelineWatchedFoldersPaths())
|
||||
.thenReturn(List.of(tempDir.toString()));
|
||||
when(runtimePathConfig.getPipelineWatchedFoldersPath()).thenReturn(tempDir.toString());
|
||||
|
||||
// This mock is used in all tests except testPathFilter
|
||||
// We use lenient to avoid UnnecessaryStubbingException in that test
|
||||
|
||||
@@ -106,29 +106,16 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
} else if (hasConfiguredOrigins) {
|
||||
// Use user-configured origins + always include Tauri origins for desktop app support
|
||||
// Use user-configured origins
|
||||
logger.info(
|
||||
"Configuring CORS with allowed origins: {}",
|
||||
applicationProperties.getSystem().getCorsAllowedOrigins());
|
||||
|
||||
// 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]);
|
||||
String[] allowedOrigins =
|
||||
applicationProperties
|
||||
.getSystem()
|
||||
.getCorsAllowedOrigins()
|
||||
.toArray(new String[0]);
|
||||
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns(allowedOrigins)
|
||||
|
||||
+4
-4
@@ -46,18 +46,18 @@ public class EditTableOfContentsController {
|
||||
@Operation(
|
||||
summary = "Extract PDF Bookmarks",
|
||||
description = "Extracts bookmarks/table of contents from a PDF document as JSON.")
|
||||
public ResponseEntity<List<Map<String, Object>>> extractBookmarks(@RequestParam("file") MultipartFile file)
|
||||
@ResponseBody
|
||||
public 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 ResponseEntity.ok(new ArrayList<>());
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
List<Map<String, Object>> bookmarks = extractBookmarkItems(document, outline);
|
||||
return ResponseEntity.ok(bookmarks);
|
||||
return extractBookmarkItems(document, outline);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-10
@@ -52,17 +52,9 @@ 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()) {
|
||||
@@ -101,10 +93,9 @@ public class PageNumbersController {
|
||||
PDPage page = document.getPage(i);
|
||||
PDRectangle pageSize = page.getMediaBox();
|
||||
|
||||
String nFormatted = String.format(formatN, pageNumber);
|
||||
String text =
|
||||
customText
|
||||
.replace("{n}", nFormatted)
|
||||
.replace("{n}", String.valueOf(pageNumber))
|
||||
.replace("{total}", String.valueOf(document.getNumberOfPages()))
|
||||
.replace(
|
||||
"{filename}",
|
||||
|
||||
+6
-59
@@ -3,7 +3,6 @@ package stirling.software.SPDF.controller.api.pipeline;
|
||||
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;
|
||||
@@ -15,7 +14,6 @@ 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;
|
||||
@@ -43,20 +41,14 @@ 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 List<String> watchedFoldersDirs;
|
||||
private final String watchedFoldersDir;
|
||||
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,
|
||||
@@ -69,26 +61,13 @@ public class PipelineDirectoryProcessor {
|
||||
this.processor = processor;
|
||||
this.fileMonitor = fileMonitor;
|
||||
this.postHogService = postHogService;
|
||||
this.watchedFoldersDirs = runtimePathConfig.getPipelineWatchedFoldersPaths();
|
||||
this.watchedFoldersDir = runtimePathConfig.getPipelineWatchedFoldersPath();
|
||||
this.finishedFoldersDir = runtimePathConfig.getPipelineFinishedFoldersPath();
|
||||
}
|
||||
|
||||
@Scheduled(fixedRate = 60000)
|
||||
public void scanFolders() {
|
||||
// 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) {
|
||||
Path watchedFolderPath = Paths.get(watchedFoldersDir).toAbsolutePath();
|
||||
if (!Files.exists(watchedFolderPath)) {
|
||||
try {
|
||||
Files.createDirectories(watchedFolderPath);
|
||||
@@ -99,34 +78,16 @@ 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)
|
||||
&& !"processing".equals(dirName)) {
|
||||
if (!dir.equals(watchedFolderPath) && !dir.endsWith("processing")) {
|
||||
handleDirectory(dir);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -137,11 +98,8 @@ public class PipelineDirectoryProcessor {
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFileFailed(Path path, IOException exc) {
|
||||
// Handle broken symlinks, permission issues, or inaccessible
|
||||
// directories
|
||||
if (exc != null) {
|
||||
log.debug("Cannot access path '{}': {}", path, exc.getMessage());
|
||||
}
|
||||
// Handle broken symlinks or inaccessible directories
|
||||
log.error("Error accessing path: {}", path, exc);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
@@ -151,17 +109,6 @@ 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);
|
||||
|
||||
-7
@@ -39,13 +39,6 @@ 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,"
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
multipart.enabled=true
|
||||
logging.level.org.springframework=WARN
|
||||
logging.level.org.springframework.security=WARN
|
||||
logging.level.org.hibernate=WARN
|
||||
logging.level.org.eclipse.jetty=WARN
|
||||
#logging.level.org.springframework.security.oauth2=DEBUG
|
||||
#logging.level.org.springframework.security=DEBUG
|
||||
#logging.level.org.springframework.security.oauth2=DEBUG
|
||||
#logging.level.org.opensaml=DEBUG
|
||||
#logging.level.stirling.software.proprietary.security=DEBUG
|
||||
logging.level.com.zaxxer.hikari=WARN
|
||||
|
||||
@@ -47,20 +47,23 @@ security:
|
||||
provider: google # set this to your OAuth Provider's name, e.g., 'google' or 'keycloak'
|
||||
saml2:
|
||||
enabled: false # Only enabled for paid enterprise clients (enterpriseEdition.enabled must be true)
|
||||
provider: "" # The name of your Provider
|
||||
enableSingleLogout: false # set to 'true' to enable Single Logout (SP-initiated SLO). Logs the user out from the IdP
|
||||
autoCreateUser: true # set to 'true' to allow auto-creation of non-existing users
|
||||
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
|
||||
registrationId: stirling # The name of your Service Provider (SP) app name. Should match the name in the path for your SSO & SLO URLs
|
||||
idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata # The uri for your Provider's metadata
|
||||
idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml # The URL for initiating SSO. Provided by your Provider
|
||||
idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml # The URL for initiating SLO. Provided by your Provider
|
||||
idpIssuer: "" # The ID of your Provider
|
||||
idpCert: classpath:okta.cert # The certificate your Provider will use to authenticate your app's SAML authentication requests. Provided by your Provider
|
||||
privateKey: classpath:saml-private-key.key # Your private key. Generated from your keypair
|
||||
spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair
|
||||
metadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata # RECOMMENDED: Your IdP's metadata URI. When provided, IdP config is auto-discovered.
|
||||
provider: # IdP manual configuration - use if metadataUri is not available
|
||||
name: "" # Display name for your IdP (optional)
|
||||
singleLoginUrl: "" # SSO URL
|
||||
singleLogoutUrl: "" # SLO URL
|
||||
entityId: "" # IdP Entity ID
|
||||
cert: classpath:okta.cert # IdP signing certificate
|
||||
sp: # Service Provider (your app) credentials for signing SAML requests. Generated from your keypair
|
||||
privateKey: classpath:saml-private-key.key # Your private key.
|
||||
cert: classpath:saml-public-cert.crt # Your signing certificate.
|
||||
# IMPORTANT: For SAML setup, download your SP metadata from the BACKEND URL: http://localhost:8080/saml2/service-provider-metadata/{registrationId}
|
||||
# Do NOT use the frontend dev server URL (localhost:5173) as it will generate incorrect ACS URLs. Always use the backend URL (localhost:8080) for SAML configuration.
|
||||
jwt: # This feature is currently under development and not yet fully supported. Do not use in production.
|
||||
jwt:
|
||||
persistence: true # Set to 'true' to enable JWT key store
|
||||
enableKeyRotation: true # Set to 'true' to enable key pair rotation
|
||||
enableKeyCleanup: true # Set to 'true' to enable key pair cleanup
|
||||
@@ -203,9 +206,7 @@ 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
|
||||
|
||||
+8
-16
@@ -85,12 +85,10 @@ class EditTableOfContentsControllerTest {
|
||||
when(mockOutlineItem.getNextSibling()).thenReturn(null);
|
||||
|
||||
// When
|
||||
ResponseEntity<List<Map<String, Object>>> response = editTableOfContentsController.extractBookmarks(mockFile);
|
||||
List<Map<String, Object>> result = editTableOfContentsController.extractBookmarks(mockFile);
|
||||
|
||||
// Then
|
||||
assertNotNull(response);
|
||||
assertNotNull(response.getBody());
|
||||
List<Map<String, Object>> result = response.getBody();
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
|
||||
Map<String, Object> bookmark = result.get(0);
|
||||
@@ -109,12 +107,10 @@ class EditTableOfContentsControllerTest {
|
||||
when(mockCatalog.getDocumentOutline()).thenReturn(null);
|
||||
|
||||
// When
|
||||
ResponseEntity<List<Map<String, Object>>> response = editTableOfContentsController.extractBookmarks(mockFile);
|
||||
List<Map<String, Object>> result = editTableOfContentsController.extractBookmarks(mockFile);
|
||||
|
||||
// Then
|
||||
assertNotNull(response);
|
||||
assertNotNull(response.getBody());
|
||||
List<Map<String, Object>> result = response.getBody();
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isEmpty());
|
||||
verify(mockDocument).close();
|
||||
}
|
||||
@@ -145,12 +141,10 @@ class EditTableOfContentsControllerTest {
|
||||
when(childItem.getNextSibling()).thenReturn(null);
|
||||
|
||||
// When
|
||||
ResponseEntity<List<Map<String, Object>>> response = editTableOfContentsController.extractBookmarks(mockFile);
|
||||
List<Map<String, Object>> result = editTableOfContentsController.extractBookmarks(mockFile);
|
||||
|
||||
// Then
|
||||
assertNotNull(response);
|
||||
assertNotNull(response.getBody());
|
||||
List<Map<String, Object>> result = response.getBody();
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
|
||||
Map<String, Object> parentBookmark = result.get(0);
|
||||
@@ -183,12 +177,10 @@ class EditTableOfContentsControllerTest {
|
||||
when(mockOutlineItem.getNextSibling()).thenReturn(null);
|
||||
|
||||
// When
|
||||
ResponseEntity<List<Map<String, Object>>> response = editTableOfContentsController.extractBookmarks(mockFile);
|
||||
List<Map<String, Object>> result = editTableOfContentsController.extractBookmarks(mockFile);
|
||||
|
||||
// Then
|
||||
assertNotNull(response);
|
||||
assertNotNull(response.getBody());
|
||||
List<Map<String, Object>> result = response.getBody();
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
|
||||
Map<String, Object> bookmark = result.get(0);
|
||||
|
||||
+4
-1
@@ -210,7 +210,10 @@ public class ProprietaryUIDataController {
|
||||
SAML2 saml2 = securityProps.getSaml2();
|
||||
// Only add SAML2 providers if loginMethod allows it
|
||||
if (securityProps.isSaml2Active() && applicationProperties.getPremium().isEnabled()) {
|
||||
String samlIdp = saml2.getProvider();
|
||||
String samlIdp =
|
||||
saml2.getProvider().getEntityId() != null
|
||||
? saml2.getProvider().getEntityId()
|
||||
: saml2.getIdpIssuer(); // legacy fallback
|
||||
String saml2AuthenticationPath = "/saml2/authenticate/" + saml2.getRegistrationId();
|
||||
|
||||
// For SAML, we need to use the backend URL directly, not a relative path
|
||||
|
||||
+153
-90
@@ -1,53 +1,58 @@
|
||||
package stirling.software.proprietary.security;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||
|
||||
import com.coveo.saml.SamlClient;
|
||||
import com.coveo.saml.SamlException;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.AppConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.OAUTH2;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.SAML2;
|
||||
import stirling.software.common.model.oauth2.KeycloakProvider;
|
||||
import stirling.software.common.util.RegexPatternUtils;
|
||||
import stirling.software.common.util.UrlUtils;
|
||||
import stirling.software.proprietary.audit.AuditEventType;
|
||||
import stirling.software.proprietary.audit.AuditLevel;
|
||||
import stirling.software.proprietary.audit.Audited;
|
||||
import stirling.software.proprietary.security.saml2.CertificateUtils;
|
||||
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||
import stirling.software.proprietary.security.service.JwtServiceInterface;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
|
||||
public static final String LOGOUT_PATH = "/login?logout=true";
|
||||
|
||||
private final ApplicationProperties.Security securityProperties;
|
||||
|
||||
private final AppConfig appConfig;
|
||||
|
||||
private final JwtServiceInterface jwtService;
|
||||
private final LogoutSuccessHandler samlLogoutHandler;
|
||||
|
||||
public CustomLogoutSuccessHandler(
|
||||
ApplicationProperties.Security securityProperties, JwtServiceInterface jwtService) {
|
||||
this(securityProperties, jwtService, null);
|
||||
}
|
||||
|
||||
public CustomLogoutSuccessHandler(
|
||||
ApplicationProperties.Security securityProperties,
|
||||
JwtServiceInterface jwtService,
|
||||
LogoutSuccessHandler samlLogoutHandler) {
|
||||
this.securityProperties = securityProperties;
|
||||
this.jwtService = jwtService;
|
||||
this.samlLogoutHandler = samlLogoutHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Audited(type = AuditEventType.USER_LOGOUT, level = AuditLevel.BASIC)
|
||||
@@ -56,13 +61,14 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
throws IOException {
|
||||
|
||||
if (!response.isCommitted()) {
|
||||
if (handleSamlLogout(request, response, authentication)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (authentication != null) {
|
||||
if (authentication instanceof Saml2Authentication samlAuthentication) {
|
||||
// Handle SAML2 logout redirection
|
||||
getRedirect_saml2(request, response, samlAuthentication);
|
||||
} else if (authentication instanceof OAuth2AuthenticationToken oAuthToken) {
|
||||
if (authentication instanceof OAuth2AuthenticationToken oAuthToken) {
|
||||
// Handle OAuth2 logout redirection
|
||||
getRedirect_oauth2(request, response, oAuthToken);
|
||||
getRedirectOauth2(request, response, oAuthToken);
|
||||
} else if (authentication instanceof UsernamePasswordAuthenticationToken) {
|
||||
// Handle Username/Password logout
|
||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||
@@ -74,71 +80,152 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||
}
|
||||
} else {
|
||||
if (jwtService != null) {
|
||||
String token = jwtService.extractToken(request);
|
||||
if (token != null && !token.isBlank()) {
|
||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Redirect to login page after logout
|
||||
String path = checkForErrors(request);
|
||||
getRedirectStrategy().sendRedirect(request, response, path);
|
||||
// Redirect to login page after logout (handles error parameters if present)
|
||||
String queryParams = checkForErrors(request);
|
||||
getRedirectStrategy().sendRedirect(request, response, "/login?" + queryParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect for SAML2 authentication logout
|
||||
private void getRedirect_saml2(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
Saml2Authentication samlAuthentication)
|
||||
/**
|
||||
* Handles SAML logout - either via IdP Single Logout (SLO) or local logout.
|
||||
*
|
||||
* @return true if this was a SAML user and logout was handled, false otherwise
|
||||
*/
|
||||
private boolean handleSamlLogout(
|
||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||
throws IOException {
|
||||
// Logout locally if this is a SAMLResponse from to /logout instead of /logout/saml2/slo
|
||||
String samlResponse = request.getParameter("SAMLResponse");
|
||||
if (samlResponse != null && !samlResponse.isBlank()) {
|
||||
if (samlResponse.contains("/saml2/slo")) {
|
||||
log.info(
|
||||
"Received SAML LogoutResponse at /logout endpoint, completing logout locally");
|
||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
SAML2 samlConf = securityProperties.getSaml2();
|
||||
String registrationId = samlConf.getRegistrationId();
|
||||
if (securityProperties.getSaml2().getEnableSingleLogout()) {
|
||||
log.info("SP-initiated SLO detected, logging out via IdP");
|
||||
|
||||
CustomSaml2AuthenticatedPrincipal principal =
|
||||
(CustomSaml2AuthenticatedPrincipal) samlAuthentication.getPrincipal();
|
||||
if (authentication instanceof Saml2Authentication samlAuthentication) {
|
||||
if (samlLogoutHandler != null) {
|
||||
try {
|
||||
samlLogoutHandler.onLogoutSuccess(request, response, samlAuthentication);
|
||||
} catch (Exception e) {
|
||||
log.error("SP-initiated SLO failed, falling back to local logout", e);
|
||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||
}
|
||||
} else {
|
||||
log.warn(
|
||||
"SAML SLO enabled but handler not configured, performing local logout only");
|
||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||
}
|
||||
|
||||
String nameIdValue = principal.name();
|
||||
return true;
|
||||
} else {
|
||||
// Reconstruct Saml2Authentication from JWT claims for SLO
|
||||
Optional<Saml2Authentication> reconstructedAuth =
|
||||
reconstructSaml2AuthenticationFromJwt(request);
|
||||
|
||||
if (reconstructedAuth.isPresent()) {
|
||||
Saml2Authentication samlAuth = reconstructedAuth.get();
|
||||
|
||||
if (samlLogoutHandler != null) {
|
||||
try {
|
||||
samlLogoutHandler.onLogoutSuccess(request, response, samlAuth);
|
||||
} catch (Exception e) {
|
||||
log.error("SP-initiated SLO failed, falling back to local logout", e);
|
||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||
}
|
||||
} else {
|
||||
log.warn(
|
||||
"SAML SLO enabled but handler not configured, performing local logout only");
|
||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstructs a Saml2Authentication from JWT claims for SAML Single Logout. This allows SLO to
|
||||
* work even with stateless JWT sessions by extracting the SAML attributes that were stored in
|
||||
* the JWT during initial authentication.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Optional<Saml2Authentication> reconstructSaml2AuthenticationFromJwt(
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
// Read certificate from the resource
|
||||
Resource certificateResource = samlConf.getSpCert();
|
||||
X509Certificate certificate = CertificateUtils.readCertificate(certificateResource);
|
||||
String token = jwtService.extractToken(request);
|
||||
if (token == null || token.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
List<X509Certificate> certificates = new ArrayList<>();
|
||||
certificates.add(certificate);
|
||||
Map<String, Object> claims = jwtService.extractClaims(token);
|
||||
Object authType = claims.get("authType");
|
||||
|
||||
// Construct URLs required for SAML configuration
|
||||
SamlClient samlClient = getSamlClient(registrationId, samlConf, certificates);
|
||||
if (authType == null || !"SAML2".equalsIgnoreCase(authType.toString())) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// Read private key for service provider
|
||||
Resource privateKeyResource = samlConf.getPrivateKey();
|
||||
RSAPrivateKey privateKey = CertificateUtils.readPrivateKey(privateKeyResource);
|
||||
// Extract SAML claims from JWT
|
||||
String username = (String) claims.get("sub");
|
||||
String nameId = (String) claims.get("samlNameId");
|
||||
String registrationId = (String) claims.get("samlRegistrationId");
|
||||
Object sessionIndexesObj = claims.get("samlSessionIndexes");
|
||||
|
||||
// Set service provider keys for the SamlClient
|
||||
samlClient.setSPKeys(certificate, privateKey);
|
||||
if (nameId == null || registrationId == null) {
|
||||
log.debug(
|
||||
"Missing required SAML claims for SLO reconstruction: nameId={}, registrationId={}",
|
||||
nameId,
|
||||
registrationId);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// Build relay state to return user to login page after IdP logout
|
||||
String relayState =
|
||||
UrlUtils.getOrigin(request) + request.getContextPath() + LOGOUT_PATH;
|
||||
List<String> sessionIndexes = Collections.emptyList();
|
||||
if (sessionIndexesObj instanceof List<?>) {
|
||||
sessionIndexes =
|
||||
((List<?>) sessionIndexesObj).stream().map(Object::toString).toList();
|
||||
}
|
||||
|
||||
// Redirect to identity provider for logout with relay state
|
||||
samlClient.redirectToIdentityProvider(response, relayState, nameIdValue);
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Error retrieving logout URL from Provider {} for user {}",
|
||||
samlConf.getProvider(),
|
||||
nameIdValue,
|
||||
e);
|
||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||
// Create principal with all SAML attributes needed for SLO
|
||||
CustomSaml2AuthenticatedPrincipal principal =
|
||||
new CustomSaml2AuthenticatedPrincipal(
|
||||
username,
|
||||
Collections.emptyMap(), // Attributes not needed for logout
|
||||
nameId,
|
||||
sessionIndexes,
|
||||
registrationId);
|
||||
|
||||
// Create Saml2Authentication with the reconstructed principal
|
||||
// The saml2Response parameter is not used by the logout handler, but constructor
|
||||
// requires non-empty value, so we provide a placeholder
|
||||
Saml2Authentication samlAuth =
|
||||
new Saml2Authentication(
|
||||
principal,
|
||||
"<!-- reconstructed for logout -->",
|
||||
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));
|
||||
|
||||
log.debug(
|
||||
"Reconstructed Saml2Authentication from JWT for user {} with registrationId {}",
|
||||
username,
|
||||
registrationId);
|
||||
return Optional.of(samlAuth);
|
||||
|
||||
} catch (Exception ex) {
|
||||
log.error("Unable to reconstruct Saml2Authentication from JWT", ex);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect for OAuth2 authentication logout
|
||||
private void getRedirect_oauth2(
|
||||
private void getRedirectOauth2(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
OAuth2AuthenticationToken oAuthToken)
|
||||
@@ -169,7 +256,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
logoutUrl +=
|
||||
"/protocol/openid-connect/logout"
|
||||
+ "?client_id="
|
||||
+ oauth.getClientId()
|
||||
+ keycloak.getClientId()
|
||||
+ "&post_logout_redirect_uri="
|
||||
+ response.encodeRedirectURL(redirectUrl);
|
||||
log.info("Redirecting to Keycloak logout URL: {}", logoutUrl);
|
||||
@@ -196,30 +283,6 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private SamlClient getSamlClient(
|
||||
String registrationId, SAML2 samlConf, List<X509Certificate> certificates)
|
||||
throws SamlException {
|
||||
String serverUrl = appConfig.getBackendUrl() + ":" + appConfig.getServerPort();
|
||||
|
||||
String relyingPartyIdentifier =
|
||||
serverUrl + "/saml2/service-provider-metadata/" + registrationId;
|
||||
|
||||
String assertionConsumerServiceUrl = serverUrl + "/login/saml2/sso/" + registrationId;
|
||||
|
||||
String idpSLOUrl = samlConf.getIdpSingleLogoutUrl();
|
||||
|
||||
String idpIssuer = samlConf.getIdpIssuer();
|
||||
|
||||
// Create SamlClient instance for SAML logout
|
||||
return new SamlClient(
|
||||
relyingPartyIdentifier,
|
||||
assertionConsumerServiceUrl,
|
||||
idpSLOUrl,
|
||||
idpIssuer,
|
||||
certificates,
|
||||
SamlClient.SamlIdpBinding.POST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles different error scenarios during logout. Will return a <code>String</code> containing
|
||||
* the error request parameter.
|
||||
|
||||
+48
-47
@@ -8,12 +8,13 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||
@@ -23,8 +24,12 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2RelyingPartyInitiatedLogoutSuccessHandler;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.logout.LogoutFilter;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
|
||||
@@ -34,7 +39,6 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.AppConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.RequestUriUtils;
|
||||
import stirling.software.proprietary.security.CustomAuthenticationFailureHandler;
|
||||
@@ -57,7 +61,6 @@ import stirling.software.proprietary.security.service.CustomUserDetailsService;
|
||||
import stirling.software.proprietary.security.service.JwtServiceInterface;
|
||||
import stirling.software.proprietary.security.service.LoginAttemptService;
|
||||
import stirling.software.proprietary.security.service.UserService;
|
||||
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@@ -73,12 +76,10 @@ public class SecurityConfiguration {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final ApplicationProperties.Security securityProperties;
|
||||
private final AppConfig appConfig;
|
||||
private final UserAuthenticationFilter userAuthenticationFilter;
|
||||
private final JwtServiceInterface jwtService;
|
||||
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
|
||||
private final LoginAttemptService loginAttemptService;
|
||||
private final SessionPersistentRegistry sessionRegistry;
|
||||
private final PersistentLoginRepository persistentLoginRepository;
|
||||
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
|
||||
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
|
||||
@@ -93,14 +94,12 @@ public class SecurityConfiguration {
|
||||
@Lazy UserService userService,
|
||||
@Qualifier("loginEnabled") boolean loginEnabledValue,
|
||||
@Qualifier("runningProOrHigher") boolean runningProOrHigher,
|
||||
AppConfig appConfig,
|
||||
ApplicationProperties applicationProperties,
|
||||
ApplicationProperties.Security securityProperties,
|
||||
UserAuthenticationFilter userAuthenticationFilter,
|
||||
JwtServiceInterface jwtService,
|
||||
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
|
||||
LoginAttemptService loginAttemptService,
|
||||
SessionPersistentRegistry sessionRegistry,
|
||||
@Autowired(required = false) GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper,
|
||||
@Autowired(required = false)
|
||||
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
|
||||
@@ -113,14 +112,12 @@ public class SecurityConfiguration {
|
||||
this.userService = userService;
|
||||
this.loginEnabledValue = loginEnabledValue;
|
||||
this.runningProOrHigher = runningProOrHigher;
|
||||
this.appConfig = appConfig;
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.securityProperties = securityProperties;
|
||||
this.userAuthenticationFilter = userAuthenticationFilter;
|
||||
this.jwtService = jwtService;
|
||||
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
|
||||
this.loginAttemptService = loginAttemptService;
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
this.persistentLoginRepository = persistentLoginRepository;
|
||||
this.oAuth2userAuthoritiesMapper = oAuth2userAuthoritiesMapper;
|
||||
this.saml2RelyingPartyRegistrations = saml2RelyingPartyRegistrations;
|
||||
@@ -186,46 +183,18 @@ public class SecurityConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
public SecurityFilterChain samlFilterChain(
|
||||
HttpSecurity http,
|
||||
@Lazy IPRateLimitingFilter rateLimitingFilter,
|
||||
@Lazy JwtAuthenticationFilter jwtAuthenticationFilter)
|
||||
throws Exception {
|
||||
http.securityMatcher("/saml2/**", "/login/saml2/**");
|
||||
|
||||
SessionCreationPolicy sessionPolicy =
|
||||
(securityProperties.isSaml2Active() && runningProOrHigher)
|
||||
? SessionCreationPolicy.IF_REQUIRED
|
||||
: SessionCreationPolicy.STATELESS;
|
||||
|
||||
return configureSecurity(http, rateLimitingFilter, jwtAuthenticationFilter, sessionPolicy);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(2)
|
||||
public SecurityFilterChain filterChain(
|
||||
HttpSecurity http,
|
||||
@Lazy IPRateLimitingFilter rateLimitingFilter,
|
||||
@Lazy JwtAuthenticationFilter jwtAuthenticationFilter)
|
||||
throws Exception {
|
||||
SessionCreationPolicy sessionPolicy = SessionCreationPolicy.STATELESS;
|
||||
return configureSecurity(http, rateLimitingFilter, jwtAuthenticationFilter, sessionPolicy);
|
||||
}
|
||||
|
||||
private SecurityFilterChain configureSecurity(
|
||||
HttpSecurity http,
|
||||
@Lazy IPRateLimitingFilter rateLimitingFilter,
|
||||
@Lazy JwtAuthenticationFilter jwtAuthenticationFilter,
|
||||
SessionCreationPolicy sessionPolicy)
|
||||
throws Exception {
|
||||
// Enable CORS only if we have configured origins
|
||||
CorsConfigurationSource corsSource = corsConfigurationSource();
|
||||
if (corsSource != null) {
|
||||
http.cors(cors -> cors.configurationSource(corsSource));
|
||||
} else {
|
||||
// Explicitly disable CORS when no origins are configured
|
||||
http.cors(cors -> cors.disable());
|
||||
http.cors(AbstractHttpConfigurer::disable);
|
||||
}
|
||||
|
||||
http.csrf(CsrfConfigurer::disable);
|
||||
@@ -256,13 +225,15 @@ public class SecurityConfiguration {
|
||||
|
||||
if (loginEnabledValue) {
|
||||
|
||||
http.addFilterBefore(
|
||||
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
.addFilterBefore(rateLimitingFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UserAuthenticationFilter.class);
|
||||
http.addFilterBefore(rateLimitingFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
.addFilterBefore(jwtAuthenticationFilter, LogoutFilter.class)
|
||||
.addFilterAfter(
|
||||
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
http.sessionManagement(
|
||||
sessionManagement -> sessionManagement.sessionCreationPolicy(sessionPolicy));
|
||||
sessionManagement ->
|
||||
sessionManagement.sessionCreationPolicy(
|
||||
SessionCreationPolicy.STATELESS));
|
||||
http.authenticationProvider(daoAuthenticationProvider());
|
||||
http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()));
|
||||
|
||||
@@ -277,17 +248,38 @@ public class SecurityConfiguration {
|
||||
return requestURI.startsWith(contextPath + "/api/");
|
||||
}));
|
||||
|
||||
// Create SAML logout handler if SAML SLO is enabled
|
||||
final LogoutSuccessHandler samlLogoutHandler;
|
||||
if (securityProperties.isSaml2Active()
|
||||
&& Boolean.TRUE.equals(securityProperties.getSaml2().getEnableSingleLogout())
|
||||
&& saml2RelyingPartyRegistrations != null) {
|
||||
log.info("Creating SAML2 SLO handler for SP-initiated logout");
|
||||
OpenSaml4LogoutRequestResolver logoutRequestResolver =
|
||||
new OpenSaml4LogoutRequestResolver(saml2RelyingPartyRegistrations);
|
||||
samlLogoutHandler =
|
||||
new Saml2RelyingPartyInitiatedLogoutSuccessHandler(logoutRequestResolver);
|
||||
} else {
|
||||
samlLogoutHandler = null;
|
||||
}
|
||||
|
||||
http.logout(
|
||||
logout ->
|
||||
// Require POST to prevent logout CSRF attacks
|
||||
logout.logoutRequestMatcher(
|
||||
PathPatternRequestMatcher.withDefaults()
|
||||
.matcher("/logout"))
|
||||
.matcher(HttpMethod.POST, "/logout"))
|
||||
.logoutSuccessHandler(
|
||||
new CustomLogoutSuccessHandler(
|
||||
securityProperties, appConfig, jwtService))
|
||||
securityProperties,
|
||||
jwtService,
|
||||
samlLogoutHandler))
|
||||
.clearAuthentication(true)
|
||||
.invalidateHttpSession(true)
|
||||
.deleteCookies("JSESSIONID", "remember-me", "stirling_jwt"));
|
||||
.deleteCookies(
|
||||
"JSESSIONID",
|
||||
"remember-me",
|
||||
"stirling_jwt",
|
||||
"stirling_logout_token"));
|
||||
http.rememberMe(
|
||||
rememberMeConfigurer -> // Use the configurator directly
|
||||
rememberMeConfigurer
|
||||
@@ -411,9 +403,18 @@ public class SecurityConfiguration {
|
||||
}
|
||||
})
|
||||
.saml2Metadata(metadata -> {});
|
||||
|
||||
// Configure SAML2 Single Logout if enabled
|
||||
// This sets up endpoints for:
|
||||
// - IdP-initiated logout: IdP sends LogoutRequest to /logout/saml2/slo
|
||||
// - SP-initiated logout response: IdP sends LogoutResponse to /logout/saml2/slo
|
||||
if (Boolean.TRUE.equals(securityProperties.getSaml2().getEnableSingleLogout())) {
|
||||
log.debug("SAML2 Single Logout (SLO) is enabled");
|
||||
http.saml2Logout(logout -> logout.logoutUrl("/logout/saml2/slo"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug("Login is not enabled.");
|
||||
log.info("Login is not enabled.");
|
||||
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||
}
|
||||
return http.build();
|
||||
|
||||
-55
@@ -186,13 +186,6 @@ public class AdminSettingsController {
|
||||
+ HtmlUtils.htmlEscape(key)));
|
||||
}
|
||||
|
||||
// Validate pipeline path settings
|
||||
String validationError = validatePipelinePathSetting(key, value);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", HtmlUtils.htmlEscape(validationError)));
|
||||
}
|
||||
|
||||
log.info("Admin updating setting: {} = {}", key, value);
|
||||
GeneralUtils.saveKeyToSettings(key, value);
|
||||
|
||||
@@ -649,54 +642,6 @@ public class AdminSettingsController {
|
||||
return true;
|
||||
}
|
||||
|
||||
private String validatePipelinePathSetting(String key, Object value) {
|
||||
// Validate pipeline path settings
|
||||
if (key.startsWith("system.customPaths.pipeline.watchedFoldersDirs")
|
||||
&& value instanceof java.util.List) {
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.List<String> paths = (java.util.List<String>) value;
|
||||
|
||||
// Check for empty or all-blank paths
|
||||
if (paths.isEmpty()) {
|
||||
return null; // Empty is OK, will use default
|
||||
}
|
||||
|
||||
// Validate each path
|
||||
java.util.Set<String> normalizedPaths = new java.util.HashSet<>();
|
||||
for (String path : paths) {
|
||||
if (path != null && !path.trim().isEmpty()) {
|
||||
try {
|
||||
java.nio.file.Path normalized =
|
||||
java.nio.file.Paths.get(path.trim()).toAbsolutePath().normalize();
|
||||
String normalizedStr = normalized.toString();
|
||||
|
||||
// Check for duplicates
|
||||
if (normalizedPaths.contains(normalizedStr)) {
|
||||
return "Duplicate path detected: " + path;
|
||||
}
|
||||
normalizedPaths.add(normalizedStr);
|
||||
} catch (java.nio.file.InvalidPathException e) {
|
||||
return "Invalid path: " + path + " - " + e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for overlapping paths
|
||||
java.util.List<String> pathList = new java.util.ArrayList<>(normalizedPaths);
|
||||
for (int i = 0; i < pathList.size(); i++) {
|
||||
java.nio.file.Path path1 = java.nio.file.Paths.get(pathList.get(i));
|
||||
for (int j = i + 1; j < pathList.size(); j++) {
|
||||
java.nio.file.Path path2 = java.nio.file.Paths.get(pathList.get(j));
|
||||
if (path1.startsWith(path2) || path2.startsWith(path1)) {
|
||||
return "Overlapping paths detected: " + path1 + " and " + path2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null; // Valid
|
||||
}
|
||||
|
||||
private Object getSettingByKey(String key) {
|
||||
if (key == null || key.trim().isEmpty()) {
|
||||
return null;
|
||||
|
||||
+1
-27
@@ -231,29 +231,6 @@ public class AuthController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout endpoint
|
||||
*
|
||||
* @param response HTTP response
|
||||
* @return Success message
|
||||
*/
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@PostMapping("/logout")
|
||||
public ResponseEntity<?> logout(HttpServletResponse response) {
|
||||
try {
|
||||
SecurityContextHolder.clearContext();
|
||||
|
||||
log.debug("User logged out successfully");
|
||||
|
||||
return ResponseEntity.ok(Map.of("message", "Logged out successfully"));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Logout error", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("error", "Internal server error"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh token
|
||||
*
|
||||
@@ -286,10 +263,7 @@ public class AuthController {
|
||||
|
||||
log.debug("Token refreshed for user: {}", username);
|
||||
|
||||
return ResponseEntity.ok(
|
||||
Map.of(
|
||||
"user", buildUserResponse(user),
|
||||
"session", Map.of("access_token", newToken, "expires_in", 3600)));
|
||||
return ResponseEntity.ok(Map.of("access_token", newToken, "expires_in", 3600));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Token refresh error", e);
|
||||
|
||||
+9
@@ -61,6 +61,11 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
String requestURI = request.getRequestURI();
|
||||
String contextPath = request.getContextPath();
|
||||
|
||||
if (isPublicAuthEndpoint(requestURI, contextPath)) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isStaticResource(contextPath, requestURI)) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
@@ -174,7 +179,11 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
userDetails, null, userDetails.getAuthorities());
|
||||
|
||||
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
log.debug("Setting authentication for user: {}", username);
|
||||
SecurityContextHolder.getContext().setAuthentication(authToken);
|
||||
log.debug(
|
||||
"Authentication set successfully: {}",
|
||||
SecurityContextHolder.getContext().getAuthentication());
|
||||
} else {
|
||||
throw new UsernameNotFoundException("User not found: " + username);
|
||||
}
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package stirling.software.proprietary.security.filter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
/**
|
||||
* Filter that redirects browser requests to /login to the frontend URL when configured. This is
|
||||
* needed for development mode where frontend runs on a different port, and for SAML logout which
|
||||
* redirects to /login?logout after SLO completes.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
@RequiredArgsConstructor
|
||||
public class LoginRedirectFilter extends OncePerRequestFilter {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
String path = request.getRequestURI();
|
||||
String frontendUrl = applicationProperties.getSystem().getFrontendUrl();
|
||||
|
||||
// Only process /login requests when frontendUrl is configured
|
||||
if (path.equals("/login") && frontendUrl != null && !frontendUrl.isBlank()) {
|
||||
// Check if this is a browser request (Accept: text/html) vs API request
|
||||
// (Accept: application/json)
|
||||
String acceptHeader = request.getHeader("Accept");
|
||||
boolean isBrowserRequest =
|
||||
acceptHeader != null
|
||||
&& acceptHeader.contains("text/html")
|
||||
&& !acceptHeader.contains("application/json");
|
||||
|
||||
if (isBrowserRequest) {
|
||||
// Preserve query parameters (e.g., ?logout=true, ?error=xxx)
|
||||
String queryString = request.getQueryString();
|
||||
String redirectUrl =
|
||||
frontendUrl + "/login" + (queryString != null ? "?" + queryString : "");
|
||||
log.debug("Redirecting browser request to frontend: {}", redirectUrl);
|
||||
response.sendRedirect(redirectUrl);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
+39
-3
@@ -4,6 +4,7 @@ import static stirling.software.common.util.RequestUriUtils.isPublicAuthEndpoint
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
@@ -32,6 +33,7 @@ import stirling.software.common.util.RequestUriUtils;
|
||||
import stirling.software.proprietary.security.model.ApiKeyAuthenticationToken;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||
import stirling.software.proprietary.security.service.JwtServiceInterface;
|
||||
import stirling.software.proprietary.security.service.UserService;
|
||||
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
||||
|
||||
@@ -43,16 +45,19 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
private final UserService userService;
|
||||
private final SessionPersistentRegistry sessionPersistentRegistry;
|
||||
private final boolean loginEnabledValue;
|
||||
private final JwtServiceInterface jwtService;
|
||||
|
||||
public UserAuthenticationFilter(
|
||||
@Lazy ApplicationProperties.Security securityProp,
|
||||
@Lazy UserService userService,
|
||||
SessionPersistentRegistry sessionPersistentRegistry,
|
||||
@Qualifier("loginEnabled") boolean loginEnabledValue) {
|
||||
@Qualifier("loginEnabled") boolean loginEnabledValue,
|
||||
@Lazy JwtServiceInterface jwtService) {
|
||||
this.securityProp = securityProp;
|
||||
this.userService = userService;
|
||||
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
||||
this.loginEnabledValue = loginEnabledValue;
|
||||
this.jwtService = jwtService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -135,7 +140,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
}
|
||||
|
||||
// Check if the authenticated user is disabled and invalidate their session if so
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
if (authentication.isAuthenticated()) {
|
||||
|
||||
UserLoginType loginMethod = UserLoginType.UNKNOWN;
|
||||
|
||||
@@ -146,7 +151,14 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
String username = null;
|
||||
if (principal instanceof UserDetails detailsUser) {
|
||||
username = detailsUser.getUsername();
|
||||
loginMethod = UserLoginType.USERDETAILS;
|
||||
loginMethod = resolveLoginTypeFromJwt(request);
|
||||
if (loginMethod == UserLoginType.SAML2USER) {
|
||||
SAML2 saml2 = securityProp.getSaml2();
|
||||
blockRegistration = saml2 != null && saml2.getBlockRegistration();
|
||||
} else if (loginMethod == UserLoginType.OAUTH2USER) {
|
||||
OAUTH2 oAuth = securityProp.getOauth2();
|
||||
blockRegistration = oAuth != null && oAuth.getBlockRegistration();
|
||||
}
|
||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||
username = oAuth2User.getName();
|
||||
loginMethod = UserLoginType.OAUTH2USER;
|
||||
@@ -241,6 +253,30 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private UserLoginType resolveLoginTypeFromJwt(HttpServletRequest request) {
|
||||
if (jwtService == null) {
|
||||
return UserLoginType.USERDETAILS;
|
||||
}
|
||||
try {
|
||||
String token = jwtService.extractToken(request);
|
||||
if (token != null && !token.isBlank()) {
|
||||
Map<String, Object> claims = jwtService.extractClaims(token);
|
||||
Object authType = claims.get("authType");
|
||||
if (authType != null) {
|
||||
String type = authType.toString().toUpperCase();
|
||||
if ("SAML2".equals(type)) {
|
||||
return UserLoginType.SAML2USER;
|
||||
} else if ("OAUTH2".equals(type)) {
|
||||
return UserLoginType.OAUTH2USER;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Unable to resolve login type from JWT claims", e);
|
||||
}
|
||||
return UserLoginType.USERDETAILS;
|
||||
}
|
||||
|
||||
private enum UserLoginType {
|
||||
USERDETAILS("UserDetails"),
|
||||
OAUTH2USER("OAuth2User"),
|
||||
|
||||
-5
@@ -53,9 +53,6 @@ public class OAuth2Configuration {
|
||||
ApplicationProperties applicationProperties, @Lazy UserService userService) {
|
||||
this.userService = userService;
|
||||
this.applicationProperties = applicationProperties;
|
||||
log.info(
|
||||
"OAuth2Configuration initialized - OAuth2 enabled: {}",
|
||||
applicationProperties.getSecurity().getOauth2().getEnabled());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -222,8 +219,6 @@ public class OAuth2Configuration {
|
||||
name,
|
||||
oauth.getIssuer(),
|
||||
REDIRECT_URI_PATH + name);
|
||||
} else {
|
||||
log.warn("OIDC OAuth2 provider validation failed - provider will not be registered");
|
||||
}
|
||||
|
||||
return isValid
|
||||
|
||||
+24
-1
@@ -12,9 +12,22 @@ public record CustomSaml2AuthenticatedPrincipal(
|
||||
String name,
|
||||
Map<String, List<Object>> attributes,
|
||||
String nameId,
|
||||
List<String> sessionIndexes)
|
||||
List<String> sessionIndexes,
|
||||
String relyingPartyRegistrationId)
|
||||
implements Saml2AuthenticatedPrincipal, Serializable {
|
||||
|
||||
/**
|
||||
* Constructor without relyingPartyRegistrationId for backwards compatibility. Sets
|
||||
* relyingPartyRegistrationId to null.
|
||||
*/
|
||||
public CustomSaml2AuthenticatedPrincipal(
|
||||
String name,
|
||||
Map<String, List<Object>> attributes,
|
||||
String nameId,
|
||||
List<String> sessionIndexes) {
|
||||
this(name, attributes, nameId, sessionIndexes, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
@@ -24,4 +37,14 @@ public record CustomSaml2AuthenticatedPrincipal(
|
||||
public Map<String, List<Object>> getAttributes() {
|
||||
return this.attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getSessionIndexes() {
|
||||
return this.sessionIndexes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRelyingPartyRegistrationId() {
|
||||
return this.relyingPartyRegistrationId;
|
||||
}
|
||||
}
|
||||
|
||||
+23
-8
@@ -7,6 +7,8 @@ import java.net.URI;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -33,6 +35,7 @@ import stirling.software.proprietary.audit.AuditEventType;
|
||||
import stirling.software.proprietary.audit.AuditLevel;
|
||||
import stirling.software.proprietary.audit.Audited;
|
||||
import stirling.software.proprietary.security.model.AuthenticationType;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.service.JwtServiceInterface;
|
||||
import stirling.software.proprietary.security.service.LoginAttemptService;
|
||||
import stirling.software.proprietary.security.service.UserService;
|
||||
@@ -70,8 +73,7 @@ public class CustomSaml2AuthenticationSuccessHandler
|
||||
|
||||
// Check if user is eligible for SAML (grandfathered or system has ENTERPRISE license)
|
||||
if (userExists) {
|
||||
stirling.software.proprietary.security.model.User user =
|
||||
userService.findByUsernameIgnoreCase(username).orElse(null);
|
||||
User user = userService.findByUsernameIgnoreCase(username).orElse(null);
|
||||
|
||||
if (user != null && !licenseSettingsService.isSamlEligible(user)) {
|
||||
// User is not grandfathered and no ENTERPRISE license - block SAML login
|
||||
@@ -172,7 +174,11 @@ public class CustomSaml2AuthenticationSuccessHandler
|
||||
|
||||
// Extract SSO provider information from SAML2 assertion
|
||||
String ssoProviderId = saml2Principal.nameId();
|
||||
String ssoProvider = "saml2"; // fixme
|
||||
String ssoProvider =
|
||||
(saml2Properties.getIdpIssuer() != null
|
||||
&& !saml2Properties.getIdpIssuer().isBlank())
|
||||
? saml2Properties.getIdpIssuer()
|
||||
: saml2Properties.getRegistrationId();
|
||||
|
||||
log.debug(
|
||||
"Processing SSO post-login for user: {} (Provider: {}, ProviderId: {})",
|
||||
@@ -188,12 +194,21 @@ public class CustomSaml2AuthenticationSuccessHandler
|
||||
SAML2);
|
||||
log.debug("Successfully processed authentication for user: {}", username);
|
||||
|
||||
// Generate JWT if v2 is enabled
|
||||
if (jwtService.isJwtEnabled()) {
|
||||
String jwt =
|
||||
jwtService.generateToken(
|
||||
authentication,
|
||||
Map.of("authType", AuthenticationType.SAML2));
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("authType", AuthenticationType.SAML2);
|
||||
claims.put("samlNameId", saml2Principal.nameId());
|
||||
List<String> sessionIndexes = saml2Principal.sessionIndexes();
|
||||
if (sessionIndexes != null && !sessionIndexes.isEmpty()) {
|
||||
claims.put("samlSessionIndexes", sessionIndexes);
|
||||
}
|
||||
if (ssoProvider != null) {
|
||||
claims.put("samlProvider", ssoProvider);
|
||||
}
|
||||
if (saml2Properties.getRegistrationId() != null) {
|
||||
claims.put("samlRegistrationId", saml2Properties.getRegistrationId());
|
||||
}
|
||||
String jwt = jwtService.generateToken(authentication, claims);
|
||||
|
||||
// Build context-aware redirect URL based on the original request
|
||||
String redirectUrl =
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package stirling.software.proprietary.security.saml2;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.security.service.JwtServiceInterface;
|
||||
|
||||
@Slf4j
|
||||
public class JwtSaml2AuthenticationRequestRepository
|
||||
implements Saml2AuthenticationRequestRepository<Saml2PostAuthenticationRequest> {
|
||||
private final Map<String, String> tokenStore;
|
||||
private final JwtServiceInterface jwtService;
|
||||
private final RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
|
||||
|
||||
private static final String SAML_REQUEST_TOKEN = "stirling_saml_request_token";
|
||||
|
||||
public JwtSaml2AuthenticationRequestRepository(
|
||||
Map<String, String> tokenStore,
|
||||
JwtServiceInterface jwtService,
|
||||
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
|
||||
this.tokenStore = tokenStore;
|
||||
this.jwtService = jwtService;
|
||||
this.relyingPartyRegistrationRepository = relyingPartyRegistrationRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveAuthenticationRequest(
|
||||
Saml2PostAuthenticationRequest authRequest,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response) {
|
||||
if (!jwtService.isJwtEnabled()) {
|
||||
log.debug("V2 is not enabled, skipping SAMLRequest token storage");
|
||||
return;
|
||||
}
|
||||
|
||||
if (authRequest == null) {
|
||||
removeAuthenticationRequest(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> claims = serializeSamlRequest(authRequest);
|
||||
String token = jwtService.generateToken("", claims);
|
||||
String relayState = authRequest.getRelayState();
|
||||
|
||||
tokenStore.put(relayState, token);
|
||||
request.setAttribute(SAML_REQUEST_TOKEN, relayState);
|
||||
response.addHeader(SAML_REQUEST_TOKEN, relayState);
|
||||
|
||||
log.debug("Saved SAMLRequest token with RelayState: {}", relayState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Saml2PostAuthenticationRequest loadAuthenticationRequest(HttpServletRequest request) {
|
||||
String token = extractTokenFromStore(request);
|
||||
|
||||
if (token == null) {
|
||||
log.debug("No SAMLResponse token found in RelayState");
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, Object> claims = jwtService.extractClaims(token);
|
||||
return deserializeSamlRequest(claims);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Saml2PostAuthenticationRequest removeAuthenticationRequest(
|
||||
HttpServletRequest request, HttpServletResponse response) {
|
||||
Saml2PostAuthenticationRequest authRequest = loadAuthenticationRequest(request);
|
||||
|
||||
String relayStateId = request.getParameter("RelayState");
|
||||
if (relayStateId != null) {
|
||||
tokenStore.remove(relayStateId);
|
||||
log.debug("Removed SAMLRequest token for RelayState ID: {}", relayStateId);
|
||||
}
|
||||
|
||||
return authRequest;
|
||||
}
|
||||
|
||||
private String extractTokenFromStore(HttpServletRequest request) {
|
||||
String authnRequestId = request.getParameter("RelayState");
|
||||
|
||||
if (authnRequestId != null && !authnRequestId.isEmpty()) {
|
||||
String token = tokenStore.get(authnRequestId);
|
||||
|
||||
if (token != null) {
|
||||
tokenStore.remove(authnRequestId);
|
||||
log.info("Retrieved SAMLRequest token for RelayState ID: {}", authnRequestId);
|
||||
return token;
|
||||
} else {
|
||||
log.warn("No SAMLRequest token found for RelayState ID: {}", authnRequestId);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Map<String, Object> serializeSamlRequest(Saml2PostAuthenticationRequest authRequest) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
|
||||
claims.put("id", authRequest.getId());
|
||||
claims.put("relyingPartyRegistrationId", authRequest.getRelyingPartyRegistrationId());
|
||||
claims.put("authenticationRequestUri", authRequest.getAuthenticationRequestUri());
|
||||
claims.put("samlRequest", authRequest.getSamlRequest());
|
||||
claims.put("relayState", authRequest.getRelayState());
|
||||
|
||||
return claims;
|
||||
}
|
||||
|
||||
private Saml2PostAuthenticationRequest deserializeSamlRequest(Map<String, Object> claims) {
|
||||
String relyingPartyRegistrationId = (String) claims.get("relyingPartyRegistrationId");
|
||||
RelyingPartyRegistration relyingPartyRegistration =
|
||||
relyingPartyRegistrationRepository.findByRegistrationId(relyingPartyRegistrationId);
|
||||
|
||||
if (relyingPartyRegistration == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Saml2PostAuthenticationRequest.withRelyingPartyRegistration(relyingPartyRegistration)
|
||||
.id((String) claims.get("id"))
|
||||
.authenticationRequestUri((String) claims.get("authenticationRequestUri"))
|
||||
.samlRequest((String) claims.get("samlRequest"))
|
||||
.relayState((String) claims.get("relayState"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+323
-39
@@ -1,8 +1,22 @@
|
||||
package stirling.software.proprietary.security.saml2;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.xml.XMLConstants;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
import org.opensaml.saml.saml2.core.AuthnRequest;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
@@ -11,11 +25,17 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
|
||||
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
@@ -24,6 +44,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.SAML2;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.proprietary.security.service.JwtServiceInterface;
|
||||
|
||||
@Configuration
|
||||
@Slf4j
|
||||
@@ -31,45 +53,34 @@ import stirling.software.common.model.ApplicationProperties.Security.SAML2;
|
||||
@RequiredArgsConstructor
|
||||
public class Saml2Configuration {
|
||||
|
||||
private static final String SAML_METADATA_NS = "urn:oasis:names:tc:SAML:2.0:metadata";
|
||||
private static final String XML_DSIG_NS = "http://www.w3.org/2000/09/xmldsig#";
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
|
||||
public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
|
||||
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
|
||||
Optional<IdpMetadataInfo> metadataInfo = loadIdpMetadata(samlConf);
|
||||
|
||||
log.info(
|
||||
"Initializing SAML2 configuration with registration ID: {}",
|
||||
samlConf.getRegistrationId());
|
||||
|
||||
// Load IdP certificate
|
||||
X509Certificate idpCert;
|
||||
try {
|
||||
Resource idpCertResource = samlConf.getIdpCert();
|
||||
log.info("Loading IdP certificate from: {}", idpCertResource.getDescription());
|
||||
if (!idpCertResource.exists()) {
|
||||
log.error(
|
||||
"SAML2 IdP certificate not found at: {}", idpCertResource.getDescription());
|
||||
throw new IllegalStateException(
|
||||
"SAML2 IdP certificate file does not exist: "
|
||||
+ idpCertResource.getDescription());
|
||||
}
|
||||
idpCert = CertificateUtils.readCertificate(idpCertResource);
|
||||
log.info(
|
||||
"Successfully loaded IdP certificate. Subject: {}",
|
||||
idpCert.getSubjectX500Principal().getName());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load SAML2 IdP certificate: {}", e.getMessage(), e);
|
||||
throw new IllegalStateException("Failed to load SAML2 IdP certificate", e);
|
||||
}
|
||||
// Load IdP certificate either from metadata or fallback resource
|
||||
X509Certificate idpCert =
|
||||
metadataInfo
|
||||
.map(IdpMetadataInfo::signingCertificate)
|
||||
.orElseGet(() -> loadIdpCertificateFromResource(samlConf));
|
||||
|
||||
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
|
||||
|
||||
// Load SP private key and certificate
|
||||
Resource privateKeyResource = samlConf.getPrivateKey();
|
||||
Resource certificateResource = samlConf.getSpCert();
|
||||
Resource privateKeyResource = samlConf.getSp().getPrivateKeyResource();
|
||||
Resource certificateResource = samlConf.getSp().getCertResource();
|
||||
|
||||
log.info("Loading SP private key from: {}", privateKeyResource.getDescription());
|
||||
log.debug("Loading SP private key from: {}", privateKeyResource.getDescription());
|
||||
if (!privateKeyResource.exists()) {
|
||||
log.error("SAML2 SP private key not found at: {}", privateKeyResource.getDescription());
|
||||
throw new IllegalStateException(
|
||||
@@ -77,7 +88,7 @@ public class Saml2Configuration {
|
||||
+ privateKeyResource.getDescription());
|
||||
}
|
||||
|
||||
log.info("Loading SP certificate from: {}", certificateResource.getDescription());
|
||||
log.debug("Loading SP certificate from: {}", certificateResource.getDescription());
|
||||
if (!certificateResource.exists()) {
|
||||
log.error(
|
||||
"SAML2 SP certificate not found at: {}", certificateResource.getDescription());
|
||||
@@ -98,6 +109,43 @@ public class Saml2Configuration {
|
||||
log.error("Failed to load SAML2 SP credentials: {}", e.getMessage(), e);
|
||||
throw new IllegalStateException("Failed to load SAML2 SP credentials", e);
|
||||
}
|
||||
// Apply metadata overrides - metadata takes precedence over manual config
|
||||
metadataInfo.ifPresent(info -> applyMetadataOverrides(samlConf, info));
|
||||
|
||||
// Get IdP configuration - prefer values from metadata, fall back to manual config
|
||||
String idpEntityId =
|
||||
metadataInfo
|
||||
.map(IdpMetadataInfo::entityId)
|
||||
.filter(id -> id != null && !id.isBlank())
|
||||
.orElseGet(() -> samlConf.getProvider().getEntityId());
|
||||
|
||||
String idpSingleLoginUrl =
|
||||
metadataInfo
|
||||
.map(IdpMetadataInfo::singleSignOnServiceUrl)
|
||||
.filter(url -> url != null && !url.isBlank())
|
||||
.orElseGet(() -> samlConf.getProvider().getSingleLoginUrl());
|
||||
|
||||
String idpSingleLogoutUrl =
|
||||
metadataInfo
|
||||
.map(IdpMetadataInfo::singleLogoutServiceUrl)
|
||||
.filter(url -> url != null && !url.isBlank())
|
||||
.orElseGet(() -> samlConf.getProvider().getSingleLogoutUrl());
|
||||
|
||||
// Validate required IdP configuration
|
||||
if (idpEntityId == null || idpEntityId.isBlank()) {
|
||||
throw new IllegalStateException(
|
||||
"SAML2 IdP Entity ID is required. Set security.saml2.entityId or provide security.saml2.metadataUri");
|
||||
}
|
||||
if (idpSingleLoginUrl == null || idpSingleLoginUrl.isBlank()) {
|
||||
throw new IllegalStateException(
|
||||
"SAML2 IdP Single Sign-On URL is required. Set security.saml2.provider.singleLoginUrl or provide security.saml2.metadataUri");
|
||||
}
|
||||
|
||||
log.info(
|
||||
"SAML2 IdP configuration: entityId={}, ssoUrl={}, sloUrl={}",
|
||||
idpEntityId,
|
||||
idpSingleLoginUrl,
|
||||
idpSingleLogoutUrl);
|
||||
|
||||
// Get backend URL from configuration (for SAML endpoints)
|
||||
String backendUrl = applicationProperties.getSystem().getBackendUrl();
|
||||
@@ -112,47 +160,57 @@ public class Saml2Configuration {
|
||||
String entityId =
|
||||
backendUrl + "/saml2/service-provider-metadata/" + samlConf.getRegistrationId();
|
||||
String acsLocation = backendUrl + "/login/saml2/sso/{registrationId}";
|
||||
String sloResponseLocation = backendUrl + "/login";
|
||||
// SP's Single Logout Service endpoint (where SP receives logout requests/responses from
|
||||
// IdP)
|
||||
String spSloLocation = backendUrl + "/logout/saml2/slo";
|
||||
|
||||
RelyingPartyRegistration rp =
|
||||
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
|
||||
.signingX509Credentials(c -> c.add(signingCredential))
|
||||
.entityId(entityId)
|
||||
.singleLogoutServiceBinding(Saml2MessageBinding.POST)
|
||||
.singleLogoutServiceLocation(samlConf.getIdpSingleLogoutUrl())
|
||||
.singleLogoutServiceResponseLocation(sloResponseLocation)
|
||||
.singleLogoutServiceLocation(spSloLocation)
|
||||
.singleLogoutServiceResponseLocation(spSloLocation)
|
||||
.assertionConsumerServiceBinding(Saml2MessageBinding.POST)
|
||||
.assertionConsumerServiceLocation(acsLocation)
|
||||
.authnRequestsSigned(true)
|
||||
.assertingPartyMetadata(
|
||||
metadata ->
|
||||
metadata.entityId(samlConf.getIdpIssuer())
|
||||
metadata.entityId(idpEntityId)
|
||||
.verificationX509Credentials(
|
||||
c -> c.add(verificationCredential))
|
||||
.singleSignOnServiceBinding(
|
||||
Saml2MessageBinding.POST)
|
||||
.singleSignOnServiceLocation(
|
||||
samlConf.getIdpSingleLoginUrl())
|
||||
.singleSignOnServiceLocation(idpSingleLoginUrl)
|
||||
.singleLogoutServiceBinding(
|
||||
Saml2MessageBinding.POST)
|
||||
.singleLogoutServiceLocation(
|
||||
samlConf.getIdpSingleLogoutUrl())
|
||||
.singleLogoutServiceResponseLocation(
|
||||
sloResponseLocation)
|
||||
.singleLogoutServiceLocation(idpSingleLogoutUrl)
|
||||
.wantAuthnRequestsSigned(true))
|
||||
.build();
|
||||
|
||||
log.info(
|
||||
"SAML2 configuration initialized successfully. Registration ID: {}, IdP: {}",
|
||||
samlConf.getRegistrationId(),
|
||||
samlConf.getIdpIssuer());
|
||||
idpEntityId);
|
||||
return new InMemoryRelyingPartyRegistrationRepository(rp);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||
public Saml2AuthenticationRequestRepository<Saml2PostAuthenticationRequest>
|
||||
saml2AuthenticationRequestRepository(
|
||||
JwtServiceInterface jwtService,
|
||||
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
|
||||
return new JwtSaml2AuthenticationRequestRepository(
|
||||
new ConcurrentHashMap<>(), jwtService, relyingPartyRegistrationRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
|
||||
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
|
||||
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository,
|
||||
Saml2AuthenticationRequestRepository<Saml2PostAuthenticationRequest>
|
||||
saml2AuthenticationRequestRepository) {
|
||||
OpenSaml4AuthenticationRequestResolver resolver =
|
||||
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
|
||||
|
||||
@@ -160,16 +218,242 @@ public class Saml2Configuration {
|
||||
customizer -> {
|
||||
HttpServletRequest request = customizer.getRequest();
|
||||
AuthnRequest authnRequest = customizer.getAuthnRequest();
|
||||
Saml2PostAuthenticationRequest saml2AuthenticationRequest =
|
||||
saml2AuthenticationRequestRepository.loadAuthenticationRequest(request);
|
||||
|
||||
// Generate a unique AuthnRequest ID for each SAML request
|
||||
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
|
||||
if (saml2AuthenticationRequest != null) {
|
||||
String sessionId = request.getSession(false).getId();
|
||||
|
||||
log.debug(
|
||||
"Retrieving SAML 2 authentication request ID from the current HTTP session {}",
|
||||
sessionId);
|
||||
|
||||
String authenticationRequestId = saml2AuthenticationRequest.getId();
|
||||
|
||||
if (!authenticationRequestId.isBlank()) {
|
||||
authnRequest.setID(authenticationRequestId);
|
||||
} else {
|
||||
log.warn(
|
||||
"No authentication request found for HTTP session {}. Generating new ID",
|
||||
sessionId);
|
||||
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
|
||||
}
|
||||
} else {
|
||||
log.debug("Generating new authentication request ID");
|
||||
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
|
||||
}
|
||||
logAuthnRequestDetails(authnRequest);
|
||||
logHttpRequestDetails(request);
|
||||
});
|
||||
return resolver;
|
||||
}
|
||||
|
||||
private X509Certificate loadIdpCertificateFromResource(SAML2 samlConf) {
|
||||
try {
|
||||
Resource idpCertResource = samlConf.getProvider().getCertResource();
|
||||
if (idpCertResource == null) {
|
||||
throw new IllegalStateException("SAML2 IdP certificate resource is not defined");
|
||||
}
|
||||
log.info("Loading IdP certificate from: {}", idpCertResource.getDescription());
|
||||
if (!idpCertResource.exists()) {
|
||||
throw new IllegalStateException(
|
||||
"SAML2 IdP certificate file does not exist: "
|
||||
+ idpCertResource.getDescription());
|
||||
}
|
||||
X509Certificate certificate = CertificateUtils.readCertificate(idpCertResource);
|
||||
log.info(
|
||||
"Successfully loaded IdP certificate. Subject: {}",
|
||||
certificate.getSubjectX500Principal().getName());
|
||||
return certificate;
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load SAML2 IdP certificate: {}", e.getMessage(), e);
|
||||
throw new IllegalStateException("Failed to load SAML2 IdP certificate", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyMetadataOverrides(SAML2 samlConf, IdpMetadataInfo metadataInfo) {
|
||||
log.info(
|
||||
"Applying IdP metadata overrides for registration: {}",
|
||||
samlConf.getRegistrationId());
|
||||
SAML2.Provider provider = samlConf.getProvider();
|
||||
overrideIfPresent(metadataInfo.entityId(), provider::setEntityId);
|
||||
overrideIfPresent(metadataInfo.singleSignOnServiceUrl(), provider::setSingleLoginUrl);
|
||||
overrideIfPresent(metadataInfo.singleLogoutServiceUrl(), provider::setSingleLogoutUrl);
|
||||
|
||||
// Persist discovered metadata values to settings.yml
|
||||
persistMetadataToSettings(metadataInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists IdP metadata discovered values to settings.yml. This ensures the discovered
|
||||
* configuration is saved for future reference and survives restarts even if the metadata
|
||||
* endpoint becomes unavailable.
|
||||
*/
|
||||
private void persistMetadataToSettings(IdpMetadataInfo metadataInfo) {
|
||||
log.info(
|
||||
"Migrating discovered IdP metadata to SAML configuration. Existing configuration will be overridden.");
|
||||
|
||||
try {
|
||||
boolean anyPersisted = false;
|
||||
|
||||
if (hasText(metadataInfo.entityId())) {
|
||||
GeneralUtils.saveKeyToSettings(
|
||||
"security.saml2.provider.entityId", metadataInfo.entityId());
|
||||
log.info(" -> Persisted provider.entityId: {}", metadataInfo.entityId());
|
||||
anyPersisted = true;
|
||||
}
|
||||
|
||||
if (hasText(metadataInfo.singleSignOnServiceUrl())) {
|
||||
GeneralUtils.saveKeyToSettings(
|
||||
"security.saml2.provider.singleLoginUrl",
|
||||
metadataInfo.singleSignOnServiceUrl());
|
||||
log.info(
|
||||
" -> Persisted provider.singleLoginUrl: {}",
|
||||
metadataInfo.singleSignOnServiceUrl());
|
||||
anyPersisted = true;
|
||||
}
|
||||
|
||||
if (hasText(metadataInfo.singleLogoutServiceUrl())) {
|
||||
GeneralUtils.saveKeyToSettings(
|
||||
"security.saml2.provider.singleLogoutUrl",
|
||||
metadataInfo.singleLogoutServiceUrl());
|
||||
log.info(
|
||||
" -> Persisted provider.singleLogoutUrl: {}",
|
||||
metadataInfo.singleLogoutServiceUrl());
|
||||
anyPersisted = true;
|
||||
}
|
||||
|
||||
if (anyPersisted) {
|
||||
log.info(
|
||||
"IdP metadata successfully persisted to settings.yml. These values will be used as fallback if metadataUri becomes unavailable.");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to persist IdP metadata to settings.yml: {}. SAML will still work but discovered values won't be saved.",
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<IdpMetadataInfo> loadIdpMetadata(SAML2 samlConf) {
|
||||
String metadataLocation = samlConf.getEffectiveMetadataUri();
|
||||
if (metadataLocation == null || metadataLocation.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try (InputStream metadataStream = samlConf.getMetadataUriAsStream()) {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
factory.setNamespaceAware(true);
|
||||
|
||||
// XXE prevention - disable all external entities and DTD processing
|
||||
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
|
||||
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", 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.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
|
||||
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
|
||||
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
Document doc = builder.parse(metadataStream);
|
||||
|
||||
Element entityDescriptor = doc.getDocumentElement();
|
||||
if (entityDescriptor == null) {
|
||||
log.warn("No EntityDescriptor found in SAML metadata: {}", metadataLocation);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
String entityId = entityDescriptor.getAttribute("entityID");
|
||||
NodeList idpDescriptors =
|
||||
entityDescriptor.getElementsByTagNameNS(SAML_METADATA_NS, "IDPSSODescriptor");
|
||||
if (idpDescriptors.getLength() == 0) {
|
||||
log.warn("No IDPSSODescriptor found in SAML metadata: {}", metadataLocation);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Element idpDescriptor = (Element) idpDescriptors.item(0);
|
||||
String ssoUrl = extractServiceLocation(idpDescriptor, "SingleSignOnService");
|
||||
String sloUrl = extractServiceLocation(idpDescriptor, "SingleLogoutService");
|
||||
X509Certificate signingCert = extractSigningCertificate(idpDescriptor);
|
||||
|
||||
log.info("Loaded IdP metadata from: {}", metadataLocation);
|
||||
return Optional.of(new IdpMetadataInfo(entityId, ssoUrl, sloUrl, signingCert));
|
||||
} catch (IOException
|
||||
| ParserConfigurationException
|
||||
| SAXException
|
||||
| CertificateException e) {
|
||||
log.warn("Failed to parse SAML metadata from {}: {}", metadataLocation, e.getMessage());
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private String extractServiceLocation(Element descriptor, String tagName) {
|
||||
NodeList services = descriptor.getElementsByTagNameNS(SAML_METADATA_NS, tagName);
|
||||
String fallback = null;
|
||||
for (int i = 0; i < services.getLength(); i++) {
|
||||
Element service = (Element) services.item(i);
|
||||
String location = service.getAttribute("Location");
|
||||
String binding = service.getAttribute("Binding");
|
||||
if (!hasText(location)) {
|
||||
continue;
|
||||
}
|
||||
if (Saml2MessageBinding.POST.getUrn().equals(binding)) {
|
||||
return location;
|
||||
}
|
||||
if (fallback == null) {
|
||||
fallback = location;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private X509Certificate extractSigningCertificate(Element descriptor)
|
||||
throws CertificateException {
|
||||
NodeList keyDescriptors =
|
||||
descriptor.getElementsByTagNameNS(SAML_METADATA_NS, "KeyDescriptor");
|
||||
for (int i = 0; i < keyDescriptors.getLength(); i++) {
|
||||
Element keyDescriptor = (Element) keyDescriptors.item(i);
|
||||
String use = keyDescriptor.getAttribute("use");
|
||||
if (hasText(use) && !"signing".equalsIgnoreCase(use)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NodeList certificateNodes =
|
||||
keyDescriptor.getElementsByTagNameNS(XML_DSIG_NS, "X509Certificate");
|
||||
if (certificateNodes.getLength() == 0) {
|
||||
continue;
|
||||
}
|
||||
String certificateValue = certificateNodes.item(0).getTextContent();
|
||||
if (!hasText(certificateValue)) {
|
||||
continue;
|
||||
}
|
||||
byte[] decoded =
|
||||
Base64.getMimeDecoder().decode(certificateValue.replaceAll("\\s+", ""));
|
||||
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
|
||||
return (X509Certificate)
|
||||
certificateFactory.generateCertificate(new ByteArrayInputStream(decoded));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean hasText(String value) {
|
||||
return value != null && !value.trim().isEmpty();
|
||||
}
|
||||
|
||||
private void overrideIfPresent(String value, Consumer<String> setter) {
|
||||
if (hasText(value)) {
|
||||
setter.accept(value.trim());
|
||||
}
|
||||
}
|
||||
|
||||
private record IdpMetadataInfo(
|
||||
String entityId,
|
||||
String singleSignOnServiceUrl,
|
||||
String singleLogoutServiceUrl,
|
||||
X509Certificate signingCertificate) {}
|
||||
|
||||
private static void logAuthnRequestDetails(AuthnRequest authnRequest) {
|
||||
String message =
|
||||
"""
|
||||
|
||||
+18
@@ -258,6 +258,24 @@ public class JwtService implements JwtServiceInterface {
|
||||
return token;
|
||||
}
|
||||
|
||||
// Check for logout cookie (set by frontend before redirecting to /logout for SAML SLO)
|
||||
if (request.getCookies() != null) {
|
||||
for (jakarta.servlet.http.Cookie cookie : request.getCookies()) {
|
||||
if ("stirling_logout_token".equals(cookie.getName())) {
|
||||
String value = cookie.getValue();
|
||||
if (value != null && !value.isBlank()) {
|
||||
try {
|
||||
return java.net.URLDecoder.decode(
|
||||
value, java.nio.charset.StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
log.debug("Failed to decode logout token cookie", e);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -67,7 +67,6 @@ public class UserService implements UserServiceInterface {
|
||||
|
||||
private final ApplicationProperties.Security.OAUTH2 oAuth2;
|
||||
|
||||
@Transactional
|
||||
public void processSSOPostLogin(
|
||||
String username,
|
||||
String ssoProviderId,
|
||||
|
||||
+37
-7
@@ -6,9 +6,9 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
@@ -26,17 +26,24 @@ class CustomLogoutSuccessHandlerTest {
|
||||
|
||||
@Mock private JwtServiceInterface jwtService;
|
||||
|
||||
@InjectMocks private CustomLogoutSuccessHandler customLogoutSuccessHandler;
|
||||
@Mock private ApplicationProperties.Security.SAML2 saml2;
|
||||
|
||||
private CustomLogoutSuccessHandler customLogoutSuccessHandler;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
customLogoutSuccessHandler = new CustomLogoutSuccessHandler(securityProperties, jwtService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccessfulLogout() throws IOException {
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
String token = "token";
|
||||
String logoutPath = "/login?logout=true";
|
||||
|
||||
when(response.isCommitted()).thenReturn(false);
|
||||
when(jwtService.extractToken(request)).thenReturn(token);
|
||||
when(securityProperties.getSaml2()).thenReturn(saml2);
|
||||
when(saml2.getEnableSingleLogout()).thenReturn(false);
|
||||
when(request.getContextPath()).thenReturn("");
|
||||
when(response.encodeRedirectURL(logoutPath)).thenReturn(logoutPath);
|
||||
|
||||
@@ -50,10 +57,10 @@ class CustomLogoutSuccessHandlerTest {
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
String logoutPath = "/login?logout=true";
|
||||
String token = "token";
|
||||
|
||||
when(response.isCommitted()).thenReturn(false);
|
||||
when(jwtService.extractToken(request)).thenReturn(token);
|
||||
when(securityProperties.getSaml2()).thenReturn(saml2);
|
||||
when(saml2.getEnableSingleLogout()).thenReturn(false);
|
||||
when(request.getContextPath()).thenReturn("");
|
||||
when(response.encodeRedirectURL(logoutPath)).thenReturn(logoutPath);
|
||||
|
||||
@@ -71,6 +78,9 @@ class CustomLogoutSuccessHandlerTest {
|
||||
mock(ApplicationProperties.Security.OAUTH2.class);
|
||||
|
||||
when(response.isCommitted()).thenReturn(false);
|
||||
when(request.getParameter("SAMLResponse")).thenReturn(null);
|
||||
when(securityProperties.getSaml2()).thenReturn(saml2);
|
||||
when(saml2.getEnableSingleLogout()).thenReturn(false);
|
||||
when(request.getParameter("oAuth2AuthenticationErrorWeb")).thenReturn(null);
|
||||
when(request.getParameter("errorOAuth")).thenReturn(null);
|
||||
when(request.getScheme()).thenReturn("http");
|
||||
@@ -96,6 +106,9 @@ class CustomLogoutSuccessHandlerTest {
|
||||
mock(ApplicationProperties.Security.OAUTH2.class);
|
||||
|
||||
when(response.isCommitted()).thenReturn(false);
|
||||
when(request.getParameter("SAMLResponse")).thenReturn(null);
|
||||
when(securityProperties.getSaml2()).thenReturn(saml2);
|
||||
when(saml2.getEnableSingleLogout()).thenReturn(false);
|
||||
when(request.getParameter("oAuth2AuthenticationErrorWeb")).thenReturn(null);
|
||||
when(request.getParameter("errorOAuth")).thenReturn(null);
|
||||
when(request.getParameter("oAuth2AutoCreateDisabled")).thenReturn(null);
|
||||
@@ -128,6 +141,9 @@ class CustomLogoutSuccessHandlerTest {
|
||||
mock(ApplicationProperties.Security.OAUTH2.class);
|
||||
|
||||
when(response.isCommitted()).thenReturn(false);
|
||||
when(request.getParameter("SAMLResponse")).thenReturn(null);
|
||||
when(securityProperties.getSaml2()).thenReturn(saml2);
|
||||
when(saml2.getEnableSingleLogout()).thenReturn(false);
|
||||
when(request.getParameter(error)).thenReturn("true");
|
||||
when(request.getScheme()).thenReturn("http");
|
||||
when(request.getServerName()).thenReturn("localhost");
|
||||
@@ -152,6 +168,9 @@ class CustomLogoutSuccessHandlerTest {
|
||||
mock(ApplicationProperties.Security.OAUTH2.class);
|
||||
|
||||
when(response.isCommitted()).thenReturn(false);
|
||||
when(request.getParameter("SAMLResponse")).thenReturn(null);
|
||||
when(securityProperties.getSaml2()).thenReturn(saml2);
|
||||
when(saml2.getEnableSingleLogout()).thenReturn(false);
|
||||
when(request.getParameter("oAuth2AuthenticationErrorWeb")).thenReturn(null);
|
||||
when(request.getParameter("errorOAuth")).thenReturn("!!!" + error + "!!!");
|
||||
when(request.getScheme()).thenReturn("http");
|
||||
@@ -177,10 +196,12 @@ class CustomLogoutSuccessHandlerTest {
|
||||
mock(ApplicationProperties.Security.OAUTH2.class);
|
||||
|
||||
when(response.isCommitted()).thenReturn(false);
|
||||
when(request.getParameter("SAMLResponse")).thenReturn(null);
|
||||
when(securityProperties.getSaml2()).thenReturn(saml2);
|
||||
when(saml2.getEnableSingleLogout()).thenReturn(false);
|
||||
when(request.getParameter("oAuth2AuthenticationErrorWeb")).thenReturn(null);
|
||||
when(request.getParameter("errorOAuth")).thenReturn(null);
|
||||
when(request.getParameter(error)).thenReturn("true");
|
||||
when(request.getContextPath()).thenReturn(url);
|
||||
when(request.getScheme()).thenReturn("http");
|
||||
when(request.getServerName()).thenReturn("localhost");
|
||||
when(request.getServerPort()).thenReturn(8080);
|
||||
@@ -204,6 +225,9 @@ class CustomLogoutSuccessHandlerTest {
|
||||
mock(ApplicationProperties.Security.OAUTH2.class);
|
||||
|
||||
when(response.isCommitted()).thenReturn(false);
|
||||
when(request.getParameter("SAMLResponse")).thenReturn(null);
|
||||
when(securityProperties.getSaml2()).thenReturn(saml2);
|
||||
when(saml2.getEnableSingleLogout()).thenReturn(false);
|
||||
when(request.getParameter("oAuth2AuthenticationErrorWeb")).thenReturn(null);
|
||||
when(request.getParameter("errorOAuth")).thenReturn(null);
|
||||
when(request.getParameter("oAuth2AutoCreateDisabled")).thenReturn(null);
|
||||
@@ -236,6 +260,9 @@ class CustomLogoutSuccessHandlerTest {
|
||||
mock(ApplicationProperties.Security.OAUTH2.class);
|
||||
|
||||
when(response.isCommitted()).thenReturn(false);
|
||||
when(request.getParameter("SAMLResponse")).thenReturn(null);
|
||||
when(securityProperties.getSaml2()).thenReturn(saml2);
|
||||
when(saml2.getEnableSingleLogout()).thenReturn(false);
|
||||
when(request.getParameter("oAuth2AuthenticationErrorWeb")).thenReturn(null);
|
||||
when(request.getParameter("errorOAuth")).thenReturn(null);
|
||||
when(request.getParameter("oAuth2AutoCreateDisabled")).thenReturn(null);
|
||||
@@ -269,6 +296,9 @@ class CustomLogoutSuccessHandlerTest {
|
||||
mock(ApplicationProperties.Security.OAUTH2.class);
|
||||
|
||||
when(response.isCommitted()).thenReturn(false);
|
||||
when(request.getParameter("SAMLResponse")).thenReturn(null);
|
||||
when(securityProperties.getSaml2()).thenReturn(saml2);
|
||||
when(saml2.getEnableSingleLogout()).thenReturn(false);
|
||||
when(request.getParameter("oAuth2AuthenticationErrorWeb")).thenReturn(null);
|
||||
when(request.getParameter("errorOAuth")).thenReturn(null);
|
||||
when(request.getParameter("oAuth2AutoCreateDisabled")).thenReturn(null);
|
||||
|
||||
+2
-3
@@ -182,9 +182,8 @@ class AuthControllerLoginTest {
|
||||
|
||||
mockMvc.perform(post("/api/v1/auth/refresh"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.user").exists())
|
||||
.andExpect(jsonPath("$.session.access_token").value("new-token"))
|
||||
.andExpect(jsonPath("$.session.expires_in").value(3600));
|
||||
.andExpect(jsonPath("$.access_token").value("new-token"))
|
||||
.andExpect(jsonPath("$.expires_in").value(3600));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
package stirling.software.proprietary.security.saml2;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.NullAndEmptySource;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
|
||||
import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadata;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import stirling.software.proprietary.security.service.JwtServiceInterface;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class JwtSaml2AuthenticationRequestRepositoryTest {
|
||||
|
||||
private static final String SAML_REQUEST_TOKEN = "stirling_saml_request_token";
|
||||
|
||||
private Map<String, String> tokenStore;
|
||||
|
||||
@Mock private JwtServiceInterface jwtService;
|
||||
|
||||
@Mock private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
|
||||
|
||||
private JwtSaml2AuthenticationRequestRepository jwtSaml2AuthenticationRequestRepository;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
tokenStore = new ConcurrentHashMap<>();
|
||||
jwtSaml2AuthenticationRequestRepository =
|
||||
new JwtSaml2AuthenticationRequestRepository(
|
||||
tokenStore, jwtService, relyingPartyRegistrationRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveAuthenticationRequest() {
|
||||
var authRequest = mock(Saml2PostAuthenticationRequest.class);
|
||||
var request = mock(MockHttpServletRequest.class);
|
||||
var response = mock(MockHttpServletResponse.class);
|
||||
String token = "testToken";
|
||||
String id = "testId";
|
||||
String relayState = "testRelayState";
|
||||
String authnRequestUri = "example.com/authnRequest";
|
||||
String samlRequest = "testSamlRequest";
|
||||
String relyingPartyRegistrationId = "stirling-pdf";
|
||||
|
||||
when(jwtService.isJwtEnabled()).thenReturn(true);
|
||||
when(authRequest.getRelayState()).thenReturn(relayState);
|
||||
when(authRequest.getId()).thenReturn(id);
|
||||
when(authRequest.getAuthenticationRequestUri()).thenReturn(authnRequestUri);
|
||||
when(authRequest.getSamlRequest()).thenReturn(samlRequest);
|
||||
when(authRequest.getRelyingPartyRegistrationId()).thenReturn(relyingPartyRegistrationId);
|
||||
when(jwtService.generateToken(eq(""), anyMap())).thenReturn(token);
|
||||
|
||||
jwtSaml2AuthenticationRequestRepository.saveAuthenticationRequest(
|
||||
authRequest, request, response);
|
||||
|
||||
verify(request).setAttribute(SAML_REQUEST_TOKEN, relayState);
|
||||
verify(response).addHeader(SAML_REQUEST_TOKEN, relayState);
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveAuthenticationRequestWithNullRequest() {
|
||||
var request = mock(MockHttpServletRequest.class);
|
||||
var response = mock(MockHttpServletResponse.class);
|
||||
|
||||
jwtSaml2AuthenticationRequestRepository.saveAuthenticationRequest(null, request, response);
|
||||
|
||||
assertTrue(tokenStore.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadAuthenticationRequest() {
|
||||
var request = mock(MockHttpServletRequest.class);
|
||||
var relyingPartyRegistration = mock(RelyingPartyRegistration.class);
|
||||
var assertingPartyMetadata = mock(AssertingPartyMetadata.class);
|
||||
String relayState = "testRelayState";
|
||||
String token = "testToken";
|
||||
Map<String, Object> claims =
|
||||
Map.of(
|
||||
"id", "testId",
|
||||
"relyingPartyRegistrationId", "stirling-pdf",
|
||||
"authenticationRequestUri", "example.com/authnRequest",
|
||||
"samlRequest", "testSamlRequest",
|
||||
"relayState", relayState);
|
||||
|
||||
when(request.getParameter("RelayState")).thenReturn(relayState);
|
||||
when(jwtService.extractClaims(token)).thenReturn(claims);
|
||||
when(relyingPartyRegistrationRepository.findByRegistrationId("stirling-pdf"))
|
||||
.thenReturn(relyingPartyRegistration);
|
||||
when(relyingPartyRegistration.getRegistrationId()).thenReturn("stirling-pdf");
|
||||
when(relyingPartyRegistration.getAssertingPartyMetadata())
|
||||
.thenReturn(assertingPartyMetadata);
|
||||
when(assertingPartyMetadata.getSingleSignOnServiceLocation())
|
||||
.thenReturn("https://example.com/sso");
|
||||
tokenStore.put(relayState, token);
|
||||
|
||||
var result = jwtSaml2AuthenticationRequestRepository.loadAuthenticationRequest(request);
|
||||
|
||||
assertNotNull(result);
|
||||
assertFalse(tokenStore.containsKey(relayState));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@NullAndEmptySource
|
||||
void loadAuthenticationRequestWithInvalidRelayState(String relayState) {
|
||||
var request = mock(MockHttpServletRequest.class);
|
||||
when(request.getParameter("RelayState")).thenReturn(relayState);
|
||||
|
||||
var result = jwtSaml2AuthenticationRequestRepository.loadAuthenticationRequest(request);
|
||||
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadAuthenticationRequestWithNonExistentToken() {
|
||||
var request = mock(MockHttpServletRequest.class);
|
||||
when(request.getParameter("RelayState")).thenReturn("nonExistentRelayState");
|
||||
|
||||
var result = jwtSaml2AuthenticationRequestRepository.loadAuthenticationRequest(request);
|
||||
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadAuthenticationRequestWithNullRelyingPartyRegistration() {
|
||||
var request = mock(MockHttpServletRequest.class);
|
||||
String relayState = "testRelayState";
|
||||
String token = "testToken";
|
||||
Map<String, Object> claims =
|
||||
Map.of(
|
||||
"id", "testId",
|
||||
"relyingPartyRegistrationId", "stirling-pdf",
|
||||
"authenticationRequestUri", "example.com/authnRequest",
|
||||
"samlRequest", "testSamlRequest",
|
||||
"relayState", relayState);
|
||||
|
||||
when(request.getParameter("RelayState")).thenReturn(relayState);
|
||||
when(jwtService.extractClaims(token)).thenReturn(claims);
|
||||
when(relyingPartyRegistrationRepository.findByRegistrationId("stirling-pdf"))
|
||||
.thenReturn(null);
|
||||
tokenStore.put(relayState, token);
|
||||
|
||||
var result = jwtSaml2AuthenticationRequestRepository.loadAuthenticationRequest(request);
|
||||
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeAuthenticationRequest() {
|
||||
var request = mock(HttpServletRequest.class);
|
||||
var response = mock(HttpServletResponse.class);
|
||||
var relyingPartyRegistration = mock(RelyingPartyRegistration.class);
|
||||
var assertingPartyMetadata = mock(AssertingPartyMetadata.class);
|
||||
String relayState = "testRelayState";
|
||||
String token = "testToken";
|
||||
Map<String, Object> claims =
|
||||
Map.of(
|
||||
"id", "testId",
|
||||
"relyingPartyRegistrationId", "stirling-pdf",
|
||||
"authenticationRequestUri", "example.com/authnRequest",
|
||||
"samlRequest", "testSamlRequest",
|
||||
"relayState", relayState);
|
||||
|
||||
when(request.getParameter("RelayState")).thenReturn(relayState);
|
||||
when(jwtService.extractClaims(token)).thenReturn(claims);
|
||||
when(relyingPartyRegistrationRepository.findByRegistrationId("stirling-pdf"))
|
||||
.thenReturn(relyingPartyRegistration);
|
||||
when(relyingPartyRegistration.getRegistrationId()).thenReturn("stirling-pdf");
|
||||
when(relyingPartyRegistration.getAssertingPartyMetadata())
|
||||
.thenReturn(assertingPartyMetadata);
|
||||
when(assertingPartyMetadata.getSingleSignOnServiceLocation())
|
||||
.thenReturn("https://example.com/sso");
|
||||
tokenStore.put(relayState, token);
|
||||
|
||||
var result =
|
||||
jwtSaml2AuthenticationRequestRepository.removeAuthenticationRequest(
|
||||
request, response);
|
||||
|
||||
assertNotNull(result);
|
||||
assertFalse(tokenStore.containsKey(relayState));
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeAuthenticationRequestWithNullRelayState() {
|
||||
var request = mock(HttpServletRequest.class);
|
||||
var response = mock(HttpServletResponse.class);
|
||||
when(request.getParameter("RelayState")).thenReturn(null);
|
||||
|
||||
var result =
|
||||
jwtSaml2AuthenticationRequestRepository.removeAuthenticationRequest(
|
||||
request, response);
|
||||
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeAuthenticationRequestWithNonExistentToken() {
|
||||
var request = mock(HttpServletRequest.class);
|
||||
var response = mock(HttpServletResponse.class);
|
||||
when(request.getParameter("RelayState")).thenReturn("nonExistentRelayState");
|
||||
|
||||
var result =
|
||||
jwtSaml2AuthenticationRequestRepository.removeAuthenticationRequest(
|
||||
request, response);
|
||||
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeAuthenticationRequestWithOnlyRelayState() {
|
||||
var request = mock(HttpServletRequest.class);
|
||||
var response = mock(HttpServletResponse.class);
|
||||
String relayState = "testRelayState";
|
||||
|
||||
when(request.getParameter("RelayState")).thenReturn(relayState);
|
||||
|
||||
var result =
|
||||
jwtSaml2AuthenticationRequestRepository.removeAuthenticationRequest(
|
||||
request, response);
|
||||
|
||||
assertNull(result);
|
||||
assertFalse(tokenStore.containsKey(relayState));
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -67,7 +67,7 @@ springBoot {
|
||||
|
||||
allprojects {
|
||||
group = 'stirling.software'
|
||||
version = '2.4.3'
|
||||
version = '2.4.2'
|
||||
|
||||
configurations.configureEach {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
|
||||
Generated
+916
-595
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0.69 5.4 400 390.62">
|
||||
<g clip-path="url(#clip0_10641_164)">
|
||||
<path d="M0.685165 65.8523C0.685165 14.7648 67.4855 -13.2029 111.616 19.4083L207.551 90.2535L288.514 44.373C331.305 21.0394 399.874 37.7748 400.685 101.559L400.685 336.622C400.685 387.71 335.845 414.343 291.715 381.732L194.031 309.34L112.973 355.23C69.7783 378.784 0.752625 360.714 0.752625 298.637C0.752654 224.124 0.685165 252.734 0.685165 65.8523Z" fill="url(#paint0_linear_10641_164)"/>
|
||||
<path d="M0.685165 65.8523C0.685165 14.7648 67.4855 -13.2029 111.616 19.4083L207.551 90.2535L288.514 44.373C331.305 21.0394 399.874 37.7748 400.685 101.559L400.685 336.622C400.685 387.71 335.845 414.343 291.715 381.732L194.031 309.34L112.973 355.23C69.7783 378.784 0.752625 360.714 0.752625 298.637C0.752654 224.124 0.685165 252.734 0.685165 65.8523Z" fill="url(#paint1_radial_10641_164)"/>
|
||||
<path d="M0.685165 65.8523C0.685165 14.7648 67.4855 -13.2029 111.616 19.4083L207.551 90.2535L288.514 44.373C331.305 21.0394 399.874 37.7748 400.685 101.559L400.685 336.622C400.685 387.71 335.845 414.343 291.715 381.732L194.031 309.34L112.973 355.23C69.7783 378.784 0.752625 360.714 0.752625 298.637C0.752654 224.124 0.685165 252.734 0.685165 65.8523Z" fill="url(#paint2_radial_10641_164)"/>
|
||||
<path d="M111.616 19.4083C67.4855 -13.2029 0.685165 14.7648 0.685165 65.8523V295.742C0.685165 290.153 3.5327 279.026 3.5327 279.026C16.6954 236.001 72.6487 219.869 111.36 248.223L291.715 381.732C335.845 414.343 400.685 387.71 400.685 336.622V101.559C400.685 145.201 343.643 189.011 289.755 151.051L111.616 19.4083Z" fill="url(#paint3_radial_10641_164)"/>
|
||||
<path d="M111.616 19.4083C67.4855 -13.2029 0.685165 14.7648 0.685165 65.8523V295.742C0.685165 290.153 3.5327 279.026 3.5327 279.026C16.6954 236.001 72.6487 219.869 111.36 248.223L291.715 381.732C335.845 414.343 400.685 387.71 400.685 336.622V101.559C400.685 145.201 343.643 189.011 289.755 151.051L111.616 19.4083Z" fill="url(#paint4_radial_10641_164)" fill-opacity="0.25"/>
|
||||
<path d="M111.616 19.4083C67.4855 -13.2029 0.685165 14.7648 0.685165 65.8523V295.742C0.685165 290.153 3.5327 279.026 3.5327 279.026C16.6954 236.001 72.6487 219.869 111.36 248.223L291.715 381.732C335.845 414.343 400.685 387.71 400.685 336.622V101.559C400.685 145.201 343.643 189.011 289.755 151.051L111.616 19.4083Z" fill="url(#paint5_radial_10641_164)" fill-opacity="0.55"/>
|
||||
<path d="M111.616 149.691C67.4855 117.08 0.685165 145.048 0.685165 196.135L0.685179 65.8523C0.685183 14.7648 67.4855 -13.2029 111.616 19.4083C170.996 63.2888 230.374 107.171 289.755 151.051C343.643 189.011 400.685 145.201 400.685 101.559V234.89C400.685 285.978 333.885 313.946 289.755 281.334C230.374 237.454 170.996 193.572 111.616 149.691Z" fill="url(#paint6_linear_10641_164)"/>
|
||||
<path d="M111.616 149.691C67.4855 117.08 0.685165 145.048 0.685165 196.135L0.685179 65.8523C0.685183 14.7648 67.4855 -13.2029 111.616 19.4083C170.996 63.2888 230.374 107.171 289.755 151.051C343.643 189.011 400.685 145.201 400.685 101.559V234.89C400.685 285.978 333.885 313.946 289.755 281.334C230.374 237.454 170.996 193.572 111.616 149.691Z" fill="url(#paint7_radial_10641_164)" fill-opacity="0.35"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_10641_164" x1="171.501" y1="396.026" x2="314.256" y2="90.5646" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.02" stop-color="#1169DA"/>
|
||||
<stop offset="0.434784" stop-color="#0151BD"/>
|
||||
<stop offset="0.614436" stop-color="#014DB7"/>
|
||||
<stop offset="1" stop-color="#126AD9"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint1_radial_10641_164" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(358.656 -48.704) rotate(110.224) scale(292.248 218.917)">
|
||||
<stop offset="0.422966" stop-color="#004AFF" stop-opacity="0.1"/>
|
||||
<stop offset="0.728672" stop-color="#014DB9"/>
|
||||
<stop offset="0.836135" stop-color="#014DB9" stop-opacity="0.9"/>
|
||||
<stop offset="0.955" stop-color="#014DB9" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint2_radial_10641_164" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(54.0821 384.087) rotate(-73.6432) scale(232.668 283.419)">
|
||||
<stop offset="0.0908532" stop-color="#004AFF" stop-opacity="0.1"/>
|
||||
<stop offset="0.56036" stop-color="#014DB9"/>
|
||||
<stop offset="0.749466" stop-color="#014DB9" stop-opacity="0.99"/>
|
||||
<stop offset="1" stop-color="#014DB9" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint3_radial_10641_164" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(439.444 5.40101) rotate(134.068) scale(563.957 434.374)">
|
||||
<stop offset="0.249322" stop-color="#23C0FE"/>
|
||||
<stop offset="0.717207" stop-color="#23C0FE"/>
|
||||
<stop offset="0.995168" stop-color="#1C91FF"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint4_radial_10641_164" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(119.961 37.1103) rotate(45.9092) scale(403.457 95.9789)">
|
||||
<stop offset="0.165" stop-color="#096DD6" stop-opacity="0"/>
|
||||
<stop offset="0.484233" stop-color="#096DD6"/>
|
||||
<stop offset="0.900505" stop-color="#0876DE" stop-opacity="0.813868"/>
|
||||
<stop offset="1" stop-color="#029AFF" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint5_radial_10641_164" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(385.776 245.022) rotate(91.6099) scale(64.799 58.3555)">
|
||||
<stop stop-color="#0068B3"/>
|
||||
<stop offset="0.93" stop-color="#006CB8" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint6_linear_10641_164" x1="114.72" y1="97.5227" x2="182.067" y2="383.699" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#92EEFE"/>
|
||||
<stop offset="0.564559" stop-color="#35DDFF"/>
|
||||
<stop offset="1" stop-color="#08B1F9"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint7_radial_10641_164" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(67.7807 5.4011) rotate(69.461) scale(99.6637 80.4988)">
|
||||
<stop stop-color="#CCF9FF" stop-opacity="0.93"/>
|
||||
<stop offset="1" stop-color="#35DDFF"/>
|
||||
</radialGradient>
|
||||
<clipPath id="clip0_10641_164">
|
||||
<rect width="400" height="400" fill="white" transform="translate(0.685181 0.713501)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><circle cx="256" cy="256" r="256" fill="#fff"/><path d="M268.6 102.4c64.4 0 116.8 52.4 116.8 116.7 0 25.3-8 49.4-23 69.6-14.8 19.9-35 34.3-58.4 41.7l-6.5 2-15.5-76.2 4.3-2c14-6.7 23-21.1 23-36.6 0-22.4-18.2-40.6-40.6-40.6S228 195.2 228 217.6c0 15.5 9 29.8 23 36.6l4.2 2-25 153.4h-69.5V102.4z" style="fill:#191919"/></svg>
|
||||
|
After Width: | Height: | Size: 404 B |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
|
||||
|
||||
<g fill="#000000">
|
||||
|
||||
<path d="M7.754 2l.463.41c.343.304.687.607 1.026.915C11.44 5.32 13.3 7.565 14.7 10.149c.072.132.137.268.202.403l.098.203-.108.057-.081-.115-.21-.299-.147-.214c-1.019-1.479-2.04-2.96-3.442-4.145a6.563 6.563 0 00-1.393-.904c-1.014-.485-1.916-.291-2.69.505-.736.757-1.118 1.697-1.463 2.653-.045.123-.092.245-.139.367l-.082.215-.172-.055c.1-.348.192-.698.284-1.049.21-.795.42-1.59.712-2.356.31-.816.702-1.603 1.093-2.39.169-.341.338-.682.5-1.025h.092z"/>
|
||||
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -439,9 +439,7 @@ fullscreen = "ملء الشاشة"
|
||||
[settings.general.updates]
|
||||
title = "تحديثات البرنامج"
|
||||
description = "التحقق من التحديثات وعرض معلومات الإصدار"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "الإصدار الحالي"
|
||||
latestVersion = "أحدث إصدار"
|
||||
checkForUpdates = "التحقق من التحديثات"
|
||||
viewDetails = "عرض التفاصيل"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "حجم الصورة"
|
||||
margin = "الهامش"
|
||||
positionAndFormatting = "الموضع والتنسيق"
|
||||
quickPosition = "اختر موضعًا على الصفحة لوضع الختم."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "حدث خطأ أثناء إضافة الختم إلى ملف PDF."
|
||||
@@ -3594,14 +3567,6 @@ failed = "حدث خطأ أثناء إضافة الختم إلى ملف PDF."
|
||||
[AddStampRequest.results]
|
||||
title = "نتائج إضافة الختم"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "إزالة الصورة,عمليات الصفحة,الخلفية,جانب الخادم"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Tam ekran"
|
||||
[settings.general.updates]
|
||||
title = "Proqram yeniləmələri"
|
||||
description = "Yeniləmələri yoxlayın və versiya məlumatlarını görün"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Cari versiya"
|
||||
latestVersion = "Ən son versiya"
|
||||
checkForUpdates = "Yeniləmələri yoxla"
|
||||
viewDetails = "Ətraflı bax"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Şəkil ölçüsü"
|
||||
margin = "Kənar boşluğu"
|
||||
positionAndFormatting = "Mövqe və formatlama"
|
||||
quickPosition = "Möhürü yerləşdirmək üçün səhifədə bir mövqe seçin."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "PDF-ə möhür əlavə edilərkən xəta baş verdi."
|
||||
@@ -3594,14 +3567,6 @@ failed = "PDF-ə möhür əlavə edilərkən xəta baş verdi."
|
||||
[AddStampRequest.results]
|
||||
title = "Möhür nəticələri"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Şəkil Sil,Səhifə Əməliyyatları,Back end,server-tərəf"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Цял екран"
|
||||
[settings.general.updates]
|
||||
title = "Актуализации на софтуера"
|
||||
description = "Проверете за актуализации и вижте информация за версията"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Текуща версия"
|
||||
latestVersion = "Последна версия"
|
||||
checkForUpdates = "Провери за актуализации"
|
||||
viewDetails = "Виж подробности"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Размер на изображението"
|
||||
margin = "Отстъп"
|
||||
positionAndFormatting = "Позиция и форматиране"
|
||||
quickPosition = "Изберете позиция на страницата, където да поставите печата."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Възникна грешка при добавяне на печат към PDF."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Възникна грешка при добавяне на печат
|
||||
[AddStampRequest.results]
|
||||
title = "Резултати от печата"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Премахване на изображение, операции на страници, админ страна, страна на сървъра"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "བརྙན་ཤེལ་ཆ་ཚང་།"
|
||||
[settings.general.updates]
|
||||
title = "མཉེན་ཆས་གསར་བརྗེ།"
|
||||
description = "གསར་བརྗེའི་གནས་ཚུལ་ལ་ཞིབ་བཤེར་དང་ཐོན་རིམ་གྱི་ཆ་འཕྲིན་ལ་ལྟོས།"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "ད་ལྟའི་ཐོན་རིམ།"
|
||||
latestVersion = "ཐོན་རིམ་གསར་ཤོས།"
|
||||
checkForUpdates = "གསར་བརྗེ་ལ་ཞིབ་བཤེར།"
|
||||
viewDetails = "ཞིབ་ཕྲའི་གནས་ཚུལ་ལ་ལྟོས།"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "པར་རིས་ཚད་གཞི།"
|
||||
margin = "མཐའ་ཤོག"
|
||||
positionAndFormatting = "ལས་གནས་དང་རྩ་སྒྲིག།"
|
||||
quickPosition = "ཤོག་ངོས་སྟེང་དུ་གནས་ཡུལ་ཞིག་འདེམས་ནས་མཚོན་རྟགས་འཇོག་དགོས།"
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "PDF ལ་མཚོན་རྟགས་ཁ་སྣོན་བྱེད་སྐབས་ནོར་འཁྲུལ་བྱུང་ཡོད།"
|
||||
@@ -3594,14 +3567,6 @@ failed = "PDF ལ་མཚོན་རྟགས་ཁ་སྣོན་བྱེ
|
||||
[AddStampRequest.results]
|
||||
title = "མཚོན་རྟགས་གྲུབ་འབྲས།"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "པར་རིས་མེད་པར་བཟོས་པ། ཤོག་ངོས་བཀོལ་སྤྱོད། རྒྱབ་མཇུག་། ཞབས་ཞུ་པ།"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Pantalla completa"
|
||||
[settings.general.updates]
|
||||
title = "Actualitzacions de programari"
|
||||
description = "Comprova actualitzacions i informació de la versió"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Versió actual"
|
||||
latestVersion = "Última versió"
|
||||
checkForUpdates = "Comprova actualitzacions"
|
||||
viewDetails = "Veure detalls"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Mida de la imatge"
|
||||
margin = "Marge"
|
||||
positionAndFormatting = "Posició i format"
|
||||
quickPosition = "Seleccioneu una posició a la pàgina per col·locar el segell."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "S'ha produït un error en afegir el segell al PDF."
|
||||
@@ -3594,14 +3567,6 @@ failed = "S'ha produït un error en afegir el segell al PDF."
|
||||
[AddStampRequest.results]
|
||||
title = "Resultats del segell"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Elimina imatge,Operacions de pàgina,Back-end,Servidor"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Celá obrazovka"
|
||||
[settings.general.updates]
|
||||
title = "Aktualizace softwaru"
|
||||
description = "Zkontrolujte aktualizace a zobrazte informace o verzi"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Aktuální verze"
|
||||
latestVersion = "Nejnovější verze"
|
||||
checkForUpdates = "Zkontrolovat aktualizace"
|
||||
viewDetails = "Zobrazit podrobnosti"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Velikost obrázku"
|
||||
margin = "Okraj"
|
||||
positionAndFormatting = "Umístění a formátování"
|
||||
quickPosition = "Vyberte na stránce pozici pro umístění razítka."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Při přidávání razítka do PDF došlo k chybě."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Při přidávání razítka do PDF došlo k chybě."
|
||||
[AddStampRequest.results]
|
||||
title = "Výsledky razítka"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Odstranit obrázek,Operace stránek,zadní strana,serverová strana"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Fuldskærm"
|
||||
[settings.general.updates]
|
||||
title = "Softwareopdateringer"
|
||||
description = "Søg efter opdateringer og se versionsinfo"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Nuværende version"
|
||||
latestVersion = "Seneste version"
|
||||
checkForUpdates = "Søg efter opdateringer"
|
||||
viewDetails = "Vis detaljer"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Billedstørrelse"
|
||||
margin = "Margen"
|
||||
positionAndFormatting = "Placering og formatering"
|
||||
quickPosition = "Vælg en placering på siden til stemplet."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Der opstod en fejl under tilføjelse af stempel til PDF'en."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Der opstod en fejl under tilføjelse af stempel til PDF'en."
|
||||
[AddStampRequest.results]
|
||||
title = "Stempelresultater"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Fjern Billede,Sideoperationer,Back end,server side"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Vollbild"
|
||||
[settings.general.updates]
|
||||
title = "Software-Updates"
|
||||
description = "Nach Updates suchen und Versionsinformationen anzeigen"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Aktuelle Version"
|
||||
latestVersion = "Neueste Version"
|
||||
checkForUpdates = "Nach Updates suchen"
|
||||
viewDetails = "Details anzeigen"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Bildgröße"
|
||||
margin = "Rand"
|
||||
positionAndFormatting = "Position & Formatierung"
|
||||
quickPosition = "Wählen Sie eine Position auf der Seite, um den Stempel zu platzieren."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Beim Hinzufügen des Stempels zum PDF ist ein Fehler aufgetreten."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Beim Hinzufügen des Stempels zum PDF ist ein Fehler aufgetreten."
|
||||
[AddStampRequest.results]
|
||||
title = "Stempel-Ergebnisse"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "bild entfernen,seitenoperationen,back end,server side"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Πλήρης οθόνη"
|
||||
[settings.general.updates]
|
||||
title = "Ενημερώσεις λογισμικού"
|
||||
description = "Έλεγχος ενημερώσεων και προβολή πληροφοριών έκδοσης"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Τρέχουσα έκδοση"
|
||||
latestVersion = "Νεότερη έκδοση"
|
||||
checkForUpdates = "Έλεγχος για ενημερώσεις"
|
||||
viewDetails = "Προβολή λεπτομερειών"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Μέγεθος εικόνας"
|
||||
margin = "Περιθώριο"
|
||||
positionAndFormatting = "Θέση & μορφοποίηση"
|
||||
quickPosition = "Επιλέξτε μια θέση στη σελίδα για τοποθέτηση της σφραγίδας."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Παρουσιάστηκε σφάλμα κατά την προσθήκη σφραγίδας στο PDF."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Παρουσιάστηκε σφάλμα κατά την προσθήκ
|
||||
[AddStampRequest.results]
|
||||
title = "Αποτελέσματα σφράγισης"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "αφαίρεση εικόνας,λειτουργίες σελίδας,backend,server side"
|
||||
|
||||
|
||||
@@ -219,9 +219,6 @@ pagesAndStarting = "Pages & Starting Number"
|
||||
positionAndPages = "Position & Pages"
|
||||
preview = "Position Selection"
|
||||
previewDisclaimer = "Preview is approximate. Final output may vary due to PDF font metrics."
|
||||
zeroPad = "Zero‑pad Width (Bates Stamping)"
|
||||
zeroPadTooltip = "Zero‑pad (Bates Stamp) page numbers to this width (e.g., 3 ⇒ 001). Set 0 to disable."
|
||||
|
||||
|
||||
[addPageNumbers.selectText]
|
||||
1 = "Select PDF file:"
|
||||
@@ -442,9 +439,7 @@ fullscreen = "Fullscreen"
|
||||
[settings.general.updates]
|
||||
title = "Software Updates"
|
||||
description = "Check for updates and view version information"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Current Version"
|
||||
latestVersion = "Latest Version"
|
||||
checkForUpdates = "Check for Updates"
|
||||
viewDetails = "View Details"
|
||||
@@ -4517,18 +4512,10 @@ description = "Configure custom file system paths for pipeline processing and ex
|
||||
[admin.settings.general.customPaths.pipeline]
|
||||
label = "Pipeline Directories"
|
||||
|
||||
[admin.settings.general.customPaths.pipeline.pipelineDir]
|
||||
label = "Pipeline Directory"
|
||||
description = "Base directory for pipeline resources (leave empty for default: /pipeline)"
|
||||
|
||||
[admin.settings.general.customPaths.pipeline.watchedFoldersDir]
|
||||
label = "Watched Folders Directory"
|
||||
description = "Directory where pipeline monitors for incoming PDFs (leave empty for default: /pipeline/watchedFolders)"
|
||||
|
||||
[admin.settings.general.customPaths.pipeline.watchedFoldersDirs]
|
||||
label = "Watched Folders Directories"
|
||||
description = "Directories where pipeline monitors for incoming PDFs (one per line or comma-separated; leave empty for default: /pipeline/watchedFolders)"
|
||||
|
||||
[admin.settings.general.customPaths.pipeline.finishedFoldersDir]
|
||||
label = "Finished Folders Directory"
|
||||
description = "Directory where processed PDFs are outputted (leave empty for default: /pipeline/finishedFolders)"
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Pantalla completa"
|
||||
[settings.general.updates]
|
||||
title = "Actualizaciones de software"
|
||||
description = "Compruebe actualizaciones y vea la información de versión"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Versión actual"
|
||||
latestVersion = "Última versión"
|
||||
checkForUpdates = "Buscar actualizaciones"
|
||||
viewDetails = "Ver detalles"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Tamaño de la imagen"
|
||||
margin = "Margen"
|
||||
positionAndFormatting = "Posición y formato"
|
||||
quickPosition = "Selecciona una posición en la página para colocar el sello."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Se produjo un error al añadir el sello al PDF."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Se produjo un error al añadir el sello al PDF."
|
||||
[AddStampRequest.results]
|
||||
title = "Resultados del sello"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Eliminar imagen,Operaciones de página,Back end,Backend"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Pantaila osoa"
|
||||
[settings.general.updates]
|
||||
title = "Software eguneratzeak"
|
||||
description = "Egiaztatu eguneratzeak eta ikusi bertsio-informazioa"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Uneko bertsioa"
|
||||
latestVersion = "Azken bertsioa"
|
||||
checkForUpdates = "Egiaztatu eguneratzeak"
|
||||
viewDetails = "Xehetasunak ikusi"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Irudiaren tamaina"
|
||||
margin = "Marjina"
|
||||
positionAndFormatting = "Kokapena eta formatua"
|
||||
quickPosition = "Hautatu orrian zigiloa kokatzeko posizio bat."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Errore bat gertatu da zigilua PDFari gehitzean."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Errore bat gertatu da zigilua PDFari gehitzean."
|
||||
[AddStampRequest.results]
|
||||
title = "Zigiluaren emaitzak"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Irudia kendu,Orrialde eragiketak,Back end,server side"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "تمامصفحه"
|
||||
[settings.general.updates]
|
||||
title = "بهروزرسانی نرمافزار"
|
||||
description = "بررسی بهروزرسانی و مشاهده اطلاعات نسخه"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "نسخه فعلی"
|
||||
latestVersion = "آخرین نسخه"
|
||||
checkForUpdates = "بررسی بهروزرسانی"
|
||||
viewDetails = "مشاهده جزئیات"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "اندازه تصویر"
|
||||
margin = "حاشیه"
|
||||
positionAndFormatting = "موقعیت و قالببندی"
|
||||
quickPosition = "یک موقعیت روی صفحه برای قرار دادن مهر انتخاب کنید."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "هنگام افزودن مهر به PDF خطایی رخ داد."
|
||||
@@ -3594,14 +3567,6 @@ failed = "هنگام افزودن مهر به PDF خطایی رخ داد."
|
||||
[AddStampRequest.results]
|
||||
title = "نتایج مهر"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "حذف تصویر، عملیات صفحه، سرور"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Plein écran"
|
||||
[settings.general.updates]
|
||||
title = "Mises à jour logicielles"
|
||||
description = "Rechercher des mises à jour et voir les informations de version"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Version actuelle"
|
||||
latestVersion = "Dernière version"
|
||||
checkForUpdates = "Rechercher des mises à jour"
|
||||
viewDetails = "Voir les détails"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Taille de l’image"
|
||||
margin = "Marge"
|
||||
positionAndFormatting = "Position et mise en forme"
|
||||
quickPosition = "Sélectionnez une position sur la page pour placer le tampon."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Une erreur s’est produite lors de l’ajout du tampon au PDF."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Une erreur s’est produite lors de l’ajout du tampon au PDF."
|
||||
[AddStampRequest.results]
|
||||
title = "Résultats du tampon"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Images,Remove Image,Page operations,Back end,server side"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Lánscáileán"
|
||||
[settings.general.updates]
|
||||
title = "Nuashonruithe Bogearraí"
|
||||
description = "Seiceáil le haghaidh nuashonruithe agus féach faisnéis leagain"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Leagan Reatha"
|
||||
latestVersion = "Leagan is Déanaí"
|
||||
checkForUpdates = "Seiceáil le haghaidh Nuashonruithe"
|
||||
viewDetails = "Féach Sonraí"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Méid Íomhá"
|
||||
margin = "Imeall"
|
||||
positionAndFormatting = "Suíomh & Formáidiú"
|
||||
quickPosition = "Roghnaigh suíomh ar an leathanach le stampa a chur."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Tharla earráid agus stampa á chur leis an PDF."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Tharla earráid agus stampa á chur leis an PDF."
|
||||
[AddStampRequest.results]
|
||||
title = "Torthaí Stampa"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Bain Íomhá, Oibríochtaí Leathanaigh, Cúl, taobh an fhreastalaí"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "फुलस्क्रीन"
|
||||
[settings.general.updates]
|
||||
title = "सॉफ़्टवेयर अपडेट्स"
|
||||
description = "अपडेट्स जाँचें और वर्ज़न जानकारी देखें"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "वर्तमान संस्करण"
|
||||
latestVersion = "नवीनतम संस्करण"
|
||||
checkForUpdates = "अपडेट्स जाँचें"
|
||||
viewDetails = "विवरण देखें"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "छवि आकार"
|
||||
margin = "हाशिया"
|
||||
positionAndFormatting = "स्थिति और फ़ॉर्मैटिंग"
|
||||
quickPosition = "मुहर रखने के लिए पृष्ठ पर एक स्थान चुनें।"
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "PDF में मुहर जोड़ते समय त्रुटि हुई।"
|
||||
@@ -3594,14 +3567,6 @@ failed = "PDF में मुहर जोड़ते समय त्रु
|
||||
[AddStampRequest.results]
|
||||
title = "मुहर के परिणाम"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "छवि हटाएं,पृष्ठ कार्य,बैक एंड,सर्वर साइड"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Cijeli zaslon"
|
||||
[settings.general.updates]
|
||||
title = "Ažuriranja softvera"
|
||||
description = "Provjerite ažuriranja i pogledajte informacije o verziji"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Trenutačna verzija"
|
||||
latestVersion = "Najnovija verzija"
|
||||
checkForUpdates = "Provjeri ažuriranja"
|
||||
viewDetails = "Prikaži detalje"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Veličina slike"
|
||||
margin = "Margina"
|
||||
positionAndFormatting = "Položaj i oblikovanje"
|
||||
quickPosition = "Odaberite položaj na stranici za postavljanje pečata."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Došlo je do pogreške pri dodavanju pečata u PDF."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Došlo je do pogreške pri dodavanju pečata u PDF."
|
||||
[AddStampRequest.results]
|
||||
title = "Rezultati pečata"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Ukloni sliku, Rad sa stranicama, Back end, server strana"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Teljes képernyő"
|
||||
[settings.general.updates]
|
||||
title = "Szoftverfrissítések"
|
||||
description = "Frissítések keresése és verzióinformációk megtekintése"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Jelenlegi verzió"
|
||||
latestVersion = "Legújabb verzió"
|
||||
checkForUpdates = "Frissítések keresése"
|
||||
viewDetails = "Részletek megtekintése"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Képméret"
|
||||
margin = "Margó"
|
||||
positionAndFormatting = "Pozíció és formázás"
|
||||
quickPosition = "Válasszon pozíciót az oldalon a bélyeg elhelyezéséhez."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Hiba történt a bélyeg hozzáadása közben a PDF-hez."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Hiba történt a bélyeg hozzáadása közben a PDF-hez."
|
||||
[AddStampRequest.results]
|
||||
title = "Bélyegzés eredményei"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Képek eltávolítása,Oldalműveletek,Backend,szerver oldali"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Layar penuh"
|
||||
[settings.general.updates]
|
||||
title = "Pembaruan Software"
|
||||
description = "Periksa pembaruan dan lihat informasi versi"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Versi Saat Ini"
|
||||
latestVersion = "Versi Terbaru"
|
||||
checkForUpdates = "Periksa Pembaruan"
|
||||
viewDetails = "Lihat Detail"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Ukuran Gambar"
|
||||
margin = "Margin"
|
||||
positionAndFormatting = "Posisi & Pemformatan"
|
||||
quickPosition = "Pilih posisi pada halaman untuk menempatkan stempel."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Terjadi kesalahan saat menambahkan stempel ke PDF."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Terjadi kesalahan saat menambahkan stempel ke PDF."
|
||||
[AddStampRequest.results]
|
||||
title = "Hasil Stempel"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Hapus Gambar,Operasi Halaman,Backend,server side"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Schermo intero"
|
||||
[settings.general.updates]
|
||||
title = "Aggiornamenti software"
|
||||
description = "Controlla aggiornamenti e visualizza informazioni sulla versione"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Versione attuale"
|
||||
latestVersion = "Ultima versione"
|
||||
checkForUpdates = "Controlla aggiornamenti"
|
||||
viewDetails = "Vedi dettagli"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Dimensione immagine"
|
||||
margin = "Margine"
|
||||
positionAndFormatting = "Posizione e formattazione"
|
||||
quickPosition = "Seleziona una posizione sulla pagina in cui posizionare il timbro."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Si è verificato un errore durante l'aggiunta del timbro al PDF."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Si è verificato un errore durante l'aggiunta del timbro al PDF."
|
||||
[AddStampRequest.results]
|
||||
title = "Risultati timbro"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Rimuovi immagine,operazioni sulla pagina,back-end,lato server"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "フルスクリーン"
|
||||
[settings.general.updates]
|
||||
title = "ソフトウェア更新"
|
||||
description = "更新の確認とバージョン情報の表示"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "現在のバージョン"
|
||||
latestVersion = "最新バージョン"
|
||||
checkForUpdates = "更新を確認"
|
||||
viewDetails = "詳細を表示"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "画像サイズ"
|
||||
margin = "余白"
|
||||
positionAndFormatting = "位置と書式"
|
||||
quickPosition = "ページ上の配置位置を選択してください。"
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "PDF にスタンプを追加中にエラーが発生しました。"
|
||||
@@ -3594,14 +3567,6 @@ failed = "PDF にスタンプを追加中にエラーが発生しました。"
|
||||
[AddStampRequest.results]
|
||||
title = "スタンプ結果"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "画像削除,ページ操作,バックエンド,サーバー側"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "전체 화면"
|
||||
[settings.general.updates]
|
||||
title = "소프트웨어 업데이트"
|
||||
description = "업데이트 확인 및 버전 정보 보기"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "현재 버전"
|
||||
latestVersion = "최신 버전"
|
||||
checkForUpdates = "업데이트 확인"
|
||||
viewDetails = "자세히 보기"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "이미지 크기"
|
||||
margin = "여백"
|
||||
positionAndFormatting = "위치 및 서식"
|
||||
quickPosition = "페이지에서 스탬프 위치를 선택하세요."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "PDF에 스탬프를 추가하는 중 오류가 발생했습니다."
|
||||
@@ -3594,14 +3567,6 @@ failed = "PDF에 스탬프를 추가하는 중 오류가 발생했습니다."
|
||||
[AddStampRequest.results]
|
||||
title = "스탬프 결과"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "이미지 제거,페이지 작업,백엔드,서버 사이드"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "ഫുൾസ്ക്രീൻ"
|
||||
[settings.general.updates]
|
||||
title = "സോഫ്റ്റ്വെയർ അപ്ഡേറ്റുകൾ"
|
||||
description = "അപ്ഡേറ്റുകൾ പരിശോധിക്കുകയും പതിപ്പിന്റെ വിവരങ്ങൾ കാണുകയും ചെയ്യുക"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "ഇപ്പോഴത്തെ പതിപ്പ്"
|
||||
latestVersion = "പുതിയ പതിപ്പ്"
|
||||
checkForUpdates = "അപ്ഡേറ്റുകൾ പരിശോധിക്കുക"
|
||||
viewDetails = "വിശദാംശങ്ങൾ കാണുക"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "ഇമേജ് വലിപ്പം"
|
||||
margin = "മാർജിൻ"
|
||||
positionAndFormatting = "സ്ഥാനം & ഫോർമാറ്റിംഗ്"
|
||||
quickPosition = "സ്റ്റാമ്പ് വയ്ക്കാൻ പേജിലെ ഒരു സ്ഥാനം തിരഞ്ഞെടുക്കുക."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "PDF-ലേക്ക് സ്റ്റാമ്പ് ചേർക്കുന്നതിനിടെ പിശക് സംഭവിച്ചു."
|
||||
@@ -3594,14 +3567,6 @@ failed = "PDF-ലേക്ക് സ്റ്റാമ്പ് ചേർക്
|
||||
[AddStampRequest.results]
|
||||
title = "സ്റ്റാമ്പ് ഫലങ്ങൾ"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "ചിത്രം നീക്കം ചെയ്യുക,പേജ് പ്രവർത്തനങ്ങൾ,ബാക്ക് എൻഡ്,സെർവർ സൈഡ്"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Volledig scherm"
|
||||
[settings.general.updates]
|
||||
title = "Software-updates"
|
||||
description = "Controleer op updates en bekijk versie-informatie"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Huidige versie"
|
||||
latestVersion = "Laatste versie"
|
||||
checkForUpdates = "Op updates controleren"
|
||||
viewDetails = "Details bekijken"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Afbeeldingsgrootte"
|
||||
margin = "Marge"
|
||||
positionAndFormatting = "Positie & opmaak"
|
||||
quickPosition = "Selecteer een positie op de pagina om de stempel te plaatsen."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Er is een fout opgetreden bij het toevoegen van een stempel aan de PDF."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Er is een fout opgetreden bij het toevoegen van een stempel aan de PDF
|
||||
[AddStampRequest.results]
|
||||
title = "Stempelresultaten"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Afbeelding verwijderen, Paginabewerkingen, Achterkant, Serverkant"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Fullskjerm"
|
||||
[settings.general.updates]
|
||||
title = "Programvareoppdateringer"
|
||||
description = "Se etter oppdateringer og vis versjonsinformasjon"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Nåværende versjon"
|
||||
latestVersion = "Siste versjon"
|
||||
checkForUpdates = "Søk etter oppdateringer"
|
||||
viewDetails = "Vis detaljer"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Bildestørrelse"
|
||||
margin = "Marg"
|
||||
positionAndFormatting = "Plassering og formatering"
|
||||
quickPosition = "Velg en posisjon på siden for å plassere stempelet."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Det oppstod en feil under tillegg av stempel til PDF-en."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Det oppstod en feil under tillegg av stempel til PDF-en."
|
||||
[AddStampRequest.results]
|
||||
title = "Stempelresultater"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Fjern Bilde,Sideoperasjoner,Backend,serverside"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Pełny ekran"
|
||||
[settings.general.updates]
|
||||
title = "Aktualizacje oprogramowania"
|
||||
description = "Sprawdź aktualizacje i informacje o wersji"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Bieżąca wersja"
|
||||
latestVersion = "Najnowsza wersja"
|
||||
checkForUpdates = "Sprawdź aktualizacje"
|
||||
viewDetails = "Pokaż szczegóły"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Rozmiar obrazu"
|
||||
margin = "Margines"
|
||||
positionAndFormatting = "Pozycja i formatowanie"
|
||||
quickPosition = "Wybierz pozycję na stronie, aby umieścić stempel."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Wystąpił błąd podczas dodawania stempla do PDF."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Wystąpił błąd podczas dodawania stempla do PDF."
|
||||
[AddStampRequest.results]
|
||||
title = "Wyniki dodawania stempla"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Usuń obraz, operacje na stronie, back-end, strona serwera"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Tela cheia"
|
||||
[settings.general.updates]
|
||||
title = "Atualizações de software"
|
||||
description = "Verifique atualizações e veja informações da versão"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Versão atual"
|
||||
latestVersion = "Última versão"
|
||||
checkForUpdates = "Verificar atualizações"
|
||||
viewDetails = "Ver detalhes"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Tamanho da imagem"
|
||||
margin = "Margem"
|
||||
positionAndFormatting = "Posição e formatação"
|
||||
quickPosition = "Selecione uma posição na página para colocar o carimbo."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Ocorreu um erro ao adicionar o carimbo ao PDF."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Ocorreu um erro ao adicionar o carimbo ao PDF."
|
||||
[AddStampRequest.results]
|
||||
title = "Resultados do carimbo"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Remover imagem,operações de página,back-end,lado do servidor"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Ecrã inteiro"
|
||||
[settings.general.updates]
|
||||
title = "Atualizações de software"
|
||||
description = "Procurar atualizações e ver informações da versão"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Versão atual"
|
||||
latestVersion = "Última versão"
|
||||
checkForUpdates = "Procurar atualizações"
|
||||
viewDetails = "Ver detalhes"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Tamanho da imagem"
|
||||
margin = "Margem"
|
||||
positionAndFormatting = "Posição e formatação"
|
||||
quickPosition = "Selecione uma posição na página para colocar o carimbo."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Ocorreu um erro ao adicionar o carimbo ao PDF."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Ocorreu um erro ao adicionar o carimbo ao PDF."
|
||||
[AddStampRequest.results]
|
||||
title = "Resultados do carimbo"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Remover Imagem,operações de página,lado servidor"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Ecran complet"
|
||||
[settings.general.updates]
|
||||
title = "Actualizări software"
|
||||
description = "Caută actualizări și vezi informații despre versiune"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Versiune curentă"
|
||||
latestVersion = "Ultima versiune"
|
||||
checkForUpdates = "Caută actualizări"
|
||||
viewDetails = "Vezi detalii"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Dimensiunea imaginii"
|
||||
margin = "Margine"
|
||||
positionAndFormatting = "Poziție și formatare"
|
||||
quickPosition = "Selectați o poziție pe pagină pentru a plasa ștampila."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "A apărut o eroare la adăugarea ștampilei în PDF."
|
||||
@@ -3594,14 +3567,6 @@ failed = "A apărut o eroare la adăugarea ștampilei în PDF."
|
||||
[AddStampRequest.results]
|
||||
title = "Rezultatele ștampilării"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Elimină Imagine,Operații pagină,Back end,server side"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Полноэкранный"
|
||||
[settings.general.updates]
|
||||
title = "Обновления ПО"
|
||||
description = "Проверка обновлений и сведения о версии"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Текущая версия"
|
||||
latestVersion = "Последняя версия"
|
||||
checkForUpdates = "Проверить обновления"
|
||||
viewDetails = "Подробнее"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Размер изображения"
|
||||
margin = "Отступ"
|
||||
positionAndFormatting = "Положение и форматирование"
|
||||
quickPosition = "Выберите положение на странице для размещения штампа."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Произошла ошибка при добавлении штампа в PDF."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Произошла ошибка при добавлении штамп
|
||||
[AddStampRequest.results]
|
||||
title = "Результаты штампа"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Удаление изображения,операции со страницами,Серверная часть"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Celá obrazovka"
|
||||
[settings.general.updates]
|
||||
title = "Aktualizácie softvéru"
|
||||
description = "Skontrolujte aktualizácie a zobrazte informácie o verzii"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Aktuálna verzia"
|
||||
latestVersion = "Najnovšia verzia"
|
||||
checkForUpdates = "Skontrolovať aktualizácie"
|
||||
viewDetails = "Zobraziť podrobnosti"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Veľkosť obrázka"
|
||||
margin = "Okraj"
|
||||
positionAndFormatting = "Poloha a formátovanie"
|
||||
quickPosition = "Vyberte polohu na stránke pre umiestnenie pečiatky."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Počas pridávania pečiatky do PDF došlo k chybe."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Počas pridávania pečiatky do PDF došlo k chybe."
|
||||
[AddStampRequest.results]
|
||||
title = "Výsledky pečiatky"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Odstrániť obrázok,Operácie so stranami,Back end,server side"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Celozaslonski"
|
||||
[settings.general.updates]
|
||||
title = "Posodobitve programske opreme"
|
||||
description = "Preverite posodobitve in glejte informacije o različici"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Trenutna različica"
|
||||
latestVersion = "Najnovejša različica"
|
||||
checkForUpdates = "Preveri posodobitve"
|
||||
viewDetails = "Poglej podrobnosti"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Velikost slike"
|
||||
margin = "Rob"
|
||||
positionAndFormatting = "Položaj in oblikovanje"
|
||||
quickPosition = "Izberite položaj na strani za postavitev žiga."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Pri dodajanju žiga v PDF je prišlo do napake."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Pri dodajanju žiga v PDF je prišlo do napake."
|
||||
[AddStampRequest.results]
|
||||
title = "Rezultati žiga"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Odstrani sliko,operacije strani,zadnja stran,strežniška stran"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Ceo ekran"
|
||||
[settings.general.updates]
|
||||
title = "Ažuriranja softvera"
|
||||
description = "Proverite ažuriranja i pogledajte informacije o verziji"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Trenutna verzija"
|
||||
latestVersion = "Najnovija verzija"
|
||||
checkForUpdates = "Proveri ažuriranja"
|
||||
viewDetails = "Prikaži detalje"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Veličina slike"
|
||||
margin = "Margina"
|
||||
positionAndFormatting = "Pozicija i formatiranje"
|
||||
quickPosition = "Izaberite poziciju na stranici za postavljanje pečata."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Došlo je do greške prilikom dodavanja pečata u PDF."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Došlo je do greške prilikom dodavanja pečata u PDF."
|
||||
[AddStampRequest.results]
|
||||
title = "Rezultati pečata"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Ukloni sliku, Zahvati na stranici, Bekend,serverska strana"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Helskärm"
|
||||
[settings.general.updates]
|
||||
title = "Programuppdateringar"
|
||||
description = "Sök efter uppdateringar och visa versionsinfo"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Aktuell version"
|
||||
latestVersion = "Senaste version"
|
||||
checkForUpdates = "Sök efter uppdateringar"
|
||||
viewDetails = "Visa detaljer"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Bildstorlek"
|
||||
margin = "Marginal"
|
||||
positionAndFormatting = "Position och formatering"
|
||||
quickPosition = "Välj en position på sidan för att placera stämpeln."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Ett fel inträffade när stämpeln skulle läggas till i PDF:en."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Ett fel inträffade när stämpeln skulle läggas till i PDF:en."
|
||||
[AddStampRequest.results]
|
||||
title = "Resultat av stämpling"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Ta bort bild,Sidoperationer,Backend,serversida"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "เต็มหน้าจอ"
|
||||
[settings.general.updates]
|
||||
title = "อัปเดตซอฟต์แวร์"
|
||||
description = "ตรวจสอบอัปเดตและดูข้อมูลเวอร์ชัน"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "เวอร์ชันปัจจุบัน"
|
||||
latestVersion = "เวอร์ชันล่าสุด"
|
||||
checkForUpdates = "ตรวจสอบอัปเดต"
|
||||
viewDetails = "ดูรายละเอียด"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "ขนาดรูปภาพ"
|
||||
margin = "ระยะขอบ"
|
||||
positionAndFormatting = "ตำแหน่งและการจัดรูปแบบ"
|
||||
quickPosition = "เลือกตำแหน่งบนหน้าสำหรับวางตราประทับ"
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "เกิดข้อผิดพลาดขณะเพิ่มตราประทับลงใน PDF"
|
||||
@@ -3594,14 +3567,6 @@ failed = "เกิดข้อผิดพลาดขณะเพิ่มต
|
||||
[AddStampRequest.results]
|
||||
title = "ผลการประทับตรา"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "ลบรูปภาพ,การจัดการหน้า,แบ็กเอนด์,ฝั่งเซิร์ฟเวอร์"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Tam ekran"
|
||||
[settings.general.updates]
|
||||
title = "Yazılım Güncellemeleri"
|
||||
description = "Güncellemeleri kontrol edin ve sürüm bilgilerini görüntüleyin"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Geçerli Sürüm"
|
||||
latestVersion = "En Son Sürüm"
|
||||
checkForUpdates = "Güncellemeleri Kontrol Et"
|
||||
viewDetails = "Ayrıntıları Görüntüle"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Görüntü Boyutu"
|
||||
margin = "Kenar Boşluğu"
|
||||
positionAndFormatting = "Konum ve Biçimlendirme"
|
||||
quickPosition = "Damgayı yerleştirmek için sayfada bir konum seçin."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "PDF'ye damga eklenirken bir hata oluştu."
|
||||
@@ -3594,14 +3567,6 @@ failed = "PDF'ye damga eklenirken bir hata oluştu."
|
||||
[AddStampRequest.results]
|
||||
title = "Damga Sonuçları"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Resmi Kaldır,Sayfa İşlemleri,Arka uç,sunucu tarafı"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Повноекранний"
|
||||
[settings.general.updates]
|
||||
title = "Оновлення ПЗ"
|
||||
description = "Перевіряйте оновлення та переглядайте інформацію про версію"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Поточна версія"
|
||||
latestVersion = "Остання версія"
|
||||
checkForUpdates = "Перевірити оновлення"
|
||||
viewDetails = "Переглянути деталі"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Розмір зображення"
|
||||
margin = "Поле"
|
||||
positionAndFormatting = "Позиція та форматування"
|
||||
quickPosition = "Виберіть позицію на сторінці для розміщення штампа."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Під час додавання штампа до PDF сталася помилка."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Під час додавання штампа до PDF сталася
|
||||
[AddStampRequest.results]
|
||||
title = "Результати додавання штампа"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "видалення зображення,операції зі сторінками,серверна частина"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "Toàn màn hình"
|
||||
[settings.general.updates]
|
||||
title = "Cập nhật phần mềm"
|
||||
description = "Kiểm tra cập nhật và xem thông tin phiên bản"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "Phiên bản hiện tại"
|
||||
latestVersion = "Phiên bản mới nhất"
|
||||
checkForUpdates = "Kiểm tra cập nhật"
|
||||
viewDetails = "Xem chi tiết"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "Kích thước ảnh"
|
||||
margin = "Lề"
|
||||
positionAndFormatting = "Vị trí & định dạng"
|
||||
quickPosition = "Chọn một vị trí trên trang để đặt con dấu."
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Đã xảy ra lỗi khi thêm con dấu vào PDF."
|
||||
@@ -3594,14 +3567,6 @@ failed = "Đã xảy ra lỗi khi thêm con dấu vào PDF."
|
||||
[AddStampRequest.results]
|
||||
title = "Kết quả đóng dấu"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "Xóa ảnh,Thao tác trang,Back end,phía máy chủ"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "全屏"
|
||||
[settings.general.updates]
|
||||
title = "软件更新"
|
||||
description = "检查更新并查看版本信息"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "当前版本"
|
||||
latestVersion = "最新版本"
|
||||
checkForUpdates = "检查更新"
|
||||
viewDetails = "查看详情"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "图像大小"
|
||||
margin = "边距"
|
||||
positionAndFormatting = "位置与格式"
|
||||
quickPosition = "选择页面上的位置以放置印章。"
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "将印章添加到 PDF 时发生错误。"
|
||||
@@ -3594,14 +3567,6 @@ failed = "将印章添加到 PDF 时发生错误。"
|
||||
[AddStampRequest.results]
|
||||
title = "印章结果"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "པར་རིས་སུབ་པ།,ཤོག་ངོས་བཀོལ་སྤྱོད།,རྒྱབ་ངོས།,ཞབས་ཞུ་ཕྱོགས།"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "全屏"
|
||||
[settings.general.updates]
|
||||
title = "软件更新"
|
||||
description = "检查更新并查看版本信息"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "当前版本"
|
||||
latestVersion = "最新版本"
|
||||
checkForUpdates = "检查更新"
|
||||
viewDetails = "查看详情"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "图像大小"
|
||||
margin = "边距"
|
||||
positionAndFormatting = "位置与格式"
|
||||
quickPosition = "选择页面上的一个位置以放置图章。"
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "向 PDF 添加图章时发生错误。"
|
||||
@@ -3594,14 +3567,6 @@ failed = "向 PDF 添加图章时发生错误。"
|
||||
[AddStampRequest.results]
|
||||
title = "图章结果"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "删除图像, 页面操作, 后端, 服务端"
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ fullscreen = "全螢幕"
|
||||
[settings.general.updates]
|
||||
title = "軟體更新"
|
||||
description = "檢查更新並檢視版本資訊"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
currentVersion = "目前版本"
|
||||
latestVersion = "最新版本"
|
||||
checkForUpdates = "檢查更新"
|
||||
viewDetails = "檢視詳細資料"
|
||||
@@ -3562,31 +3560,6 @@ imageSize = "影像大小"
|
||||
margin = "邊距"
|
||||
positionAndFormatting = "位置與格式"
|
||||
quickPosition = "選擇頁面上的位置以放置印章。"
|
||||
clickToExpand = "Click to expand"
|
||||
customDateDesc = "Custom format"
|
||||
dateDesc = "Current date"
|
||||
dateTimeVars = "Date & Time"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
fileVars = "File Information"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
otherVars = "Other"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
preview = "Preview:"
|
||||
selectTemplate = "Select a template..."
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
timeDesc = "Current time"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "將印章加入 PDF 時發生錯誤。"
|
||||
@@ -3594,14 +3567,6 @@ failed = "將印章加入 PDF 時發生錯誤。"
|
||||
[AddStampRequest.results]
|
||||
title = "蓋章結果"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
draftWatermark = "Draft Watermark"
|
||||
europeanDate = "European Date"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
|
||||
[removeImagePdf]
|
||||
tags = "移除圖片,頁面操作,後端,伺服器端"
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- Enable printing capability -->
|
||||
<key>com.apple.security.print</key>
|
||||
<true/>
|
||||
|
||||
<!-- Enable network access for connecting to backend -->
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
|
||||
<!-- Enable file access for PDF handling -->
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
|
||||
<!-- Allow execution of bundled JRE -->
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -11,11 +11,8 @@ use rand::distributions::Alphanumeric;
|
||||
|
||||
const STORE_FILE: &str = "connection.json";
|
||||
const USER_INFO_KEY: &str = "user_info";
|
||||
const TOKENS_STORE_FILE: &str = "tokens.json";
|
||||
const REFRESH_TOKEN_STORE_KEY: &str = "refresh_token";
|
||||
const KEYRING_SERVICE: &str = "stirling-pdf";
|
||||
const KEYRING_TOKEN_KEY: &str = "auth-token";
|
||||
const KEYRING_REFRESH_TOKEN_KEY: &str = "refresh-token";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UserInfo {
|
||||
@@ -34,11 +31,6 @@ fn get_keyring_entry() -> Result<Entry, String> {
|
||||
Ok(entry)
|
||||
}
|
||||
|
||||
fn get_refresh_token_keyring_entry() -> Result<Entry, String> {
|
||||
Entry::new(KEYRING_SERVICE, KEYRING_REFRESH_TOKEN_KEY)
|
||||
.map_err(|e| format!("Failed to access keyring: {}", e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_auth_token(_app_handle: AppHandle, token: String) -> Result<(), String> {
|
||||
let trimmed = token.trim();
|
||||
@@ -109,112 +101,6 @@ pub async fn clear_auth_token(_app_handle: AppHandle) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_refresh_token(app_handle: AppHandle, token: String) -> Result<(), String> {
|
||||
log::info!("Saving refresh token - trying keyring first");
|
||||
|
||||
let entry = get_refresh_token_keyring_entry()?;
|
||||
|
||||
// Try keyring (works in production with code signing)
|
||||
match entry.set_password(&token) {
|
||||
Ok(_) => {
|
||||
// Verify it persists (fails in unsigned dev builds)
|
||||
match entry.get_password() {
|
||||
Ok(saved) if saved == token => {
|
||||
log::info!("✅ Refresh token saved to keyring (production mode)");
|
||||
return Ok(());
|
||||
}
|
||||
_ => {
|
||||
log::info!("Keyring doesn't persist - using Tauri Store fallback (dev mode)");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::info!("Keyring failed: {} - using Tauri Store fallback", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to Tauri Store (dev mode without code signing)
|
||||
let store = app_handle
|
||||
.store(TOKENS_STORE_FILE)
|
||||
.map_err(|e| format!("Failed to access tokens store: {}", e))?;
|
||||
|
||||
store.set(
|
||||
REFRESH_TOKEN_STORE_KEY,
|
||||
serde_json::to_value(&token)
|
||||
.map_err(|e| format!("Failed to serialize token: {}", e))?,
|
||||
);
|
||||
|
||||
store
|
||||
.save()
|
||||
.map_err(|e| format!("Failed to save tokens store: {}", e))?;
|
||||
|
||||
log::info!("✅ Refresh token saved to Tauri Store (fallback)");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_refresh_token(app_handle: AppHandle) -> Result<Option<String>, String> {
|
||||
// Try keyring first (production)
|
||||
let entry = get_refresh_token_keyring_entry()?;
|
||||
match entry.get_password() {
|
||||
Ok(token) => {
|
||||
log::info!("✅ Refresh token retrieved from keyring");
|
||||
return Ok(Some(token));
|
||||
}
|
||||
Err(keyring::Error::NoEntry) => {
|
||||
log::debug!("No token in keyring, trying Tauri Store");
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Keyring error: {} - trying Tauri Store", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to Tauri Store (dev)
|
||||
let store = app_handle
|
||||
.store(TOKENS_STORE_FILE)
|
||||
.map_err(|e| format!("Failed to access tokens store: {}", e))?;
|
||||
|
||||
let token: Option<String> = store
|
||||
.get(REFRESH_TOKEN_STORE_KEY)
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok());
|
||||
|
||||
if token.is_some() {
|
||||
log::info!("✅ Refresh token retrieved from Tauri Store");
|
||||
} else {
|
||||
log::info!("No refresh token found");
|
||||
}
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn clear_refresh_token(app_handle: AppHandle) -> Result<(), String> {
|
||||
log::info!("Clearing refresh token from all storage");
|
||||
|
||||
// Clear from keyring
|
||||
let entry = get_refresh_token_keyring_entry()?;
|
||||
match entry.delete_credential() {
|
||||
Ok(_) => log::info!("Cleared from keyring"),
|
||||
Err(keyring::Error::NoEntry) => log::debug!("Not in keyring"),
|
||||
Err(e) => log::warn!("Keyring clear error: {}", e),
|
||||
}
|
||||
|
||||
// Clear from Tauri Store
|
||||
let store = app_handle
|
||||
.store(TOKENS_STORE_FILE)
|
||||
.map_err(|e| format!("Failed to access tokens store: {}", e))?;
|
||||
|
||||
store.delete(REFRESH_TOKEN_STORE_KEY);
|
||||
|
||||
store
|
||||
.save()
|
||||
.map_err(|e| format!("Failed to save tokens store: {}", e))?;
|
||||
|
||||
log::info!("✅ Refresh token cleared");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_user_info(
|
||||
app_handle: AppHandle,
|
||||
|
||||
@@ -14,14 +14,11 @@ pub use connection::{
|
||||
};
|
||||
pub use auth::{
|
||||
clear_auth_token,
|
||||
clear_refresh_token,
|
||||
clear_user_info,
|
||||
get_auth_token,
|
||||
get_refresh_token,
|
||||
get_user_info,
|
||||
login,
|
||||
save_auth_token,
|
||||
save_refresh_token,
|
||||
save_user_info,
|
||||
start_oauth_login,
|
||||
};
|
||||
|
||||
@@ -9,20 +9,17 @@ use commands::{
|
||||
cleanup_backend,
|
||||
clear_auth_token,
|
||||
clear_opened_files,
|
||||
clear_refresh_token,
|
||||
clear_user_info,
|
||||
is_default_pdf_handler,
|
||||
get_auth_token,
|
||||
get_backend_port,
|
||||
get_connection_config,
|
||||
get_opened_files,
|
||||
get_refresh_token,
|
||||
get_user_info,
|
||||
is_first_launch,
|
||||
login,
|
||||
reset_setup_completion,
|
||||
save_auth_token,
|
||||
save_refresh_token,
|
||||
save_user_info,
|
||||
set_connection_mode,
|
||||
set_as_default_pdf_handler,
|
||||
@@ -146,9 +143,6 @@ pub fn run() {
|
||||
save_auth_token,
|
||||
get_auth_token,
|
||||
clear_auth_token,
|
||||
save_refresh_token,
|
||||
get_refresh_token,
|
||||
clear_refresh_token,
|
||||
save_user_info,
|
||||
get_user_info,
|
||||
clear_user_info,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "Stirling-PDF",
|
||||
"version": "2.4.3",
|
||||
"version": "2.4.2",
|
||||
"identifier": "stirling.pdf.dev",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
"devUrl": "http://localhost:5174",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"beforeDevCommand": "npm run dev -- --mode desktop",
|
||||
"beforeBuildCommand": "npm run build -- --mode desktop"
|
||||
},
|
||||
@@ -67,7 +67,7 @@
|
||||
"macOS": {
|
||||
"minimumSystemVersion": "10.15",
|
||||
"signingIdentity": null,
|
||||
"entitlements": "entitlements.plist",
|
||||
"entitlements": null,
|
||||
"providerShortName": null
|
||||
}
|
||||
},
|
||||
|
||||
@@ -141,15 +141,14 @@ export default function Workbench() {
|
||||
);
|
||||
|
||||
case "pageEditor":
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', flex: '1 1 0', height: 0 }}>
|
||||
<>
|
||||
<PageEditor
|
||||
onFunctionsReady={setPageEditorFunctions}
|
||||
/>
|
||||
{pageEditorFunctions && (
|
||||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 100 }}>
|
||||
<PageEditorControls
|
||||
<PageEditorControls
|
||||
onClosePdf={pageEditorFunctions.closePdf}
|
||||
onUndo={pageEditorFunctions.handleUndo}
|
||||
onRedo={pageEditorFunctions.handleRedo}
|
||||
@@ -169,9 +168,8 @@ export default function Workbench() {
|
||||
splitPositions={pageEditorFunctions.splitPositions}
|
||||
totalPages={pageEditorFunctions.totalPages}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
default:
|
||||
@@ -209,10 +207,9 @@ export default function Workbench() {
|
||||
|
||||
{/* Main content area */}
|
||||
<Box
|
||||
className={`flex-1 min-h-0 z-10 ${currentView === 'pageEditor' ? 'relative flex flex-col' : `relative ${styles.workbenchScrollable}`}`}
|
||||
className={`flex-1 min-h-0 relative z-10 ${styles.workbenchScrollable}`}
|
||||
style={{
|
||||
transition: 'opacity 0.15s ease-in-out',
|
||||
...(currentView === 'pageEditor' && { height: 0 }),
|
||||
}}
|
||||
>
|
||||
{renderMainContent()}
|
||||
|
||||
@@ -37,7 +37,6 @@ interface DragDropGridProps<T extends DragDropItem> {
|
||||
getThumbnailData?: (itemId: string) => { src: string; rotation: number } | null;
|
||||
zoomLevel?: number;
|
||||
selectedFileIds?: string[];
|
||||
onVisibleItemsChange?: (items: T[]) => void;
|
||||
}
|
||||
|
||||
type DropSide = 'left' | 'right' | null;
|
||||
@@ -199,7 +198,7 @@ interface DraggableItemProps<T extends DragDropItem> {
|
||||
zoomLevel: number;
|
||||
}
|
||||
|
||||
const DraggableItemInner = <T extends DragDropItem>({ item, index, itemRefs, boxSelectedPageIds, clearBoxSelection, getBoxSelection, activeId, activeDragIds, justMoved, getThumbnailData, renderItem, onUpdateDropTarget, zoomLevel }: DraggableItemProps<T>) => {
|
||||
const DraggableItem = <T extends DragDropItem>({ item, index, itemRefs, boxSelectedPageIds, clearBoxSelection, getBoxSelection, activeId, activeDragIds, justMoved, getThumbnailData, renderItem, onUpdateDropTarget, zoomLevel }: DraggableItemProps<T>) => {
|
||||
const isPlaceholder = Boolean(item.isPlaceholder);
|
||||
const pageNumber = (item as any).pageNumber ?? index + 1;
|
||||
const { attributes, listeners, setNodeRef: setDraggableRef } = useDraggable({
|
||||
@@ -253,31 +252,6 @@ const DraggableItemInner = <T extends DragDropItem>({ item, index, itemRefs, box
|
||||
);
|
||||
};
|
||||
|
||||
// Memoize to prevent unnecessary re-renders and hook thrashing
|
||||
const DraggableItem = React.memo(DraggableItemInner, (prevProps, nextProps) => {
|
||||
// Return true to SKIP re-render (props are equal)
|
||||
// Return false to RE-RENDER (props changed)
|
||||
|
||||
// Check if item reference or content changed (including thumbnail)
|
||||
const itemChanged = prevProps.item !== nextProps.item;
|
||||
|
||||
// If item object reference changed, we need to re-render
|
||||
if (itemChanged) {
|
||||
return false; // Props changed, re-render needed
|
||||
}
|
||||
|
||||
// Item reference is same, check other props
|
||||
return (
|
||||
prevProps.item.id === nextProps.item.id &&
|
||||
prevProps.index === nextProps.index &&
|
||||
prevProps.activeId === nextProps.activeId &&
|
||||
prevProps.justMoved === nextProps.justMoved &&
|
||||
prevProps.zoomLevel === nextProps.zoomLevel &&
|
||||
prevProps.activeDragIds.length === nextProps.activeDragIds.length &&
|
||||
prevProps.boxSelectedPageIds.length === nextProps.boxSelectedPageIds.length
|
||||
);
|
||||
}) as typeof DraggableItemInner;
|
||||
|
||||
const DragDropGrid = <T extends DragDropItem>({
|
||||
items,
|
||||
renderItem,
|
||||
@@ -285,7 +259,6 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
getThumbnailData,
|
||||
zoomLevel = 1.0,
|
||||
selectedFileIds,
|
||||
onVisibleItemsChange,
|
||||
}: DragDropGridProps<T>) => {
|
||||
const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -448,21 +421,6 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
overscan: OVERSCAN,
|
||||
});
|
||||
|
||||
const virtualRows = rowVirtualizer.getVirtualItems();
|
||||
|
||||
useEffect(() => {
|
||||
if (!onVisibleItemsChange) return;
|
||||
|
||||
const visibleItemsForCallback: T[] = [];
|
||||
virtualRows.forEach((row) => {
|
||||
const startIndex = row.index * itemsPerRow;
|
||||
const endIndex = Math.min(startIndex + itemsPerRow, visibleItems.length);
|
||||
visibleItemsForCallback.push(...visibleItems.slice(startIndex, endIndex));
|
||||
});
|
||||
|
||||
onVisibleItemsChange(visibleItemsForCallback);
|
||||
}, [virtualRows, visibleItems, itemsPerRow, onVisibleItemsChange]);
|
||||
|
||||
// Re-measure virtualizer when zoom or items per row changes
|
||||
useEffect(() => {
|
||||
rowVirtualizer.measure();
|
||||
@@ -761,7 +719,7 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
margin: '0 auto',
|
||||
}}
|
||||
>
|
||||
{virtualRows.map((virtualRow) => {
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const startIndex = virtualRow.index * itemsPerRow;
|
||||
const endIndex = Math.min(startIndex + itemsPerRow, visibleItems.length);
|
||||
const rowItems = visibleItems.slice(startIndex, endIndex);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core";
|
||||
import { useFileState, useFileActions } from "@app/contexts/FileContext";
|
||||
import { useNavigationGuard } from "@app/contexts/NavigationContext";
|
||||
import { usePageEditor } from "@app/contexts/PageEditorContext";
|
||||
import { PageEditorFunctions, PDFPage } from "@app/types/pageEditor";
|
||||
import { PageEditorFunctions } from "@app/types/pageEditor";
|
||||
// Thumbnail generation is now handled by individual PageThumbnail components
|
||||
import '@app/components/pageEditor/PageEditor.module.css';
|
||||
import PageThumbnail from '@app/components/pageEditor/PageThumbnail';
|
||||
@@ -23,7 +23,6 @@ import { useUndoManagerState } from "@app/components/pageEditor/hooks/useUndoMan
|
||||
import { usePageSelectionManager } from "@app/components/pageEditor/hooks/usePageSelectionManager";
|
||||
import { usePageEditorCommands } from "@app/components/pageEditor/hooks/useEditorCommands";
|
||||
import { usePageEditorExport } from "@app/components/pageEditor/hooks/usePageEditorExport";
|
||||
import { useThumbnailGeneration } from "@app/hooks/useThumbnailGeneration";
|
||||
|
||||
export interface PageEditorProps {
|
||||
onFunctionsReady?: (functions: PageEditorFunctions) => void;
|
||||
@@ -41,27 +40,7 @@ const PageEditor = ({
|
||||
const { setHasUnsavedChanges } = useNavigationGuard();
|
||||
|
||||
// Get PageEditor coordination functions
|
||||
const {
|
||||
updateFileOrderFromPages,
|
||||
fileOrder,
|
||||
reorderedPages,
|
||||
clearReorderedPages,
|
||||
updateCurrentPages,
|
||||
savePersistedDocument,
|
||||
} = usePageEditor();
|
||||
|
||||
const [visiblePageIds, setVisiblePageIds] = useState<string[]>([]);
|
||||
const thumbnailRequestsRef = useRef<Set<string>>(new Set());
|
||||
const { requestThumbnail, getThumbnailFromCache } = useThumbnailGeneration();
|
||||
const handleVisibleItemsChange = useCallback((items: PDFPage[]) => {
|
||||
setVisiblePageIds(prev => {
|
||||
const ids = items.map(item => item.id);
|
||||
if (prev.length === ids.length && prev.every((id, index) => id === ids[index])) {
|
||||
return prev;
|
||||
}
|
||||
return ids;
|
||||
});
|
||||
}, []);
|
||||
const { updateFileOrderFromPages, fileOrder, reorderedPages, clearReorderedPages, updateCurrentPages } = usePageEditor();
|
||||
|
||||
// Zoom state management
|
||||
const [zoomLevel, setZoomLevel] = useState(1.0);
|
||||
@@ -170,21 +149,6 @@ const PageEditor = ({
|
||||
updateCurrentPages,
|
||||
});
|
||||
|
||||
const displayDocumentRef = useRef(displayDocument);
|
||||
useEffect(() => {
|
||||
displayDocumentRef.current = displayDocument;
|
||||
}, [displayDocument]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
const doc = displayDocumentRef.current;
|
||||
if (doc && doc.pages.length > 0) {
|
||||
const signature = doc.pages.map(page => page.id).join(',');
|
||||
savePersistedDocument(doc, signature);
|
||||
}
|
||||
};
|
||||
}, [savePersistedDocument]);
|
||||
|
||||
// UI state management
|
||||
const {
|
||||
selectionMode, selectedPageIds, movingPage, isAnimating, splitPositions, exportLoading,
|
||||
@@ -267,92 +231,6 @@ const PageEditor = ({
|
||||
setSplitPositions,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!displayDocument || visiblePageIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pending = thumbnailRequestsRef.current.size;
|
||||
const MAX_CONCURRENT_THUMBNAILS = 12;
|
||||
const available = Math.max(0, MAX_CONCURRENT_THUMBNAILS - pending);
|
||||
if (available === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toLoad: string[] = [];
|
||||
for (const pageId of visiblePageIds) {
|
||||
if (toLoad.length >= available) break;
|
||||
if (thumbnailRequestsRef.current.has(pageId)) continue;
|
||||
const page = displayDocument.pages.find(p => p.id === pageId);
|
||||
if (!page || page.thumbnail) continue;
|
||||
toLoad.push(pageId);
|
||||
}
|
||||
|
||||
if (toLoad.length === 0) return;
|
||||
|
||||
toLoad.forEach(pageId => {
|
||||
const page = displayDocument.pages.find(p => p.id === pageId);
|
||||
if (!page) return;
|
||||
|
||||
const cached = getThumbnailFromCache(pageId);
|
||||
if (cached) {
|
||||
thumbnailRequestsRef.current.add(pageId);
|
||||
Promise.resolve(cached)
|
||||
.then(cache => {
|
||||
setEditedDocument(prev => {
|
||||
if (!prev) return prev;
|
||||
const pageIndex = prev.pages.findIndex(p => p.id === pageId);
|
||||
if (pageIndex === -1) return prev;
|
||||
|
||||
// Only create new page object for the changed page, reuse rest
|
||||
const updated = [...prev.pages];
|
||||
updated[pageIndex] = { ...prev.pages[pageIndex], thumbnail: cache };
|
||||
return { ...prev, pages: updated };
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
thumbnailRequestsRef.current.delete(pageId);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const fileId = page.originalFileId;
|
||||
if (!fileId) return;
|
||||
const file = selectors.getFile(fileId);
|
||||
if (!file) return;
|
||||
|
||||
thumbnailRequestsRef.current.add(pageId);
|
||||
requestThumbnail(pageId, file, page.originalPageNumber || page.pageNumber)
|
||||
.then(thumbnail => {
|
||||
if (thumbnail) {
|
||||
setEditedDocument(prev => {
|
||||
if (!prev) return prev;
|
||||
const pageIndex = prev.pages.findIndex(p => p.id === pageId);
|
||||
if (pageIndex === -1) return prev;
|
||||
|
||||
// Only create new page object for the changed page, reuse rest
|
||||
const updated = [...prev.pages];
|
||||
updated[pageIndex] = { ...prev.pages[pageIndex], thumbnail };
|
||||
return { ...prev, pages: updated };
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[Thumbnail Loading] Error:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
thumbnailRequestsRef.current.delete(pageId);
|
||||
});
|
||||
});
|
||||
}, [
|
||||
displayDocument,
|
||||
visiblePageIds,
|
||||
selectors,
|
||||
requestThumbnail,
|
||||
getThumbnailFromCache,
|
||||
setEditedDocument,
|
||||
]);
|
||||
|
||||
// Derived values for right rail and usePageEditorRightRailButtons (must be after displayDocument)
|
||||
const selectedPageCount = selectedPageIds.length;
|
||||
const activeFileIds = selectedFileIds;
|
||||
@@ -467,17 +345,12 @@ const PageEditor = ({
|
||||
const fileColorIndexMap = useFileColorMap(orderedFileIds);
|
||||
|
||||
return (
|
||||
<div
|
||||
<Box
|
||||
ref={containerRef}
|
||||
pos="relative"
|
||||
data-scrolling-container="true"
|
||||
onMouseEnter={() => setIsContainerHovered(true)}
|
||||
onMouseLeave={() => setIsContainerHovered(false)}
|
||||
style={{
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<LoadingOverlay visible={globalProcessing && !initialDocument} />
|
||||
|
||||
@@ -499,7 +372,7 @@ const PageEditor = ({
|
||||
)}
|
||||
|
||||
{displayDocument && (
|
||||
<Box ref={gridContainerRef} p={0} pt="2rem" pb="4rem" style={{ position: 'relative' }}>
|
||||
<Box ref={gridContainerRef} p={0} pt="2rem" pb="15rem" style={{ position: 'relative' }}>
|
||||
|
||||
{/* Split Lines Overlay */}
|
||||
<div
|
||||
@@ -577,7 +450,6 @@ const PageEditor = ({
|
||||
onReorderPages={handleReorderPages}
|
||||
zoomLevel={zoomLevel}
|
||||
selectedFileIds={selectedFileIds}
|
||||
onVisibleItemsChange={handleVisibleItemsChange}
|
||||
getThumbnailData={(pageId) => {
|
||||
const page = displayDocument.pages.find(p => p.id === pageId);
|
||||
if (!page?.thumbnail) return null;
|
||||
@@ -596,6 +468,7 @@ const PageEditor = ({
|
||||
page={page}
|
||||
index={index}
|
||||
totalPages={displayDocument.pages.length}
|
||||
originalFile={(page as any).originalFileId ? selectors.getFile((page as any).originalFileId) : undefined}
|
||||
fileColorIndex={fileColorIndex}
|
||||
selectedPageIds={selectedPageIds}
|
||||
selectionMode={selectionMode}
|
||||
@@ -639,7 +512,7 @@ const PageEditor = ({
|
||||
}}
|
||||
/>
|
||||
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import ContentCutIcon from '@mui/icons-material/ContentCut';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import { PDFPage, PDFDocument } from '@app/types/pageEditor';
|
||||
import { useThumbnailGeneration } from '@app/hooks/useThumbnailGeneration';
|
||||
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
|
||||
import { getFileColorWithOpacity } from '@app/components/pageEditor/fileColors';
|
||||
import styles from '@app/components/pageEditor/PageEditor.module.css';
|
||||
@@ -21,6 +22,7 @@ interface PageThumbnailProps {
|
||||
page: PDFPage;
|
||||
index: number;
|
||||
totalPages: number;
|
||||
originalFile?: File;
|
||||
fileColorIndex: number;
|
||||
selectedPageIds: string[];
|
||||
selectionMode: boolean;
|
||||
@@ -53,6 +55,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
page,
|
||||
index: _index,
|
||||
totalPages,
|
||||
originalFile,
|
||||
fileColorIndex,
|
||||
selectedPageIds,
|
||||
selectionMode,
|
||||
@@ -87,6 +90,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
|
||||
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
|
||||
const elementRef = useRef<HTMLDivElement | null>(null);
|
||||
const { getThumbnailFromCache, requestThumbnail} = useThumbnailGeneration();
|
||||
const { openFilesModal } = useFilesModalContext();
|
||||
|
||||
// Check if this page is currently being dragged
|
||||
@@ -111,6 +115,43 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
}
|
||||
}, [page.thumbnail, thumbnailUrl]);
|
||||
|
||||
// Request thumbnail if missing (on-demand, virtualized approach)
|
||||
useEffect(() => {
|
||||
let isCancelled = false;
|
||||
|
||||
// If we already have a thumbnail, use it
|
||||
if (page.thumbnail) {
|
||||
setThumbnailUrl(page.thumbnail);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
const cachedThumbnail = getThumbnailFromCache(page.id);
|
||||
if (cachedThumbnail) {
|
||||
setThumbnailUrl(cachedThumbnail);
|
||||
return;
|
||||
}
|
||||
|
||||
// Request thumbnail generation if we have the original file
|
||||
if (originalFile) {
|
||||
const pageNumber = page.originalPageNumber;
|
||||
|
||||
requestThumbnail(page.id, originalFile, pageNumber)
|
||||
.then(thumbnail => {
|
||||
if (!isCancelled && thumbnail) {
|
||||
setThumbnailUrl(thumbnail);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.warn(`Failed to generate thumbnail for ${page.id}:`, error);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [page.id, page.thumbnail, originalFile, getThumbnailFromCache, requestThumbnail]);
|
||||
|
||||
// Merge refs - combine our ref tracking with dnd-kit's ref
|
||||
const mergedRef = useCallback((element: HTMLDivElement | null) => {
|
||||
// Track in our refs map
|
||||
|
||||
@@ -748,49 +748,43 @@ export class InsertFilesCommand extends DOMCommand {
|
||||
console.log('Pages:', pages.length);
|
||||
console.log('ArrayBuffer size:', arrayBuffer?.byteLength || 'undefined');
|
||||
|
||||
try {
|
||||
if (arrayBuffer && arrayBuffer.byteLength > 0) {
|
||||
// Extract page numbers for all pages from this file
|
||||
const pageNumbers = pages.map(page => {
|
||||
const pageNumMatch = page.id.match(/-page-(\d+)$/);
|
||||
return pageNumMatch ? parseInt(pageNumMatch[1]) : 1;
|
||||
});
|
||||
if (arrayBuffer && arrayBuffer.byteLength > 0) {
|
||||
// Extract page numbers for all pages from this file
|
||||
const pageNumbers = pages.map(page => {
|
||||
const pageNumMatch = page.id.match(/-page-(\d+)$/);
|
||||
return pageNumMatch ? parseInt(pageNumMatch[1]) : 1;
|
||||
});
|
||||
|
||||
console.log('Generating thumbnails for page numbers:', pageNumbers);
|
||||
console.log('Generating thumbnails for page numbers:', pageNumbers);
|
||||
|
||||
// Generate thumbnails for all pages from this file at once
|
||||
const results = await thumbnailGenerationService.generateThumbnails(
|
||||
fileId,
|
||||
arrayBuffer,
|
||||
pageNumbers,
|
||||
{ scale: 0.2, quality: 0.8 }
|
||||
);
|
||||
// Generate thumbnails for all pages from this file at once
|
||||
const results = await thumbnailGenerationService.generateThumbnails(
|
||||
fileId,
|
||||
arrayBuffer,
|
||||
pageNumbers,
|
||||
{ scale: 0.2, quality: 0.8 }
|
||||
);
|
||||
|
||||
console.log('Thumbnail generation results:', results.length, 'thumbnails generated');
|
||||
console.log('Thumbnail generation results:', results.length, 'thumbnails generated');
|
||||
|
||||
// Update pages with generated thumbnails
|
||||
for (let i = 0; i < results.length && i < pages.length; i++) {
|
||||
const result = results[i];
|
||||
const page = pages[i];
|
||||
// Update pages with generated thumbnails
|
||||
for (let i = 0; i < results.length && i < pages.length; i++) {
|
||||
const result = results[i];
|
||||
const page = pages[i];
|
||||
|
||||
if (result.success) {
|
||||
const pageIndex = updatedDocument.pages.findIndex(p => p.id === page.id);
|
||||
if (pageIndex >= 0) {
|
||||
updatedDocument.pages[pageIndex].thumbnail = result.thumbnail;
|
||||
console.log('Updated thumbnail for page:', page.id);
|
||||
}
|
||||
if (result.success) {
|
||||
const pageIndex = updatedDocument.pages.findIndex(p => p.id === page.id);
|
||||
if (pageIndex >= 0) {
|
||||
updatedDocument.pages[pageIndex].thumbnail = result.thumbnail;
|
||||
console.log('Updated thumbnail for page:', page.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger re-render by updating the document
|
||||
this.setDocument({ ...updatedDocument });
|
||||
} else {
|
||||
console.error('No valid ArrayBuffer found for file ID:', fileId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to generate thumbnails for file:', fileId, error);
|
||||
} finally {
|
||||
this.fileDataMap.delete(fileId);
|
||||
|
||||
// Trigger re-render by updating the document
|
||||
this.setDocument({ ...updatedDocument });
|
||||
} else {
|
||||
console.error('No valid ArrayBuffer found for file ID:', fileId);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,6 +3,6 @@ export const GRID_CONSTANTS = {
|
||||
ITEM_WIDTH: '20rem', // page width
|
||||
ITEM_HEIGHT: '21.5rem', // 20rem + 1.5rem gap
|
||||
ITEM_GAP: '1.5rem', // gap between items
|
||||
OVERSCAN_SMALL: 8, // Overscan for normal documents
|
||||
OVERSCAN_LARGE: 12, // Overscan for large documents (12 rows = ~96 pages pre-rendered)
|
||||
OVERSCAN_SMALL: 4, // Overscan for normal documents
|
||||
OVERSCAN_LARGE: 8, // Overscan for large documents (>1000 pages)
|
||||
} as const;
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useMemo, useEffect, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useFileState } from '@app/contexts/FileContext';
|
||||
import { usePageEditor } from '@app/contexts/PageEditorContext';
|
||||
import { PDFDocument, PDFPage } from '@app/types/pageEditor';
|
||||
import { FileId } from '@app/types/file';
|
||||
import { FileAnalyzer } from '@app/services/fileAnalyzer';
|
||||
|
||||
export interface PageDocumentHook {
|
||||
document: PDFDocument | null;
|
||||
@@ -17,7 +16,7 @@ export interface PageDocumentHook {
|
||||
*/
|
||||
export function usePageDocument(): PageDocumentHook {
|
||||
const { state, selectors } = useFileState();
|
||||
const { fileOrder, currentPages, persistedDocument, persistedDocumentSignature } = usePageEditor();
|
||||
const { fileOrder, currentPages } = usePageEditor();
|
||||
|
||||
// Use PageEditorContext's fileOrder instead of FileContext's global order
|
||||
// This ensures the page editor respects its own workspace ordering
|
||||
@@ -59,63 +58,6 @@ export function usePageDocument(): PageDocumentHook {
|
||||
const processedFilePages = primaryStirlingFileStub?.processedFile?.pages;
|
||||
const processedFileTotalPages = primaryStirlingFileStub?.processedFile?.totalPages;
|
||||
|
||||
const [placeholderDocument, setPlaceholderDocument] = useState<PDFDocument | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!primaryFileId) {
|
||||
setPlaceholderDocument(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (primaryStirlingFileStub?.processedFile) {
|
||||
setPlaceholderDocument(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const file = selectors.getFile(primaryFileId);
|
||||
if (!file) {
|
||||
setPlaceholderDocument(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let canceled = false;
|
||||
|
||||
const loadPlaceholder = async () => {
|
||||
try {
|
||||
const analysis = await FileAnalyzer.quickPDFAnalysis(file);
|
||||
if (canceled) return;
|
||||
const totalPages = Math.max(1, analysis.pageCount || 1);
|
||||
const pages: PDFPage[] = Array.from({ length: totalPages }, (_, index) => ({
|
||||
id: `placeholder-${primaryFileId}-page-${index + 1}`,
|
||||
pageNumber: index + 1,
|
||||
thumbnail: null,
|
||||
rotation: 0,
|
||||
selected: false,
|
||||
originalFileId: primaryFileId,
|
||||
originalPageNumber: index + 1,
|
||||
}));
|
||||
|
||||
setPlaceholderDocument({
|
||||
id: `placeholder-${primaryFileId}`,
|
||||
name: selectors.getStirlingFileStub(primaryFileId)?.name ?? file.name,
|
||||
file,
|
||||
pages,
|
||||
totalPages,
|
||||
});
|
||||
} catch {
|
||||
if (!canceled) {
|
||||
setPlaceholderDocument(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadPlaceholder();
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [primaryFileId, primaryStirlingFileStub?.processedFile, selectors]);
|
||||
|
||||
// Compute merged document with stable signature (prevents infinite loops)
|
||||
const currentPagesSignature = useMemo(() => {
|
||||
return currentPages ? currentPages.map(page => page.id).join(',') : '';
|
||||
@@ -124,20 +66,7 @@ export function usePageDocument(): PageDocumentHook {
|
||||
const mergedPdfDocument = useMemo((): PDFDocument | null => {
|
||||
if (activeFileIds.length === 0) return null;
|
||||
|
||||
if (
|
||||
persistedDocument &&
|
||||
persistedDocumentSignature &&
|
||||
persistedDocumentSignature === currentPagesSignature &&
|
||||
currentPagesSignature.length > 0
|
||||
) {
|
||||
return persistedDocument;
|
||||
}
|
||||
|
||||
if (!primaryStirlingFileStub?.processedFile && placeholderDocument) {
|
||||
return placeholderDocument;
|
||||
}
|
||||
|
||||
const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null;
|
||||
const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null;
|
||||
|
||||
// If we have file IDs but no file record, something is wrong - return null to show loading
|
||||
if (!primaryStirlingFileStub) {
|
||||
@@ -316,24 +245,7 @@ export function usePageDocument(): PageDocumentHook {
|
||||
};
|
||||
|
||||
return mergedDoc;
|
||||
}, [
|
||||
activeFileIds,
|
||||
selectedActiveFileIds,
|
||||
primaryFileId,
|
||||
primaryStirlingFileStub,
|
||||
processedFilePages,
|
||||
processedFileTotalPages,
|
||||
selectors,
|
||||
activeFilesSignature,
|
||||
selectedFileIdsKey,
|
||||
state.ui.selectedFileIds,
|
||||
allFileIds,
|
||||
currentPagesSignature,
|
||||
currentPages,
|
||||
persistedDocument,
|
||||
persistedDocumentSignature,
|
||||
placeholderDocument,
|
||||
]);
|
||||
}, [activeFileIds, selectedActiveFileIds, primaryFileId, primaryStirlingFileStub, processedFilePages, processedFileTotalPages, selectors, activeFilesSignature, selectedFileIdsKey, state.ui.selectedFileIds, allFileIds, currentPagesSignature, currentPages]);
|
||||
|
||||
// Large document detection for smart loading
|
||||
const isVeryLargeDocument = useMemo(() => {
|
||||
|
||||
@@ -1,32 +1,15 @@
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import {
|
||||
Paper,
|
||||
Stack,
|
||||
Switch,
|
||||
Text,
|
||||
Tooltip,
|
||||
NumberInput,
|
||||
SegmentedControl,
|
||||
Code,
|
||||
Group,
|
||||
Anchor,
|
||||
ActionIcon,
|
||||
Button,
|
||||
Badge,
|
||||
Alert,
|
||||
} from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { usePreferences } from "@app/contexts/PreferencesContext";
|
||||
import { useAppConfig } from "@app/contexts/AppConfigContext";
|
||||
import type { ToolPanelMode } from "@app/constants/toolPanel";
|
||||
import LocalIcon from "@app/components/shared/LocalIcon";
|
||||
import { updateService, UpdateSummary } from "@app/services/updateService";
|
||||
import UpdateModal from "@app/components/shared/UpdateModal";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Paper, Stack, Switch, Text, Tooltip, NumberInput, SegmentedControl, Code, Group, Anchor, ActionIcon, Button, Badge, Alert } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePreferences } from '@app/contexts/PreferencesContext';
|
||||
import { useAppConfig } from '@app/contexts/AppConfigContext';
|
||||
import type { ToolPanelMode } from '@app/constants/toolPanel';
|
||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
import { updateService, UpdateSummary } from '@app/services/updateService';
|
||||
import UpdateModal from '@app/components/shared/UpdateModal';
|
||||
|
||||
const DEFAULT_AUTO_UNZIP_FILE_LIMIT = 4;
|
||||
const BANNER_DISMISSED_KEY = "stirlingpdf_features_banner_dismissed";
|
||||
const BANNER_DISMISSED_KEY = 'stirlingpdf_features_banner_dismissed';
|
||||
|
||||
interface GeneralSectionProps {
|
||||
hideTitle?: boolean;
|
||||
@@ -39,15 +22,11 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
|
||||
const [fileLimitInput, setFileLimitInput] = useState<number | string>(preferences.autoUnzipFileLimit);
|
||||
const [bannerDismissed, setBannerDismissed] = useState(() => {
|
||||
// Check localStorage on mount
|
||||
return localStorage.getItem(BANNER_DISMISSED_KEY) === "true";
|
||||
return localStorage.getItem(BANNER_DISMISSED_KEY) === 'true';
|
||||
});
|
||||
const [updateSummary, setUpdateSummary] = useState<UpdateSummary | null>(null);
|
||||
const [updateModalOpened, setUpdateModalOpened] = useState(false);
|
||||
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
||||
const [mismatchVersion, setMismatchVersion] = useState(false);
|
||||
const isTauriApp = useMemo(() => isTauri(), []);
|
||||
const [appVersion, setAppVersion] = useState<string | null>(null);
|
||||
const frontendVersionLabel = appVersion ?? t("common.loading", "Loading...");
|
||||
|
||||
// Sync local state with preference changes
|
||||
useEffect(() => {
|
||||
@@ -70,7 +49,7 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
|
||||
const machineInfo = {
|
||||
machineType: config.machineType,
|
||||
activeSecurity: config.activeSecurity ?? false,
|
||||
licenseType: config.license ?? "NORMAL",
|
||||
licenseType: config.license ?? 'NORMAL',
|
||||
};
|
||||
|
||||
const summary = await updateService.getUpdateSummary(config.appVersion, machineInfo);
|
||||
@@ -89,126 +68,67 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
|
||||
setCheckingUpdate(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauriApp) {
|
||||
setMismatchVersion(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
const fetchFrontendVersion = async () => {
|
||||
try {
|
||||
const frontendVersion = await getVersion();
|
||||
if (!cancelled) {
|
||||
setAppVersion(frontendVersion);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[GeneralSection] Failed to fetch frontend version:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchFrontendVersion();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isTauriApp]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauriApp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!appVersion || !config?.appVersion) {
|
||||
setMismatchVersion(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (appVersion !== config.appVersion) {
|
||||
console.warn("[GeneralSection] Mismatch between Tauri version and AppConfig version:", {
|
||||
backendVersion: config.appVersion,
|
||||
frontendVersion: appVersion,
|
||||
});
|
||||
setMismatchVersion(true);
|
||||
} else {
|
||||
setMismatchVersion(false);
|
||||
}
|
||||
}, [isTauriApp, appVersion, config?.appVersion]);
|
||||
|
||||
// Check if login is disabled
|
||||
const loginDisabled = !config?.enableLogin;
|
||||
|
||||
const handleDismissBanner = () => {
|
||||
setBannerDismissed(true);
|
||||
localStorage.setItem(BANNER_DISMISSED_KEY, "true");
|
||||
localStorage.setItem(BANNER_DISMISSED_KEY, 'true');
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
{!hideTitle && (
|
||||
<div>
|
||||
<Text fw={600} size="lg">
|
||||
{t("settings.general.title", "General")}
|
||||
</Text>
|
||||
<Text fw={600} size="lg">{t('settings.general.title', 'General')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("settings.general.description", "Configure general application preferences.")}
|
||||
{t('settings.general.description', 'Configure general application preferences.')}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loginDisabled && !bannerDismissed && (
|
||||
<Paper withBorder p="md" radius="md" style={{ background: "var(--mantine-color-blue-0)", position: "relative" }}>
|
||||
<Paper withBorder p="md" radius="md" style={{ background: 'var(--mantine-color-blue-0)', position: 'relative' }}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="sm"
|
||||
style={{ position: "absolute", top: "0.5rem", right: "0.5rem" }}
|
||||
style={{ position: 'absolute', top: '0.5rem', right: '0.5rem' }}
|
||||
onClick={handleDismissBanner}
|
||||
aria-label={t("settings.general.enableFeatures.dismiss", "Dismiss")}
|
||||
aria-label={t('settings.general.enableFeatures.dismiss', 'Dismiss')}
|
||||
>
|
||||
<LocalIcon icon="close-rounded" width="1rem" height="1rem" />
|
||||
</ActionIcon>
|
||||
<Stack gap="sm">
|
||||
<Group gap="xs">
|
||||
<LocalIcon
|
||||
icon="admin-panel-settings-rounded"
|
||||
width="1.2rem"
|
||||
height="1.2rem"
|
||||
style={{ color: "var(--mantine-color-blue-6)" }}
|
||||
/>
|
||||
<Text fw={600} size="sm" style={{ color: "var(--mantine-color-blue-9)" }}>
|
||||
{t("settings.general.enableFeatures.title", "For System Administrators")}
|
||||
<LocalIcon icon="admin-panel-settings-rounded" width="1.2rem" height="1.2rem" style={{ color: 'var(--mantine-color-blue-6)' }} />
|
||||
<Text fw={600} size="sm" style={{ color: 'var(--mantine-color-blue-9)' }}>
|
||||
{t('settings.general.enableFeatures.title', 'For System Administrators')}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t(
|
||||
"settings.general.enableFeatures.intro",
|
||||
"Enable user authentication, team management, and workspace features for your organization.",
|
||||
)}
|
||||
{t('settings.general.enableFeatures.intro', 'Enable user authentication, team management, and workspace features for your organization.')}
|
||||
</Text>
|
||||
<Group gap="xs" wrap="wrap">
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("settings.general.enableFeatures.action", "Configure")}
|
||||
{t('settings.general.enableFeatures.action', 'Configure')}
|
||||
</Text>
|
||||
<Code>SECURITY_ENABLELOGIN=true</Code>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("settings.general.enableFeatures.and", "and")}
|
||||
{t('settings.general.enableFeatures.and', 'and')}
|
||||
</Text>
|
||||
<Code>DISABLE_ADDITIONAL_FEATURES=false</Code>
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed" fs="italic">
|
||||
{t(
|
||||
"settings.general.enableFeatures.benefit",
|
||||
"Enables user roles, team collaboration, admin controls, and enterprise features.",
|
||||
)}
|
||||
{t('settings.general.enableFeatures.benefit', 'Enables user roles, team collaboration, admin controls, and enterprise features.')}
|
||||
</Text>
|
||||
<Anchor
|
||||
href="https://docs.stirlingpdf.com/Configuration/System%20and%20Security/"
|
||||
href="https://docs.stirlingpdf.com/Advanced%20Configuration/System%20and%20Security"
|
||||
target="_blank"
|
||||
size="sm"
|
||||
style={{ color: "var(--mantine-color-blue-6)" }}
|
||||
style={{ color: 'var(--mantine-color-blue-6)' }}
|
||||
>
|
||||
{t("settings.general.enableFeatures.learnMore", "Learn more in documentation")} →
|
||||
{t('settings.general.enableFeatures.learnMore', 'Learn more in documentation')} →
|
||||
</Anchor>
|
||||
</Stack>
|
||||
</Paper>
|
||||
@@ -222,52 +142,36 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
|
||||
<Group justify="space-between" align="center">
|
||||
<div>
|
||||
<Text fw={600} size="sm">
|
||||
{t("settings.general.updates.title", "Software Updates")}
|
||||
{t('settings.general.updates.title', 'Software Updates')}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t("settings.general.updates.description", "Check for updates and view version information")}
|
||||
{t('settings.general.updates.description', 'Check for updates and view version information')}
|
||||
</Text>
|
||||
</div>
|
||||
{updateSummary && (
|
||||
<Badge color={updateSummary.max_priority === "urgent" ? "red" : "blue"} variant="filled">
|
||||
{updateSummary.max_priority === "urgent"
|
||||
? t("update.urgentUpdateAvailable", "Urgent Update")
|
||||
: t("update.updateAvailable", "Update Available")}
|
||||
<Badge
|
||||
color={updateSummary.max_priority === 'urgent' ? 'red' : 'blue'}
|
||||
variant="filled"
|
||||
>
|
||||
{updateSummary.max_priority === 'urgent'
|
||||
? t('update.urgentUpdateAvailable', 'Urgent Update')
|
||||
: t('update.updateAvailable', 'Update Available')}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
</div>
|
||||
{isTauriApp && (
|
||||
<Group justify="space-between" align="center">
|
||||
<div>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("settings.general.updates.currentFrontendVersion", "Current Frontend Version")}:{" "}
|
||||
<Text component="span" fw={500}>
|
||||
{frontendVersionLabel}
|
||||
</Text>
|
||||
</Text>
|
||||
{mismatchVersion && (
|
||||
<Text size="sm" c="red" mt={4}>
|
||||
{t(
|
||||
"settings.general.updates.versionMismatch",
|
||||
"Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version.",
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
<Group justify="space-between" align="center">
|
||||
<div>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("settings.general.updates.currentBackendVersion", "Current Backend Version")}:{" "}
|
||||
{t('settings.general.updates.currentVersion', 'Current Version')}:{' '}
|
||||
<Text component="span" fw={500}>
|
||||
{config.appVersion}
|
||||
</Text>
|
||||
</Text>
|
||||
{updateSummary && (
|
||||
<Text size="sm" c="dimmed" mt={4}>
|
||||
{t("settings.general.updates.latestVersion", "Latest Version")}:{" "}
|
||||
{t('settings.general.updates.latestVersion', 'Latest Version')}:{' '}
|
||||
<Text component="span" fw={500} c="blue">
|
||||
{updateSummary.latest_version}
|
||||
</Text>
|
||||
@@ -282,16 +186,16 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
|
||||
loading={checkingUpdate}
|
||||
leftSection={<LocalIcon icon="refresh-rounded" width="1rem" height="1rem" />}
|
||||
>
|
||||
{t("settings.general.updates.checkForUpdates", "Check for Updates")}
|
||||
{t('settings.general.updates.checkForUpdates', 'Check for Updates')}
|
||||
</Button>
|
||||
{updateSummary && (
|
||||
<Button
|
||||
size="sm"
|
||||
color={updateSummary.max_priority === "urgent" ? "red" : "blue"}
|
||||
color={updateSummary.max_priority === 'urgent' ? 'red' : 'blue'}
|
||||
onClick={() => setUpdateModalOpened(true)}
|
||||
leftSection={<LocalIcon icon="system-update-alt-rounded" width="1rem" height="1rem" />}
|
||||
>
|
||||
{t("settings.general.updates.viewDetails", "View Details")}
|
||||
{t('settings.general.updates.viewDetails', 'View Details')}
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
@@ -300,15 +204,15 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
|
||||
{updateSummary?.any_breaking && (
|
||||
<Alert
|
||||
color="orange"
|
||||
title={t("update.breakingChangesDetected", "Breaking Changes Detected")}
|
||||
title={t('update.breakingChangesDetected', 'Breaking Changes Detected')}
|
||||
styles={{
|
||||
title: { fontWeight: 600 },
|
||||
title: { fontWeight: 600 }
|
||||
}}
|
||||
>
|
||||
<Text size="sm">
|
||||
{t(
|
||||
"update.breakingChangesMessage",
|
||||
"Some versions contain breaking changes. Please review the migration guides before updating.",
|
||||
'update.breakingChangesMessage',
|
||||
'Some versions contain breaking changes. Please review the migration guides before updating.'
|
||||
)}
|
||||
</Text>
|
||||
</Alert>
|
||||
@@ -319,102 +223,87 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
|
||||
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">
|
||||
{t("settings.general.defaultToolPickerMode", "Default tool picker mode")}
|
||||
{t('settings.general.defaultToolPickerMode', 'Default tool picker mode')}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t(
|
||||
"settings.general.defaultToolPickerModeDescription",
|
||||
"Choose whether the tool picker opens in fullscreen or sidebar by default",
|
||||
)}
|
||||
{t('settings.general.defaultToolPickerModeDescription', 'Choose whether the tool picker opens in fullscreen or sidebar by default')}
|
||||
</Text>
|
||||
</div>
|
||||
<SegmentedControl
|
||||
value={preferences.defaultToolPanelMode}
|
||||
onChange={(val: string) => updatePreference("defaultToolPanelMode", val as ToolPanelMode)}
|
||||
onChange={(val: string) => updatePreference('defaultToolPanelMode', val as ToolPanelMode)}
|
||||
data={[
|
||||
{ label: t("settings.general.mode.sidebar", "Sidebar"), value: "sidebar" },
|
||||
{ label: t("settings.general.mode.fullscreen", "Fullscreen"), value: "fullscreen" },
|
||||
{ label: t('settings.general.mode.sidebar', 'Sidebar'), value: 'sidebar' },
|
||||
{ label: t('settings.general.mode.fullscreen', 'Fullscreen'), value: 'fullscreen' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">
|
||||
{t("settings.general.hideUnavailableTools", "Hide unavailable tools")}
|
||||
{t('settings.general.hideUnavailableTools', 'Hide unavailable tools')}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t(
|
||||
"settings.general.hideUnavailableToolsDescription",
|
||||
"Remove tools that have been disabled by your server instead of showing them greyed out.",
|
||||
)}
|
||||
{t('settings.general.hideUnavailableToolsDescription', 'Remove tools that have been disabled by your server instead of showing them greyed out.')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={preferences.hideUnavailableTools}
|
||||
onChange={(event) => updatePreference("hideUnavailableTools", event.currentTarget.checked)}
|
||||
onChange={(event) => updatePreference('hideUnavailableTools', event.currentTarget.checked)}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">
|
||||
{t("settings.general.hideUnavailableConversions", "Hide unavailable conversions")}
|
||||
{t('settings.general.hideUnavailableConversions', 'Hide unavailable conversions')}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t(
|
||||
"settings.general.hideUnavailableConversionsDescription",
|
||||
"Remove disabled conversion options in the Convert tool instead of showing them greyed out.",
|
||||
)}
|
||||
{t('settings.general.hideUnavailableConversionsDescription', 'Remove disabled conversion options in the Convert tool instead of showing them greyed out.')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={preferences.hideUnavailableConversions}
|
||||
onChange={(event) => updatePreference("hideUnavailableConversions", event.currentTarget.checked)}
|
||||
onChange={(event) => updatePreference('hideUnavailableConversions', event.currentTarget.checked)}
|
||||
/>
|
||||
</div>
|
||||
<Tooltip
|
||||
label={t(
|
||||
"settings.general.autoUnzipTooltip",
|
||||
"Automatically extract ZIP files returned from API operations. Disable to keep ZIP files intact. This does not affect automation workflows.",
|
||||
)}
|
||||
label={t('settings.general.autoUnzipTooltip', 'Automatically extract ZIP files returned from API operations. Disable to keep ZIP files intact. This does not affect automation workflows.')}
|
||||
multiline
|
||||
w={300}
|
||||
withArrow
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", cursor: "help" }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'help' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">
|
||||
{t("settings.general.autoUnzip", "Auto-unzip API responses")}
|
||||
{t('settings.general.autoUnzip', 'Auto-unzip API responses')}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t("settings.general.autoUnzipDescription", "Automatically extract files from ZIP responses")}
|
||||
{t('settings.general.autoUnzipDescription', 'Automatically extract files from ZIP responses')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={preferences.autoUnzip}
|
||||
onChange={(event) => updatePreference("autoUnzip", event.currentTarget.checked)}
|
||||
onChange={(event) => updatePreference('autoUnzip', event.currentTarget.checked)}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
label={t(
|
||||
"settings.general.autoUnzipFileLimitTooltip",
|
||||
"Only unzip if the ZIP contains this many files or fewer. Set higher to extract larger ZIPs.",
|
||||
)}
|
||||
label={t('settings.general.autoUnzipFileLimitTooltip', 'Only unzip if the ZIP contains this many files or fewer. Set higher to extract larger ZIPs.')}
|
||||
multiline
|
||||
w={300}
|
||||
withArrow
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", cursor: "help" }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'help' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">
|
||||
{t("settings.general.autoUnzipFileLimit", "Auto-unzip file limit")}
|
||||
{t('settings.general.autoUnzipFileLimit', 'Auto-unzip file limit')}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t("settings.general.autoUnzipFileLimitDescription", "Maximum number of files to extract from ZIP")}
|
||||
{t('settings.general.autoUnzipFileLimitDescription', 'Maximum number of files to extract from ZIP')}
|
||||
</Text>
|
||||
</div>
|
||||
<NumberInput
|
||||
@@ -422,12 +311,9 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
|
||||
onChange={setFileLimitInput}
|
||||
onBlur={() => {
|
||||
const numValue = Number(fileLimitInput);
|
||||
const finalValue =
|
||||
!fileLimitInput || isNaN(numValue) || numValue < 1 || numValue > 100
|
||||
? DEFAULT_AUTO_UNZIP_FILE_LIMIT
|
||||
: numValue;
|
||||
const finalValue = (!fileLimitInput || isNaN(numValue) || numValue < 1 || numValue > 100) ? DEFAULT_AUTO_UNZIP_FILE_LIMIT : numValue;
|
||||
setFileLimitInput(finalValue);
|
||||
updatePreference("autoUnzipFileLimit", finalValue);
|
||||
updatePreference('autoUnzipFileLimit', finalValue);
|
||||
}}
|
||||
min={1}
|
||||
max={100}
|
||||
@@ -450,7 +336,7 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
|
||||
machineInfo={{
|
||||
machineType: config.machineType,
|
||||
activeSecurity: config.activeSecurity ?? false,
|
||||
licenseType: config.license ?? "NORMAL",
|
||||
licenseType: config.license ?? 'NORMAL',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -486,12 +486,6 @@ export const SAML2_PROVIDER: Provider = {
|
||||
description: 'Enable SAML2 authentication (Enterprise only)',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'provider',
|
||||
type: 'text',
|
||||
label: 'Provider Name',
|
||||
description: 'The name of your SAML2 provider',
|
||||
},
|
||||
{
|
||||
key: 'registrationId',
|
||||
type: 'text',
|
||||
@@ -500,51 +494,66 @@ export const SAML2_PROVIDER: Provider = {
|
||||
defaultValue: 'stirling',
|
||||
},
|
||||
{
|
||||
key: 'idpMetadataUri',
|
||||
key: 'metadataUri',
|
||||
type: 'text',
|
||||
label: 'IDP Metadata URI',
|
||||
description: 'The URI for your provider\'s metadata',
|
||||
label: 'Metadata URI',
|
||||
description: 'Your IdP\'s metadata URI (recommended - auto-discovers IdP config)',
|
||||
placeholder: 'https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata',
|
||||
},
|
||||
{
|
||||
key: 'idpSingleLoginUrl',
|
||||
key: 'enableSingleLogout',
|
||||
type: 'switch',
|
||||
label: 'Enable Single Logout',
|
||||
description: 'Enable SP-initiated Single Logout (SLO)',
|
||||
defaultValue: false,
|
||||
},
|
||||
// IdP Provider settings (manual config - only needed if metadataUri not available)
|
||||
{
|
||||
key: 'provider.name',
|
||||
type: 'text',
|
||||
label: 'IDP Single Login URL',
|
||||
description: 'The URL for initiating SSO',
|
||||
placeholder: 'https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml',
|
||||
label: 'Provider Name',
|
||||
description: 'Display name for your IdP (optional, for reference only)',
|
||||
},
|
||||
{
|
||||
key: 'idpSingleLogoutUrl',
|
||||
key: 'provider.singleLoginUrl',
|
||||
type: 'text',
|
||||
label: 'IDP Single Logout URL',
|
||||
description: 'The URL for initiating SLO',
|
||||
placeholder: 'https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml',
|
||||
label: 'SSO URL',
|
||||
description: 'IdP Single Sign-On URL (auto-discovered from metadata if set)',
|
||||
placeholder: 'https://dev-XXXXXXXX.okta.com/app/.../sso/saml',
|
||||
},
|
||||
{
|
||||
key: 'idpIssuer',
|
||||
key: 'provider.singleLogoutUrl',
|
||||
type: 'text',
|
||||
label: 'IDP Issuer',
|
||||
description: 'The ID of your provider',
|
||||
label: 'SLO URL',
|
||||
description: 'IdP Single Logout URL (auto-discovered from metadata if set)',
|
||||
placeholder: 'https://dev-XXXXXXXX.okta.com/app/.../slo/saml',
|
||||
},
|
||||
{
|
||||
key: 'idpCert',
|
||||
key: 'provider.entityId',
|
||||
type: 'text',
|
||||
label: 'IDP Certificate',
|
||||
description: 'The certificate path (e.g., classpath:okta.cert)',
|
||||
label: 'IdP Entity ID',
|
||||
description: 'IdP Entity ID (auto-discovered from metadata if set)',
|
||||
},
|
||||
{
|
||||
key: 'provider.cert',
|
||||
type: 'text',
|
||||
label: 'IdP Certificate',
|
||||
description: 'IdP signing certificate path (auto-discovered from metadata if set)',
|
||||
placeholder: 'classpath:okta.cert',
|
||||
},
|
||||
// SP credentials
|
||||
{
|
||||
key: 'privateKey',
|
||||
key: 'sp.privateKey',
|
||||
type: 'text',
|
||||
label: 'Private Key',
|
||||
description: 'Your private key path',
|
||||
label: 'SP Private Key',
|
||||
description: 'Your Service Provider private key path',
|
||||
placeholder: 'classpath:saml-private-key.key',
|
||||
},
|
||||
{
|
||||
key: 'spCert',
|
||||
key: 'sp.cert',
|
||||
type: 'text',
|
||||
label: 'SP Certificate',
|
||||
description: 'Your signing certificate path',
|
||||
description: 'Your Service Provider signing certificate path',
|
||||
placeholder: 'classpath:saml-public-cert.crt',
|
||||
},
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user