mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 06:09:23 +00:00
Merge remote-tracking branch 'origin' into feature/v2/pagemanager-improvements
This commit is contained in:
commit
d614efc92b
@ -7,17 +7,13 @@
|
||||
"Bash(grep:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(npm test)",
|
||||
"Bash(npm test:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(npx tsc:*)",
|
||||
"Bash(node:*)",
|
||||
"Bash(npm run dev:*)",
|
||||
"Bash(sed:*)",
|
||||
"Bash(cp:*)",
|
||||
"Bash(rm:*)"
|
||||
"Bash(grep:*)",
|
||||
"Bash(rg:*)",
|
||||
"Bash(strings:*)",
|
||||
"Bash(pkill:*)",
|
||||
"Bash(true)"
|
||||
],
|
||||
"deny": [],
|
||||
"defaultMode": "acceptEdits"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
1
.github/config/.files.yaml
vendored
1
.github/config/.files.yaml
vendored
@ -29,3 +29,4 @@ project: &project
|
||||
- settings.gradle
|
||||
- frontend/**
|
||||
- docker/**
|
||||
- testing/**
|
||||
|
8
.github/scripts/requirements_dev.in
vendored
Normal file
8
.github/scripts/requirements_dev.in
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
pip
|
||||
setuptools
|
||||
WeasyPrint
|
||||
pdf2image
|
||||
pillow
|
||||
unoserver
|
||||
opencv-python-headless
|
||||
pre-commit
|
638
.github/scripts/requirements_dev.txt
vendored
Normal file
638
.github/scripts/requirements_dev.txt
vendored
Normal file
@ -0,0 +1,638 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.10
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --allow-unsafe --generate-hashes --output-file='.github\scripts\requirements_dev.txt' --strip-extras '.github\scripts\requirements_dev.in'
|
||||
#
|
||||
brotli==1.1.0 \
|
||||
--hash=sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208 \
|
||||
--hash=sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48 \
|
||||
--hash=sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354 \
|
||||
--hash=sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419 \
|
||||
--hash=sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a \
|
||||
--hash=sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128 \
|
||||
--hash=sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c \
|
||||
--hash=sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088 \
|
||||
--hash=sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9 \
|
||||
--hash=sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a \
|
||||
--hash=sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3 \
|
||||
--hash=sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757 \
|
||||
--hash=sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2 \
|
||||
--hash=sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438 \
|
||||
--hash=sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578 \
|
||||
--hash=sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b \
|
||||
--hash=sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b \
|
||||
--hash=sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68 \
|
||||
--hash=sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0 \
|
||||
--hash=sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d \
|
||||
--hash=sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943 \
|
||||
--hash=sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd \
|
||||
--hash=sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409 \
|
||||
--hash=sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28 \
|
||||
--hash=sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da \
|
||||
--hash=sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50 \
|
||||
--hash=sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f \
|
||||
--hash=sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0 \
|
||||
--hash=sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547 \
|
||||
--hash=sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180 \
|
||||
--hash=sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0 \
|
||||
--hash=sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d \
|
||||
--hash=sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a \
|
||||
--hash=sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb \
|
||||
--hash=sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112 \
|
||||
--hash=sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc \
|
||||
--hash=sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2 \
|
||||
--hash=sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265 \
|
||||
--hash=sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327 \
|
||||
--hash=sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95 \
|
||||
--hash=sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec \
|
||||
--hash=sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd \
|
||||
--hash=sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c \
|
||||
--hash=sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38 \
|
||||
--hash=sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914 \
|
||||
--hash=sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0 \
|
||||
--hash=sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a \
|
||||
--hash=sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7 \
|
||||
--hash=sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368 \
|
||||
--hash=sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c \
|
||||
--hash=sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0 \
|
||||
--hash=sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f \
|
||||
--hash=sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451 \
|
||||
--hash=sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f \
|
||||
--hash=sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8 \
|
||||
--hash=sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e \
|
||||
--hash=sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248 \
|
||||
--hash=sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c \
|
||||
--hash=sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91 \
|
||||
--hash=sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724 \
|
||||
--hash=sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7 \
|
||||
--hash=sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966 \
|
||||
--hash=sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9 \
|
||||
--hash=sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97 \
|
||||
--hash=sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d \
|
||||
--hash=sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5 \
|
||||
--hash=sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf \
|
||||
--hash=sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac \
|
||||
--hash=sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b \
|
||||
--hash=sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951 \
|
||||
--hash=sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74 \
|
||||
--hash=sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648 \
|
||||
--hash=sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60 \
|
||||
--hash=sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c \
|
||||
--hash=sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1 \
|
||||
--hash=sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8 \
|
||||
--hash=sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d \
|
||||
--hash=sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc \
|
||||
--hash=sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61 \
|
||||
--hash=sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460 \
|
||||
--hash=sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751 \
|
||||
--hash=sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9 \
|
||||
--hash=sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2 \
|
||||
--hash=sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0 \
|
||||
--hash=sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1 \
|
||||
--hash=sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474 \
|
||||
--hash=sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75 \
|
||||
--hash=sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5 \
|
||||
--hash=sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f \
|
||||
--hash=sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2 \
|
||||
--hash=sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f \
|
||||
--hash=sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb \
|
||||
--hash=sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6 \
|
||||
--hash=sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9 \
|
||||
--hash=sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111 \
|
||||
--hash=sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2 \
|
||||
--hash=sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01 \
|
||||
--hash=sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467 \
|
||||
--hash=sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619 \
|
||||
--hash=sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf \
|
||||
--hash=sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408 \
|
||||
--hash=sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579 \
|
||||
--hash=sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84 \
|
||||
--hash=sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7 \
|
||||
--hash=sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c \
|
||||
--hash=sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284 \
|
||||
--hash=sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52 \
|
||||
--hash=sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b \
|
||||
--hash=sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59 \
|
||||
--hash=sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752 \
|
||||
--hash=sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1 \
|
||||
--hash=sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80 \
|
||||
--hash=sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839 \
|
||||
--hash=sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0 \
|
||||
--hash=sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2 \
|
||||
--hash=sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3 \
|
||||
--hash=sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64 \
|
||||
--hash=sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089 \
|
||||
--hash=sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643 \
|
||||
--hash=sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b \
|
||||
--hash=sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e \
|
||||
--hash=sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985 \
|
||||
--hash=sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596 \
|
||||
--hash=sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2 \
|
||||
--hash=sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064
|
||||
# via fonttools
|
||||
cffi==1.17.1 \
|
||||
--hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \
|
||||
--hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \
|
||||
--hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \
|
||||
--hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \
|
||||
--hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \
|
||||
--hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \
|
||||
--hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \
|
||||
--hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \
|
||||
--hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \
|
||||
--hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \
|
||||
--hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \
|
||||
--hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \
|
||||
--hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \
|
||||
--hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \
|
||||
--hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \
|
||||
--hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \
|
||||
--hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \
|
||||
--hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \
|
||||
--hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \
|
||||
--hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \
|
||||
--hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \
|
||||
--hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \
|
||||
--hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \
|
||||
--hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \
|
||||
--hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \
|
||||
--hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \
|
||||
--hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \
|
||||
--hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \
|
||||
--hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \
|
||||
--hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \
|
||||
--hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \
|
||||
--hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \
|
||||
--hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \
|
||||
--hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \
|
||||
--hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \
|
||||
--hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \
|
||||
--hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \
|
||||
--hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \
|
||||
--hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \
|
||||
--hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \
|
||||
--hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \
|
||||
--hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \
|
||||
--hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \
|
||||
--hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \
|
||||
--hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \
|
||||
--hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \
|
||||
--hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \
|
||||
--hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \
|
||||
--hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \
|
||||
--hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \
|
||||
--hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \
|
||||
--hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \
|
||||
--hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \
|
||||
--hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \
|
||||
--hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \
|
||||
--hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \
|
||||
--hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \
|
||||
--hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \
|
||||
--hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \
|
||||
--hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \
|
||||
--hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \
|
||||
--hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \
|
||||
--hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \
|
||||
--hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \
|
||||
--hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \
|
||||
--hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \
|
||||
--hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b
|
||||
# via weasyprint
|
||||
cfgv==3.4.0 \
|
||||
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
|
||||
--hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
|
||||
# via pre-commit
|
||||
cssselect2==0.8.0 \
|
||||
--hash=sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e \
|
||||
--hash=sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a
|
||||
# via weasyprint
|
||||
distlib==0.4.0 \
|
||||
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
||||
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
||||
# via virtualenv
|
||||
filelock==3.18.0 \
|
||||
--hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \
|
||||
--hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de
|
||||
# via virtualenv
|
||||
fonttools==4.59.0 \
|
||||
--hash=sha256:052444a5d0151878e87e3e512a1aa1a0ab35ee4c28afde0a778e23b0ace4a7de \
|
||||
--hash=sha256:169b99a2553a227f7b5fea8d9ecd673aa258617f466b2abc6091fe4512a0dcd0 \
|
||||
--hash=sha256:209b75943d158f610b78320eacb5539aa9e920bee2c775445b2846c65d20e19d \
|
||||
--hash=sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df \
|
||||
--hash=sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d \
|
||||
--hash=sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe \
|
||||
--hash=sha256:2e7cf8044ce2598bb87e44ba1d2c6e45d7a8decf56055b92906dc53f67c76d64 \
|
||||
--hash=sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e \
|
||||
--hash=sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01 \
|
||||
--hash=sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705 \
|
||||
--hash=sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c \
|
||||
--hash=sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2 \
|
||||
--hash=sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b \
|
||||
--hash=sha256:4c908a7036f0f3677f8afa577bcd973e3e20ddd2f7c42a33208d18bee95cdb6f \
|
||||
--hash=sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97 \
|
||||
--hash=sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96 \
|
||||
--hash=sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2 \
|
||||
--hash=sha256:60f6665579e909b618282f3c14fa0b80570fbf1ee0e67678b9a9d43aa5d67a37 \
|
||||
--hash=sha256:62224a9bb85b4b66d1b46d45cbe43d71cbf8f527d332b177e3b96191ffbc1e64 \
|
||||
--hash=sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757 \
|
||||
--hash=sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e \
|
||||
--hash=sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3 \
|
||||
--hash=sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2 \
|
||||
--hash=sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c \
|
||||
--hash=sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0 \
|
||||
--hash=sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1 \
|
||||
--hash=sha256:8b4309a2775e4feee7356e63b163969a215d663399cce1b3d3b65e7ec2d9680e \
|
||||
--hash=sha256:8d77f92438daeaddc05682f0f3dac90c5b9829bcac75b57e8ce09cb67786073c \
|
||||
--hash=sha256:902425f5afe28572d65d2bf9c33edd5265c612ff82c69e6f83ea13eafc0dcbea \
|
||||
--hash=sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5 \
|
||||
--hash=sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6 \
|
||||
--hash=sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c \
|
||||
--hash=sha256:b818db35879d2edf7f46c7e729c700a0bce03b61b9412f5a7118406687cb151d \
|
||||
--hash=sha256:b8974b2a266b54c96709bd5e239979cddfd2dbceed331aa567ea1d7c4a2202db \
|
||||
--hash=sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14 \
|
||||
--hash=sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38 \
|
||||
--hash=sha256:d40dcf533ca481355aa7b682e9e079f766f35715defa4929aeb5597f9604272e \
|
||||
--hash=sha256:e93df708c69a193fc7987192f94df250f83f3851fda49413f02ba5dded639482 \
|
||||
--hash=sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4 \
|
||||
--hash=sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b \
|
||||
--hash=sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464 \
|
||||
--hash=sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b
|
||||
# via weasyprint
|
||||
identify==2.6.13 \
|
||||
--hash=sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b \
|
||||
--hash=sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32
|
||||
# via pre-commit
|
||||
nodeenv==1.9.1 \
|
||||
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
||||
# via pre-commit
|
||||
numpy==2.2.6 \
|
||||
--hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \
|
||||
--hash=sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47 \
|
||||
--hash=sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84 \
|
||||
--hash=sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d \
|
||||
--hash=sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6 \
|
||||
--hash=sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f \
|
||||
--hash=sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b \
|
||||
--hash=sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49 \
|
||||
--hash=sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163 \
|
||||
--hash=sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571 \
|
||||
--hash=sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42 \
|
||||
--hash=sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff \
|
||||
--hash=sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491 \
|
||||
--hash=sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4 \
|
||||
--hash=sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566 \
|
||||
--hash=sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf \
|
||||
--hash=sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40 \
|
||||
--hash=sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd \
|
||||
--hash=sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06 \
|
||||
--hash=sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282 \
|
||||
--hash=sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680 \
|
||||
--hash=sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db \
|
||||
--hash=sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3 \
|
||||
--hash=sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90 \
|
||||
--hash=sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1 \
|
||||
--hash=sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289 \
|
||||
--hash=sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab \
|
||||
--hash=sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c \
|
||||
--hash=sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d \
|
||||
--hash=sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb \
|
||||
--hash=sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d \
|
||||
--hash=sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a \
|
||||
--hash=sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf \
|
||||
--hash=sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1 \
|
||||
--hash=sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2 \
|
||||
--hash=sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a \
|
||||
--hash=sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543 \
|
||||
--hash=sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00 \
|
||||
--hash=sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c \
|
||||
--hash=sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f \
|
||||
--hash=sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd \
|
||||
--hash=sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868 \
|
||||
--hash=sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303 \
|
||||
--hash=sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83 \
|
||||
--hash=sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3 \
|
||||
--hash=sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d \
|
||||
--hash=sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87 \
|
||||
--hash=sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa \
|
||||
--hash=sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f \
|
||||
--hash=sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae \
|
||||
--hash=sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda \
|
||||
--hash=sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 \
|
||||
--hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \
|
||||
--hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \
|
||||
--hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8
|
||||
# via opencv-python-headless
|
||||
opencv-python-headless==4.12.0.88 \
|
||||
--hash=sha256:1e58d664809b3350c1123484dd441e1667cd7bed3086db1b9ea1b6f6cb20b50e \
|
||||
--hash=sha256:236c8df54a90f4d02076e6f9c1cc763d794542e886c576a6fee46ec8ff75a7a9 \
|
||||
--hash=sha256:365bb2e486b50feffc2d07a405b953a8f3e8eaa63865bc650034e5c71e7a5154 \
|
||||
--hash=sha256:86b413bdd6c6bf497832e346cd5371995de148e579b9774f8eba686dee3f5528 \
|
||||
--hash=sha256:aeb4b13ecb8b4a0beb2668ea07928160ea7c2cd2d9b5ef571bbee6bafe9cc8d0 \
|
||||
--hash=sha256:cfdc017ddf2e59b6c2f53bc12d74b6b0be7ded4ec59083ea70763921af2b6c09 \
|
||||
--hash=sha256:fde2cf5c51e4def5f2132d78e0c08f9c14783cd67356922182c6845b9af87dbd
|
||||
# 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==11.3.0 \
|
||||
--hash=sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2 \
|
||||
--hash=sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214 \
|
||||
--hash=sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e \
|
||||
--hash=sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59 \
|
||||
--hash=sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50 \
|
||||
--hash=sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632 \
|
||||
--hash=sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06 \
|
||||
--hash=sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a \
|
||||
--hash=sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51 \
|
||||
--hash=sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced \
|
||||
--hash=sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f \
|
||||
--hash=sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12 \
|
||||
--hash=sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8 \
|
||||
--hash=sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6 \
|
||||
--hash=sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580 \
|
||||
--hash=sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f \
|
||||
--hash=sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac \
|
||||
--hash=sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860 \
|
||||
--hash=sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd \
|
||||
--hash=sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722 \
|
||||
--hash=sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8 \
|
||||
--hash=sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4 \
|
||||
--hash=sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673 \
|
||||
--hash=sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788 \
|
||||
--hash=sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542 \
|
||||
--hash=sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e \
|
||||
--hash=sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd \
|
||||
--hash=sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8 \
|
||||
--hash=sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523 \
|
||||
--hash=sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967 \
|
||||
--hash=sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809 \
|
||||
--hash=sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477 \
|
||||
--hash=sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027 \
|
||||
--hash=sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae \
|
||||
--hash=sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b \
|
||||
--hash=sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c \
|
||||
--hash=sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f \
|
||||
--hash=sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e \
|
||||
--hash=sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b \
|
||||
--hash=sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7 \
|
||||
--hash=sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27 \
|
||||
--hash=sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361 \
|
||||
--hash=sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae \
|
||||
--hash=sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d \
|
||||
--hash=sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc \
|
||||
--hash=sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58 \
|
||||
--hash=sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad \
|
||||
--hash=sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6 \
|
||||
--hash=sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024 \
|
||||
--hash=sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978 \
|
||||
--hash=sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb \
|
||||
--hash=sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d \
|
||||
--hash=sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0 \
|
||||
--hash=sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9 \
|
||||
--hash=sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f \
|
||||
--hash=sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874 \
|
||||
--hash=sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa \
|
||||
--hash=sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081 \
|
||||
--hash=sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149 \
|
||||
--hash=sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6 \
|
||||
--hash=sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d \
|
||||
--hash=sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd \
|
||||
--hash=sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f \
|
||||
--hash=sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c \
|
||||
--hash=sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31 \
|
||||
--hash=sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e \
|
||||
--hash=sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db \
|
||||
--hash=sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6 \
|
||||
--hash=sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f \
|
||||
--hash=sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494 \
|
||||
--hash=sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69 \
|
||||
--hash=sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94 \
|
||||
--hash=sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77 \
|
||||
--hash=sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d \
|
||||
--hash=sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7 \
|
||||
--hash=sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a \
|
||||
--hash=sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438 \
|
||||
--hash=sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288 \
|
||||
--hash=sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b \
|
||||
--hash=sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635 \
|
||||
--hash=sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3 \
|
||||
--hash=sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d \
|
||||
--hash=sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe \
|
||||
--hash=sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0 \
|
||||
--hash=sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe \
|
||||
--hash=sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a \
|
||||
--hash=sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805 \
|
||||
--hash=sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8 \
|
||||
--hash=sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36 \
|
||||
--hash=sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a \
|
||||
--hash=sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b \
|
||||
--hash=sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e \
|
||||
--hash=sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25 \
|
||||
--hash=sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12 \
|
||||
--hash=sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada \
|
||||
--hash=sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c \
|
||||
--hash=sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71 \
|
||||
--hash=sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d \
|
||||
--hash=sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c \
|
||||
--hash=sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6 \
|
||||
--hash=sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1 \
|
||||
--hash=sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50 \
|
||||
--hash=sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653 \
|
||||
--hash=sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c \
|
||||
--hash=sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4 \
|
||||
--hash=sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3
|
||||
# via
|
||||
# -r .github\scripts\requirements_dev.in
|
||||
# pdf2image
|
||||
# weasyprint
|
||||
platformdirs==4.3.8 \
|
||||
--hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \
|
||||
--hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4
|
||||
# via virtualenv
|
||||
pre-commit==4.3.0 \
|
||||
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
|
||||
--hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16
|
||||
# via -r .github\scripts\requirements_dev.in
|
||||
pycparser==2.22 \
|
||||
--hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
|
||||
--hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
|
||||
# via cffi
|
||||
pydyf==0.11.0 \
|
||||
--hash=sha256:0aaf9e2ebbe786ec7a78ec3fbffa4cdcecde53fd6f563221d53c6bc1328848a3 \
|
||||
--hash=sha256:394dddf619cca9d0c55715e3c55ea121a9bf9cbc780cdc1201a2427917b86b64
|
||||
# via weasyprint
|
||||
pyphen==0.17.2 \
|
||||
--hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \
|
||||
--hash=sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3
|
||||
# via weasyprint
|
||||
pyyaml==6.0.2 \
|
||||
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
|
||||
--hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
|
||||
--hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \
|
||||
--hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \
|
||||
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
|
||||
--hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
|
||||
--hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
|
||||
--hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
|
||||
--hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
|
||||
--hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \
|
||||
--hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \
|
||||
--hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \
|
||||
--hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \
|
||||
--hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \
|
||||
--hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
|
||||
--hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \
|
||||
--hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
|
||||
--hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \
|
||||
--hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
|
||||
--hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
|
||||
--hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
|
||||
--hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \
|
||||
--hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \
|
||||
--hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
|
||||
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
|
||||
--hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
|
||||
--hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \
|
||||
--hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
|
||||
--hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
|
||||
--hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \
|
||||
--hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
|
||||
--hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \
|
||||
--hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
|
||||
--hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \
|
||||
--hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \
|
||||
--hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
|
||||
--hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \
|
||||
--hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \
|
||||
--hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \
|
||||
--hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
|
||||
--hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
|
||||
--hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
|
||||
--hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
|
||||
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
|
||||
--hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \
|
||||
--hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \
|
||||
--hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \
|
||||
--hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
|
||||
--hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \
|
||||
--hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
|
||||
--hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
|
||||
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
|
||||
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
|
||||
# via pre-commit
|
||||
tinycss2==1.4.0 \
|
||||
--hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \
|
||||
--hash=sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289
|
||||
# via
|
||||
# cssselect2
|
||||
# weasyprint
|
||||
tinyhtml5==2.0.0 \
|
||||
--hash=sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc \
|
||||
--hash=sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e
|
||||
# via weasyprint
|
||||
unoserver==3.3.2 \
|
||||
--hash=sha256:1eeb7467cf6b56b8eff3b576e2d1b2b2ff4e0eb2052e995ac80a1456de300639 \
|
||||
--hash=sha256:87e144f903ee21951b2e06a97549450c13ed7eca5bcebad942d3352d4e882616
|
||||
# via -r .github\scripts\requirements_dev.in
|
||||
virtualenv==20.33.1 \
|
||||
--hash=sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67 \
|
||||
--hash=sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8
|
||||
# via pre-commit
|
||||
weasyprint==66.0 \
|
||||
--hash=sha256:82b0783b726fcd318e2c977dcdddca76515b30044bc7a830cc4fbe717582a6d0 \
|
||||
--hash=sha256:da71dc87dc129ac9cffdc65e5477e90365ab9dbae45c744014ec1d06303dde40
|
||||
# via -r .github\scripts\requirements_dev.in
|
||||
webencodings==0.5.1 \
|
||||
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
|
||||
--hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
|
||||
# via
|
||||
# cssselect2
|
||||
# tinycss2
|
||||
# tinyhtml5
|
||||
zopfli==0.2.3.post1 \
|
||||
--hash=sha256:0aa5f90d6298bda02a95bc8dc8c3c19004d5a4e44bda00b67ca7431d857b4b54 \
|
||||
--hash=sha256:0cc20b02a9531559945324c38302fd4ba763311632d0ec8a1a0aa9c10ea363e6 \
|
||||
--hash=sha256:1d8cc06605519e82b16df090e17cb3990d1158861b2872c3117f1168777b81e4 \
|
||||
--hash=sha256:1f990634fd5c5c8ced8edddd8bd45fab565123b4194d6841e01811292650acae \
|
||||
--hash=sha256:2345e713260a350bea0b01a816a469ea356bc2d63d009a0d777691ecbbcf7493 \
|
||||
--hash=sha256:2768c877f76c8a0e7519b1c86c93757f3c01492ddde55751e9988afb7eff64e1 \
|
||||
--hash=sha256:29ea74e72ffa6e291b8c6f2504ce6c146b4fe990c724c1450eb8e4c27fd31431 \
|
||||
--hash=sha256:34a99592f3d9eb6f737616b5bd74b48a589fdb3cb59a01a50d636ea81d6af272 \
|
||||
--hash=sha256:3654bfc927bc478b1c3f3ff5056ed7b20a1a37fa108ca503256d0a699c03bbb1 \
|
||||
--hash=sha256:3657e416ffb8f31d9d3424af12122bb251befae109f2e271d87d825c92fc5b7b \
|
||||
--hash=sha256:37d011e92f7b9622742c905fdbed9920a1d0361df84142807ea2a528419dea7f \
|
||||
--hash=sha256:3827170de28faf144992d3d4dcf8f3998fe3c8a6a6f4a08f1d42c2ec6119d2bb \
|
||||
--hash=sha256:39e576f93576c5c223b41d9c780bbb91fd6db4babf3223d2a4fe7bf568e2b5a8 \
|
||||
--hash=sha256:3a89277ed5f8c0fb2d0b46d669aa0633123aa7381f1f6118c12f15e0fb48f8ca \
|
||||
--hash=sha256:3c163911f8bad94b3e1db0a572e7c28ba681a0c91d0002ea1e4fa9264c21ef17 \
|
||||
--hash=sha256:3f0197b6aa6eb3086ae9e66d6dd86c4d502b6c68b0ec490496348ae8c05ecaef \
|
||||
--hash=sha256:48dba9251060289101343110ab47c0756f66f809bb4d1ddbb6d5c7e7752115c5 \
|
||||
--hash=sha256:4915a41375bdee4db749ecd07d985a0486eb688a6619f713b7bf6fbfd145e960 \
|
||||
--hash=sha256:4c1226a7e2c7105ac31503a9bb97454743f55d88164d6d46bc138051b77f609b \
|
||||
--hash=sha256:4e50ffac74842c1c1018b9b73875a0d0a877c066ab06bf7cccbaa84af97e754f \
|
||||
--hash=sha256:518f1f4ed35dd69ce06b552f84e6d081f07c552b4c661c5312d950a0b764a58a \
|
||||
--hash=sha256:5aad740b4d4fcbaaae4887823925166ffd062db3b248b3f432198fc287381d1a \
|
||||
--hash=sha256:5f272186e03ad55e7af09ab78055535c201b1a0bcc2944edb1768298d9c483a4 \
|
||||
--hash=sha256:5fcfc0dc2761e4fcc15ad5d273b4d58c2e8e059d3214a7390d4d3c8e2aee644e \
|
||||
--hash=sha256:60db20f06c3d4c5934b16cfa62a2cc5c3f0686bffe0071ed7804d3c31ab1a04e \
|
||||
--hash=sha256:615a8ac9dda265e9cc38b2a76c3142e4a9f30fea4a79c85f670850783bc6feb4 \
|
||||
--hash=sha256:6482db9876c68faac2d20a96b566ffbf65ddaadd97b222e4e73641f4f8722fc4 \
|
||||
--hash=sha256:6617fb10f9e4393b331941861d73afb119cd847e88e4974bdbe8068ceef3f73f \
|
||||
--hash=sha256:676919fba7311125244eb0c4393679ac5fe856e5864a15d122bd815205369fa0 \
|
||||
--hash=sha256:6c2d2bc8129707e34c51f9352c4636ca313b52350bbb7e04637c46c1818a2a70 \
|
||||
--hash=sha256:71390dbd3fbf6ebea9a5d85ffed8c26ee1453ee09248e9b88486e30e0397b775 \
|
||||
--hash=sha256:716cdbfc57bfd3d3e31a58e6246e8190e6849b7dbb7c4ce39ef8bbf0edb8f6d5 \
|
||||
--hash=sha256:75a26a2307b10745a83b660c404416e984ee6fca515ec7f0765f69af3ce08072 \
|
||||
--hash=sha256:7be5cc6732eb7b4df17305d8a7b293223f934a31783a874a01164703bc1be6cd \
|
||||
--hash=sha256:7cce242b5df12b2b172489daf19c32e5577dd2fac659eb4b17f6a6efb446fd5c \
|
||||
--hash=sha256:81c341d9bb87a6dbbb0d45d6e272aca80c7c97b4b210f9b6e233bf8b87242f29 \
|
||||
--hash=sha256:89899641d4de97dbad8e0cde690040d078b6aea04066dacaab98e0b5a23573f2 \
|
||||
--hash=sha256:8d5ab297d660b75c159190ce6d73035502310e40fd35170aed7d1a1aea7ddd65 \
|
||||
--hash=sha256:8fbe5bcf10d01aab3513550f284c09fef32f342b36f56bfae2120a9c4d12c130 \
|
||||
--hash=sha256:91a2327a4d7e77471fa4fbb26991c6de4a738c6fc6a33e09bb25f56a870a4b7b \
|
||||
--hash=sha256:95a260cafd56b8fffa679918937401c80bb38e1681c448b988022e4c3610965d \
|
||||
--hash=sha256:96484dc0f48be1c5d7ae9f38ed1ce41e3675fd506b27c11a6607f14b49101e99 \
|
||||
--hash=sha256:9a6aec38a989bad7ddd1ef53f1265699e49e294d08231b5313d61293f3cd6237 \
|
||||
--hash=sha256:9ba214f4f45bec195ee8559651154d3ac2932470b9d91c5715fc29c013349f8c \
|
||||
--hash=sha256:9f4a7ec2770e6af05f5a02733fd3900f30a9cd58e5d6d3727e14c5bcd6e7d587 \
|
||||
--hash=sha256:a1cf720896d2ce998bc8e051d4b4ce0d8bec007aab6243102e8e1d22a0b2fb3f \
|
||||
--hash=sha256:a241a68581d34d67b40c425cce3d1fd211c092f99d9250947824ccba9f491949 \
|
||||
--hash=sha256:a53b18797cdef27e019db595d66c4b077325afe2fd62145953275f53d84ce40c \
|
||||
--hash=sha256:a82fc2dbebe6eb908b9c665e71496f8525c1bc4d2e3a7a7722ef2b128b6227c8 \
|
||||
--hash=sha256:a86eb88e06bd87e1fff31dac878965c26b0c26db59ddcf78bb0379a954b120de \
|
||||
--hash=sha256:aa588b21044f8a74e423d8c8a4c7fc9988501878aacced793467010039c50734 \
|
||||
--hash=sha256:b05296e8bc88c92e2b21e0a9bae4740c1551ee613c1d93a51fd28a7a0b2b6fbb \
|
||||
--hash=sha256:b0ec13f352ea5ae0fc91f98a48540512eed0767d0ec4f7f3cb92d92797983d18 \
|
||||
--hash=sha256:b3df42f52502438ee973042cc551877d24619fa1cd38ef7b7e9ac74200daca8b \
|
||||
--hash=sha256:b78008a69300d929ca2efeffec951b64a312e9a811e265ea4a907ab546d79fa6 \
|
||||
--hash=sha256:b9026a21b6d41eb0e2e63f5bc1242c3fcc43ecb770963cda99a4307863dac12e \
|
||||
--hash=sha256:bbe429fc50686bb2a2608a30843e36fbaa123462a5284f136c7d9e0145220bfd \
|
||||
--hash=sha256:bfa1eb759e07d8b7aa7a310a2bc535e127ee70addf90dc8d4b946b593c3e51a8 \
|
||||
--hash=sha256:c1e0ed5d84ffa2d677cc9582fc01e61dab2e7ef8b8996e055f0a76167b1b94df \
|
||||
--hash=sha256:c4278d1873ce6e803e5d4f8d702fd3026bd67fca744aa98881324d1157ddf748 \
|
||||
--hash=sha256:cac2b37ab21c2b36a10b685b1893ebd6b0f83ae26004838ac817680881576567 \
|
||||
--hash=sha256:cbe6df25807227519debd1a57ab236f5f6bad441500e85b13903e51f93a43214 \
|
||||
--hash=sha256:cd2c002f160502608dcc822ed2441a0f4509c52e86fcfd1a09e937278ed1ca14 \
|
||||
--hash=sha256:e0137dd64a493ba6a4be37405cfd6febe650a98cc1e9dca8f6b8c63b1db11b41 \
|
||||
--hash=sha256:e63d558847166543c2c9789e6f985400a520b7eacc4b99181668b2c3aeadd352 \
|
||||
--hash=sha256:eb45a34f23da4f8bc712b6376ca5396914b0b7c09adbb001dad964eb7f3132f8 \
|
||||
--hash=sha256:ecb7572df5372abce8073df078207d9d1749f20b8b136089916a4a0868d56051 \
|
||||
--hash=sha256:f12000a6accdd4bf0a3fa6eaa1b1c7a7bc80af0a2edf3f89d770d3dcce1d0e22 \
|
||||
--hash=sha256:f7d69c1a7168ad0e9cb864e8663acb232986a0c9c9cb9801f56bf6214f53a54d \
|
||||
--hash=sha256:f815fcc2b2a457977724bad97fb4854022980f51ce7b136925e336b530545ae1 \
|
||||
--hash=sha256:fc39f5c27f962ec8660d8d20c24762431131b5d8c672b44b0a54cf2b5bcde9b9
|
||||
# via fonttools
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==25.2 \
|
||||
--hash=sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2 \
|
||||
--hash=sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717
|
||||
# via -r .github\scripts\requirements_dev.in
|
||||
setuptools==80.9.0 \
|
||||
--hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \
|
||||
--hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c
|
||||
# via -r .github\scripts\requirements_dev.in
|
28
.github/scripts/requirements_pre_commit.txt
vendored
28
.github/scripts/requirements_pre_commit.txt
vendored
@ -8,17 +8,17 @@ cfgv==3.4.0 \
|
||||
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
|
||||
--hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
|
||||
# via pre-commit
|
||||
distlib==0.3.9 \
|
||||
--hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \
|
||||
--hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403
|
||||
distlib==0.4.0 \
|
||||
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
||||
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
||||
# via virtualenv
|
||||
filelock==3.18.0 \
|
||||
--hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \
|
||||
--hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de
|
||||
# via virtualenv
|
||||
identify==2.6.12 \
|
||||
--hash=sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2 \
|
||||
--hash=sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6
|
||||
identify==2.6.13 \
|
||||
--hash=sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b \
|
||||
--hash=sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32
|
||||
# via pre-commit
|
||||
nodeenv==1.9.1 \
|
||||
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||
@ -28,9 +28,9 @@ platformdirs==4.3.8 \
|
||||
--hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \
|
||||
--hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4
|
||||
# via virtualenv
|
||||
pre-commit==4.2.0 \
|
||||
--hash=sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146 \
|
||||
--hash=sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd
|
||||
pre-commit==4.3.0 \
|
||||
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
|
||||
--hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16
|
||||
# via -r .github\scripts\requirements_pre_commit.in
|
||||
pyyaml==6.0.2 \
|
||||
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
|
||||
@ -87,7 +87,11 @@ pyyaml==6.0.2 \
|
||||
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
|
||||
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
|
||||
# via pre-commit
|
||||
virtualenv==20.31.2 \
|
||||
--hash=sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11 \
|
||||
--hash=sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af
|
||||
typing-extensions==4.14.1 \
|
||||
--hash=sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36 \
|
||||
--hash=sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76
|
||||
# via virtualenv
|
||||
virtualenv==20.34.0 \
|
||||
--hash=sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026 \
|
||||
--hash=sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a
|
||||
# via pre-commit
|
||||
|
28
.github/workflows/PR-Demo-Comment-with-react.yml
vendored
28
.github/workflows/PR-Demo-Comment-with-react.yml
vendored
@ -34,8 +34,6 @@ jobs:
|
||||
)
|
||||
outputs:
|
||||
pr_number: ${{ steps.get-pr.outputs.pr_number }}
|
||||
pr_repository: ${{ steps.get-pr-info.outputs.repository }}
|
||||
pr_ref: ${{ steps.get-pr-info.outputs.ref }}
|
||||
comment_id: ${{ github.event.comment.id }}
|
||||
disable_security: ${{ steps.check-security-flag.outputs.disable_security }}
|
||||
enable_pro: ${{ steps.check-pro-flag.outputs.enable_pro }}
|
||||
@ -67,29 +65,6 @@ jobs:
|
||||
console.log(`PR Number: ${prNumber}`);
|
||||
core.setOutput('pr_number', prNumber);
|
||||
|
||||
- name: Get PR repository and ref
|
||||
id: get-pr-info
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = context.payload.issue.number;
|
||||
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber,
|
||||
});
|
||||
|
||||
// For forks, use the full repository name, for internal PRs use the current repo
|
||||
const repository = pr.head.repo.fork ? pr.head.repo.full_name : `${owner}/${repo}`;
|
||||
|
||||
console.log(`PR Repository: ${repository}`);
|
||||
console.log(`PR Branch: ${pr.head.ref}`);
|
||||
|
||||
core.setOutput('repository', repository);
|
||||
core.setOutput('ref', pr.head.ref);
|
||||
|
||||
- name: Check for security/login flag
|
||||
id: check-security-flag
|
||||
env:
|
||||
@ -172,8 +147,7 @@ jobs:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
repository: ${{ needs.check-comment.outputs.pr_repository }}
|
||||
ref: ${{ needs.check-comment.outputs.pr_ref }}
|
||||
ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
|
||||
- name: Set up JDK
|
||||
|
25
.github/workflows/build.yml
vendored
25
.github/workflows/build.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
||||
project: ${{ steps.changes.outputs.project }}
|
||||
openapi: ${{ steps.changes.outputs.openapi }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4.3.0
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@v3.0.2
|
||||
@ -55,7 +55,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.3.0
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Set up JDK ${{ matrix.jdk-version }}
|
||||
uses: actions/setup-java@v4.7.1
|
||||
@ -63,7 +63,7 @@ jobs:
|
||||
java-version: ${{ matrix.jdk-version }}
|
||||
distribution: "temurin"
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4.4.2
|
||||
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
- name: Build with Gradle and spring security ${{ matrix.spring-security }}
|
||||
@ -111,14 +111,17 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.3.0
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4.7.1
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
- uses: gradle/actions/setup-gradle@v4.4.2
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
|
||||
- name: Generate OpenAPI documentation
|
||||
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||
env:
|
||||
@ -168,15 +171,21 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.3.0
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4.7.1
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
- name: check the licenses for compatibility
|
||||
|
||||
- name: Check licenses for compatibility
|
||||
run: ./gradlew clean checkLicense
|
||||
- name: FAILED - check the licenses for compatibility
|
||||
env:
|
||||
DISABLE_ADDITIONAL_FEATURES: false
|
||||
STIRLING_PDF_DESKTOP_UI: true
|
||||
|
||||
- name: FAILED - Check licenses for compatibility
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
|
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
@ -24,4 +24,4 @@ jobs:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1
|
||||
uses: actions/dependency-review-action@bc41886e18ea39df68b1b1245f4184881938e050 # v4.7.2
|
||||
|
3
.github/workflows/licenses-update.yml
vendored
3
.github/workflows/licenses-update.yml
vendored
@ -58,6 +58,9 @@ jobs:
|
||||
|
||||
- name: Check licenses for compatibility
|
||||
run: ./gradlew clean checkLicense
|
||||
env:
|
||||
DISABLE_ADDITIONAL_FEATURES: false
|
||||
STIRLING_PDF_DESKTOP_UI: true
|
||||
|
||||
- name: Upload artifact on failure
|
||||
if: failure()
|
||||
|
2
.github/workflows/scorecards.yml
vendored
2
.github/workflows/scorecards.yml
vendored
@ -74,6 +74,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.5
|
||||
uses: github/codeql-action/upload-sarif@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.5
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
60
Dockerfile.dev
Normal file
60
Dockerfile.dev
Normal file
@ -0,0 +1,60 @@
|
||||
# dockerfile.dev
|
||||
|
||||
# Basisimage: Gradle mit JDK 17 (Debian-basiert)
|
||||
FROM gradle:8.14-jdk17
|
||||
|
||||
# Als Root-Benutzer arbeiten, um benötigte Pakete zu installieren
|
||||
USER root
|
||||
|
||||
# Set GRADLE_HOME und füge Gradle zum PATH hinzu
|
||||
ENV GRADLE_HOME=/opt/gradle
|
||||
ENV PATH="$GRADLE_HOME/bin:$PATH"
|
||||
|
||||
# Update und Installation zusätzlicher Pakete (Debian/Ubuntu-basiert)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
sudo \
|
||||
libreoffice \
|
||||
poppler-utils \
|
||||
qpdf \
|
||||
# settings.yml | tessdataDir: /usr/share/tesseract-ocr/5/tessdata
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-eng \
|
||||
fonts-terminus fonts-dejavu fonts-font-awesome fonts-noto fonts-noto-core fonts-noto-cjk fonts-noto-extra fonts-liberation fonts-linuxlibertine fonts-urw-base35 \
|
||||
python3-uno \
|
||||
python3-venv \
|
||||
# ss -tln
|
||||
iproute2 \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Setze die Environment Variable für setuptools
|
||||
ENV SETUPTOOLS_USE_DISTUTILS=local \
|
||||
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
||||
TMPDIR=/tmp/stirling-pdf \
|
||||
TEMP=/tmp/stirling-pdf \
|
||||
TMP=/tmp/stirling-pdf
|
||||
|
||||
# Installation der benötigten Python-Pakete
|
||||
COPY .github/scripts/requirements_dev.txt /tmp/requirements_dev.txt
|
||||
RUN python3 -m venv --system-site-packages /opt/venv \
|
||||
&& . /opt/venv/bin/activate \
|
||||
&& pip install --no-cache-dir --require-hashes -r /tmp/requirements_dev.txt
|
||||
|
||||
# Füge den venv-Pfad zur globalen PATH-Variable hinzu, damit die Tools verfügbar sind
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
COPY . /workspace
|
||||
|
||||
RUN mkdir -p /tmp/stirling-pdf \
|
||||
&& fc-cache -f -v \
|
||||
&& adduser --disabled-password --gecos '' devuser \
|
||||
&& chown -R devuser:devuser /home/devuser /workspace /tmp/stirling-pdf
|
||||
RUN echo "devuser ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/devuser \
|
||||
&& chmod 0440 /etc/sudoers.d/devuser
|
||||
|
||||
# Setze das Arbeitsverzeichnis (wird später per Bind-Mount überschrieben)
|
||||
WORKDIR /workspace
|
||||
|
||||
RUN chmod +x /workspace/.devcontainer/git-init.sh /workspace/.devcontainer/init-setup.sh
|
||||
|
||||
# Wechsel zum Nicht‑Root Benutzer
|
||||
USER devuser
|
@ -39,7 +39,7 @@ dependencies {
|
||||
api "org.apache.pdfbox:pdfbox:$pdfboxVersion"
|
||||
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
||||
api 'org.snakeyaml:snakeyaml-engine:2.10'
|
||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9"
|
||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.11"
|
||||
api 'jakarta.mail:jakarta.mail-api:2.1.3'
|
||||
runtimeOnly 'org.eclipse.angus:angus-mail:2.0.4'
|
||||
}
|
||||
|
@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.FileOrUploadService;
|
||||
import stirling.software.common.service.FileStorage;
|
||||
import stirling.software.common.service.JobExecutorService;
|
||||
|
||||
@ -34,7 +33,6 @@ public class AutoJobAspect {
|
||||
|
||||
private final JobExecutorService jobExecutorService;
|
||||
private final HttpServletRequest request;
|
||||
private final FileOrUploadService fileOrUploadService;
|
||||
private final FileStorage fileStorage;
|
||||
|
||||
@Around("@annotation(autoJobPostMapping)")
|
||||
@ -53,7 +51,8 @@ public class AutoJobAspect {
|
||||
boolean trackProgress = autoJobPostMapping.trackProgress();
|
||||
|
||||
log.debug(
|
||||
"AutoJobPostMapping execution with async={}, timeout={}, retryCount={}, trackProgress={}",
|
||||
"AutoJobPostMapping execution with async={}, timeout={}, retryCount={},"
|
||||
+ " trackProgress={}",
|
||||
async,
|
||||
timeout > 0 ? timeout : "default",
|
||||
retryCount,
|
||||
@ -148,7 +147,8 @@ public class AutoJobAspect {
|
||||
} catch (Throwable ex) {
|
||||
lastException = ex;
|
||||
log.error(
|
||||
"AutoJobAspect caught exception during job execution (attempt {}/{}): {}",
|
||||
"AutoJobAspect caught exception during job execution (attempt"
|
||||
+ " {}/{}): {}",
|
||||
currentAttempt,
|
||||
maxRetries,
|
||||
ex.getMessage(),
|
||||
|
@ -57,7 +57,6 @@ public class AppConfig {
|
||||
return v2Enabled;
|
||||
}
|
||||
|
||||
/* Commented out Thymeleaf template engine bean - to be removed when frontend migration is complete
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true")
|
||||
public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) {
|
||||
|
@ -23,10 +23,30 @@ import stirling.software.common.util.YamlHelper;
|
||||
@Slf4j
|
||||
public class ConfigInitializer {
|
||||
|
||||
private static final int MIN_SETTINGS_FILE_LINES = 31;
|
||||
|
||||
public void ensureConfigExists() throws IOException, URISyntaxException {
|
||||
// 1) If settings file doesn't exist, create from template
|
||||
Path destPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
||||
if (Files.notExists(destPath)) {
|
||||
|
||||
boolean settingsFileExists = Files.exists(destPath);
|
||||
|
||||
long lineCount = settingsFileExists ? Files.readAllLines(destPath).size() : 0;
|
||||
|
||||
log.info("Current settings file line count: {}", lineCount);
|
||||
|
||||
if (!settingsFileExists || lineCount < MIN_SETTINGS_FILE_LINES) {
|
||||
if (settingsFileExists) {
|
||||
// move settings.yml to settings.yml.{timestamp}.bak
|
||||
Path backupPath =
|
||||
Paths.get(
|
||||
InstallationPathConfig.getSettingsPath()
|
||||
+ "."
|
||||
+ System.currentTimeMillis()
|
||||
+ ".bak");
|
||||
Files.move(destPath, backupPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
log.info("Moved existing settings file to backup: {}", backupPath);
|
||||
}
|
||||
Files.createDirectories(destPath.getParent());
|
||||
try (InputStream in =
|
||||
getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
|
||||
|
@ -14,12 +14,17 @@ public class InstallationPathConfig {
|
||||
private static final String CONFIG_PATH;
|
||||
private static final String CUSTOM_FILES_PATH;
|
||||
private static final String CLIENT_WEBUI_PATH;
|
||||
private static final String SCRIPTS_PATH;
|
||||
private static final String PIPELINE_PATH;
|
||||
|
||||
// Config paths
|
||||
private static final String SETTINGS_PATH;
|
||||
private static final String CUSTOM_SETTINGS_PATH;
|
||||
private static final String SCRIPTS_PATH;
|
||||
private static final String BACKUP_PATH;
|
||||
|
||||
// Backup paths
|
||||
private static final String BACKUP_DB_PATH;
|
||||
private static final String BACKUP_PRIVATE_KEY_PATH;
|
||||
|
||||
// Custom file paths
|
||||
private static final String STATIC_PATH;
|
||||
@ -41,6 +46,11 @@ public class InstallationPathConfig {
|
||||
SETTINGS_PATH = CONFIG_PATH + "settings.yml";
|
||||
CUSTOM_SETTINGS_PATH = CONFIG_PATH + "custom_settings.yml";
|
||||
SCRIPTS_PATH = CONFIG_PATH + "scripts" + File.separator;
|
||||
BACKUP_PATH = CONFIG_PATH + "backup" + File.separator;
|
||||
|
||||
// Initialize backup paths
|
||||
BACKUP_DB_PATH = BACKUP_PATH + "db" + File.separator;
|
||||
BACKUP_PRIVATE_KEY_PATH = BACKUP_PATH + "keys" + File.separator;
|
||||
|
||||
// Initialize custom file paths
|
||||
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
|
||||
@ -124,6 +134,10 @@ public class InstallationPathConfig {
|
||||
}
|
||||
|
||||
public static String getPrivateKeyPath() {
|
||||
return PRIVATE_KEY_PATH;
|
||||
return BACKUP_PRIVATE_KEY_PATH;
|
||||
}
|
||||
|
||||
public static String getBackupPath() {
|
||||
return BACKUP_DB_PATH;
|
||||
}
|
||||
}
|
||||
|
@ -8,9 +8,11 @@ import java.util.Locale;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class FileInfo {
|
||||
private static final DateTimeFormatter DATE_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
@ -2,11 +2,15 @@ package stirling.software.common.model;
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PdfMetadata {
|
||||
private String author;
|
||||
private String producer;
|
||||
|
@ -252,8 +252,10 @@ public class JobExecutorService {
|
||||
}
|
||||
}
|
||||
|
||||
if (response.getHeaders().getContentType() != null) {
|
||||
contentType = response.getHeaders().getContentType().toString();
|
||||
MediaType mediaType = response.getHeaders().getContentType();
|
||||
|
||||
if (mediaType != null) {
|
||||
contentType = mediaType.toString();
|
||||
}
|
||||
|
||||
// Store byte array directly to disk
|
||||
|
@ -1,5 +1,7 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.UnknownHostException;
|
||||
@ -51,16 +53,12 @@ public class SsrfProtectionService {
|
||||
|
||||
SsrfProtectionLevel level = parseProtectionLevel(config.getLevel());
|
||||
|
||||
switch (level) {
|
||||
case OFF:
|
||||
return true;
|
||||
case MAX:
|
||||
return isMaxSecurityAllowed(trimmedUrl, config);
|
||||
case MEDIUM:
|
||||
return isMediumSecurityAllowed(trimmedUrl, config);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return switch (level) {
|
||||
case OFF -> true;
|
||||
case MAX -> isMaxSecurityAllowed(trimmedUrl, config);
|
||||
case MEDIUM -> isMediumSecurityAllowed(trimmedUrl, config);
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private SsrfProtectionLevel parseProtectionLevel(String level) {
|
||||
@ -172,15 +170,61 @@ public class SsrfProtectionService {
|
||||
}
|
||||
|
||||
private boolean isPrivateAddress(InetAddress address) {
|
||||
return address.isSiteLocalAddress()
|
||||
|| address.isAnyLocalAddress()
|
||||
|| isPrivateIPv4Range(address.getHostAddress());
|
||||
if (address.isAnyLocalAddress() || address.isLoopbackAddress()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (address instanceof Inet4Address) {
|
||||
return isPrivateIPv4Range(address.getHostAddress());
|
||||
}
|
||||
|
||||
if (address instanceof Inet6Address addr6) {
|
||||
if (addr6.isLinkLocalAddress() || addr6.isSiteLocalAddress()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
byte[] bytes = addr6.getAddress();
|
||||
if (isIpv4MappedAddress(bytes)) {
|
||||
String ipv4 =
|
||||
(bytes[12] & 0xff)
|
||||
+ "."
|
||||
+ (bytes[13] & 0xff)
|
||||
+ "."
|
||||
+ (bytes[14] & 0xff)
|
||||
+ "."
|
||||
+ (bytes[15] & 0xff);
|
||||
return isPrivateIPv4Range(ipv4);
|
||||
}
|
||||
|
||||
int firstByte = bytes[0] & 0xff;
|
||||
// Check for IPv6 unique local addresses (fc00::/7)
|
||||
if ((firstByte & 0xfe) == 0xfc) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isIpv4MappedAddress(byte[] addr) {
|
||||
if (addr.length != 16) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (addr[i] != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// For IPv4-mapped IPv6 addresses, bytes 10 and 11 must be 0xff (i.e., address is ::ffff:w.x.y.z)
|
||||
return addr[10] == (byte) 0xff && addr[11] == (byte) 0xff;
|
||||
}
|
||||
|
||||
private boolean isPrivateIPv4Range(String ip) {
|
||||
// Includes RFC1918, loopback, link-local, and unspecified addresses
|
||||
return ip.startsWith("10.")
|
||||
|| ip.startsWith("192.168.")
|
||||
|| (ip.startsWith("172.") && isInRange172(ip))
|
||||
|| ip.startsWith("169.254.")
|
||||
|| ip.startsWith("127.")
|
||||
|| "0.0.0.0".equals(ip);
|
||||
}
|
||||
@ -192,17 +236,31 @@ public class SsrfProtectionService {
|
||||
int secondOctet = Integer.parseInt(parts[1]);
|
||||
return secondOctet >= 16 && secondOctet <= 31;
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isCloudMetadataAddress(String ip) {
|
||||
String normalizedIp = normalizeIpv4MappedAddress(ip);
|
||||
// Cloud metadata endpoints for AWS, GCP, Azure, Oracle Cloud, and IBM Cloud
|
||||
return ip.startsWith("169.254.169.254") // AWS/GCP/Azure
|
||||
|| ip.startsWith("fd00:ec2::254") // AWS IPv6
|
||||
|| ip.startsWith("169.254.169.253") // Oracle Cloud
|
||||
|| ip.startsWith("169.254.169.250"); // IBM Cloud
|
||||
return normalizedIp.startsWith("169.254.169.254") // AWS/GCP/Azure
|
||||
|| normalizedIp.startsWith("fd00:ec2::254") // AWS IPv6
|
||||
|| normalizedIp.startsWith("169.254.169.253") // Oracle Cloud
|
||||
|| normalizedIp.startsWith("169.254.169.250"); // IBM Cloud
|
||||
}
|
||||
|
||||
private String normalizeIpv4MappedAddress(String ip) {
|
||||
if (ip == null) {
|
||||
return "";
|
||||
}
|
||||
if (ip.startsWith("::ffff:")) {
|
||||
return ip.substring(7);
|
||||
}
|
||||
int lastColon = ip.lastIndexOf(':');
|
||||
if (lastColon >= 0 && ip.indexOf('.') > lastColon) {
|
||||
return ip.substring(lastColon + 1);
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import org.owasp.html.AttributePolicy;
|
||||
import org.owasp.html.HtmlPolicyBuilder;
|
||||
import org.owasp.html.PolicyFactory;
|
||||
import org.owasp.html.Sanitizers;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
@ -16,7 +15,6 @@ public class CustomHtmlSanitizer {
|
||||
private final SsrfProtectionService ssrfProtectionService;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@Autowired
|
||||
public CustomHtmlSanitizer(
|
||||
SsrfProtectionService ssrfProtectionService,
|
||||
ApplicationProperties applicationProperties) {
|
||||
|
@ -29,7 +29,6 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import stirling.software.common.aop.AutoJobAspect;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.FileOrUploadService;
|
||||
import stirling.software.common.service.FileStorage;
|
||||
import stirling.software.common.service.JobExecutorService;
|
||||
import stirling.software.common.service.JobQueue;
|
||||
@ -44,8 +43,6 @@ class AutoJobPostMappingIntegrationTest {
|
||||
|
||||
@Mock private HttpServletRequest request;
|
||||
|
||||
@Mock private FileOrUploadService fileOrUploadService;
|
||||
|
||||
@Mock private FileStorage fileStorage;
|
||||
|
||||
@Mock private ResourceMonitor resourceMonitor;
|
||||
@ -54,8 +51,7 @@ class AutoJobPostMappingIntegrationTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
autoJobAspect =
|
||||
new AutoJobAspect(jobExecutorService, request, fileOrUploadService, fileStorage);
|
||||
autoJobAspect = new AutoJobAspect(jobExecutorService, request, fileStorage);
|
||||
}
|
||||
|
||||
@Mock private ProceedingJoinPoint joinPoint;
|
||||
|
@ -179,7 +179,7 @@ class ApplicationPropertiesLogicTest {
|
||||
assertEquals(30, t.getOcrMyPdfTimeoutMinutes());
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Deprecated(since = "0.45.0")
|
||||
@Test
|
||||
void enterprise_metadata_defaults() {
|
||||
ApplicationProperties.EnterpriseEdition ee = new ApplicationProperties.EnterpriseEdition();
|
||||
|
@ -586,7 +586,10 @@ class EmlToPdfTest {
|
||||
when(mockPdDocument.getNumberOfPages()).thenReturn(1);
|
||||
|
||||
try (MockedStatic<FileToPdf> fileToPdf =
|
||||
mockStatic(FileToPdf.class, org.mockito.Mockito.withSettings().lenient())) {
|
||||
mockStatic(
|
||||
FileToPdf.class,
|
||||
org.mockito.Mockito.withSettings()
|
||||
.defaultAnswer(org.mockito.Answers.RETURNS_DEFAULTS))) {
|
||||
fileToPdf
|
||||
.when(
|
||||
() ->
|
||||
@ -657,7 +660,10 @@ class EmlToPdfTest {
|
||||
when(mockPdDocument.getNumberOfPages()).thenReturn(1);
|
||||
|
||||
try (MockedStatic<FileToPdf> fileToPdf =
|
||||
mockStatic(FileToPdf.class, org.mockito.Mockito.withSettings().lenient())) {
|
||||
mockStatic(
|
||||
FileToPdf.class,
|
||||
org.mockito.Mockito.withSettings()
|
||||
.defaultAnswer(org.mockito.Answers.RETURNS_DEFAULTS))) {
|
||||
fileToPdf
|
||||
.when(
|
||||
() ->
|
||||
@ -724,7 +730,10 @@ class EmlToPdfTest {
|
||||
String errorMessage = "Conversion failed";
|
||||
|
||||
try (MockedStatic<FileToPdf> fileToPdf =
|
||||
mockStatic(FileToPdf.class, org.mockito.Mockito.withSettings().lenient())) {
|
||||
mockStatic(
|
||||
FileToPdf.class,
|
||||
org.mockito.Mockito.withSettings()
|
||||
.defaultAnswer(org.mockito.Answers.RETURNS_DEFAULTS))) {
|
||||
fileToPdf
|
||||
.when(
|
||||
() ->
|
||||
|
@ -58,7 +58,7 @@ dependencies {
|
||||
implementation 'commons-io:commons-io:2.20.0'
|
||||
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
||||
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
||||
implementation 'io.micrometer:micrometer-core:1.15.2'
|
||||
implementation 'io.micrometer:micrometer-core:1.15.3'
|
||||
implementation 'com.google.zxing:core:3.5.3'
|
||||
implementation "org.commonmark:commonmark:$commonmarkVersion" // https://mvnrepository.com/artifact/org.commonmark/commonmark
|
||||
implementation "org.commonmark:commonmark-ext-gfm-tables:$commonmarkVersion"
|
||||
@ -114,11 +114,6 @@ sourceSets {
|
||||
}
|
||||
test {
|
||||
java {
|
||||
if (System.getenv('DOCKER_ENABLE_SECURITY') == 'false' || System.getenv('DISABLE_ADDITIONAL_FEATURES') == 'true'
|
||||
|| (project.hasProperty('DISABLE_ADDITIONAL_FEATURES')
|
||||
&& System.getProperty('DISABLE_ADDITIONAL_FEATURES') == 'true')) {
|
||||
exclude 'stirling/software/proprietary/security/**'
|
||||
}
|
||||
if (System.getenv('STIRLING_PDF_DESKTOP_UI') == 'false') {
|
||||
exclude 'stirling/software/SPDF/UI/impl/**'
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.common.configuration.AppConfig;
|
||||
import stirling.software.common.configuration.ConfigInitializer;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.UrlUtils;
|
||||
|
||||
@Slf4j
|
||||
@ -44,16 +43,15 @@ public class SPDFApplication {
|
||||
|
||||
private final AppConfig appConfig;
|
||||
private final Environment env;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
// private final WebBrowser webBrowser; // Removed - desktop UI eliminated
|
||||
private final WebBrowser webBrowser;
|
||||
|
||||
public SPDFApplication(
|
||||
AppConfig appConfig, Environment env, ApplicationProperties applicationProperties) {
|
||||
AppConfig appConfig,
|
||||
Environment env,
|
||||
@Autowired(required = false) WebBrowser webBrowser) {
|
||||
this.appConfig = appConfig;
|
||||
this.env = env;
|
||||
this.applicationProperties = applicationProperties;
|
||||
// this.webBrowser = webBrowser; // Removed - desktop UI eliminated
|
||||
this.webBrowser = webBrowser;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException, InterruptedException {
|
||||
|
@ -20,6 +20,8 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
||||
"endpoints",
|
||||
"logout",
|
||||
"error",
|
||||
"days",
|
||||
"date",
|
||||
"errorOAuth",
|
||||
"file",
|
||||
"messageType",
|
||||
|
@ -6,8 +6,6 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
@ -18,11 +16,12 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class EndpointInspector implements ApplicationListener<ContextRefreshedEvent> {
|
||||
private static final Logger logger = LoggerFactory.getLogger(EndpointInspector.class);
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
private final Set<String> validGetEndpoints = new HashSet<>();
|
||||
@ -71,13 +70,13 @@ public class EndpointInspector implements ApplicationListener<ContextRefreshedEv
|
||||
}
|
||||
|
||||
if (validGetEndpoints.isEmpty()) {
|
||||
logger.warn("No endpoints discovered. Adding common endpoints as fallback.");
|
||||
log.warn("No endpoints discovered. Adding common endpoints as fallback.");
|
||||
validGetEndpoints.add("/");
|
||||
validGetEndpoints.add("/api/**");
|
||||
validGetEndpoints.add("/**");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Error discovering endpoints", e);
|
||||
log.error("Error discovering endpoints", e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -203,10 +202,10 @@ public class EndpointInspector implements ApplicationListener<ContextRefreshedEv
|
||||
private void logAllEndpoints() {
|
||||
Set<String> sortedEndpoints = new TreeSet<>(validGetEndpoints);
|
||||
|
||||
logger.info("=== BEGIN: All discovered GET endpoints ===");
|
||||
log.info("=== BEGIN: All discovered GET endpoints ===");
|
||||
for (String endpoint : sortedEndpoints) {
|
||||
logger.info("Endpoint: {}", endpoint);
|
||||
log.info("Endpoint: {}", endpoint);
|
||||
}
|
||||
logger.info("=== END: All discovered GET endpoints ===");
|
||||
log.info("=== END: All discovered GET endpoints ===");
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -2,8 +2,10 @@ package stirling.software.SPDF.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SignatureFile {
|
||||
private String fileName;
|
||||
|
@ -6,7 +6,7 @@ import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.apache.pdfbox.text.TextPosition;
|
||||
|
||||
@ -17,91 +17,203 @@ import stirling.software.SPDF.model.PDFText;
|
||||
@Slf4j
|
||||
public class TextFinder extends PDFTextStripper {
|
||||
|
||||
private final String searchText;
|
||||
private final String searchTerm;
|
||||
private final boolean useRegex;
|
||||
private final boolean wholeWordSearch;
|
||||
private final List<PDFText> textOccurrences = new ArrayList<>();
|
||||
private final List<PDFText> foundTexts = new ArrayList<>();
|
||||
|
||||
public TextFinder(String searchText, boolean useRegex, boolean wholeWordSearch)
|
||||
private final List<TextPosition> pageTextPositions = new ArrayList<>();
|
||||
private final StringBuilder pageTextBuilder = new StringBuilder();
|
||||
|
||||
public TextFinder(String searchTerm, boolean useRegex, boolean wholeWordSearch)
|
||||
throws IOException {
|
||||
this.searchText = searchText.toLowerCase();
|
||||
this.searchTerm = searchTerm;
|
||||
this.useRegex = useRegex;
|
||||
this.wholeWordSearch = wholeWordSearch;
|
||||
setSortByPosition(true);
|
||||
this.setWordSeparator(" ");
|
||||
}
|
||||
|
||||
private List<MatchInfo> findOccurrencesInText(String searchText, String content) {
|
||||
List<MatchInfo> matches = new ArrayList<>();
|
||||
|
||||
Pattern pattern;
|
||||
|
||||
if (useRegex) {
|
||||
// Use regex-based search
|
||||
pattern =
|
||||
wholeWordSearch
|
||||
? Pattern.compile("\\b" + searchText + "\\b")
|
||||
: Pattern.compile(searchText);
|
||||
} else {
|
||||
// Use normal text search
|
||||
pattern =
|
||||
wholeWordSearch
|
||||
? Pattern.compile("\\b" + Pattern.quote(searchText) + "\\b")
|
||||
: Pattern.compile(Pattern.quote(searchText));
|
||||
}
|
||||
|
||||
Matcher matcher = pattern.matcher(content);
|
||||
while (matcher.find()) {
|
||||
matches.add(new MatchInfo(matcher.start(), matcher.end() - matcher.start()));
|
||||
}
|
||||
return matches;
|
||||
@Override
|
||||
protected void startPage(PDPage page) throws IOException {
|
||||
super.startPage(page);
|
||||
pageTextPositions.clear();
|
||||
pageTextBuilder.setLength(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeString(String text, List<TextPosition> textPositions) {
|
||||
for (MatchInfo match : findOccurrencesInText(searchText, text.toLowerCase())) {
|
||||
int index = match.startIndex;
|
||||
if (index + match.matchLength <= textPositions.size()) {
|
||||
// Initial values based on the first character
|
||||
TextPosition first = textPositions.get(index);
|
||||
float minX = first.getX();
|
||||
float minY = first.getY();
|
||||
float maxX = first.getX() + first.getWidth();
|
||||
float maxY = first.getY() + first.getHeight();
|
||||
pageTextBuilder.append(text);
|
||||
pageTextPositions.addAll(textPositions);
|
||||
}
|
||||
|
||||
// Loop over the rest of the characters and adjust bounding box values
|
||||
for (int i = index; i < index + match.matchLength; i++) {
|
||||
TextPosition position = textPositions.get(i);
|
||||
minX = Math.min(minX, position.getX());
|
||||
minY = Math.min(minY, position.getY());
|
||||
maxX = Math.max(maxX, position.getX() + position.getWidth());
|
||||
maxY = Math.max(maxY, position.getY() + position.getHeight());
|
||||
}
|
||||
@Override
|
||||
protected void writeWordSeparator() {
|
||||
pageTextBuilder.append(getWordSeparator());
|
||||
pageTextPositions.add(null); // Placeholder for separator
|
||||
}
|
||||
|
||||
textOccurrences.add(
|
||||
new PDFText(getCurrentPageNo() - 1, minX, minY, maxX, maxY, text));
|
||||
@Override
|
||||
protected void writeLineSeparator() {
|
||||
pageTextBuilder.append(getLineSeparator());
|
||||
pageTextPositions.add(null); // Placeholder for separator
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void endPage(PDPage page) throws IOException {
|
||||
String text = pageTextBuilder.toString();
|
||||
if (text.isEmpty() || this.searchTerm == null || this.searchTerm.isEmpty()) {
|
||||
super.endPage(page);
|
||||
return;
|
||||
}
|
||||
|
||||
String processedSearchTerm = this.searchTerm.trim();
|
||||
if (processedSearchTerm.isEmpty()) {
|
||||
super.endPage(page);
|
||||
return;
|
||||
}
|
||||
String regex = this.useRegex ? processedSearchTerm : "\\Q" + processedSearchTerm + "\\E";
|
||||
if (this.wholeWordSearch) {
|
||||
if (processedSearchTerm.length() == 1
|
||||
&& Character.isDigit(processedSearchTerm.charAt(0))) {
|
||||
regex = "(?<![\\w])(?<!\\d[\\.,])" + regex + "(?![\\w])(?![\\.,]\\d)";
|
||||
} else if (processedSearchTerm.length() == 1) {
|
||||
regex = "(?<![\\w])" + regex + "(?![\\w])";
|
||||
} else {
|
||||
regex = "\\b" + regex + "\\b";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<PDFText> getTextLocations(PDDocument document) throws Exception {
|
||||
this.getText(document);
|
||||
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
|
||||
Matcher matcher = pattern.matcher(text);
|
||||
|
||||
log.debug(
|
||||
"Found "
|
||||
+ textOccurrences.size()
|
||||
+ " occurrences of '"
|
||||
+ searchText
|
||||
+ "' in the document.");
|
||||
"Searching for '{}' in page {} with regex '{}' (wholeWord: {}, useRegex: {})",
|
||||
processedSearchTerm,
|
||||
getCurrentPageNo(),
|
||||
regex,
|
||||
wholeWordSearch,
|
||||
useRegex);
|
||||
|
||||
return textOccurrences;
|
||||
int matchCount = 0;
|
||||
while (matcher.find()) {
|
||||
matchCount++;
|
||||
int matchStart = matcher.start();
|
||||
int matchEnd = matcher.end();
|
||||
|
||||
log.debug(
|
||||
"Found match #{} at positions {}-{}: '{}'",
|
||||
matchCount,
|
||||
matchStart,
|
||||
matchEnd,
|
||||
matcher.group());
|
||||
|
||||
float minX = Float.MAX_VALUE;
|
||||
float minY = Float.MAX_VALUE;
|
||||
float maxX = Float.MIN_VALUE;
|
||||
float maxY = Float.MIN_VALUE;
|
||||
boolean foundPosition = false;
|
||||
|
||||
for (int i = matchStart; i < matchEnd; i++) {
|
||||
if (i >= pageTextPositions.size()) {
|
||||
log.debug(
|
||||
"Position index {} exceeds available positions ({})",
|
||||
i,
|
||||
pageTextPositions.size());
|
||||
continue;
|
||||
}
|
||||
TextPosition pos = pageTextPositions.get(i);
|
||||
if (pos != null) {
|
||||
foundPosition = true;
|
||||
minX = Math.min(minX, pos.getX());
|
||||
maxX = Math.max(maxX, pos.getX() + pos.getWidth());
|
||||
minY = Math.min(minY, pos.getY() - pos.getHeight());
|
||||
maxY = Math.max(maxY, pos.getY());
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundPosition && matchStart < pageTextPositions.size()) {
|
||||
log.debug(
|
||||
"Attempting to find nearby positions for match at {}-{}",
|
||||
matchStart,
|
||||
matchEnd);
|
||||
|
||||
for (int i = Math.max(0, matchStart - 5);
|
||||
i < Math.min(pageTextPositions.size(), matchEnd + 5);
|
||||
i++) {
|
||||
TextPosition pos = pageTextPositions.get(i);
|
||||
if (pos != null) {
|
||||
foundPosition = true;
|
||||
minX = Math.min(minX, pos.getX());
|
||||
maxX = Math.max(maxX, pos.getX() + pos.getWidth());
|
||||
minY = Math.min(minY, pos.getY() - pos.getHeight());
|
||||
maxY = Math.max(maxY, pos.getY());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundPosition) {
|
||||
foundTexts.add(
|
||||
new PDFText(
|
||||
this.getCurrentPageNo() - 1,
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
matcher.group()));
|
||||
log.debug(
|
||||
"Added PDFText for match: page={}, bounds=({},{},{},{}), text='{}'",
|
||||
getCurrentPageNo() - 1,
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
matcher.group());
|
||||
} else {
|
||||
log.warn(
|
||||
"Found text match '{}' but no valid position data at {}-{}",
|
||||
matcher.group(),
|
||||
matchStart,
|
||||
matchEnd);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug(
|
||||
"Page {} search complete: found {} matches for '{}'",
|
||||
getCurrentPageNo(),
|
||||
matchCount,
|
||||
processedSearchTerm);
|
||||
|
||||
super.endPage(page);
|
||||
}
|
||||
|
||||
private class MatchInfo {
|
||||
int startIndex;
|
||||
int matchLength;
|
||||
public List<PDFText> getFoundTexts() {
|
||||
return foundTexts;
|
||||
}
|
||||
|
||||
MatchInfo(int startIndex, int matchLength) {
|
||||
this.startIndex = startIndex;
|
||||
this.matchLength = matchLength;
|
||||
public String getDebugInfo() {
|
||||
StringBuilder debug = new StringBuilder();
|
||||
debug.append("Extracted text length: ").append(pageTextBuilder.length()).append("\n");
|
||||
debug.append("Position count: ").append(pageTextPositions.size()).append("\n");
|
||||
debug.append("Text content: '")
|
||||
.append(pageTextBuilder.toString().replace("\n", "\\n").replace("\r", "\\r"))
|
||||
.append("'\n");
|
||||
|
||||
String text = pageTextBuilder.toString();
|
||||
for (int i = 0; i < Math.min(text.length(), 50); i++) {
|
||||
char c = text.charAt(i);
|
||||
TextPosition pos = i < pageTextPositions.size() ? pageTextPositions.get(i) : null;
|
||||
debug.append(
|
||||
String.format(
|
||||
" [%d] '%c' (0x%02X) -> %s\n",
|
||||
i,
|
||||
c,
|
||||
(int) c,
|
||||
pos != null
|
||||
? String.format("(%.1f,%.1f)", pos.getX(), pos.getY())
|
||||
: "null"));
|
||||
}
|
||||
|
||||
return debug.toString();
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,6 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@ -13,15 +11,15 @@ import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.search.Search;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointInspector;
|
||||
import stirling.software.common.service.PostHogService;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class MetricsAggregatorService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(MetricsAggregatorService.class);
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
private final PostHogService postHogService;
|
||||
private final EndpointInspector endpointInspector;
|
||||
@ -66,7 +64,7 @@ public class MetricsAggregatorService {
|
||||
if ("GET".equals(method)
|
||||
&& validateGetEndpoints
|
||||
&& !endpointInspector.isValidGetEndpoint(uri)) {
|
||||
logger.debug("Skipping invalid GET endpoint: {}", uri);
|
||||
log.debug("Skipping invalid GET endpoint: {}", uri);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -77,7 +75,7 @@ public class MetricsAggregatorService {
|
||||
double lastCount = lastSentMetrics.getOrDefault(key, 0.0);
|
||||
double difference = currentCount - lastCount;
|
||||
if (difference > 0) {
|
||||
logger.debug("{}, {}", key, difference);
|
||||
log.debug("{}, {}", key, difference);
|
||||
metrics.put(key, difference);
|
||||
lastSentMetrics.put(key, currentCount);
|
||||
}
|
||||
|
@ -0,0 +1,351 @@
|
||||
package stirling.software.SPDF.utils.text;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.font.PDFont;
|
||||
import org.apache.pdfbox.pdmodel.font.PDSimpleFont;
|
||||
import org.apache.pdfbox.pdmodel.font.encoding.DictionaryEncoding;
|
||||
import org.apache.pdfbox.pdmodel.font.encoding.Encoding;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class TextEncodingHelper {
|
||||
|
||||
public static boolean canEncodeCharacters(PDFont font, String text) {
|
||||
if (font == null || text == null || text.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: Primary check - full-string encoding (permissive for "good" cases)
|
||||
byte[] encoded = font.encode(text);
|
||||
if (encoded.length > 0) {
|
||||
log.debug(
|
||||
"Text '{}' has good full-string encoding for font {} - permissively allowing",
|
||||
text,
|
||||
font.getName() != null ? font.getName() : "Unknown");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Step 2: Smart array-based fallback for TJ operator-style text
|
||||
log.debug(
|
||||
"Full encoding failed for '{}' - using array-based fallback for font {}",
|
||||
text,
|
||||
font.getName() != null ? font.getName() : "Unknown");
|
||||
|
||||
return validateAsCodePointArray(font, text);
|
||||
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
log.debug(
|
||||
"Encoding exception for text '{}' with font {} - trying array fallback: {}",
|
||||
text,
|
||||
font.getName() != null ? font.getName() : "Unknown",
|
||||
e.getMessage());
|
||||
|
||||
if (isFontSubset(font.getName()) || hasCustomEncoding(font)) {
|
||||
return validateAsCodePointArray(font, text);
|
||||
}
|
||||
|
||||
return false; // Non-subset fonts with encoding exceptions are likely problematic
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean validateAsCodePointArray(PDFont font, String text) {
|
||||
int totalCodePoints = 0;
|
||||
int successfulCodePoints = 0;
|
||||
|
||||
// Iterate through code points (handles surrogates correctly per Unicode docs)
|
||||
for (int i = 0; i < text.length(); ) {
|
||||
int codePoint = text.codePointAt(i);
|
||||
String charStr = new String(Character.toChars(codePoint));
|
||||
totalCodePoints++;
|
||||
|
||||
try {
|
||||
// Test encoding for this code point
|
||||
byte[] charEncoded = font.encode(charStr);
|
||||
if (charEncoded.length > 0) {
|
||||
float charWidth = font.getStringWidth(charStr);
|
||||
|
||||
if (charWidth >= 0) {
|
||||
successfulCodePoints++;
|
||||
log.debug(
|
||||
"Code point '{}' (U+{}) encoded successfully",
|
||||
charStr,
|
||||
Integer.toHexString(codePoint).toUpperCase());
|
||||
} else {
|
||||
log.debug(
|
||||
"Code point '{}' (U+{}) has invalid width: {}",
|
||||
charStr,
|
||||
Integer.toHexString(codePoint).toUpperCase(),
|
||||
charWidth);
|
||||
}
|
||||
} else {
|
||||
log.debug(
|
||||
"Code point '{}' (U+{}) encoding failed - empty result",
|
||||
charStr,
|
||||
Integer.toHexString(codePoint).toUpperCase());
|
||||
}
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
log.debug(
|
||||
"Code point '{}' (U+{}) validation failed: {}",
|
||||
charStr,
|
||||
Integer.toHexString(codePoint).toUpperCase(),
|
||||
e.getMessage());
|
||||
}
|
||||
|
||||
i += Character.charCount(codePoint); // Handle surrogates properly
|
||||
}
|
||||
|
||||
double successRate =
|
||||
totalCodePoints > 0 ? (double) successfulCodePoints / totalCodePoints : 0;
|
||||
boolean isAcceptable = successRate >= 0.95;
|
||||
|
||||
log.debug(
|
||||
"Array validation for '{}': {}/{} code points successful ({:.1f}%) - {}",
|
||||
text,
|
||||
successfulCodePoints,
|
||||
totalCodePoints,
|
||||
successRate * 100,
|
||||
isAcceptable ? "ALLOWING" : "rejecting");
|
||||
|
||||
return isAcceptable;
|
||||
}
|
||||
|
||||
public static boolean isTextSegmentRemovable(PDFont font, String text) {
|
||||
if (font == null || text == null || text.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Log the attempt
|
||||
log.debug(
|
||||
"Evaluating text segment for removal: '{}' with font {}",
|
||||
text,
|
||||
font.getName() != null ? font.getName() : "Unknown Font");
|
||||
|
||||
if (isSimpleCharacter(text)) {
|
||||
try {
|
||||
font.encode(text);
|
||||
font.getStringWidth(text);
|
||||
log.debug(
|
||||
"Text '{}' is a simple character and passed validation - allowing removal",
|
||||
text);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.debug(
|
||||
"Simple character '{}' failed basic validation with font {}: {}",
|
||||
text,
|
||||
font.getName() != null ? font.getName() : "Unknown",
|
||||
e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// For complex text, require comprehensive validation
|
||||
return isTextFullyRemovable(font, text);
|
||||
}
|
||||
|
||||
public static boolean isTextFullyRemovable(PDFont font, String text) {
|
||||
if (font == null || text == null || text.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check 1: Verify encoding capability using new smart approach
|
||||
if (!canEncodeCharacters(font, text)) {
|
||||
log.debug(
|
||||
"Text '{}' failed encoding validation for font {}",
|
||||
text,
|
||||
font.getName() != null ? font.getName() : "Unknown");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check 2: Validate width calculation capability
|
||||
float width = font.getStringWidth(text);
|
||||
if (width < 0) { // Allow zero width (invisible chars) but reject negative (invalid)
|
||||
log.debug(
|
||||
"Text '{}' has invalid width {} for font {}",
|
||||
text,
|
||||
width,
|
||||
font.getName() != null ? font.getName() : "Unknown");
|
||||
return false; // Invalid metrics prevent accurate removal
|
||||
}
|
||||
|
||||
// Check 3: Verify font descriptor completeness for redaction area calculation
|
||||
if (font.getFontDescriptor() == null) {
|
||||
log.debug(
|
||||
"Missing font descriptor for font {}",
|
||||
font.getName() != null ? font.getName() : "Unknown");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check 4: Test bounding box calculation for redaction area
|
||||
try {
|
||||
font.getFontDescriptor().getFontBoundingBox();
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.debug(
|
||||
"Font bounding box unavailable for font {}: {}",
|
||||
font.getName() != null ? font.getName() : "Unknown",
|
||||
e.getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
log.debug(
|
||||
"Text '{}' passed comprehensive validation for font {}",
|
||||
text,
|
||||
font.getName() != null ? font.getName() : "Unknown");
|
||||
return true;
|
||||
|
||||
} catch (IOException e) {
|
||||
log.debug(
|
||||
"Text '{}' failed validation for font {} due to IO error: {}",
|
||||
text,
|
||||
font.getName() != null ? font.getName() : "Unknown",
|
||||
e.getMessage());
|
||||
return false;
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.debug(
|
||||
"Text '{}' failed validation for font {} due to argument error: {}",
|
||||
text,
|
||||
font.getName() != null ? font.getName() : "Unknown",
|
||||
e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isSimpleCharacter(String text) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (text.length() > 20) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
|
||||
// Allow letters, digits, and whitespace (most common cases)
|
||||
if (Character.isLetterOrDigit(c) || Character.isWhitespace(c)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Allow common ASCII punctuation
|
||||
if (c >= 32 && c <= 126 && ".,!?;:()-[]{}\"'/@#$%&*+=<>|\\~`".indexOf(c) >= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean hasCustomEncoding(PDFont font) {
|
||||
try {
|
||||
if (font instanceof PDSimpleFont simpleFont) {
|
||||
try {
|
||||
Encoding encoding = simpleFont.getEncoding();
|
||||
if (encoding != null) {
|
||||
// Check for dictionary-based custom encodings
|
||||
if (encoding instanceof DictionaryEncoding) {
|
||||
log.debug("Font {} uses DictionaryEncoding (custom)", font.getName());
|
||||
return true;
|
||||
}
|
||||
|
||||
String encodingName = encoding.getClass().getSimpleName();
|
||||
if (encodingName.contains("Custom")
|
||||
|| encodingName.contains("Dictionary")) {
|
||||
log.debug(
|
||||
"Font {} uses custom encoding: {}",
|
||||
font.getName(),
|
||||
encodingName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug(
|
||||
"Encoding detection failed for font {}: {}",
|
||||
font.getName(),
|
||||
e.getMessage());
|
||||
return true; // Assume custom if detection fails
|
||||
}
|
||||
}
|
||||
|
||||
if (font instanceof org.apache.pdfbox.pdmodel.font.PDType0Font) {
|
||||
log.debug(
|
||||
"Font {} is Type0 (CID) - generally uses standard CMaps",
|
||||
font.getName() != null ? font.getName() : "Unknown");
|
||||
return false;
|
||||
}
|
||||
|
||||
log.debug(
|
||||
"Font {} type {} - assuming standard encoding",
|
||||
font.getName() != null ? font.getName() : "Unknown",
|
||||
font.getClass().getSimpleName());
|
||||
return false;
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.debug(
|
||||
"Custom encoding detection failed for font {}: {}",
|
||||
font.getName() != null ? font.getName() : "Unknown",
|
||||
e.getMessage());
|
||||
return false; // Be forgiving on detection failure
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean fontSupportsCharacter(PDFont font, String character) {
|
||||
if (font == null || character == null || character.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] encoded = font.encode(character);
|
||||
if (encoded.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float width = font.getStringWidth(character);
|
||||
return width > 0;
|
||||
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
log.debug(
|
||||
"Character '{}' not supported by font {}: {}",
|
||||
character,
|
||||
font.getName() != null ? font.getName() : "Unknown",
|
||||
e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isFontSubset(String fontName) {
|
||||
if (fontName == null) {
|
||||
return false;
|
||||
}
|
||||
return fontName.matches("^[A-Z]{6}\\+.*");
|
||||
}
|
||||
|
||||
public static boolean canCalculateBasicWidths(PDFont font) {
|
||||
try {
|
||||
float spaceWidth = font.getStringWidth(" ");
|
||||
if (spaceWidth <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] testChars = {"a", "A", "0", ".", "e", "!"};
|
||||
for (String ch : testChars) {
|
||||
try {
|
||||
float width = font.getStringWidth(ch);
|
||||
if (width > 0) {
|
||||
return true;
|
||||
}
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
}
|
||||
}
|
||||
|
||||
return false; // Can't calculate width for any test characters
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
return false; // Font failed basic width calculation
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
package stirling.software.SPDF.utils.text;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDResources;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class TextFinderUtils {
|
||||
|
||||
public static boolean validateFontReliability(org.apache.pdfbox.pdmodel.font.PDFont font) {
|
||||
if (font == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (font.isDamaged()) {
|
||||
log.debug(
|
||||
"Font {} is marked as damaged - using TextEncodingHelper validation",
|
||||
font.getName());
|
||||
}
|
||||
|
||||
if (TextEncodingHelper.canCalculateBasicWidths(font)) {
|
||||
log.debug(
|
||||
"Font {} passed basic width calculations - considering reliable",
|
||||
font.getName());
|
||||
return true;
|
||||
}
|
||||
|
||||
String[] basicTests = {"1", "2", "3", "a", "A", "e", "E", " "};
|
||||
|
||||
int workingChars = 0;
|
||||
for (String testChar : basicTests) {
|
||||
if (TextEncodingHelper.canEncodeCharacters(font, testChar)) {
|
||||
workingChars++;
|
||||
}
|
||||
}
|
||||
|
||||
if (workingChars > 0) {
|
||||
log.debug(
|
||||
"Font {} can process {}/{} basic characters - considering reliable",
|
||||
font.getName(),
|
||||
workingChars,
|
||||
basicTests.length);
|
||||
return true;
|
||||
}
|
||||
|
||||
log.debug("Font {} failed all basic tests - considering unreliable", font.getName());
|
||||
return false;
|
||||
}
|
||||
|
||||
public static List<Pattern> createOptimizedSearchPatterns(
|
||||
Set<String> searchTerms, boolean useRegex, boolean wholeWordSearch) {
|
||||
List<Pattern> patterns = new ArrayList<>();
|
||||
|
||||
for (String term : searchTerms) {
|
||||
if (term == null || term.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
String patternString = useRegex ? term.trim() : Pattern.quote(term.trim());
|
||||
|
||||
if (wholeWordSearch) {
|
||||
patternString = applyWordBoundaries(term.trim(), patternString);
|
||||
}
|
||||
|
||||
Pattern pattern =
|
||||
Pattern.compile(
|
||||
patternString, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
|
||||
patterns.add(pattern);
|
||||
|
||||
log.debug("Created search pattern: '{}' -> '{}'", term.trim(), patternString);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to create pattern for term '{}': {}", term, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return patterns;
|
||||
}
|
||||
|
||||
private static String applyWordBoundaries(String originalTerm, String patternString) {
|
||||
if (originalTerm.length() == 1 && Character.isDigit(originalTerm.charAt(0))) {
|
||||
return "(?<![\\w])" + patternString + "(?![\\w])";
|
||||
} else if (originalTerm.length() == 1) {
|
||||
return "(?<![\\w])" + patternString + "(?![\\w])";
|
||||
} else {
|
||||
return "\\b" + patternString + "\\b";
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hasProblematicFonts(PDPage page) {
|
||||
if (page == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
PDResources resources = page.getResources();
|
||||
if (resources == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int totalFonts = 0;
|
||||
int completelyUnusableFonts = 0;
|
||||
|
||||
for (org.apache.pdfbox.cos.COSName fontName : resources.getFontNames()) {
|
||||
try {
|
||||
org.apache.pdfbox.pdmodel.font.PDFont font = resources.getFont(fontName);
|
||||
if (font != null) {
|
||||
totalFonts++;
|
||||
if (!validateFontReliability(font)) {
|
||||
completelyUnusableFonts++;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Font loading failed for {}: {}", fontName.getName(), e.getMessage());
|
||||
totalFonts++;
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasProblems = totalFonts > 0 && (completelyUnusableFonts * 2 > totalFonts);
|
||||
log.debug(
|
||||
"Page font analysis: {}/{} fonts are completely unusable - page {} problematic",
|
||||
completelyUnusableFonts,
|
||||
totalFonts,
|
||||
hasProblems ? "IS" : "is NOT");
|
||||
|
||||
return hasProblems;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("Font analysis failed for page: {}", e.getMessage());
|
||||
return false; // Be permissive if analysis fails
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,136 @@
|
||||
package stirling.software.SPDF.utils.text;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.font.PDFont;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class WidthCalculator {
|
||||
|
||||
private static final int FONT_SCALE_FACTOR = 1000;
|
||||
|
||||
public static float calculateAccurateWidth(PDFont font, String text, float fontSize) {
|
||||
if (font == null || text == null || text.isEmpty() || fontSize <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!TextEncodingHelper.canEncodeCharacters(font, text)) {
|
||||
log.debug(
|
||||
"Text cannot be encoded by font {}, using fallback width calculation",
|
||||
font.getName());
|
||||
return calculateFallbackWidth(font, text, fontSize);
|
||||
}
|
||||
|
||||
try {
|
||||
float rawWidth = font.getStringWidth(text);
|
||||
float scaledWidth = (rawWidth / FONT_SCALE_FACTOR) * fontSize;
|
||||
|
||||
log.debug(
|
||||
"Direct width calculation successful for font {}: {} -> {}",
|
||||
font.getName(),
|
||||
rawWidth,
|
||||
scaledWidth);
|
||||
return scaledWidth;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.debug(
|
||||
"Direct width calculation failed for font {}: {}",
|
||||
font.getName(),
|
||||
e.getMessage());
|
||||
return calculateWidthWithCharacterIteration(font, text, fontSize);
|
||||
}
|
||||
}
|
||||
|
||||
private static float calculateWidthWithCharacterIteration(
|
||||
PDFont font, String text, float fontSize) {
|
||||
try {
|
||||
float totalWidth = 0;
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
String character = text.substring(i, i + 1);
|
||||
try {
|
||||
byte[] encoded = font.encode(character);
|
||||
if (encoded.length > 0) {
|
||||
int glyphCode = encoded[0] & 0xFF;
|
||||
float glyphWidth = font.getWidth(glyphCode);
|
||||
|
||||
if (glyphWidth == 0) {
|
||||
try {
|
||||
glyphWidth = font.getWidthFromFont(glyphCode);
|
||||
} catch (Exception e2) {
|
||||
glyphWidth = font.getAverageFontWidth();
|
||||
}
|
||||
}
|
||||
|
||||
totalWidth += (glyphWidth / FONT_SCALE_FACTOR) * fontSize;
|
||||
} else {
|
||||
totalWidth += (font.getAverageFontWidth() / FONT_SCALE_FACTOR) * fontSize;
|
||||
}
|
||||
} catch (Exception e2) {
|
||||
totalWidth += (font.getAverageFontWidth() / FONT_SCALE_FACTOR) * fontSize;
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Character iteration width calculation: {}", totalWidth);
|
||||
return totalWidth;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.debug("Character iteration failed: {}", e.getMessage());
|
||||
return calculateFallbackWidth(font, text, fontSize);
|
||||
}
|
||||
}
|
||||
|
||||
private static float calculateFallbackWidth(PDFont font, String text, float fontSize) {
|
||||
try {
|
||||
if (font.getFontDescriptor() != null
|
||||
&& font.getFontDescriptor().getFontBoundingBox() != null) {
|
||||
|
||||
PDRectangle bbox = font.getFontDescriptor().getFontBoundingBox();
|
||||
float avgCharWidth =
|
||||
bbox.getWidth() / FONT_SCALE_FACTOR * 0.6f; // Conservative estimate
|
||||
float fallbackWidth = text.length() * avgCharWidth * fontSize;
|
||||
|
||||
log.debug("Bounding box fallback width: {}", fallbackWidth);
|
||||
return fallbackWidth;
|
||||
}
|
||||
|
||||
float avgWidth = font.getAverageFontWidth();
|
||||
float fallbackWidth = (text.length() * avgWidth / FONT_SCALE_FACTOR) * fontSize;
|
||||
|
||||
log.debug("Average width fallback: {}", fallbackWidth);
|
||||
return fallbackWidth;
|
||||
|
||||
} catch (Exception e) {
|
||||
float conservativeWidth = text.length() * 0.5f * fontSize;
|
||||
log.debug(
|
||||
"Conservative fallback width for font {}: {}",
|
||||
font.getName(),
|
||||
conservativeWidth);
|
||||
return conservativeWidth;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isWidthCalculationReliable(PDFont font) {
|
||||
if (font == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (font.isDamaged()) {
|
||||
log.debug("Font {} is damaged", font.getName());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TextEncodingHelper.canCalculateBasicWidths(font)) {
|
||||
log.debug("Font {} cannot perform basic width calculations", font.getName());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TextEncodingHelper.hasCustomEncoding(font)) {
|
||||
log.debug("Font {} has custom encoding", font.getName());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -10,8 +10,12 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -27,6 +31,8 @@ import stirling.software.common.service.TaskManager;
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "Job Management", description = "Job Management API")
|
||||
public class JobController {
|
||||
|
||||
private final TaskManager taskManager;
|
||||
@ -40,7 +46,8 @@ public class JobController {
|
||||
* @param jobId The job ID
|
||||
* @return The job result
|
||||
*/
|
||||
@GetMapping("/api/v1/general/job/{jobId}")
|
||||
@GetMapping("/job/{jobId}")
|
||||
@Operation(summary = "Get job status")
|
||||
public ResponseEntity<?> getJobStatus(@PathVariable("jobId") String jobId) {
|
||||
JobResult result = taskManager.getJobResult(jobId);
|
||||
if (result == null) {
|
||||
@ -68,7 +75,8 @@ public class JobController {
|
||||
* @param jobId The job ID
|
||||
* @return The job result
|
||||
*/
|
||||
@GetMapping("/api/v1/general/job/{jobId}/result")
|
||||
@GetMapping("/job/{jobId}/result")
|
||||
@Operation(summary = "Get job result")
|
||||
public ResponseEntity<?> getJobResult(@PathVariable("jobId") String jobId) {
|
||||
JobResult result = taskManager.getJobResult(jobId);
|
||||
if (result == null) {
|
||||
@ -130,7 +138,8 @@ public class JobController {
|
||||
* @param jobId The job ID
|
||||
* @return Response indicating whether the job was cancelled
|
||||
*/
|
||||
@DeleteMapping("/api/v1/general/job/{jobId}")
|
||||
@DeleteMapping("/job/{jobId}")
|
||||
@Operation(summary = "Cancel a job")
|
||||
public ResponseEntity<?> cancelJob(@PathVariable("jobId") String jobId) {
|
||||
log.debug("Request to cancel job: {}", jobId);
|
||||
|
||||
@ -197,7 +206,8 @@ public class JobController {
|
||||
* @param jobId The job ID
|
||||
* @return List of files for the job
|
||||
*/
|
||||
@GetMapping("/api/v1/general/job/{jobId}/result/files")
|
||||
@GetMapping("/job/{jobId}/result/files")
|
||||
@Operation(summary = "Get job result files")
|
||||
public ResponseEntity<?> getJobFiles(@PathVariable("jobId") String jobId) {
|
||||
JobResult result = taskManager.getJobResult(jobId);
|
||||
if (result == null) {
|
||||
@ -226,7 +236,8 @@ public class JobController {
|
||||
* @param fileId The file ID
|
||||
* @return The file metadata
|
||||
*/
|
||||
@GetMapping("/api/v1/general/files/{fileId}/metadata")
|
||||
@GetMapping("/files/{fileId}/metadata")
|
||||
@Operation(summary = "Get file metadata")
|
||||
public ResponseEntity<?> getFileMetadata(@PathVariable("fileId") String fileId) {
|
||||
try {
|
||||
// Verify file exists
|
||||
@ -266,7 +277,8 @@ public class JobController {
|
||||
* @param fileId The file ID
|
||||
* @return The file content
|
||||
*/
|
||||
@GetMapping("/api/v1/general/files/{fileId}")
|
||||
@GetMapping("/files/{fileId}")
|
||||
@Operation(summary = "Download a file")
|
||||
public ResponseEntity<?> downloadFile(@PathVariable("fileId") String fileId) {
|
||||
try {
|
||||
// Verify file exists
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=الصفحات المراد ترقيمها
|
||||
addPageNumbers.selectText.6=نص مخصص
|
||||
addPageNumbers.customTextDesc=نص مخصص
|
||||
addPageNumbers.numberPagesDesc=أي الصفحات المراد ترقيمها، الافتراضي 'الكل'، يقبل أيضًا 1-5 أو 2,5,9 إلخ
|
||||
addPageNumbers.customNumberDesc=الافتراضي هو {n}، يقبل أيضًا 'الصفحة {n} من {total}'، 'نص-{n}'، '{filename}-{n}
|
||||
addPageNumbers.customNumberDesc=الافتراضي هو {n}، يقبل أيضًا 'الصفحة {n} من {total}'، 'نص-{n}'، '{filename}-{n}'
|
||||
addPageNumbers.submit=إضافة أرقام الصفحات
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Səhifələrə nömrə əlavə edin
|
||||
addPageNumbers.selectText.6=Fərdi Mətn
|
||||
addPageNumbers.customTextDesc=Fərdi Mətn
|
||||
addPageNumbers.numberPagesDesc=Hansı səhifələrin nömrələnəcəyini seçin, default 'all', və ya 1-5, 2,5,9 kimi yazılış qəbul olunur
|
||||
addPageNumbers.customNumberDesc=Defolt olaraq {n}, və ya 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}
|
||||
addPageNumbers.customNumberDesc=Defolt olaraq {n}, və ya 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Səhifə Nömrələri əlavə edin
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Страници към номер
|
||||
addPageNumbers.selectText.6=Персонализиран текст
|
||||
addPageNumbers.customTextDesc=Персонализиран текст
|
||||
addPageNumbers.numberPagesDesc=Кои страници да номерирате, по подразбиране 'всички', също приема 1-5 или 2,5,9 и т.н.
|
||||
addPageNumbers.customNumberDesc=По подразбиране е {n}, също приема 'Страница {n} от {total}', 'Текст-{n}', '{filename}-{n}
|
||||
addPageNumbers.customNumberDesc=По подразбиране е {n}, също приема 'Страница {n} от {total}', 'Текст-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Добавяне на номера на страници
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Sider at nummerere
|
||||
addPageNumbers.selectText.6=Brugerdefineret Tekst
|
||||
addPageNumbers.customTextDesc=Brugerdefineret Tekst
|
||||
addPageNumbers.numberPagesDesc=Hvilke sider der skal nummereres, standard 'alle', accepterer også 1-5 eller 2,5,9 osv.
|
||||
addPageNumbers.customNumberDesc=Standard er {n}, accepterer også 'Side {n} af {total}', 'Tekst-{n}', '{filnavn}-{n}
|
||||
addPageNumbers.customNumberDesc=Standard er {n}, accepterer også 'Side {n} af {total}', 'Tekst-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Tilføj Sidenumre
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Σελίδες προς αρίθμηση
|
||||
addPageNumbers.selectText.6=Προσαρμοσμένο κείμενο
|
||||
addPageNumbers.customTextDesc=Προσαρμοσμένο κείμενο
|
||||
addPageNumbers.numberPagesDesc=Ποιες σελίδες να αριθμηθούν, προεπιλογή 'all', δέχεται επίσης 1-5 ή 2,5,9 κλπ
|
||||
addPageNumbers.customNumberDesc=Προεπιλογή σε {n}, δέχεται επίσης 'Σελίδα {n} από {total}', 'Κείμενο-{n}', '{filename}-{n}
|
||||
addPageNumbers.customNumberDesc=Προεπιλογή σε {n}, δέχεται επίσης 'Σελίδα {n} από {total}', 'Κείμενο-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Προσθήκη αριθμών σελίδων
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Pages to Number
|
||||
addPageNumbers.selectText.6=Custom Text
|
||||
addPageNumbers.customTextDesc=Custom Text
|
||||
addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc
|
||||
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}
|
||||
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Add Page Numbers
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Pages to Number
|
||||
addPageNumbers.selectText.6=Custom Text
|
||||
addPageNumbers.customTextDesc=Custom Text
|
||||
addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc
|
||||
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}
|
||||
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Add Page Numbers
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Páginas a numerar
|
||||
addPageNumbers.selectText.6=Texto personalizado
|
||||
addPageNumbers.customTextDesc=Texto personalizado
|
||||
addPageNumbers.numberPagesDesc=Qué páginas numerar, por defecto 'todas', también acepta 1-5 o 2,5,9 etc
|
||||
addPageNumbers.customNumberDesc=Por defecto a {n}, también acepta 'Página {n} de {total}', 'Texto-{n}', '{filename}-{n}
|
||||
addPageNumbers.customNumberDesc=Por defecto a {n}, también acepta 'Página {n} de {total}', 'Texto-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Añadir Números de Página
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Leathanaigh go hUimhir
|
||||
addPageNumbers.selectText.6=Téacs Saincheaptha
|
||||
addPageNumbers.customTextDesc=Téacs Saincheaptha
|
||||
addPageNumbers.numberPagesDesc=Cé na leathanaigh le huimhriú, réamhshocraithe 'gach duine', a ghlacann freisin 1-5 nó 2,5,9 etc
|
||||
addPageNumbers.customNumberDesc=Réamhshocrú go {n}, glacann sé freisin le 'Leathanach {n} de {total}', 'Text-{n}', '{filename}-{n}
|
||||
addPageNumbers.customNumberDesc=Réamhshocrú go {n}, glacann sé freisin le 'Leathanach {n} de {total}', 'Text-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Cuir Uimhreacha Leathanaigh leis
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Brojanje stranica
|
||||
addPageNumbers.selectText.6=Prilagođeni tekst
|
||||
addPageNumbers.customTextDesc=Prilagođeni tekst
|
||||
addPageNumbers.numberPagesDesc=Koje stranice numerirati, zadano je 'sve', također prihvaća 1-5 ili 2,5,9 itd.
|
||||
addPageNumbers.customNumberDesc=Zadano je {n}, također prihvaća 'Stranica {n} od {total}', 'Tekst-{n}', '{ime datoteke}-{n}'
|
||||
addPageNumbers.customNumberDesc=Zadano je {n}, također prihvaća 'Stranica {n} od {total}', 'Tekst-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Dodaj brojeve stranica
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Halaman ke Nomor
|
||||
addPageNumbers.selectText.6=Teks Khusus
|
||||
addPageNumbers.customTextDesc=Teks Khusus
|
||||
addPageNumbers.numberPagesDesc=Halaman mana yang akan diberi nomor, default 'semua', juga menerima 1-5 atau 2,5,9, dll.
|
||||
addPageNumbers.customNumberDesc=Default untuk {n}, juga menerima 'Halaman {n} dari {total}', 'Teks-{n}', '{nama berkas}-{n}'
|
||||
addPageNumbers.customNumberDesc=Default untuk {n}, juga menerima 'Halaman {n} dari {total}', 'Teks-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Tambahkan Nomor Halaman
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Pagine da numerare
|
||||
addPageNumbers.selectText.6=Testo personalizzato
|
||||
addPageNumbers.customTextDesc=Testo personalizzato
|
||||
addPageNumbers.numberPagesDesc=Quali pagine numerare, impostazione predefinita "tutte", accetta anche 1-5 o 2,5,9 ecc
|
||||
addPageNumbers.customNumberDesc=Il valore predefinito è {n}, accetta anche 'Pagina {n} di {total}', 'Testo-{n}', '{filename}-{n}
|
||||
addPageNumbers.customNumberDesc=Il valore predefinito è {n}, accetta anche 'Pagina {n} di {total}', 'Testo-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Aggiungi numeri di pagina
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Pagina's om te nummeren
|
||||
addPageNumbers.selectText.6=Aangepaste tekst
|
||||
addPageNumbers.customTextDesc=Aangepaste tekst
|
||||
addPageNumbers.numberPagesDesc=Welke pagina's genummerd moeten worden, standaard 'all', accepteert ook 1-5 of 2,5,9 etc
|
||||
addPageNumbers.customNumberDesc=Standaard {n}, accepteert ook 'Pagina {n} van {total}', 'Tekst-{n}', '{filename}-{n}
|
||||
addPageNumbers.customNumberDesc=Standaard {n}, accepteert ook 'Pagina {n} van {total}', 'Tekst-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Paginanummers toevoegen
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Sider å nummerere
|
||||
addPageNumbers.selectText.6=Tilpasset Tekst
|
||||
addPageNumbers.customTextDesc=Tilpasset Tekst
|
||||
addPageNumbers.numberPagesDesc=Hvilke sider som skal nummereres, standard 'alle', aksepterer også 1-5 eller 2,5,9 osv.
|
||||
addPageNumbers.customNumberDesc=Standard til {n}, aksepterer også 'Side {n} av {total}', 'Tekst-{n}', '{filnavn}-{n}
|
||||
addPageNumbers.customNumberDesc=Standard til {n}, aksepterer også 'Side {n} av {total}', 'Tekst-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Legg til Sidetall
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Ilość stron do ponumerowania
|
||||
addPageNumbers.selectText.6=Tekst własny
|
||||
addPageNumbers.customTextDesc=Tekst własny
|
||||
addPageNumbers.numberPagesDesc=Strony do numeracji, wszystkie (all), 1-5, 2, 5, 9
|
||||
addPageNumbers.customNumberDesc=Domyślnie do {n}, również akceptuje 'Strona {n} z {total},Teskt-{n},'{filename}-{n}
|
||||
addPageNumbers.customNumberDesc=Domyślnie do {n}, również akceptuje 'Strona {n} z {total}', 'Tekst-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Dodaj numerację stron
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Páginas a numerar:
|
||||
addPageNumbers.selectText.6=Texto personalizado:
|
||||
addPageNumbers.customTextDesc=Texto personalizado:
|
||||
addPageNumbers.numberPagesDesc=Quais páginas numerar, padrão 'todas', também aceita 1-5 ou 2,5,9,etc.
|
||||
addPageNumbers.customNumberDesc=O padrão é {n}, também aceita 'Página {n} de {total}', 'Texto-{n}', '{nome do arquivo}-{n}'
|
||||
addPageNumbers.customNumberDesc=O padrão é {n}, também aceita 'Página {n} de {total}', 'Texto-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Adicionar Números de Página
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Páginas a Numerar
|
||||
addPageNumbers.selectText.6=Texto Personalizado
|
||||
addPageNumbers.customTextDesc=Texto Personalizado
|
||||
addPageNumbers.numberPagesDesc=Quais páginas a numerar, predefinição 'todas', também aceita 1-5 ou 2,5,9 etc
|
||||
addPageNumbers.customNumberDesc=Predefinição {n}, também aceita 'Página {n} de {total}', 'Texto-{n}', '{filename}-{n}
|
||||
addPageNumbers.customNumberDesc=Predefinição {n}, também aceita 'Página {n} de {total}', 'Texto-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Adicionar Números de Página
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Pagini de Numerotat
|
||||
addPageNumbers.selectText.6=Text Personalizat
|
||||
addPageNumbers.customTextDesc=Text Personalizat
|
||||
addPageNumbers.numberPagesDesc=Ce pagini să numeroteze, implicit 'toate', acceptă și 1-5 sau 2,5,9 etc
|
||||
addPageNumbers.customNumberDesc=Implicit la {n}, acceptă și 'Pagina {n} din {total}', 'Text-{n}', '{nume_fisier}-{n}
|
||||
addPageNumbers.customNumberDesc=Implicit la {n}, acceptă și 'Pagina {n} din {total}', 'Text-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Adaugă Numere de Pagină
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Stránky na číslovanie
|
||||
addPageNumbers.selectText.6=Vlastný text
|
||||
addPageNumbers.customTextDesc=Vlastný text
|
||||
addPageNumbers.numberPagesDesc=Ktoré stránky číslovať, predvolené 'všetky', tiež akceptuje 1-5 alebo 2,5,9 atď.
|
||||
addPageNumbers.customNumberDesc=Predvolené {n}, tiež akceptuje 'Strana {n} z {total}', 'Text-{n}', '{filename}-{n}
|
||||
addPageNumbers.customNumberDesc=Predvolené {n}, tiež akceptuje 'Strana {n} z {total}', 'Text-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Pridať čísla stránok
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Strani v številko
|
||||
addPageNumbers.selectText.6=Besedilo po meri
|
||||
addPageNumbers.customTextDesc=Besedilo po meri
|
||||
addPageNumbers.numberPagesDesc=Katere strani oštevilčiti, privzeto 'vse', sprejema tudi 1-5 ali 2,5,9 itd.
|
||||
addPageNumbers.customNumberDesc=Privzeto na {n}, sprejema tudi 'Stran {n} od {total}', 'Besedilo-{n}', '{filename}-{n}
|
||||
addPageNumbers.customNumberDesc=Privzeto na {n}, sprejema tudi 'Stran {n} od {total}', 'Besedilo-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Dodaj številke strani
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Stranice za numerisanje:
|
||||
addPageNumbers.selectText.6=Prilagođeni tekst:
|
||||
addPageNumbers.customTextDesc=Prilagođeni tekst
|
||||
addPageNumbers.numberPagesDesc=Koje stranice brojati, podrazumevano 'sve', takođe prihvata 1-5 ili 2,5,9 itd.
|
||||
addPageNumbers.customNumberDesc=Podrazumevano je {n}, takođe prihvata 'Stranica {n} od {ukupno}', 'Tekst-{n}', '{ime_fajla}-{n}'
|
||||
addPageNumbers.customNumberDesc=Podrazumevano je {n}, takođe prihvata 'Stranica {n} od {total}', 'Tekst-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Numeriši
|
||||
|
||||
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Sidor att numrera
|
||||
addPageNumbers.selectText.6=Anpassad text
|
||||
addPageNumbers.customTextDesc=Anpassad text
|
||||
addPageNumbers.numberPagesDesc=Vilka sidor som ska numreras, standard 'all', accepterar även 1-5 eller 2,5,9 etc
|
||||
addPageNumbers.customNumberDesc=Standard är {n}, accepterar även 'Sida {n} av {total}', 'Text-{n}', '{filnamn}-{n}
|
||||
addPageNumbers.customNumberDesc=Standard är {n}, accepterar även 'Sida {n} av {total}', 'Text-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Lägg till sidnummer
|
||||
|
||||
|
||||
|
@ -1,138 +1,138 @@
|
||||
###########
|
||||
# Generic #
|
||||
###########
|
||||
# the direction that the language is written (ltr = left to right, rtl = right to left)
|
||||
# the direction that the language is written (ltr=left to right, rtl=right to left)
|
||||
language.direction=ltr
|
||||
|
||||
# Language names for reuse throughout the application
|
||||
lang.afr=Afrikaans
|
||||
lang.amh=Amharic
|
||||
lang.ara=Arabic
|
||||
lang.asm=Assamese
|
||||
lang.aze=Azerbaijani
|
||||
lang.aze_cyrl=Azerbaijani (Cyrillic)
|
||||
lang.bel=Belarusian
|
||||
lang.ben=Bengali
|
||||
lang.bod=Tibetan
|
||||
lang.bos=Bosnian
|
||||
lang.bre=Breton
|
||||
lang.bul=Bulgarian
|
||||
lang.cat=Catalan
|
||||
lang.afr=Afrikaanca
|
||||
lang.amh=Amharca
|
||||
lang.ara=Arapça
|
||||
lang.asm=Assamca
|
||||
lang.aze=Azerice
|
||||
lang.aze_cyrl=Azerice (Kiril)
|
||||
lang.bel=Beyaz Rusça (Belarusça)
|
||||
lang.ben=Bengalce
|
||||
lang.bod=Tibetçe
|
||||
lang.bos=Boşnakça
|
||||
lang.bre=Bretonca
|
||||
lang.bul=Bulgarca
|
||||
lang.cat=Katalanca
|
||||
lang.ceb=Cebuano
|
||||
lang.ces=Czech
|
||||
lang.chi_sim=Chinese (Simplified)
|
||||
lang.chi_sim_vert=Chinese (Simplified, Vertical)
|
||||
lang.chi_tra=Chinese (Traditional)
|
||||
lang.chi_tra_vert=Chinese (Traditional, Vertical)
|
||||
lang.chr=Cherokee
|
||||
lang.cos=Corsican
|
||||
lang.cym=Welsh
|
||||
lang.dan=Danish
|
||||
lang.dan_frak=Danish (Fraktur)
|
||||
lang.deu=German
|
||||
lang.deu_frak=German (Fraktur)
|
||||
lang.div=Divehi
|
||||
lang.ces=Çekçe
|
||||
lang.chi_sim=Çince (Basitleştirilmiş)
|
||||
lang.chi_sim_vert=Çince (Basitleştirilmiş, Dikey)
|
||||
lang.chi_tra=Çince (Geleneksel)
|
||||
lang.chi_tra_vert=Çince (Geleneksel, Dikey)
|
||||
lang.chr=Çerokice
|
||||
lang.cos=Korsikaca
|
||||
lang.cym=Gallerce (Galce)
|
||||
lang.dan=Danca
|
||||
lang.dan_frak=Danca (Fraktur)
|
||||
lang.deu=Almanca
|
||||
lang.deu_frak=Almanca (Fraktur)
|
||||
lang.div=Maldivce (Divehi)
|
||||
lang.dzo=Dzongkha
|
||||
lang.ell=Greek
|
||||
lang.eng=English
|
||||
lang.enm=English, Middle (1100-1500)
|
||||
lang.ell=Yunanca
|
||||
lang.eng=İngilizce
|
||||
lang.enm=İngilizce, Orta Çağ (1100-1500)
|
||||
lang.epo=Esperanto
|
||||
lang.equ=Math / equation detection module
|
||||
lang.est=Estonian
|
||||
lang.eus=Basque
|
||||
lang.fao=Faroese
|
||||
lang.fas=Persian
|
||||
lang.fil=Filipino
|
||||
lang.fin=Finnish
|
||||
lang.fra=French
|
||||
lang.frk=Frankish
|
||||
lang.frm=French, Middle (ca.1400-1600)
|
||||
lang.fry=Western Frisian
|
||||
lang.gla=Scottish Gaelic
|
||||
lang.gle=Irish
|
||||
lang.glg=Galician
|
||||
lang.grc=Ancient Greek
|
||||
lang.guj=Gujarati
|
||||
lang.hat=Haitian, Haitian Creole
|
||||
lang.heb=Hebrew
|
||||
lang.hin=Hindi
|
||||
lang.hrv=Croatian
|
||||
lang.hun=Hungarian
|
||||
lang.hye=Armenian
|
||||
lang.iku=Inuktitut
|
||||
lang.ind=Indonesian
|
||||
lang.isl=Icelandic
|
||||
lang.ita=Italian
|
||||
lang.ita_old=Italian (Old)
|
||||
lang.jav=Javanese
|
||||
lang.jpn=Japanese
|
||||
lang.jpn_vert=Japanese (Vertical)
|
||||
lang.equ=Matematik / denklem tanıma modülü
|
||||
lang.est=Estonca
|
||||
lang.eus=Baskça
|
||||
lang.fao=Faroece
|
||||
lang.fas=Farsça
|
||||
lang.fil=Filipince
|
||||
lang.fin=Fince
|
||||
lang.fra=Fransızca
|
||||
lang.frk=Frankça
|
||||
lang.frm=Fransızca, Orta Çağ (yaklaşık 1400-1600)
|
||||
lang.fry=Batı Frizce
|
||||
lang.gla=İskoç Galcesi
|
||||
lang.gle=İrlandaca
|
||||
lang.glg=Galiçyaca
|
||||
lang.grc=Antik Yunanca
|
||||
lang.guj=Gujaratça
|
||||
lang.hat=Haiti Creole
|
||||
lang.heb=İbranice
|
||||
lang.hin=Hintçe
|
||||
lang.hrv=Hırvatça
|
||||
lang.hun=Macarca
|
||||
lang.hye=Ermenice
|
||||
lang.iku=İnuktitut
|
||||
lang.ind=Endonezce
|
||||
lang.isl=İzlandaca
|
||||
lang.ita=İtalyanca
|
||||
lang.ita_old=İtalyanca (Eski)
|
||||
lang.jav=Cava dili
|
||||
lang.jpn=Japonca
|
||||
lang.jpn_vert=Japonca (Dikey)
|
||||
lang.kan=Kannada
|
||||
lang.kat=Georgian
|
||||
lang.kat_old=Georgian (Old)
|
||||
lang.kaz=Kazakh
|
||||
lang.khm=Central Khmer
|
||||
lang.kir=Kirghiz, Kyrgyz
|
||||
lang.kmr=Northern Kurdish
|
||||
lang.kor=Korean
|
||||
lang.kor_vert=Korean (Vertical)
|
||||
lang.lao=Lao
|
||||
lang.lat=Latin
|
||||
lang.lav=Latvian
|
||||
lang.lit=Lithuanian
|
||||
lang.ltz=Luxembourgish
|
||||
lang.mal=Malayalam
|
||||
lang.kat=Gürcüce
|
||||
lang.kat_old=Gürcüce (Eski)
|
||||
lang.kaz=Kazakça
|
||||
lang.khm=Merkez Khmer dili
|
||||
lang.kir=Kırgızca
|
||||
lang.kmr=Kuzey Kürtçesi
|
||||
lang.kor=Korece
|
||||
lang.kor_vert=Korece (Dikey)
|
||||
lang.lao=Laosça
|
||||
lang.lat=Latince
|
||||
lang.lav=Letonca
|
||||
lang.lit=Litvanca
|
||||
lang.ltz=Lüksemburgca
|
||||
lang.mal=Malayalamca
|
||||
lang.mar=Marathi
|
||||
lang.mkd=Macedonian
|
||||
lang.mlt=Maltese
|
||||
lang.mon=Mongolian
|
||||
lang.mri=Maori
|
||||
lang.msa=Malay
|
||||
lang.mya=Burmese
|
||||
lang.nep=Nepali
|
||||
lang.nld=Dutch; Flemish
|
||||
lang.nor=Norwegian
|
||||
lang.oci=Occitan (post 1500)
|
||||
lang.mkd=Makedonca
|
||||
lang.mlt=Maltaca
|
||||
lang.mon=Moğolca
|
||||
lang.mri=Maorice
|
||||
lang.msa=Malayca
|
||||
lang.mya=Birmanca (Burma)
|
||||
lang.nep=Nepalce
|
||||
lang.nld=Hollandaca; Flamanca
|
||||
lang.nor=Norveççe
|
||||
lang.oci=Oksitanca (1500 sonrası)
|
||||
lang.ori=Oriya
|
||||
lang.osd=Orientation and script detection module
|
||||
lang.pan=Panjabi, Punjabi
|
||||
lang.pol=Polish
|
||||
lang.por=Portuguese
|
||||
lang.pus=Pushto, Pashto
|
||||
lang.que=Quechua
|
||||
lang.ron=Romanian, Moldavian, Moldovan
|
||||
lang.rus=Russian
|
||||
lang.san=Sanskrit
|
||||
lang.sin=Sinhala, Sinhalese
|
||||
lang.slk=Slovak
|
||||
lang.slk_frak=Slovak (Fraktur)
|
||||
lang.slv=Slovenian
|
||||
lang.snd=Sindhi
|
||||
lang.spa=Spanish
|
||||
lang.spa_old=Spanish (Old)
|
||||
lang.sqi=Albanian
|
||||
lang.srp=Serbian
|
||||
lang.srp_latn=Serbian (Latin)
|
||||
lang.sun=Sundanese
|
||||
lang.swa=Swahili
|
||||
lang.swe=Swedish
|
||||
lang.syr=Syriac
|
||||
lang.tam=Tamil
|
||||
lang.tat=Tatar
|
||||
lang.osd=Yönlendirme ve yazı tipi algılama modülü
|
||||
lang.pan=Pencapça
|
||||
lang.pol=Lehçe (Polonyaca)
|
||||
lang.por=Portekizce
|
||||
lang.pus=Peştuca
|
||||
lang.que=Keçuva dili
|
||||
lang.ron=Rumence, Moldovca
|
||||
lang.rus=Rusça
|
||||
lang.san=Sanskritçe
|
||||
lang.sin=Seylanca (Sinhala)
|
||||
lang.slk=Slovakça
|
||||
lang.slk_frak=Slovakça (Fraktur)
|
||||
lang.slv=Slovence
|
||||
lang.snd=Sindhice
|
||||
lang.spa=İspanyolca
|
||||
lang.spa_old=İspanyolca (Eski)
|
||||
lang.sqi=Arnavutça
|
||||
lang.srp=Sırpça
|
||||
lang.srp_latn=Sırpça (Latin alfabesiyle)
|
||||
lang.sun=Sundaca
|
||||
lang.swa=Svahili dili
|
||||
lang.swe=İsveççe
|
||||
lang.syr=Süryanice
|
||||
lang.tam=Tamilce
|
||||
lang.tat=Tatarca
|
||||
lang.tel=Telugu
|
||||
lang.tgk=Tajik
|
||||
lang.tgk=Tacikçe
|
||||
lang.tgl=Tagalog
|
||||
lang.tha=Thai
|
||||
lang.tha=Tayca
|
||||
lang.tir=Tigrinya
|
||||
lang.ton=Tonga (Tonga Islands)
|
||||
lang.tur=Turkish
|
||||
lang.uig=Uighur, Uyghur
|
||||
lang.ukr=Ukrainian
|
||||
lang.urd=Urdu
|
||||
lang.uzb=Uzbek
|
||||
lang.uzb_cyrl=Uzbek (Cyrillic)
|
||||
lang.vie=Vietnamese
|
||||
lang.yid=Yiddish
|
||||
lang.ton=Tonga dili (Tonga Adaları)
|
||||
lang.tur=Türkçe
|
||||
lang.uig=Uygurca
|
||||
lang.ukr=Ukraynaca
|
||||
lang.urd=Urduca
|
||||
lang.uzb=Özbekçe
|
||||
lang.uzb_cyrl=Özbekçe (Kiril)
|
||||
lang.vie=Vietnamca
|
||||
lang.yid=Yidiş
|
||||
lang.yor=Yoruba
|
||||
|
||||
addPageNumbers.fontSize=Font Büyüklüğü
|
||||
@ -146,8 +146,8 @@ uploadLimit=Maksimum dosya boyutu:
|
||||
uploadLimitExceededSingular=çok büyük. İzin verilen maksimum boyut:
|
||||
uploadLimitExceededPlural=çok büyük. İzin verilen maksimum boyut:
|
||||
processTimeWarning=Uyarı: Bu işlem, dosya boyutuna bağlı olarak bir dakikaya kadar sürebilir.
|
||||
pageOrderPrompt=Özel Sayfa Sırası (Virgülle ayrılmış sayfa numaraları veya 2n+1 gibi bir fonksiyon girin) :
|
||||
pageSelectionPrompt=Özel Sayfa Seçimi (1,5,6 sayfa numaralarının virgülle ayrılmış bir listesini veya 2n+1 gibi bir fonksiyon girin) :
|
||||
pageOrderPrompt=Özel Sayfa Sırası (Virgülle ayrılmış sayfa numaraları veya 2n+1 gibi bir fonksiyon girin):
|
||||
pageSelectionPrompt=Özel Sayfa Seçimi (1,5,6 sayfa numaralarının virgülle ayrılmış bir listesini veya 2n+1 gibi bir fonksiyon girin):
|
||||
goToPage=Sayfaya Git
|
||||
true=Doğru
|
||||
false=Yanlış
|
||||
@ -170,67 +170,67 @@ sizes.medium=Orta
|
||||
sizes.large=Büyük
|
||||
sizes.x-large=Çok Büyük
|
||||
error.pdfPassword=PDF belgesi şifreli ve şifre ya sağlanmadı ya da yanlış.
|
||||
error.pdfCorrupted=PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.
|
||||
error.pdfCorruptedMultiple=One or more PDF files appear to be corrupted or damaged. Please try using the 'Repair PDF' feature on each file first before attempting to merge them.
|
||||
error.pdfCorruptedDuring=Error {0}: PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.
|
||||
error.pdfCorrupted=PDF dosyası bozuk veya hasarlı görünüyor. Lütfen bu işlemi gerçekleştirmeden önce dosyayı düzeltmek için 'PDF Onar' özelliğini kullanmayı deneyin.
|
||||
error.pdfCorruptedMultiple=Bir veya daha fazla PDF dosyası bozuk veya hasarlı görünüyor. Lütfen birleştirmeye çalışmadan önce her dosya için 'PDF Onar' özelliğini kullanmayı deneyin.
|
||||
error.pdfCorruptedDuring=Hata {0}: PDF dosyası bozuk veya hasarlı görünüyor. Lütfen bu işlemi gerçekleştirmeden önce dosyayı düzeltmek için 'PDF Onar' özelliğini kullanmayı deneyin.
|
||||
|
||||
# Frontend corruption error messages
|
||||
error.pdfInvalid=The PDF file "{0}" appears to be corrupted or has an invalid structure. Please try using the 'Repair PDF' feature to fix the file before proceeding.
|
||||
error.tryRepair=Try using the Repair PDF feature to fix corrupted files.
|
||||
error.pdfInvalid="{0}" adlı PDF dosyası bozuk görünüyor veya geçersiz bir yapıya sahip. Lütfen işlemi gerçekleştirmeden önce dosyayı düzeltmek için 'PDF Onar' özelliğini kullanmayı deneyin.
|
||||
error.tryRepair=Bozuk dosyaları düzeltmek için PDF Onar özelliğini kullanmayı deneyin.
|
||||
|
||||
# Additional error messages
|
||||
error.pdfEncryption=The PDF appears to have corrupted encryption data. This can happen when the PDF was created with incompatible encryption methods. Please try using the 'Repair PDF' feature first, or contact the document creator for a new copy.
|
||||
error.fileProcessing=An error occurred while processing the file during {0} operation: {1}
|
||||
error.pdfEncryption=PDF dosyasının şifreleme verileri bozulmuş görünüyor. Bu, PDF uyumsuz şifreleme yöntemleriyle oluşturulduğunda meydana gelebilir. Lütfen önce 'PDF Onar' özelliğini kullanmayı deneyin veya belgenin oluşturucusuyla iletişime geçerek yeni bir kopya isteyin.
|
||||
error.fileProcessing={0} işlemi sırasında dosya işlenirken bir hata oluştu: {1}
|
||||
|
||||
# Generic error message templates
|
||||
error.toolNotInstalled={0} is not installed
|
||||
error.toolRequired={0} is required for {1}
|
||||
error.conversionFailed={0} conversion failed
|
||||
error.commandFailed={0} command failed
|
||||
error.algorithmNotAvailable={0} algorithm not available
|
||||
error.optionsNotSpecified={0} options are not specified
|
||||
error.fileFormatRequired=File must be in {0} format
|
||||
error.invalidFormat=Invalid {0} format: {1}
|
||||
error.endpointDisabled=This endpoint has been disabled by the admin
|
||||
error.urlNotReachable=URL is not reachable, please provide a valid URL
|
||||
error.toolNotInstalled={0} yüklü değil
|
||||
error.toolRequired={1} işlemi için {0} gereklidir
|
||||
error.conversionFailed={0} dönüştürme işlemi başarısız oldu
|
||||
error.commandFailed={0} komutu başarısız oldu
|
||||
error.algorithmNotAvailable={0} algoritması kullanılamıyor
|
||||
error.optionsNotSpecified={0} seçenekleri belirtilmemiş
|
||||
error.fileFormatRequired=Dosya {0} formatında olmalıdır
|
||||
error.invalidFormat=Geçersiz {0} formatı: {1}
|
||||
error.endpointDisabled=Bu uç nokta yönetici tarafından devre dışı bırakılmıştır
|
||||
error.urlNotReachable=URL erişilebilir değil, lütfen geçerli bir URL sağlayın
|
||||
|
||||
# DPI and image rendering messages - used by frontend for dynamic translation
|
||||
# Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message
|
||||
# Frontend parses this and replaces with localized versions using these keys
|
||||
error.dpiExceedsLimit=DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause memory issues and crashes. Please use a lower DPI value.
|
||||
error.pageTooBigForDpi=PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less).
|
||||
error.pageTooBigExceedsArray=PDF page {0} is too large to render at {1} DPI. The resulting image would exceed Java's maximum array size. Please try a lower DPI value (recommended: 150 or less).
|
||||
error.pageTooBigFor300Dpi=PDF page {0} is too large to render at 300 DPI. The resulting image would exceed Java's maximum array size. Please use a lower DPI value for PDF-to-image conversion.
|
||||
error.dpiExceedsLimit=DPI değeri {0}, maksimum güvenli sınır olan {1} değerini aşıyor. Yüksek DPI değerleri bellek sorunlarına ve çökme hatalarına neden olabilir. Lütfen daha düşük bir DPI değeri kullanın.
|
||||
error.pageTooBigForDpi=PDF sayfası {0}, {1} DPI değerinde işlenemeyecek kadar büyük. Lütfen daha düşük bir DPI değeri deneyin (önerilen: 150 veya daha az).
|
||||
error.pageTooBigExceedsArray=PDF sayfası {0}, {1} DPI değerinde işlenemeyecek kadar büyük. Ortaya çıkan görüntü Java'nın maksimum dizi boyutunu aşacaktır. Lütfen daha düşük bir DPI değeri deneyin (önerilen: 150 veya daha az).
|
||||
error.pageTooBigFor300Dpi=PDF sayfası {0}, 300 DPI değerinde işlenemeyecek kadar büyük. Ortaya çıkan görüntü Java'nın maksimum dizi boyutunu aşacaktır. Lütfen PDF'den görüntüye dönüştürme işlemi için daha düşük bir DPI değeri kullanın.
|
||||
|
||||
# URL and website conversion messages
|
||||
|
||||
# System requirements messages
|
||||
|
||||
# Authentication and security messages
|
||||
error.apiKeyInvalid=API key is not valid.
|
||||
error.userNotFound=User not found.
|
||||
error.passwordRequired=Password must not be null.
|
||||
error.accountLocked=Your account has been locked due to too many failed login attempts.
|
||||
error.invalidEmail=Invalid email addresses provided.
|
||||
error.emailAttachmentRequired=An attachment is required to send the email.
|
||||
error.signatureNotFound=Signature file not found.
|
||||
error.apiKeyInvalid=API anahtarı geçerli değil.
|
||||
error.userNotFound=Kullanıcı bulunamadı.
|
||||
error.passwordRequired=Parola boş bırakılamaz.
|
||||
error.accountLocked=Çok fazla başarısız giriş denemesi nedeniyle hesabınız kilitlendi.
|
||||
error.invalidEmail=Geçersiz e-posta adresleri sağlandı.
|
||||
error.emailAttachmentRequired=E-posta gönderebilmek için bir ek dosya gereklidir.
|
||||
error.signatureNotFound=İmza dosyası bulunamadı.
|
||||
|
||||
# File processing messages
|
||||
error.fileNotFound=File not found with ID: {0}
|
||||
error.fileNotFound=Dosya bulunamadı. Dosya kimliği: {0}
|
||||
|
||||
# Database and configuration messages
|
||||
error.noBackupScripts=No backup scripts were found.
|
||||
error.unsupportedProvider={0} is not currently supported.
|
||||
error.pathTraversalDetected=Path traversal detected for security reasons.
|
||||
error.noBackupScripts=Yedekleme betikleri bulunamadı.
|
||||
error.unsupportedProvider={0} şu anda desteklenmiyor.
|
||||
error.pathTraversalDetected=Güvenlik nedeniyle yol geçişi (path traversal) tespit edildi.
|
||||
|
||||
# Validation messages
|
||||
error.invalidArgument=Invalid argument: {0}
|
||||
error.argumentRequired={0} must not be null
|
||||
error.operationFailed=Operation failed: {0}
|
||||
error.angleNotMultipleOf90=Angle must be a multiple of 90
|
||||
error.pdfBookmarksNotFound=No PDF bookmarks/outline found in document
|
||||
error.fontLoadingFailed=Error processing font file
|
||||
error.fontDirectoryReadFailed=Failed to read font directory
|
||||
error.invalidArgument=Geçersiz argüman: {0}
|
||||
error.argumentRequired={0} boş olamaz
|
||||
error.operationFailed=İşlem başarısız oldu: {0}
|
||||
error.angleNotMultipleOf90=Açı 90'ın katı olmalıdır
|
||||
error.pdfBookmarksNotFound=Belgede herhangi bir PDF yer imi / içindekiler bulunamadı
|
||||
error.fontLoadingFailed=Yazı tipi dosyası işlenirken hata oluştu
|
||||
error.fontDirectoryReadFailed=Yazı tipi dizini okunamadı
|
||||
delete=Sil
|
||||
username=Kullanıcı Adı
|
||||
password=Parola
|
||||
@ -260,7 +260,7 @@ disabledCurrentUserMessage=Mevcut kullanıcı devre dışı bırakılamaz
|
||||
downgradeCurrentUserLongMessage=Mevcut kullanıcının rolü düşürülemiyor. Bu nedenle, mevcut kullanıcı gösterilmeyecektir.
|
||||
userAlreadyExistsOAuthMessage=Kullanıcı zaten bir OAuth2 kullanıcısı olarak mevcut.
|
||||
userAlreadyExistsWebMessage=Kullanıcı zaten bir web kullanıcısı olarak mevcut.
|
||||
invalidRoleMessage=Invalid role.
|
||||
invalidRoleMessage=Geçersiz rol.
|
||||
error=Hata
|
||||
oops=Tüh!
|
||||
help=Yardım
|
||||
@ -273,7 +273,7 @@ color=Renk
|
||||
sponsor=Bağış
|
||||
info=Bilgi
|
||||
pro=Pro
|
||||
proFeatures=Pro Features
|
||||
proFeatures=Pro Özellikler
|
||||
page=Sayfa
|
||||
pages=Sayfalar
|
||||
loading=Yükleniyor...
|
||||
@ -281,11 +281,11 @@ addToDoc=Dökümana Ekle
|
||||
reset=Sıfırla
|
||||
apply=Uygula
|
||||
noFileSelected=Hiçbir dosya seçilmedi. Lütfen bir dosya yükleyin.
|
||||
view=View
|
||||
view=Görüntüle
|
||||
cancel=İptal
|
||||
|
||||
back.toSettings=Ayarlar’a Geri Dön
|
||||
back.toHome=Ana Sayfa’ya Geri Dön
|
||||
back.toSettings=Ayarlar'a Geri Dön
|
||||
back.toHome=Ana Sayfa'ya Geri Dön
|
||||
back.toAdmin=Yönetim Paneline Geri Dön
|
||||
|
||||
legal.privacy=Gizlilik Politikası
|
||||
@ -327,15 +327,15 @@ enterpriseEdition.button=Pro Sürümüne Yükselt
|
||||
enterpriseEdition.warning=Bu özellik yalnızca Pro kullanıcılarına sunulmaktadır.
|
||||
enterpriseEdition.yamlAdvert=Stirling PDF Pro, YAML yapılandırma dosyalarını ve diğer SSO özelliklerini destekler.
|
||||
enterpriseEdition.ssoAdvert=Daha fazla kullanıcı yönetimi özelliği mi arıyorsunuz? Stirling PDF Pro'ya göz atın
|
||||
enterpriseEdition.proTeamFeatureDisabled=Team management features require a Pro licence or higher
|
||||
enterpriseEdition.proTeamFeatureDisabled=Takım yönetimi özellikleri Pro lisansı veya daha üstü gerektirir
|
||||
|
||||
|
||||
#################
|
||||
# Analytics #
|
||||
#################
|
||||
analytics.title=Stirling PDF’i daha iyi hale getirmek ister misiniz?
|
||||
analytics.title=Stirling PDF'i daha iyi hale getirmek ister misiniz?
|
||||
analytics.paragraph1=Stirling PDF, ürünü geliştirmemize yardımcı olmak için isteğe bağlı analizleri içerir. Kişisel bilgileri veya dosya içeriklerini asla takip etmiyoruz.
|
||||
analytics.paragraph2=Stirling PDF’in büyümesine destek olmak ve kullanıcılarımızı daha iyi anlayabilmemiz için analizleri etkinleştirmeyi düşünebilirsiniz.
|
||||
analytics.paragraph2=Stirling PDF'in büyümesine destek olmak ve kullanıcılarımızı daha iyi anlayabilmemiz için analizleri etkinleştirmeyi düşünebilirsiniz.
|
||||
analytics.enable=Analizi Etkinleştir
|
||||
analytics.disable=Analizi Devre Dışı Bırak
|
||||
analytics.settings=Analiz ayarlarını config/settings.yml dosyasından değiştirebilirsiniz
|
||||
@ -345,7 +345,7 @@ analytics.settings=Analiz ayarlarını config/settings.yml dosyasından değişt
|
||||
# NAVBAR #
|
||||
#############
|
||||
navbar.favorite=Favoriler
|
||||
navbar.recent=New and recently updated
|
||||
navbar.recent=Yeni ve son güncellenenler
|
||||
navbar.darkmode=Karanlık Mod
|
||||
navbar.language=Diller
|
||||
navbar.settings=Ayarlar
|
||||
@ -368,36 +368,36 @@ settings.update=Güncelleme mevcut
|
||||
settings.updateAvailable={0} mevcut kurulu sürümdür. Yeni bir sürüm ({1}) mevcuttur.
|
||||
|
||||
# Update modal and notification strings
|
||||
update.urgentUpdateAvailable=🚨 Update Available
|
||||
update.updateAvailable=Update Available
|
||||
update.modalTitle=Update Available
|
||||
update.current=Current
|
||||
update.latest=Latest
|
||||
update.latestStable=Latest Stable
|
||||
update.priority=Priority
|
||||
update.recommendedAction=Recommended Action
|
||||
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||
update.migrationGuides=Migration Guides:
|
||||
update.viewGuide=View Guide
|
||||
update.loadingDetailedInfo=Loading detailed version information...
|
||||
update.close=Close
|
||||
update.viewAllReleases=View All Releases
|
||||
update.downloadLatest=Download Latest
|
||||
update.availableUpdates=Available Updates:
|
||||
update.unableToLoadDetails=Unable to load detailed version information.
|
||||
update.version=Version
|
||||
update.urgentUpdateAvailable=🚨 Güncelleme Mevcut
|
||||
update.updateAvailable=Güncelleme Mevcut
|
||||
update.modalTitle=Güncelleme Mevcut
|
||||
update.current=Mevcut
|
||||
update.latest=En Yeni
|
||||
update.latestStable=En Yeni Kararlı
|
||||
update.priority=Öncelik
|
||||
update.recommendedAction=Önerilen İşlem
|
||||
update.breakingChangesDetected=⚠️ Kırıcı Değişiklikler Tespit Edildi
|
||||
update.breakingChangesMessage=Bu güncelleme kırıcı değişiklikler içeriyor. Lütfen aşağıdaki geçiş kılavuzlarını inceleyin.
|
||||
update.migrationGuides=Geçiş Kılavuzları:
|
||||
update.viewGuide=Kılavuzu Görüntüle
|
||||
update.loadingDetailedInfo=Ayrıntılı sürüm bilgileri yükleniyor...
|
||||
update.close=Kapat
|
||||
update.viewAllReleases=Tüm Sürümleri Görüntüle
|
||||
update.downloadLatest=En Yeniyi İndir
|
||||
update.availableUpdates=Mevcut Güncellemeler:
|
||||
update.unableToLoadDetails=Ayrıntılı sürüm bilgileri yüklenemedi.
|
||||
update.version=Sürüm
|
||||
|
||||
# Update priority levels
|
||||
update.priority.urgent=URGENT
|
||||
update.priority.urgent=ACİL
|
||||
update.priority.normal=NORMAL
|
||||
update.priority.minor=MINOR
|
||||
update.priority.low=LOW
|
||||
update.priority.minor=ÖNEMSİZ
|
||||
update.priority.low=DÜŞÜK
|
||||
|
||||
# Breaking changes text
|
||||
update.breakingChanges=Breaking Changes:
|
||||
update.breakingChangesDefault=This version contains breaking changes
|
||||
update.migrationGuide=Migration Guide
|
||||
update.breakingChanges=Kırıcı Değişiklikler:
|
||||
update.breakingChangesDefault=Bu sürüm kırıcı değişiklikler içeriyor
|
||||
update.migrationGuide=Geçiş Kılavuzu
|
||||
settings.appVersion=Uygulama Sürümü:
|
||||
settings.downloadOption.title=İndirme seçeneği seçin (Zip olmayan tek dosya indirmeler için):
|
||||
settings.downloadOption.1=Aynı pencerede aç
|
||||
@ -472,18 +472,18 @@ adminUserSettings.disabledUsers=Devre Dışı Kullanıcılar:
|
||||
adminUserSettings.totalUsers=Toplam Kullanıcılar:
|
||||
adminUserSettings.lastRequest=Son İstek
|
||||
adminUserSettings.usage=Kullanımı Görüntüle
|
||||
adminUserSettings.teams=View/Edit Teams
|
||||
adminUserSettings.team=Team
|
||||
adminUserSettings.manageTeams=Manage Teams
|
||||
adminUserSettings.createTeam=Create Team
|
||||
adminUserSettings.viewTeam=View Team
|
||||
adminUserSettings.deleteTeam=Delete Team
|
||||
adminUserSettings.teamName=Team Name
|
||||
adminUserSettings.teamExists=Team already exists
|
||||
adminUserSettings.teamCreated=Team created successfully
|
||||
adminUserSettings.teamChanged=User's team was updated
|
||||
adminUserSettings.teamHidden=Hidden
|
||||
adminUserSettings.totalMembers=Total Members
|
||||
adminUserSettings.teams=Takımları Görüntüle/Düzenle
|
||||
adminUserSettings.team=Takım
|
||||
adminUserSettings.manageTeams=Takımları Yönet
|
||||
adminUserSettings.createTeam=Takım Oluştur
|
||||
adminUserSettings.viewTeam=Takımı Görüntüle
|
||||
adminUserSettings.deleteTeam=Takımı Sil
|
||||
adminUserSettings.teamName=Takım Adı
|
||||
adminUserSettings.teamExists=Takım zaten mevcut
|
||||
adminUserSettings.teamCreated=Takım başarıyla oluşturuldu
|
||||
adminUserSettings.teamChanged=Kullanıcının takımı güncellendi
|
||||
adminUserSettings.teamHidden=Gizli
|
||||
adminUserSettings.totalMembers=Toplam Üye
|
||||
adminUserSettings.confirmDeleteTeam=Bu takımı silmek istediğinizden emin misiniz?
|
||||
|
||||
teamCreated=Takım başarıyla oluşturuldu
|
||||
@ -538,7 +538,7 @@ endpointStatistics.home=Ana Sayfa
|
||||
endpointStatistics.login=Giriş
|
||||
endpointStatistics.top=En Çok
|
||||
endpointStatistics.numberOfVisits=Ziyaret Sayısı
|
||||
endpointStatistics.visitsTooltip=Ziyaret: {0} (toplamın %{1}’i)
|
||||
endpointStatistics.visitsTooltip=Ziyaret: {0} (toplamın %{1}'i)
|
||||
endpointStatistics.retry=Yeniden Dene
|
||||
|
||||
database.title=Veri Tabanını İçe/Dışa Aktar
|
||||
@ -617,9 +617,9 @@ home.addImage.title=Resim Ekle
|
||||
home.addImage.desc=PDF'e belirli bir konuma resim ekler
|
||||
addImage.tags=img,jpg,fotoğraf,resim
|
||||
|
||||
home.attachments.title=Add Attachments
|
||||
home.attachments.desc=Add or remove embedded files (attachments) to/from a PDF
|
||||
attachments.tags=embed,attach,file,attachment,attachments
|
||||
home.attachments.title=Ekleri Ekle
|
||||
home.attachments.desc=PDF'ye gömülü dosyalar (ekler) ekle veya kaldır
|
||||
attachments.tags=gömme,ekle,dosya,ek,ekler
|
||||
|
||||
home.watermark.title=Filigran Ekle
|
||||
home.watermark.desc=PDF belgenize özel bir filigran ekleyin.
|
||||
@ -772,21 +772,21 @@ home.HTMLToPDF.desc=Herhangi bir HTML dosyasını veya zip'i PDF'e dönüştür
|
||||
HTMLToPDF.tags=biçimlendirme,web-içeriği,dönüşüm,dönüştür
|
||||
|
||||
#eml-to-pdf
|
||||
home.EMLToPDF.title=Email to PDF
|
||||
home.EMLToPDF.desc=Converts email (EML) files to PDF format including headers, body, and inline images
|
||||
EMLToPDF.tags=email,conversion,eml,message,transformation,convert,mail
|
||||
home.EMLToPDF.title=E-postayı PDF'ye Dönüştür
|
||||
home.EMLToPDF.desc=Başlıklar, gövde ve satır içi resimler dahil olmak üzere e-posta (EML) dosyalarını PDF formatına dönüştürür
|
||||
EMLToPDF.tags=e-posta, dönüşüm, eml, mesaj, dönüşüm, dönüştür, posta
|
||||
|
||||
EMLToPDF.title=Email To PDF
|
||||
EMLToPDF.header=Email To PDF
|
||||
EMLToPDF.submit=Convert
|
||||
EMLToPDF.downloadHtml=Download HTML intermediate file instead of PDF
|
||||
EMLToPDF.downloadHtmlHelp=This allows you to see the HTML version before PDF conversion and can help debug formatting issues
|
||||
EMLToPDF.includeAttachments=Include attachments in PDF
|
||||
EMLToPDF.maxAttachmentSize=Maximum attachment size (MB)
|
||||
EMLToPDF.help=Converts email (EML) files to PDF format including headers, body, and inline images
|
||||
EMLToPDF.troubleshootingTip1=Email to HTML is a more reliable process, so with batch-processing it is recommended to save both
|
||||
EMLToPDF.troubleshootingTip2=With a small number of Emails, if the PDF is malformed, you can download HTML and override some of the problematic HTML/CSS code.
|
||||
EMLToPDF.troubleshootingTip3=Embeddings, however, do not work with HTMLs
|
||||
EMLToPDF.title=E-postayı PDF'ye Dönüştür
|
||||
EMLToPDF.header=E-postayı PDF'ye Dönüştür
|
||||
EMLToPDF.submit=Dönüştür
|
||||
EMLToPDF.downloadHtml=PDF yerine HTML ara dosyasını indir
|
||||
EMLToPDF.downloadHtmlHelp=Bu, PDF dönüşümünden önce HTML sürümünü görmenizi sağlar ve biçimlendirme sorunlarını çözmeye yardımcı olabilir
|
||||
EMLToPDF.includeAttachments=PDF'ye ekleri dahil et
|
||||
EMLToPDF.maxAttachmentSize=Maksimum ek boyutu (MB)
|
||||
EMLToPDF.help=Başlıklar, gövde ve satır içi resimler dahil olmak üzere e-posta (EML) dosyalarını PDF formatına dönüştürür
|
||||
EMLToPDF.troubleshootingTip1=E-postayı HTML'ye dönüştürmek daha güvenilir bir işlemdir, bu nedenle toplu işleme yaparken her ikisini de kaydetmek önerilir
|
||||
EMLToPDF.troubleshootingTip2=Az sayıda e-posta için, PDF bozuksa HTML dosyasını indirip bazı sorunlu HTML/CSS kodlarını değiştirebilirsiniz
|
||||
EMLToPDF.troubleshootingTip3=Ancak, gömülü içerikler HTML ile çalışmaz
|
||||
|
||||
home.MarkdownToPDF.title=Markdown'dan PDF'e
|
||||
home.MarkdownToPDF.desc=Herhangi bir Markdown dosyasını PDF'e dönüştürür
|
||||
@ -907,8 +907,8 @@ login.userIsDisabled=Kullanıcı devre dışı bırakıldı, şu anda bu kullan
|
||||
login.alreadyLoggedIn=Zaten şu cihazlarda oturum açılmış:
|
||||
login.alreadyLoggedIn2=Lütfen bu cihazlardan çıkış yaparak tekrar deneyin.
|
||||
login.toManySessions=Çok fazla aktif oturumunuz var
|
||||
login.logoutMessage=You have been logged out.
|
||||
login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator.
|
||||
login.logoutMessage=Oturumunuz kapatıldı.
|
||||
login.invalidInResponseTo=İstenen SAML yanıtı geçersiz veya süresi dolmuş. Lütfen yöneticiyle iletişime geçin.
|
||||
|
||||
#auto-redact
|
||||
autoRedact.title=Otomatik Karartma
|
||||
@ -974,28 +974,28 @@ getPdfInfo.title=PDF Hakkında Bilgi Al
|
||||
getPdfInfo.header=PDF Hakkında Bilgi Al
|
||||
getPdfInfo.submit=Bilgi Al
|
||||
getPdfInfo.downloadJson=JSON İndir
|
||||
getPdfInfo.summary=PDF Summary
|
||||
getPdfInfo.summary.encrypted=This PDF is encrypted so may face issues with some applications
|
||||
getPdfInfo.summary.permissions=This PDF has {0} restricted permissions which may limit what you can do with it
|
||||
getPdfInfo.summary.compliance=This PDF complies with the {0} standard
|
||||
getPdfInfo.summary.basicInfo=Basic Information
|
||||
getPdfInfo.summary.docInfo=Document Information
|
||||
getPdfInfo.summary.encrypted.alert=Encrypted PDF - This document is password protected
|
||||
getPdfInfo.summary.not.encrypted.alert=Unencrypted PDF - No password protection
|
||||
getPdfInfo.summary.permissions.alert=Restricted Permissions - {0} actions are not allowed
|
||||
getPdfInfo.summary.all.permissions.alert=All Permissions Allowed
|
||||
getPdfInfo.summary.compliance.alert={0} Compliant
|
||||
getPdfInfo.summary.no.compliance.alert=No Compliance Standards
|
||||
getPdfInfo.summary.security.section=Security Status
|
||||
getPdfInfo.section.BasicInfo=Basic Information about the PDF document including file size, page count, and language
|
||||
getPdfInfo.section.Metadata=Document metadata including title, author, creation date and other document properties
|
||||
getPdfInfo.section.DocumentInfo=Technical details about the PDF document structure and version
|
||||
getPdfInfo.section.Compliancy=PDF standards compliance information (PDF/A, PDF/X, etc.)
|
||||
getPdfInfo.section.Encryption=Security and encryption details of the document
|
||||
getPdfInfo.section.Permissions=Document permission settings that control what actions can be performed
|
||||
getPdfInfo.section.Other=Additional document components like bookmarks, layers, and embedded files
|
||||
getPdfInfo.section.FormFields=Interactive form fields present in the document
|
||||
getPdfInfo.section.PerPageInfo=Detailed information about each page in the document
|
||||
getPdfInfo.summary=PDF Özeti
|
||||
getPdfInfo.summary.encrypted=Bu PDF şifreli olduğu için bazı uygulamalarda sorun yaşanabilir
|
||||
getPdfInfo.summary.permissions=Bu PDF'de {0} kısıtlanmış izinler var, bu da yapabileceklerinizi sınırlayabilir
|
||||
getPdfInfo.summary.compliance=Bu PDF {0} standardına uygundur
|
||||
getPdfInfo.summary.basicInfo=Temel Bilgiler
|
||||
getPdfInfo.summary.docInfo=Belge Bilgileri
|
||||
getPdfInfo.summary.encrypted.alert=Şifreli PDF - Bu belge parola korumalıdır
|
||||
getPdfInfo.summary.not.encrypted.alert=Şifresiz PDF - Parola koruması yok
|
||||
getPdfInfo.summary.permissions.alert=Kısıtlanmış İzinler - {0} işlem izin verilmemiştir
|
||||
getPdfInfo.summary.all.permissions.alert=Tüm İzinler Verildi
|
||||
getPdfInfo.summary.compliance.alert={0} Uygun
|
||||
getPdfInfo.summary.no.compliance.alert=Uygunluk Standardı Yok
|
||||
getPdfInfo.summary.security.section=Güvenlik Durumu
|
||||
getPdfInfo.section.BasicInfo=PDF belgesinin dosya boyutu, sayfa sayısı ve dili dahil temel bilgileri
|
||||
getPdfInfo.section.Metadata=Başlık, yazar, oluşturulma tarihi ve diğer belge özelliklerini içeren belge meta verisi
|
||||
getPdfInfo.section.DocumentInfo=PDF belge yapısı ve sürümü hakkında teknik detaylar
|
||||
getPdfInfo.section.Compliancy=PDF standartlarına uygunluk bilgisi (PDF/A, PDF/X, vb.)
|
||||
getPdfInfo.section.Encryption=Belgenin güvenlik ve şifreleme detayları
|
||||
getPdfInfo.section.Permissions=Hangi işlemlerin yapılabileceğini kontrol eden belge izin ayarları
|
||||
getPdfInfo.section.Other=Yer imleri, katmanlar ve gömülü dosyalar gibi ek belge bileşenleri
|
||||
getPdfInfo.section.FormFields=Belgede bulunan etkileşimli form alanları
|
||||
getPdfInfo.section.PerPageInfo=Belgedeki her sayfa hakkında ayrıntılı bilgiler
|
||||
|
||||
|
||||
#markdown-to-pdf
|
||||
@ -1007,9 +1007,9 @@ MarkdownToPDF.credit=WeasyPrint Kullanıyor
|
||||
|
||||
|
||||
#pdf-to-markdown
|
||||
PDFToMarkdown.title=PDF To Markdown
|
||||
PDFToMarkdown.header=PDF To Markdown
|
||||
PDFToMarkdown.submit=Convert
|
||||
PDFToMarkdown.title=PDF'den Markdown'a
|
||||
PDFToMarkdown.header=PDF'den Markdown'a
|
||||
PDFToMarkdown.submit=Dönüştür
|
||||
|
||||
|
||||
#url-to-pdf
|
||||
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Numaralandırılacak Sayfalar
|
||||
addPageNumbers.selectText.6=Özel Metin
|
||||
addPageNumbers.customTextDesc=Özel Metin
|
||||
addPageNumbers.numberPagesDesc=Hangi sayfaların numaralandırılacağını, varsayılan 'all', ayrıca 1-5 veya 2,5,9 vb. kabul eder
|
||||
addPageNumbers.customNumberDesc=Varsayılan {n}, ayrıca 'Sayfa {n} / {total}', 'Metin-{n}', '{filename}-{n} kabul eder
|
||||
addPageNumbers.customNumberDesc=Varsayılan {n}, ayrıca 'Sayfa {n} / {total}', 'Metin-{n}', '{filename}-{n}' kabul eder
|
||||
addPageNumbers.submit=Sayfa Numaraları Ekle
|
||||
|
||||
|
||||
@ -1136,7 +1136,7 @@ pageLayout.submit=Gönder
|
||||
scalePages.title=Sayfa Ölçeğini Ayarla
|
||||
scalePages.header=Sayfa Ölçeğini Ayarla
|
||||
scalePages.pageSize=Belgenin bir sayfa boyutu.
|
||||
scalePages.keepPageSize=Original Size
|
||||
scalePages.keepPageSize=Orijinal Boyut
|
||||
scalePages.scaleFactor=Bir sayfanın yakınlaştırma seviyesi (kırpma).
|
||||
scalePages.submit=Gönder
|
||||
|
||||
@ -1156,7 +1156,7 @@ certSign.showSig=İmzayı Göster
|
||||
certSign.reason=Neden
|
||||
certSign.location=Konum
|
||||
certSign.name=İsim
|
||||
certSign.showLogo=Show Logo
|
||||
certSign.showLogo=Logoyu Göster
|
||||
certSign.submit=PDF'i İmzala
|
||||
|
||||
|
||||
@ -1171,7 +1171,7 @@ removeCertSign.submit=İmzayı Kaldır
|
||||
removeBlanks.title=Boşları Kaldır
|
||||
removeBlanks.header=Boş Sayfaları Kaldır
|
||||
removeBlanks.threshold=Pixel Beyazlık Eşiği:
|
||||
removeBlanks.thresholdDesc=Bir beyaz pixelin 'Beyaz' olarak sınıflandırılması için ne kadar beyaz olması gerektiğini belirlemek için eşik. 0 = Siyah, 255 saf beyaz.
|
||||
removeBlanks.thresholdDesc=Bir beyaz pixelin 'Beyaz' olarak sınıflandırılması için ne kadar beyaz olması gerektiğini belirlemek için eşik. 0=Siyah, 255 saf beyaz.
|
||||
removeBlanks.whitePercent=Beyaz Yüzde (%):
|
||||
removeBlanks.whitePercentDesc=Bir sayfanın 'beyaz' pixel olması gereken yüzdesi
|
||||
removeBlanks.submit=Boşları Kaldır
|
||||
@ -1287,8 +1287,8 @@ compress.title=Sıkıştır
|
||||
compress.header=PDF'i Sıkıştır
|
||||
compress.credit=Bu hizmet PDF Sıkıştırma/Optimizasyonu için qpdf kullanır.
|
||||
compress.grayscale.label=Sıkıştırma için Gri Ton Uygula
|
||||
compress.selectText.1=Compression Settings
|
||||
compress.selectText.1.1=1-3 PDF compression,</br> 4-6 lite image compression,</br> 7-9 intense image compression Will dramatically reduce image quality
|
||||
compress.selectText.1=Sıkıştırma Ayarları
|
||||
compress.selectText.1.1=1-3 PDF sıkıştırma,</br>4-6 hafif görüntü sıkıştırma,</br>7-9 yoğun görüntü sıkıştırma Görüntü kalitesini ciddi şekilde düşürecektir
|
||||
compress.selectText.2=Optimizasyon seviyesi:
|
||||
compress.selectText.4=Otomatik mod - PDF'in tam boyutuna ulaşmak için kaliteyi otomatik ayarlar
|
||||
compress.selectText.5=Beklenen PDF Boyutu (örn. 25MB, 10.8MB, 25KB)
|
||||
@ -1303,11 +1303,11 @@ addImage.upload=Resim ekle
|
||||
addImage.submit=Resim ekle
|
||||
|
||||
#attachments
|
||||
attachments.title=Add Attachments
|
||||
attachments.header=Add attachments
|
||||
attachments.description=Allows you to add attachments to the PDF
|
||||
attachments.descriptionPlaceholder=Enter a description for the attachments...
|
||||
attachments.addButton=Add Attachments
|
||||
attachments.title=Ekler Ekle
|
||||
attachments.header=Ekler Ekle
|
||||
attachments.description=PDF'ye ekler eklemenizi sağlar
|
||||
attachments.descriptionPlaceholder=Ekler için bir açıklama girin...
|
||||
attachments.addButton=Ekleri Ekle
|
||||
|
||||
#merge
|
||||
merge.title=Birleştir
|
||||
@ -1315,7 +1315,7 @@ merge.header=Çoklu PDF'leri Birleştir (2+)
|
||||
merge.sortByName=İsme göre sırala
|
||||
merge.sortByDate=Tarihe göre sırala
|
||||
merge.removeCertSign=Birleştirilen dosyadaki dijital imza kaldırılsın mı?
|
||||
merge.generateToc=Generate table of contents in the merged file?
|
||||
merge.generateToc=Birleştirilen dosyada içindekiler tablosu oluşturulsun mu?
|
||||
merge.submit=Birleştir
|
||||
|
||||
|
||||
@ -1435,7 +1435,7 @@ pdfToImage.colorType=Renk türü
|
||||
pdfToImage.color=Renk
|
||||
pdfToImage.grey=Gri tonlama
|
||||
pdfToImage.blackwhite=Siyah ve Beyaz (Veri kaybolabilir!)
|
||||
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||
pdfToImage.dpi=DPI (Sunucu limiti {0} dpi)
|
||||
pdfToImage.submit=Dönüştür
|
||||
pdfToImage.info=Python kurulu değil. WebP dönüşümü için gereklidir.
|
||||
pdfToImage.placeholder=(örneğin 1,2,8 veya 4,7,12-16 ya da 2n-1)
|
||||
@ -1652,9 +1652,9 @@ survey.meeting.1=Eğer Stirling PDF'i iş yerinizde kullanıyorsanız, sizinle g
|
||||
survey.meeting.2=Bu fırsat sayesinde:
|
||||
survey.meeting.3=Kurulum, entegrasyonlar veya sorun giderme konularında yardım alabilirsiniz
|
||||
survey.meeting.4=Performans, uç durumlar ve eksik özellikler hakkında doğrudan geri bildirim sağlayabilirsiniz
|
||||
survey.meeting.5=Stirling PDF’i gerçek dünya kurumsal kullanımı için daha iyi hale getirmemize yardımcı olabilirsiniz
|
||||
survey.meeting.5=Stirling PDF'i gerçek dünya kurumsal kullanımı için daha iyi hale getirmemize yardımcı olabilirsiniz
|
||||
survey.meeting.6=İlgileniyorsanız, ekibimizden doğrudan zaman ayırabilirsiniz. (Yalnızca İngilizce)
|
||||
survey.meeting.7=Kullanım senaryolarınızı dinlemeyi ve Stirling PDF’i daha da iyi hale getirmeyi sabırsızlıkla bekliyoruz!
|
||||
survey.meeting.7=Kullanım senaryolarınızı dinlemeyi ve Stirling PDF'i daha da iyi hale getirmeyi sabırsızlıkla bekliyoruz!
|
||||
survey.meeting.notInterested=Kurumsal kullanıcı değilseniz ve/veya görüşmeye ilgi duymuyorsanız
|
||||
survey.meeting.button=Görüşme Planla
|
||||
|
||||
@ -1740,23 +1740,23 @@ validateSignature.cert.keySize=Anahtar Boyutu
|
||||
validateSignature.cert.version=Sürüm
|
||||
validateSignature.cert.keyUsage=Anahtar Kullanımı
|
||||
validateSignature.cert.selfSigned=Kendi Kendine İmzalı
|
||||
validateSignature.cert.bits=bits
|
||||
validateSignature.cert.bits=bit
|
||||
|
||||
# Audit Dashboard
|
||||
audit.dashboard.title=Audit Dashboard
|
||||
audit.dashboard.systemStatus=Audit System Status
|
||||
audit.dashboard.title=Denetim Kontrol Paneli
|
||||
audit.dashboard.systemStatus=Denetim Sistemi Durumu
|
||||
audit.dashboard.status=Durum
|
||||
audit.dashboard.enabled=Etkin
|
||||
audit.dashboard.disabled=Devre Dışı
|
||||
audit.dashboard.currentLevel=Current Level
|
||||
audit.dashboard.retentionPeriod=Retention Period
|
||||
audit.dashboard.days=days
|
||||
audit.dashboard.totalEvents=Total Events
|
||||
audit.dashboard.currentLevel=Mevcut Seviye
|
||||
audit.dashboard.retentionPeriod=Saklama Süresi
|
||||
audit.dashboard.days=gün
|
||||
audit.dashboard.totalEvents=Toplam Olay
|
||||
|
||||
# Audit Dashboard Tabs
|
||||
audit.dashboard.tab.dashboard=Dashboard
|
||||
audit.dashboard.tab.events=Audit Events
|
||||
audit.dashboard.tab.export=Export
|
||||
audit.dashboard.tab.dashboard=Gösterge Paneli
|
||||
audit.dashboard.tab.events=Denetim Olayları
|
||||
audit.dashboard.tab.export=Dışa Aktar
|
||||
# Dashboard Charts
|
||||
audit.dashboard.eventsByType=Türüne Göre Olaylar
|
||||
audit.dashboard.eventsByUser=Kullanıcıya Göre Olaylar
|
||||
@ -1766,23 +1766,23 @@ audit.dashboard.period.30days=30 Gün
|
||||
audit.dashboard.period.90days=90 Gün
|
||||
|
||||
# Events Tab
|
||||
audit.dashboard.auditEvents=Audit Events
|
||||
audit.dashboard.filter.eventType=Event Type
|
||||
audit.dashboard.filter.allEventTypes=All event types
|
||||
audit.dashboard.filter.user=User
|
||||
audit.dashboard.filter.userPlaceholder=Filter by user
|
||||
audit.dashboard.filter.startDate=Start Date
|
||||
audit.dashboard.filter.endDate=End Date
|
||||
audit.dashboard.filter.apply=Apply Filters
|
||||
audit.dashboard.filter.reset=Reset Filters
|
||||
audit.dashboard.auditEvents=Denetim Olayları
|
||||
audit.dashboard.filter.eventType=Olay Türü
|
||||
audit.dashboard.filter.allEventTypes=Tüm olay türleri
|
||||
audit.dashboard.filter.user=Kullanıcı
|
||||
audit.dashboard.filter.userPlaceholder=Kullanıcıya göre filtrele
|
||||
audit.dashboard.filter.startDate=Başlangıç Tarihi
|
||||
audit.dashboard.filter.endDate=Bitiş Tarihi
|
||||
audit.dashboard.filter.apply=Filtreleri Uygula
|
||||
audit.dashboard.filter.reset=Filtreleri Sıfırla
|
||||
|
||||
# Table Headers
|
||||
audit.dashboard.table.id=ID
|
||||
audit.dashboard.table.time=Time
|
||||
audit.dashboard.table.user=User
|
||||
audit.dashboard.table.type=Type
|
||||
audit.dashboard.table.details=Details
|
||||
audit.dashboard.table.viewDetails=View Details
|
||||
audit.dashboard.table.time=Zaman
|
||||
audit.dashboard.table.user=Kullanıcı
|
||||
audit.dashboard.table.type=Tür
|
||||
audit.dashboard.table.details=Detaylar
|
||||
audit.dashboard.table.viewDetails=Detayları Görüntüle
|
||||
|
||||
# Pagination
|
||||
audit.dashboard.pagination.show=Göster
|
||||
@ -1792,10 +1792,10 @@ audit.dashboard.pagination.pageInfo2=/
|
||||
audit.dashboard.pagination.totalRecords=Toplam kayıt:
|
||||
|
||||
# Modal
|
||||
audit.dashboard.modal.eventDetails=Event Details
|
||||
audit.dashboard.modal.eventDetails=Olay Detayları
|
||||
audit.dashboard.modal.id=ID
|
||||
audit.dashboard.modal.user=Kullanıcı
|
||||
audit.dashboard.modal.type=Type
|
||||
audit.dashboard.modal.type=Tip
|
||||
audit.dashboard.modal.time=Zaman
|
||||
audit.dashboard.modal.data=Veri
|
||||
|
||||
@ -1824,8 +1824,8 @@ audit.dashboard.js.loadingPage=Sayfa yükleniyor
|
||||
# Cookie banner #
|
||||
####################
|
||||
cookieBanner.popUp.title=Çerezleri Nasıl Kullanıyoruz
|
||||
cookieBanner.popUp.description.1=Stirling PDF’yi sizin için daha iyi çalıştırmak için çerezler ve diğer teknolojileri kullanıyoruz — araçlarımızı geliştirmemize ve seveceğiniz özellikler oluşturmamıza yardımcı oluyorlar.
|
||||
cookieBanner.popUp.description.2=İstemiyorsanız, ‘Hayır Teşekkürler’ butonuna tıklayarak yalnızca temel, gerekli çerezleri etkinleştirebilirsiniz.
|
||||
cookieBanner.popUp.description.1=Stirling PDF'yi sizin için daha iyi çalıştırmak için çerezler ve diğer teknolojileri kullanıyoruz — araçlarımızı geliştirmemize ve seveceğiniz özellikler oluşturmamıza yardımcı oluyorlar.
|
||||
cookieBanner.popUp.description.2=İstemiyorsanız, 'Hayır Teşekkürler' butonuna tıklayarak yalnızca temel, gerekli çerezleri etkinleştirebilirsiniz.
|
||||
cookieBanner.popUp.acceptAllBtn=Tamam
|
||||
cookieBanner.popUp.acceptNecessaryBtn=Hayır Teşekkürler
|
||||
cookieBanner.popUp.showPreferencesBtn=Tercihleri Yönet
|
||||
@ -1846,20 +1846,20 @@ cookieBanner.preferencesModal.analytics.title=Analitik
|
||||
cookieBanner.preferencesModal.analytics.description=Bu çerezler, araçlarımızın nasıl kullanıldığını anlamamıza yardımcı olur, böylece topluluğumuzun en çok değer verdiği özellikleri geliştirmeye odaklanabiliriz. İçiniz rahat olsun — Stirling PDF, belgelerinizin içeriğini asla takip etmez ve etmeyecektir.
|
||||
|
||||
#scannerEffect
|
||||
scannerEffect.title=Scanner Effect
|
||||
scannerEffect.header=Scanner Effect
|
||||
scannerEffect.description=Create a PDF that looks like it was scanned
|
||||
scannerEffect.selectPDF=Select PDF:
|
||||
scannerEffect.quality=Scan Quality
|
||||
scannerEffect.quality.low=Low
|
||||
scannerEffect.quality.medium=Medium
|
||||
scannerEffect.quality.high=High
|
||||
scannerEffect.rotation=Rotation Angle
|
||||
scannerEffect.rotation.none=None
|
||||
scannerEffect.rotation.slight=Slight
|
||||
scannerEffect.rotation.moderate=Moderate
|
||||
scannerEffect.rotation.severe=Severe
|
||||
scannerEffect.submit=Create Scanner Effect
|
||||
scannerEffect.title=Tarayıcı Efekti
|
||||
scannerEffect.header=Tarayıcı Efekti
|
||||
scannerEffect.description=Taranmış gibi görünen bir PDF oluştur
|
||||
scannerEffect.selectPDF=PDF Seç:
|
||||
scannerEffect.quality=Tarama Kalitesi
|
||||
scannerEffect.quality.low=Düşük
|
||||
scannerEffect.quality.medium=Orta
|
||||
scannerEffect.quality.high=Yüksek
|
||||
scannerEffect.rotation=Döndürme Açısı
|
||||
scannerEffect.rotation.none=Yok
|
||||
scannerEffect.rotation.slight=Hafif
|
||||
scannerEffect.rotation.moderate=Orta
|
||||
scannerEffect.rotation.severe=Şiddetli
|
||||
scannerEffect.submit=Tarayıcı Efekti Oluştur
|
||||
|
||||
#home.scannerEffect
|
||||
home.scannerEffect.title=Sahte Tarama
|
||||
@ -1893,12 +1893,12 @@ editTableOfContents.replaceExisting=Mevcut yer işaretlerini değiştir (var ola
|
||||
editTableOfContents.editorTitle=Yer İşareti Düzenleyici
|
||||
editTableOfContents.editorDesc=Aşağıdan yer işaretleri ekleyin ve düzenleyin. Alt yer işareti eklemek için + simgesine tıklayın.
|
||||
editTableOfContents.addBookmark=Yeni Yer İşareti Ekle
|
||||
editTableOfContents.importBookmarksDefault=Import
|
||||
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||
editTableOfContents.exportBookmarksDefault=Export
|
||||
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||
editTableOfContents.importBookmarksDefault=İçe Aktar
|
||||
editTableOfContents.importBookmarksFromJsonFile=JSON dosyası yükle
|
||||
editTableOfContents.importBookmarksFromClipboard=Panodan yapıştır
|
||||
editTableOfContents.exportBookmarksDefault=Dışa Aktar
|
||||
editTableOfContents.exportBookmarksAsJson=JSON olarak indir
|
||||
editTableOfContents.exportBookmarksAsText=Metin olarak kopyala
|
||||
editTableOfContents.desc.1=Bu araç, bir PDF belgesine içindekiler tablosu (yer işaretleri) eklemenizi veya mevcut olanları düzenlemenizi sağlar.
|
||||
editTableOfContents.desc.2=Alt yer işaretleri ekleyerek hiyerarşik bir yapı oluşturabilirsiniz.
|
||||
editTableOfContents.desc.3=Her yer işareti bir başlık ve hedef sayfa numarası gerektirir.
|
||||
|
@ -1081,7 +1081,7 @@ addPageNumbers.selectText.5=Trang cần đánh số
|
||||
addPageNumbers.selectText.6=Văn bản tùy chỉnh
|
||||
addPageNumbers.customTextDesc=Văn bản tùy chỉnh
|
||||
addPageNumbers.numberPagesDesc=Những trang cần đánh số, mặc định là 'all', cũng chấp nhận 1-5 hoặc 2,5,9 v.v.
|
||||
addPageNumbers.customNumberDesc=Mặc định là {n}, cũng chấp nhận 'Trang {n} / {total}', 'Văn bản-{n}', '{filename}-{n}
|
||||
addPageNumbers.customNumberDesc=Mặc định là {n}, cũng chấp nhận 'Trang {n} / {total}', 'Văn bản-{n}', '{filename}-{n}'
|
||||
addPageNumbers.submit=Thêm số trang
|
||||
|
||||
|
||||
|
@ -325,7 +325,7 @@ pipelineOptions.validateButton=驗證
|
||||
########################
|
||||
enterpriseEdition.button=升級至專業版
|
||||
enterpriseEdition.warning=此功能僅提供給專業版使用者使用。
|
||||
enterpriseEdition.yamlAdvert=Stirling PDF 專業版支援 YAML 設定檔和其他單一登入 (SSO) 功能。
|
||||
enterpriseEdition.yamlAdvert=Stirling PDF 專業版支援 YAML 設定檔和其他 SSO 登入功能。
|
||||
enterpriseEdition.ssoAdvert=需要更多使用者管理功能嗎?請參考 Stirling PDF 專業版
|
||||
enterpriseEdition.proTeamFeatureDisabled=團隊管理功能需要專業版或更進階的授權
|
||||
|
||||
@ -364,42 +364,42 @@ navbar.sections.popular=熱門功能
|
||||
# SETTINGS #
|
||||
#############
|
||||
settings.title=設定
|
||||
settings.update=有更新可用
|
||||
settings.updateAvailable=目前安裝的版本是 {0}。有新版本({1})可供使用。
|
||||
settings.update=有可用更新
|
||||
settings.updateAvailable=目前安裝的版本為 {0},已有新版本({1})可供更新。
|
||||
|
||||
# Update modal and notification strings
|
||||
update.urgentUpdateAvailable=🚨 Update Available
|
||||
update.updateAvailable=Update Available
|
||||
update.modalTitle=Update Available
|
||||
update.current=Current
|
||||
update.latest=Latest
|
||||
update.latestStable=Latest Stable
|
||||
update.priority=Priority
|
||||
update.recommendedAction=Recommended Action
|
||||
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||
update.migrationGuides=Migration Guides:
|
||||
update.viewGuide=View Guide
|
||||
update.loadingDetailedInfo=Loading detailed version information...
|
||||
update.close=Close
|
||||
update.viewAllReleases=View All Releases
|
||||
update.downloadLatest=Download Latest
|
||||
update.availableUpdates=Available Updates:
|
||||
update.unableToLoadDetails=Unable to load detailed version information.
|
||||
update.version=Version
|
||||
update.urgentUpdateAvailable=🚨 有緊急更新
|
||||
update.updateAvailable=有可用更新
|
||||
update.modalTitle=有可用更新
|
||||
update.current=目前版本
|
||||
update.latest=最新版本
|
||||
update.latestStable=最新穩定版本
|
||||
update.priority=優先等級
|
||||
update.recommendedAction=建議操作
|
||||
update.breakingChangesDetected=⚠️ 偵測到重大變更
|
||||
update.breakingChangesMessage=此更新包含可能無法向下相容的重大變更,請先參閱下方的遷移指南。
|
||||
update.migrationGuides=遷移指南:
|
||||
update.viewGuide=檢視指南
|
||||
update.loadingDetailedInfo=正在載入版本詳細資訊...
|
||||
update.close=關閉
|
||||
update.viewAllReleases=檢視所有版本
|
||||
update.downloadLatest=下載最新版
|
||||
update.availableUpdates=可用更新:
|
||||
update.unableToLoadDetails=無法載入版本詳細資訊。
|
||||
update.version=版本
|
||||
|
||||
# Update priority levels
|
||||
update.priority.urgent=URGENT
|
||||
update.priority.normal=NORMAL
|
||||
update.priority.minor=MINOR
|
||||
update.priority.low=LOW
|
||||
update.priority.urgent=緊急更新
|
||||
update.priority.normal=一般更新
|
||||
update.priority.minor=次要更新
|
||||
update.priority.low=低優先度更新
|
||||
|
||||
# Breaking changes text
|
||||
update.breakingChanges=Breaking Changes:
|
||||
update.breakingChangesDefault=This version contains breaking changes
|
||||
update.migrationGuide=Migration Guide
|
||||
update.breakingChanges=重大變更:
|
||||
update.breakingChangesDefault=此版本包含可能無法向下相容的重大變更
|
||||
update.migrationGuide=遷移指南
|
||||
settings.appVersion=應用程式版本:
|
||||
settings.downloadOption.title=選擇下載選項(適用於單一檔案非壓縮下載):
|
||||
settings.downloadOption.title=選擇下載選項(適用於單檔無壓縮下載):
|
||||
settings.downloadOption.1=在同一視窗中開啟
|
||||
settings.downloadOption.2=在新視窗中開啟
|
||||
settings.downloadOption.3=下載檔案
|
||||
@ -806,7 +806,7 @@ home.extractPage.desc=從 PDF 中提取選定的頁面
|
||||
extractPage.tags=提取
|
||||
|
||||
|
||||
home.PdfToSinglePage.title=PDF 轉單一大頁面
|
||||
home.PdfToSinglePage.title=PDF 轉單一大型頁面
|
||||
home.PdfToSinglePage.desc=將所有 PDF 頁面合併為一個大的單一頁面
|
||||
PdfToSinglePage.tags=單一頁面
|
||||
|
||||
@ -893,7 +893,7 @@ login.rememberme=記住我
|
||||
login.invalid=使用者名稱或密碼無效。
|
||||
login.locked=您的帳號已被鎖定。
|
||||
login.signinTitle=請登入
|
||||
login.ssoSignIn=透過 SSO 單一登入
|
||||
login.ssoSignIn=透過 SSO 登入
|
||||
login.oAuth2AutoCreateDisabled=OAuth 2.0 自動建立使用者功能已停用
|
||||
login.oAuth2AdminBlockedUser=目前不允許未註冊的使用者註冊或登入。請聯絡系統管理員。
|
||||
login.oauth2RequestNotFound=找不到驗證請求
|
||||
@ -1109,14 +1109,14 @@ crop.submit=送出
|
||||
#autoSplitPDF
|
||||
autoSplitPDF.title=自動分割 PDF
|
||||
autoSplitPDF.header=自動分割 PDF
|
||||
autoSplitPDF.description=列印,插入,掃描,上傳,讓 Stirling PDF 處理其餘的工作。不需要手動工作排序。
|
||||
autoSplitPDF.selectText.1=從下面列印一些分隔紙張(黑白即可)。
|
||||
autoSplitPDF.selectText.2=透過在它們之間插入分隔紙張一次掃描所有文件。
|
||||
autoSplitPDF.selectText.3=上傳單一大的掃描 PDF 檔案,讓 Stirling PDF 處理其餘的工作。
|
||||
autoSplitPDF.selectText.4=自動偵測並移除分隔頁面,確保最終文件整潔。
|
||||
autoSplitPDF.formPrompt=送出包含 Stirling-PDF 頁面分隔器的 PDF:
|
||||
autoSplitPDF.description=列印、插入、掃描、上傳,剩下的就交給 Stirling PDF 自動處理,無需手動排序。
|
||||
autoSplitPDF.selectText.1=從下方列印分隔頁(黑白列印即可)。
|
||||
autoSplitPDF.selectText.2=將分隔頁夾在文件之間,一次掃描全部文件。
|
||||
autoSplitPDF.selectText.3=上傳完整的單一掃描 PDF 檔,剩下的交給 Stirling PDF 自動處理。
|
||||
autoSplitPDF.selectText.4=系統會自動偵測並移除分隔頁,確保輸出的文件整齊乾淨。
|
||||
autoSplitPDF.formPrompt=送出包含 Stirling PDF 分隔頁的 PDF 檔案:
|
||||
autoSplitPDF.duplexMode=雙面模式(正反面掃描)
|
||||
autoSplitPDF.dividerDownload2=下載 '自動分割器分隔器(帶說明).pdf'
|
||||
autoSplitPDF.dividerDownload2=下載《自動分割用分隔頁(含使用說明).pdf》
|
||||
autoSplitPDF.submit=送出
|
||||
|
||||
|
||||
@ -1429,7 +1429,7 @@ pdfToImage.title=PDF 轉圖片
|
||||
pdfToImage.header=PDF 轉圖片
|
||||
pdfToImage.selectText=影像格式
|
||||
pdfToImage.singleOrMultiple=頁面到影像的結果類型
|
||||
pdfToImage.single=單一大影像結合所有頁面
|
||||
pdfToImage.single=單一大型影像結合所有頁面
|
||||
pdfToImage.multi=多個影像,每頁一個影像
|
||||
pdfToImage.colorType=顏色類型
|
||||
pdfToImage.color=顏色
|
||||
@ -1893,12 +1893,12 @@ editTableOfContents.replaceExisting=取代現有書籤 (取消勾選以附加到
|
||||
editTableOfContents.editorTitle=書籤編輯器
|
||||
editTableOfContents.editorDesc=在下方新增和排列書籤。點選 + 新增子書籤。
|
||||
editTableOfContents.addBookmark=新增書籤
|
||||
editTableOfContents.importBookmarksDefault=Import
|
||||
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||
editTableOfContents.exportBookmarksDefault=Export
|
||||
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||
editTableOfContents.importBookmarksDefault=匯入
|
||||
editTableOfContents.importBookmarksFromJsonFile=上傳 JSON 檔案
|
||||
editTableOfContents.importBookmarksFromClipboard=從剪貼簿貼上
|
||||
editTableOfContents.exportBookmarksDefault=匯出
|
||||
editTableOfContents.exportBookmarksAsJson=下載為 JSON
|
||||
editTableOfContents.exportBookmarksAsText=複製為文字
|
||||
editTableOfContents.desc.1=此工具可讓您在 PDF 文件中新增或編輯目錄 (書籤)。
|
||||
editTableOfContents.desc.2=您可以透過將子書籤新增至父書籤來建立階層式結構。
|
||||
editTableOfContents.desc.3=每個書籤都需要標題和目標頁碼。
|
||||
|
@ -504,7 +504,7 @@
|
||||
{
|
||||
"moduleName": "com.zaxxer:HikariCP",
|
||||
"moduleUrl": "https://github.com/brettwooldridge/HikariCP",
|
||||
"moduleVersion": "6.3.1",
|
||||
"moduleVersion": "6.3.2",
|
||||
"moduleLicense": "The Apache Software License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
@ -559,56 +559,56 @@
|
||||
{
|
||||
"moduleName": "io.jsonwebtoken:jjwt-api",
|
||||
"moduleUrl": "https://github.com/jwtk/jjwt",
|
||||
"moduleVersion": "0.12.6",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleVersion": "0.12.7",
|
||||
"moduleLicense": "Apache-2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "io.jsonwebtoken:jjwt-impl",
|
||||
"moduleUrl": "https://github.com/jwtk/jjwt",
|
||||
"moduleVersion": "0.12.6",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleVersion": "0.12.7",
|
||||
"moduleLicense": "Apache-2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "io.jsonwebtoken:jjwt-jackson",
|
||||
"moduleUrl": "https://github.com/jwtk/jjwt",
|
||||
"moduleVersion": "0.12.6",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleVersion": "0.12.7",
|
||||
"moduleLicense": "Apache-2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "io.micrometer:micrometer-commons",
|
||||
"moduleUrl": "https://github.com/micrometer-metrics/micrometer",
|
||||
"moduleVersion": "1.15.2",
|
||||
"moduleVersion": "1.15.3",
|
||||
"moduleLicense": "The Apache Software License, Version 2.0",
|
||||
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
{
|
||||
"moduleName": "io.micrometer:micrometer-core",
|
||||
"moduleUrl": "https://github.com/micrometer-metrics/micrometer",
|
||||
"moduleVersion": "1.15.2",
|
||||
"moduleVersion": "1.15.3",
|
||||
"moduleLicense": "The Apache Software License, Version 2.0",
|
||||
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
{
|
||||
"moduleName": "io.micrometer:micrometer-jakarta9",
|
||||
"moduleUrl": "https://github.com/micrometer-metrics/micrometer",
|
||||
"moduleVersion": "1.15.2",
|
||||
"moduleVersion": "1.15.3",
|
||||
"moduleLicense": "The Apache Software License, Version 2.0",
|
||||
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
{
|
||||
"moduleName": "io.micrometer:micrometer-observation",
|
||||
"moduleUrl": "https://github.com/micrometer-metrics/micrometer",
|
||||
"moduleVersion": "1.15.2",
|
||||
"moduleVersion": "1.15.3",
|
||||
"moduleLicense": "The Apache Software License, Version 2.0",
|
||||
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
{
|
||||
"moduleName": "io.micrometer:micrometer-registry-prometheus",
|
||||
"moduleUrl": "https://github.com/micrometer-metrics/micrometer",
|
||||
"moduleVersion": "1.15.2",
|
||||
"moduleVersion": "1.15.3",
|
||||
"moduleLicense": "The Apache Software License, Version 2.0",
|
||||
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
@ -657,21 +657,21 @@
|
||||
{
|
||||
"moduleName": "io.swagger.core.v3:swagger-annotations-jakarta",
|
||||
"moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-annotations",
|
||||
"moduleVersion": "2.2.35",
|
||||
"moduleVersion": "2.2.36",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "io.swagger.core.v3:swagger-core-jakarta",
|
||||
"moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-core",
|
||||
"moduleVersion": "2.2.35",
|
||||
"moduleVersion": "2.2.36",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "io.swagger.core.v3:swagger-models-jakarta",
|
||||
"moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-models",
|
||||
"moduleVersion": "2.2.35",
|
||||
"moduleVersion": "2.2.36",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
@ -803,7 +803,7 @@
|
||||
},
|
||||
{
|
||||
"moduleName": "net.bytebuddy:byte-buddy",
|
||||
"moduleVersion": "1.17.6",
|
||||
"moduleVersion": "1.17.7",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
@ -946,7 +946,7 @@
|
||||
{
|
||||
"moduleName": "org.apache.tomcat.embed:tomcat-embed-el",
|
||||
"moduleUrl": "https://tomcat.apache.org/",
|
||||
"moduleVersion": "10.1.43",
|
||||
"moduleVersion": "10.1.44",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
@ -1041,196 +1041,196 @@
|
||||
{
|
||||
"moduleName": "org.eclipse.angus:angus-mail",
|
||||
"moduleUrl": "https://www.eclipse.org",
|
||||
"moduleVersion": "2.0.3",
|
||||
"moduleVersion": "2.0.4",
|
||||
"moduleLicense": "GPL2 w/ CPE",
|
||||
"moduleLicenseUrl": "https://www.gnu.org/software/classpath/license.html"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.angus:jakarta.mail",
|
||||
"moduleUrl": "https://www.eclipse.org",
|
||||
"moduleVersion": "2.0.3",
|
||||
"moduleVersion": "2.0.4",
|
||||
"moduleLicense": "GPL2 w/ CPE",
|
||||
"moduleLicenseUrl": "https://www.gnu.org/software/classpath/license.html"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-client",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-common",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-server",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-servlet",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty.ee10:jetty-ee10-annotations",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty.ee10:jetty-ee10-plus",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty.ee10:jetty-ee10-servlet",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty.ee10:jetty-ee10-servlets",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty.ee10:jetty-ee10-webapp",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty.websocket:jetty-websocket-core-client",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty.websocket:jetty-websocket-core-common",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty.websocket:jetty-websocket-core-server",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty.websocket:jetty-websocket-jetty-api",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty.websocket:jetty-websocket-jetty-common",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty:jetty-alpn-client",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty:jetty-client",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty:jetty-ee",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty:jetty-http",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty:jetty-io",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty:jetty-plus",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty:jetty-security",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty:jetty-server",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty:jetty-session",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty:jetty-util",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.eclipse.jetty:jetty-xml",
|
||||
"moduleUrl": "https://jetty.org/",
|
||||
"moduleVersion": "12.0.23",
|
||||
"moduleVersion": "12.0.25",
|
||||
"moduleLicense": "Eclipse Public License - Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/"
|
||||
},
|
||||
@ -1272,15 +1272,16 @@
|
||||
{
|
||||
"moduleName": "org.hibernate.orm:hibernate-core",
|
||||
"moduleUrl": "https://www.hibernate.org/orm/6.6",
|
||||
"moduleVersion": "6.6.22.Final",
|
||||
"moduleVersion": "6.6.26.Final",
|
||||
"moduleLicense": "GNU Library General Public License v2.1 or later",
|
||||
"moduleLicenseUrl": "https://www.opensource.org/licenses/LGPL-2.1"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.hibernate.validator:hibernate-validator",
|
||||
"moduleVersion": "8.0.2.Final",
|
||||
"moduleUrl": "https://hibernate.org/validator",
|
||||
"moduleVersion": "8.0.3.Final",
|
||||
"moduleLicense": "Apache License 2.0",
|
||||
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.jboss.logging:jboss-logging",
|
||||
@ -1470,320 +1471,334 @@
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springdoc:springdoc-openapi-starter-common",
|
||||
"moduleVersion": "2.8.9",
|
||||
"moduleVersion": "2.8.11",
|
||||
"moduleLicense": "The Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springdoc:springdoc-openapi-starter-webmvc-api",
|
||||
"moduleVersion": "2.8.9",
|
||||
"moduleVersion": "2.8.11",
|
||||
"moduleLicense": "The Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springdoc:springdoc-openapi-starter-webmvc-ui",
|
||||
"moduleVersion": "2.8.9",
|
||||
"moduleVersion": "2.8.11",
|
||||
"moduleLicense": "The Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.4",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-actuator",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.4",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-actuator-autoconfigure",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.4",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-autoconfigure",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.4",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-starter",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.4",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-starter-actuator",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.4",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-starter-aop",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.4",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-starter-cache",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.4",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-starter-cache",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-starter-cache",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-starter-data-jpa",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.4",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-starter-jdbc",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.4",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-starter-jetty",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.4",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-starter-json",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.4",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-starter-logging",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.4",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-starter-mail",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.4",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-starter-oauth2-client",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.4",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-starter-security",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.4",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-starter-thymeleaf",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.4",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-starter-validation",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.4",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.boot:spring-boot-starter-web",
|
||||
"moduleUrl": "https://spring.io/projects/spring-boot",
|
||||
"moduleVersion": "3.5.4",
|
||||
"moduleVersion": "3.5.5",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.data:spring-data-commons",
|
||||
"moduleUrl": "https://spring.io/projects/spring-data",
|
||||
"moduleVersion": "3.5.2",
|
||||
"moduleVersion": "3.5.3",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.data:spring-data-jpa",
|
||||
"moduleUrl": "https://projects.spring.io/spring-data-jpa",
|
||||
"moduleVersion": "3.5.2",
|
||||
"moduleVersion": "3.5.3",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.security:spring-security-config",
|
||||
"moduleUrl": "https://spring.io/projects/spring-security",
|
||||
"moduleVersion": "6.5.2",
|
||||
"moduleVersion": "6.5.3",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.security:spring-security-core",
|
||||
"moduleUrl": "https://spring.io/projects/spring-security",
|
||||
"moduleVersion": "6.5.2",
|
||||
"moduleVersion": "6.5.3",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.security:spring-security-crypto",
|
||||
"moduleUrl": "https://spring.io/projects/spring-security",
|
||||
"moduleVersion": "6.5.2",
|
||||
"moduleVersion": "6.5.3",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.security:spring-security-oauth2-client",
|
||||
"moduleUrl": "https://spring.io/projects/spring-security",
|
||||
"moduleVersion": "6.5.2",
|
||||
"moduleVersion": "6.5.3",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.security:spring-security-oauth2-core",
|
||||
"moduleUrl": "https://spring.io/projects/spring-security",
|
||||
"moduleVersion": "6.5.2",
|
||||
"moduleVersion": "6.5.3",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.security:spring-security-oauth2-jose",
|
||||
"moduleUrl": "https://spring.io/projects/spring-security",
|
||||
"moduleVersion": "6.5.2",
|
||||
"moduleVersion": "6.5.3",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.security:spring-security-saml2-service-provider",
|
||||
"moduleUrl": "https://spring.io/projects/spring-security",
|
||||
"moduleVersion": "6.5.2",
|
||||
"moduleVersion": "6.5.3",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.security:spring-security-web",
|
||||
"moduleUrl": "https://spring.io/projects/spring-security",
|
||||
"moduleVersion": "6.5.2",
|
||||
"moduleVersion": "6.5.3",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework.session:spring-session-core",
|
||||
"moduleUrl": "https://spring.io/projects/spring-session",
|
||||
"moduleVersion": "3.5.1",
|
||||
"moduleVersion": "3.5.2",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework:spring-aop",
|
||||
"moduleUrl": "https://github.com/spring-projects/spring-framework",
|
||||
"moduleVersion": "6.2.9",
|
||||
"moduleVersion": "6.2.10",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework:spring-aspects",
|
||||
"moduleUrl": "https://github.com/spring-projects/spring-framework",
|
||||
"moduleVersion": "6.2.9",
|
||||
"moduleVersion": "6.2.10",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework:spring-beans",
|
||||
"moduleUrl": "https://github.com/spring-projects/spring-framework",
|
||||
"moduleVersion": "6.2.9",
|
||||
"moduleVersion": "6.2.10",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework:spring-context",
|
||||
"moduleUrl": "https://github.com/spring-projects/spring-framework",
|
||||
"moduleVersion": "6.2.9",
|
||||
"moduleVersion": "6.2.10",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework:spring-context-support",
|
||||
"moduleUrl": "https://github.com/spring-projects/spring-framework",
|
||||
"moduleVersion": "6.2.9",
|
||||
"moduleVersion": "6.2.10",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework:spring-core",
|
||||
"moduleUrl": "https://github.com/spring-projects/spring-framework",
|
||||
"moduleVersion": "6.2.9",
|
||||
"moduleVersion": "6.2.10",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework:spring-expression",
|
||||
"moduleUrl": "https://github.com/spring-projects/spring-framework",
|
||||
"moduleVersion": "6.2.9",
|
||||
"moduleVersion": "6.2.10",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework:spring-jcl",
|
||||
"moduleUrl": "https://github.com/spring-projects/spring-framework",
|
||||
"moduleVersion": "6.2.9",
|
||||
"moduleVersion": "6.2.10",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework:spring-jdbc",
|
||||
"moduleUrl": "https://github.com/spring-projects/spring-framework",
|
||||
"moduleVersion": "6.2.9",
|
||||
"moduleVersion": "6.2.10",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework:spring-orm",
|
||||
"moduleUrl": "https://github.com/spring-projects/spring-framework",
|
||||
"moduleVersion": "6.2.9",
|
||||
"moduleVersion": "6.2.10",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework:spring-tx",
|
||||
"moduleUrl": "https://github.com/spring-projects/spring-framework",
|
||||
"moduleVersion": "6.2.9",
|
||||
"moduleVersion": "6.2.10",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework:spring-web",
|
||||
"moduleUrl": "https://github.com/spring-projects/spring-framework",
|
||||
"moduleVersion": "6.2.9",
|
||||
"moduleVersion": "6.2.10",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.springframework:spring-webmvc",
|
||||
"moduleUrl": "https://github.com/spring-projects/spring-framework",
|
||||
"moduleVersion": "6.2.9",
|
||||
"moduleVersion": "6.2.10",
|
||||
"moduleLicense": "Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
@ -1821,7 +1836,7 @@
|
||||
{
|
||||
"moduleName": "org.webjars:swagger-ui",
|
||||
"moduleUrl": "https://www.webjars.org",
|
||||
"moduleVersion": "5.21.0",
|
||||
"moduleVersion": "5.27.1",
|
||||
"moduleLicense": "Apache-2.0"
|
||||
},
|
||||
{
|
||||
@ -1860,4 +1875,4 @@
|
||||
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -482,6 +482,11 @@
|
||||
}
|
||||
progressBar.css('width', '100%');
|
||||
progressBar.attr('aria-valuenow', Array.from(files).length);
|
||||
setTimeout(() => {
|
||||
progressBar.closest('.progressBarContainer').hide();
|
||||
progressBar.css('width', '0%');
|
||||
progressBar.attr('aria-valuenow', 0);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function updateProgressBar(progressBar, files) {
|
||||
|
@ -20,6 +20,7 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
@ -202,7 +203,9 @@ class EditTableOfContentsControllerTest {
|
||||
bookmarks.add(bookmark);
|
||||
|
||||
when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDocument);
|
||||
when(objectMapper.readValue(eq(request.getBookmarkData()), any(TypeReference.class)))
|
||||
when(objectMapper.readValue(
|
||||
eq(request.getBookmarkData()),
|
||||
ArgumentMatchers.<TypeReference<List<BookmarkItem>>>any()))
|
||||
.thenReturn(bookmarks);
|
||||
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
|
||||
when(mockDocument.getNumberOfPages()).thenReturn(5);
|
||||
@ -242,7 +245,8 @@ class EditTableOfContentsControllerTest {
|
||||
request.setFileInput(mockFile);
|
||||
|
||||
String bookmarkJson =
|
||||
"[{\"title\":\"Chapter 1\",\"pageNumber\":1,\"children\":[{\"title\":\"Section 1.1\",\"pageNumber\":2,\"children\":[]}]}]";
|
||||
"[{\"title\":\"Chapter 1\",\"pageNumber\":1,\"children\":[{\"title\":\"Section"
|
||||
+ " 1.1\",\"pageNumber\":2,\"children\":[]}]}]";
|
||||
request.setBookmarkData(bookmarkJson);
|
||||
|
||||
List<BookmarkItem> bookmarks = new ArrayList<>();
|
||||
@ -261,7 +265,9 @@ class EditTableOfContentsControllerTest {
|
||||
bookmarks.add(parentBookmark);
|
||||
|
||||
when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDocument);
|
||||
when(objectMapper.readValue(eq(bookmarkJson), any(TypeReference.class)))
|
||||
when(objectMapper.readValue(
|
||||
eq(bookmarkJson),
|
||||
ArgumentMatchers.<TypeReference<List<BookmarkItem>>>any()))
|
||||
.thenReturn(bookmarks);
|
||||
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
|
||||
when(mockDocument.getNumberOfPages()).thenReturn(5);
|
||||
@ -292,7 +298,8 @@ class EditTableOfContentsControllerTest {
|
||||
EditTableOfContentsRequest request = new EditTableOfContentsRequest();
|
||||
request.setFileInput(mockFile);
|
||||
request.setBookmarkData(
|
||||
"[{\"title\":\"Chapter 1\",\"pageNumber\":-5,\"children\":[]},{\"title\":\"Chapter 2\",\"pageNumber\":100,\"children\":[]}]");
|
||||
"[{\"title\":\"Chapter 1\",\"pageNumber\":-5,\"children\":[]},{\"title\":\"Chapter"
|
||||
+ " 2\",\"pageNumber\":100,\"children\":[]}]");
|
||||
|
||||
List<BookmarkItem> bookmarks = new ArrayList<>();
|
||||
|
||||
@ -310,7 +317,9 @@ class EditTableOfContentsControllerTest {
|
||||
bookmarks.add(bookmark2);
|
||||
|
||||
when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDocument);
|
||||
when(objectMapper.readValue(eq(request.getBookmarkData()), any(TypeReference.class)))
|
||||
when(objectMapper.readValue(
|
||||
eq(request.getBookmarkData()),
|
||||
ArgumentMatchers.<TypeReference<List<BookmarkItem>>>any()))
|
||||
.thenReturn(bookmarks);
|
||||
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
|
||||
when(mockDocument.getNumberOfPages()).thenReturn(5);
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,668 @@
|
||||
package stirling.software.SPDF.pdf;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
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.ValueSource;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import stirling.software.SPDF.model.PDFText;
|
||||
|
||||
@DisplayName("PDF Text Finder tests")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TextFinderTest {
|
||||
|
||||
private PDDocument document;
|
||||
private PDPage page;
|
||||
|
||||
// Helpers
|
||||
private void testTextFinding(
|
||||
String pageContent,
|
||||
String searchTerm,
|
||||
boolean useRegex,
|
||||
boolean wholeWord,
|
||||
String[] expectedTexts,
|
||||
int expectedCount)
|
||||
throws IOException {
|
||||
addTextToPage(pageContent);
|
||||
TextFinder textFinder = new TextFinder(searchTerm, useRegex, wholeWord);
|
||||
|
||||
textFinder.getText(document);
|
||||
List<PDFText> foundTexts = textFinder.getFoundTexts();
|
||||
|
||||
assertEquals(
|
||||
expectedCount,
|
||||
foundTexts.size(),
|
||||
String.format(
|
||||
"Expected %d matches for search term '%s'", expectedCount, searchTerm));
|
||||
|
||||
if (expectedTexts != null) {
|
||||
for (String expectedText : expectedTexts) {
|
||||
assertTrue(
|
||||
foundTexts.stream().anyMatch(text -> text.getText().equals(expectedText)),
|
||||
String.format("Expected to find text: '%s'", expectedText));
|
||||
}
|
||||
}
|
||||
|
||||
// Verify basic properties of found texts
|
||||
foundTexts.forEach(
|
||||
text -> {
|
||||
assertNotNull(text.getText());
|
||||
assertTrue(text.getX1() >= 0);
|
||||
assertTrue(text.getY1() >= 0);
|
||||
assertTrue(text.getX2() >= text.getX1());
|
||||
assertTrue(text.getY2() >= text.getY1());
|
||||
assertEquals(0, text.getPageIndex()); // Single page test
|
||||
});
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
document = new PDDocument();
|
||||
page = new PDPage(PDRectangle.A4);
|
||||
document.addPage(page);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws IOException {
|
||||
if (document != null) {
|
||||
document.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Basic Text Search")
|
||||
class BasicSearchTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should find simple text correctly")
|
||||
void findSimpleText() throws IOException {
|
||||
testTextFinding(
|
||||
"This is a confidential document with secret information.",
|
||||
"confidential",
|
||||
false,
|
||||
false,
|
||||
new String[] {"confidential"},
|
||||
1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should perform case-insensitive search")
|
||||
void performCaseInsensitiveSearch() throws IOException {
|
||||
testTextFinding(
|
||||
"This document contains CONFIDENTIAL information.",
|
||||
"confidential",
|
||||
false,
|
||||
false,
|
||||
new String[] {"CONFIDENTIAL"},
|
||||
1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should find multiple occurrences of same term")
|
||||
void findMultipleOccurrences() throws IOException {
|
||||
testTextFinding(
|
||||
"The secret code is secret123. Keep this secret safe!",
|
||||
"secret",
|
||||
false,
|
||||
false,
|
||||
new String[] {"secret", "secret", "secret"},
|
||||
3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle empty search term gracefully")
|
||||
void handleEmptySearchTerm() throws IOException {
|
||||
testTextFinding("This is a test document.", "", false, false, null, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle null search term gracefully")
|
||||
void handleNullSearchTerm() throws IOException {
|
||||
testTextFinding("This is a test document.", null, false, false, null, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return no results when no match found")
|
||||
void returnNoResultsWhenNoMatch() throws IOException {
|
||||
testTextFinding("This is a test document.", "nonexistent", false, false, null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Whole Word Search")
|
||||
class WholeWordSearchTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should find only whole words when enabled")
|
||||
void findOnlyWholeWords() throws IOException {
|
||||
testTextFinding(
|
||||
"This is a test testing document with tested results.",
|
||||
"test",
|
||||
false,
|
||||
true,
|
||||
new String[] {"test"},
|
||||
1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should find partial matches when whole word search disabled")
|
||||
void findPartialMatches() throws IOException {
|
||||
testTextFinding(
|
||||
"This is a test testing document with tested results.",
|
||||
"test",
|
||||
false,
|
||||
false,
|
||||
new String[] {"test", "test", "test"},
|
||||
3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle punctuation boundaries correctly")
|
||||
void handlePunctuationBoundaries() throws IOException {
|
||||
testTextFinding(
|
||||
"Hello, world! Testing: test-case (test).",
|
||||
"test",
|
||||
false,
|
||||
true,
|
||||
new String[] {"test"},
|
||||
2); // Both standalone "test" and "test" in "test-case"
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle word boundaries with special characters")
|
||||
void handleSpecialCharacterBoundaries() throws IOException {
|
||||
testTextFinding(
|
||||
"Email: test@example.com and test.txt file",
|
||||
"test",
|
||||
false,
|
||||
true,
|
||||
new String[] {"test"},
|
||||
2); // Both in email and filename should match
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Regular Expression Search")
|
||||
class RegexSearchTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should find text matching regex pattern")
|
||||
void findTextMatchingRegex() throws IOException {
|
||||
testTextFinding(
|
||||
"Contact John at 123-45-6789 or Jane at 987-65-4321 for details.",
|
||||
"\\d{3}-\\d{2}-\\d{4}",
|
||||
true,
|
||||
false,
|
||||
new String[] {"123-45-6789", "987-65-4321"},
|
||||
2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should find email addresses with regex")
|
||||
void findEmailAddresses() throws IOException {
|
||||
testTextFinding(
|
||||
"Email: test@example.com and admin@test.org",
|
||||
"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}",
|
||||
true,
|
||||
false,
|
||||
new String[] {"test@example.com", "admin@test.org"},
|
||||
2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should combine regex with whole word search")
|
||||
void combineRegexWithWholeWord() throws IOException {
|
||||
testTextFinding(
|
||||
"Email: test@example.com and admin@test.org",
|
||||
"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}",
|
||||
true,
|
||||
true,
|
||||
new String[] {"test@example.com", "admin@test.org"},
|
||||
2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should find currency patterns")
|
||||
void findCurrencyPatterns() throws IOException {
|
||||
testTextFinding(
|
||||
"Price: $100.50 and €75.25",
|
||||
"\\$\\d+\\.\\d{2}",
|
||||
true,
|
||||
false,
|
||||
new String[] {"$100.50"},
|
||||
1);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(
|
||||
strings = {
|
||||
"\\d{4}-\\d{2}-\\d{2}", // Date pattern
|
||||
"\\b[A-Z]{2,}\\b", // Uppercase words
|
||||
"\\w+@\\w+\\.\\w+", // Simple email pattern
|
||||
"\\$\\d+", // Simple currency
|
||||
"\\b\\d{3,4}\\b" // 3-4 digit numbers
|
||||
})
|
||||
@DisplayName("Should handle various regex patterns")
|
||||
void handleVariousRegexPatterns(String regexPattern) throws IOException {
|
||||
String testContent =
|
||||
"Date: 2023-12-25, Email: test@domain.com, Price: $250, Code: ABC123, Number: 1234";
|
||||
addTextToPage(testContent);
|
||||
|
||||
TextFinder textFinder = new TextFinder(regexPattern, true, false);
|
||||
textFinder.getText(document);
|
||||
List<PDFText> foundTexts = textFinder.getFoundTexts();
|
||||
|
||||
// Each pattern should find at least one match in our test content
|
||||
assertFalse(
|
||||
foundTexts.isEmpty(),
|
||||
String.format("Pattern '%s' should find at least one match", regexPattern));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle invalid regex gracefully")
|
||||
void handleInvalidRegex() throws IOException {
|
||||
addTextToPage("This is test content.");
|
||||
|
||||
try {
|
||||
TextFinder textFinder = new TextFinder("[invalid regex(", true, false);
|
||||
textFinder.getText(document);
|
||||
List<PDFText> foundTexts = textFinder.getFoundTexts();
|
||||
assertNotNull(foundTexts);
|
||||
} catch (java.util.regex.PatternSyntaxException e) {
|
||||
assertNotNull(e.getMessage());
|
||||
assertTrue(
|
||||
e.getMessage().contains("Unclosed character class")
|
||||
|| e.getMessage().contains("syntax"),
|
||||
"Exception should indicate regex syntax error");
|
||||
} catch (RuntimeException | IOException e) {
|
||||
assertNotNull(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Special Characters and Encoding")
|
||||
class SpecialCharacterTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle international characters")
|
||||
void handleInternationalCharacters() throws IOException {
|
||||
testTextFinding(
|
||||
"Hello café naïve résumé", "café", false, false, new String[] {"café"}, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should find text with accented characters")
|
||||
void findAccentedCharacters() throws IOException {
|
||||
testTextFinding(
|
||||
"Café, naïve, résumé, piñata",
|
||||
"café",
|
||||
false,
|
||||
false,
|
||||
new String[] {"Café"},
|
||||
1); // Case insensitive
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle special symbols")
|
||||
void handleSpecialSymbols() throws IOException {
|
||||
testTextFinding("Symbols: © ® ™ ± × ÷ § ¶", "©", false, false, new String[] {"©"}, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should find currency symbols")
|
||||
void findCurrencySymbols() throws IOException {
|
||||
testTextFinding(
|
||||
"Prices: $100 €75 £50 ¥1000",
|
||||
"[€£¥]",
|
||||
true,
|
||||
false,
|
||||
new String[] {"€", "£", "¥"},
|
||||
3);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Multi-page Document Tests")
|
||||
class MultiPageTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should find text across multiple pages")
|
||||
void findTextAcrossPages() throws IOException {
|
||||
PDPage secondPage = new PDPage(PDRectangle.A4);
|
||||
document.addPage(secondPage);
|
||||
|
||||
addTextToPage("First page with confidential data.");
|
||||
|
||||
addTextToPage(secondPage, "Second page with secret information.");
|
||||
|
||||
TextFinder textFinder = new TextFinder("confidential|secret", true, false);
|
||||
textFinder.getText(document);
|
||||
List<PDFText> foundTexts = textFinder.getFoundTexts();
|
||||
|
||||
assertEquals(2, foundTexts.size());
|
||||
|
||||
long page0Count = foundTexts.stream().filter(text -> text.getPageIndex() == 0).count();
|
||||
long page1Count = foundTexts.stream().filter(text -> text.getPageIndex() == 1).count();
|
||||
|
||||
assertEquals(1, page0Count);
|
||||
assertEquals(1, page1Count);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle empty pages gracefully")
|
||||
void handleEmptyPages() throws IOException {
|
||||
PDPage emptyPage = new PDPage(PDRectangle.A4);
|
||||
document.addPage(emptyPage);
|
||||
|
||||
addTextToPage("Content on first page only.");
|
||||
|
||||
TextFinder textFinder = new TextFinder("content", false, false);
|
||||
textFinder.getText(document);
|
||||
List<PDFText> foundTexts = textFinder.getFoundTexts();
|
||||
|
||||
assertEquals(1, foundTexts.size());
|
||||
assertEquals(0, foundTexts.get(0).getPageIndex());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Performance and Boundary Tests")
|
||||
class PerformanceTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle very long search terms")
|
||||
void handleLongSearchTerms() throws IOException {
|
||||
String longTerm = "a".repeat(1000);
|
||||
String content = "Short text with " + longTerm + " embedded.";
|
||||
|
||||
testTextFinding(content, longTerm, false, false, new String[] {longTerm}, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle documents with many pages efficiently")
|
||||
void handleManyPages() throws IOException {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (i > 0) { // The first page already exists
|
||||
document.addPage(new PDPage(PDRectangle.A4));
|
||||
}
|
||||
addTextToPage(document.getPage(i), "Page " + i + " contains searchable content.");
|
||||
}
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
TextFinder textFinder = new TextFinder("searchable", false, false);
|
||||
textFinder.getText(document);
|
||||
List<PDFText> foundTexts = textFinder.getFoundTexts();
|
||||
long endTime = System.currentTimeMillis();
|
||||
|
||||
assertEquals(10, foundTexts.size());
|
||||
assertTrue(
|
||||
endTime - startTime < 3000,
|
||||
"Multi-page search should complete within 3 seconds");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Error Handling and Edge Cases")
|
||||
class ErrorHandlingTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle null document gracefully")
|
||||
void handleNullDocument() throws IOException {
|
||||
TextFinder textFinder = new TextFinder("test", false, false);
|
||||
|
||||
try {
|
||||
textFinder.getText(null);
|
||||
List<PDFText> foundTexts = textFinder.getFoundTexts();
|
||||
assertNotNull(foundTexts);
|
||||
assertEquals(0, foundTexts.size());
|
||||
} catch (Exception e) {
|
||||
assertNotNull(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle document without pages")
|
||||
void handleDocumentWithoutPages() throws IOException {
|
||||
try (PDDocument emptyDocument = new PDDocument()) {
|
||||
TextFinder textFinder = new TextFinder("test", false, false);
|
||||
textFinder.getText(emptyDocument);
|
||||
List<PDFText> foundTexts = textFinder.getFoundTexts();
|
||||
assertEquals(0, foundTexts.size());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle pages without content")
|
||||
void handlePagesWithoutContent() throws IOException {
|
||||
TextFinder textFinder = new TextFinder("test", false, false);
|
||||
textFinder.getText(document);
|
||||
List<PDFText> foundTexts = textFinder.getFoundTexts();
|
||||
|
||||
assertEquals(0, foundTexts.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle extremely complex regex patterns")
|
||||
void handleComplexRegexPatterns() throws IOException {
|
||||
addTextToPage("Complex content with various patterns: abc123, def456, XYZ789");
|
||||
|
||||
String complexRegex = "(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])[a-zA-Z\\d]{6}";
|
||||
|
||||
assertDoesNotThrow(
|
||||
() -> {
|
||||
TextFinder textFinder = new TextFinder(complexRegex, true, false);
|
||||
textFinder.getText(document);
|
||||
List<PDFText> foundTexts = textFinder.getFoundTexts();
|
||||
assertNotNull(foundTexts);
|
||||
});
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"", " ", "\t", "\n", "\r\n", " \t\n "})
|
||||
@DisplayName("Should handle whitespace-only search terms")
|
||||
void handleWhitespaceSearchTerms(String whitespacePattern) throws IOException {
|
||||
addTextToPage("This is normal text content.");
|
||||
|
||||
TextFinder textFinder = new TextFinder(whitespacePattern, false, false);
|
||||
textFinder.getText(document);
|
||||
List<PDFText> foundTexts = textFinder.getFoundTexts();
|
||||
|
||||
assertEquals(0, foundTexts.size());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Text Coordinate Verification")
|
||||
class CoordinateTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should provide accurate text coordinates")
|
||||
void provideAccurateCoordinates() throws IOException {
|
||||
addTextToPage("Sample text for coordinate testing.");
|
||||
|
||||
TextFinder textFinder = new TextFinder("coordinate", false, false);
|
||||
textFinder.getText(document);
|
||||
List<PDFText> foundTexts = textFinder.getFoundTexts();
|
||||
|
||||
assertEquals(1, foundTexts.size());
|
||||
PDFText foundText = foundTexts.get(0);
|
||||
|
||||
assertTrue(foundText.getX1() >= 0, "X1 should be non-negative");
|
||||
assertTrue(foundText.getY1() >= 0, "Y1 should be non-negative");
|
||||
assertTrue(foundText.getX2() > foundText.getX1(), "X2 should be greater than X1");
|
||||
assertTrue(foundText.getY2() > foundText.getY1(), "Y2 should be greater than Y1");
|
||||
|
||||
double width = foundText.getX2() - foundText.getX1();
|
||||
double height = foundText.getY2() - foundText.getY1();
|
||||
|
||||
assertTrue(width > 0, "Text width should be positive");
|
||||
assertTrue(height > 0, "Text height should be positive");
|
||||
assertTrue(width < 1000, "Text width should be reasonable");
|
||||
assertTrue(height < 100, "Text height should be reasonable");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle overlapping text regions")
|
||||
void handleOverlappingTextRegions() throws IOException {
|
||||
addTextToPage("Overlapping test text content.");
|
||||
|
||||
TextFinder textFinder = new TextFinder("test", false, false);
|
||||
textFinder.getText(document);
|
||||
List<PDFText> foundTexts = textFinder.getFoundTexts();
|
||||
|
||||
assertFalse(foundTexts.isEmpty());
|
||||
foundTexts.forEach(
|
||||
text -> {
|
||||
assertNotNull(text.getText());
|
||||
assertTrue(text.getX1() >= 0 && text.getY1() >= 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Single Character and Digit Tests")
|
||||
class SingleCharacterAndDigitTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should find single digits in various contexts with whole word search")
|
||||
void findSingleDigitsWholeWord() throws IOException {
|
||||
String content = "Item 1 of 5 costs $2.50. Order number: 1234. Reference: A1B.";
|
||||
addTextToPage(content);
|
||||
|
||||
TextFinder textFinder = new TextFinder("1", false, true);
|
||||
textFinder.getText(document);
|
||||
List<PDFText> foundTexts = textFinder.getFoundTexts();
|
||||
|
||||
assertEquals(
|
||||
1,
|
||||
foundTexts.size(),
|
||||
"Should find exactly one standalone '1', not the ones embedded in other numbers/codes");
|
||||
assertEquals("1", foundTexts.get(0).getText());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should find single digits without whole word search")
|
||||
void findSingleDigitsNoWholeWord() throws IOException {
|
||||
String content = "Item 1 of 5 costs $2.50. Order number: 1234. Reference: A1B.";
|
||||
addTextToPage(content);
|
||||
|
||||
TextFinder textFinder = new TextFinder("1", false, false);
|
||||
textFinder.getText(document);
|
||||
List<PDFText> foundTexts = textFinder.getFoundTexts();
|
||||
|
||||
assertTrue(
|
||||
foundTexts.size() >= 3,
|
||||
"Should find multiple instances of '1' including standalone, in '1234', and in 'A1B'");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should find single characters in various contexts")
|
||||
void findSingleCharacters() throws IOException {
|
||||
String content =
|
||||
"Grade: A. Section B has item A-1. The letter A appears multiple times.";
|
||||
addTextToPage(content);
|
||||
|
||||
TextFinder textFinder = new TextFinder("A", false, true);
|
||||
textFinder.getText(document);
|
||||
List<PDFText> foundTexts = textFinder.getFoundTexts();
|
||||
|
||||
assertTrue(foundTexts.size() >= 2, "Should find multiple standalone 'A' characters");
|
||||
|
||||
for (PDFText found : foundTexts) {
|
||||
assertEquals("A", found.getText());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Digits as strict standalone tokens (exclude decimals and suffixes)")
|
||||
void findDigitsAtWordBoundaries() throws IOException {
|
||||
String content =
|
||||
"Numbers: 1, 2, 3. Code: 123. Version: 1.0. Item1 and Item2. Price: 2,50€";
|
||||
addTextToPage(content);
|
||||
|
||||
TextFinder textFinder1 = new TextFinder("1", false, true);
|
||||
textFinder1.getText(document);
|
||||
List<PDFText> foundTexts1 = textFinder1.getFoundTexts();
|
||||
|
||||
assertEquals(
|
||||
1,
|
||||
foundTexts1.size(),
|
||||
"Should find only the standalone '1'; do not count the '1' in '1.0' or in 'Item1'.");
|
||||
|
||||
TextFinder textFinder2 = new TextFinder("2", false, true);
|
||||
textFinder2.getText(document);
|
||||
List<PDFText> foundTexts2 = textFinder2.getFoundTexts();
|
||||
|
||||
assertEquals(
|
||||
1,
|
||||
foundTexts2.size(),
|
||||
"Should find only the standalone '2' in the number list");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle special characters and punctuation boundaries")
|
||||
void findDigitsWithPunctuationBoundaries() throws IOException {
|
||||
String content = "Items: (1), [2], {3}, item#4, price$5, and 6%.";
|
||||
addTextToPage(content);
|
||||
|
||||
TextFinder textFinder = new TextFinder("1", false, true);
|
||||
textFinder.getText(document);
|
||||
List<PDFText> foundTexts = textFinder.getFoundTexts();
|
||||
|
||||
assertEquals(1, foundTexts.size(), "Should find '1' surrounded by parentheses");
|
||||
assertEquals("1", foundTexts.get(0).getText());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle edge case with spacing and formatting")
|
||||
void findDigitsWithSpacingIssues() throws IOException {
|
||||
String content = "List: 1 , 2 , 3 and item 1 here.";
|
||||
addTextToPage(content);
|
||||
|
||||
TextFinder textFinder = new TextFinder("1", false, true);
|
||||
textFinder.getText(document);
|
||||
List<PDFText> foundTexts = textFinder.getFoundTexts();
|
||||
|
||||
assertEquals(
|
||||
2,
|
||||
foundTexts.size(),
|
||||
"Should find both '1' instances despite spacing variations");
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private void addTextToPage(String text) throws IOException {
|
||||
addTextToPage(page, text);
|
||||
}
|
||||
|
||||
private void addTextToPage(PDPage targetPage, String text) throws IOException {
|
||||
try (PDPageContentStream contentStream = new PDPageContentStream(document, targetPage)) {
|
||||
contentStream.beginText();
|
||||
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
||||
contentStream.newLineAtOffset(50, 750);
|
||||
contentStream.showText(text);
|
||||
contentStream.endText();
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ repositories {
|
||||
}
|
||||
|
||||
ext {
|
||||
jwtVersion = '0.12.6'
|
||||
jwtVersion = '0.12.7'
|
||||
}
|
||||
|
||||
bootRun {
|
||||
@ -51,7 +51,7 @@ dependencies {
|
||||
api 'org.springframework.boot:spring-boot-starter-mail'
|
||||
api 'org.springframework.boot:spring-boot-starter-cache'
|
||||
api 'com.github.ben-manes.caffeine:caffeine'
|
||||
api 'io.swagger.core.v3:swagger-core-jakarta:2.2.35'
|
||||
api 'io.swagger.core.v3:swagger-core-jakarta:2.2.36'
|
||||
implementation 'com.bucket4j:bucket4j_jdk17-core:8.14.0'
|
||||
|
||||
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
|
||||
|
@ -1,4 +1,4 @@
|
||||
package stirling.software.proprietary.controller;
|
||||
package stirling.software.proprietary.controller.api;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@ -6,8 +6,12 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@ -22,6 +26,9 @@ import stirling.software.common.service.TaskManager;
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@RequestMapping("/api/v1/admin")
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@Tag(name = "Admin Job Management", description = "Admin-only Job Management APIs")
|
||||
public class AdminJobController {
|
||||
|
||||
private final TaskManager taskManager;
|
||||
@ -32,7 +39,8 @@ public class AdminJobController {
|
||||
*
|
||||
* @return Job statistics
|
||||
*/
|
||||
@GetMapping("/api/v1/admin/job/stats")
|
||||
@GetMapping("/job/stats")
|
||||
@Operation(summary = "Get job statistics")
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
public ResponseEntity<JobStats> getJobStats() {
|
||||
JobStats stats = taskManager.getJobStats();
|
||||
@ -48,7 +56,8 @@ public class AdminJobController {
|
||||
*
|
||||
* @return Queue statistics
|
||||
*/
|
||||
@GetMapping("/api/v1/admin/job/queue/stats")
|
||||
@GetMapping("/job/queue/stats")
|
||||
@Operation(summary = "Get job queue statistics")
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
public ResponseEntity<?> getQueueStats() {
|
||||
Map<String, Object> queueStats = jobQueue.getQueueStats();
|
||||
@ -61,7 +70,8 @@ public class AdminJobController {
|
||||
*
|
||||
* @return A response indicating how many jobs were cleaned up
|
||||
*/
|
||||
@PostMapping("/api/v1/admin/job/cleanup")
|
||||
@PostMapping("/job/cleanup")
|
||||
@Operation(summary = "Cleanup old jobs")
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
public ResponseEntity<?> cleanupOldJobs() {
|
||||
int beforeCount = taskManager.getJobStats().getTotalJobs();
|
@ -1,4 +1,4 @@
|
||||
package stirling.software.proprietary.controller;
|
||||
package stirling.software.proprietary.controller.api;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
@ -6,13 +6,13 @@ import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@ -22,132 +22,101 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.audit.AuditEventType;
|
||||
import stirling.software.proprietary.audit.AuditLevel;
|
||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||
import stirling.software.proprietary.model.api.audit.AuditDataRequest;
|
||||
import stirling.software.proprietary.model.api.audit.AuditDataResponse;
|
||||
import stirling.software.proprietary.model.api.audit.AuditExportRequest;
|
||||
import stirling.software.proprietary.model.api.audit.AuditStatsResponse;
|
||||
import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
||||
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
||||
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||
|
||||
/** Controller for the audit dashboard. Admin-only access. */
|
||||
/** REST endpoints for the audit dashboard. */
|
||||
@Slf4j
|
||||
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
|
||||
@RequestMapping("/audit")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/audit")
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@RequiredArgsConstructor
|
||||
@EnterpriseEndpoint
|
||||
@Tag(name = "Audit", description = "Only Enterprise - Audit related operations")
|
||||
public class AuditDashboardController {
|
||||
|
||||
private final PersistentAuditEventRepository auditRepository;
|
||||
private final AuditConfigurationProperties auditConfig;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/** Display the audit dashboard. */
|
||||
@GetMapping
|
||||
public String showDashboard(Model model) {
|
||||
model.addAttribute("auditEnabled", auditConfig.isEnabled());
|
||||
model.addAttribute("auditLevel", auditConfig.getAuditLevel());
|
||||
model.addAttribute("auditLevelInt", auditConfig.getLevel());
|
||||
model.addAttribute("retentionDays", auditConfig.getRetentionDays());
|
||||
|
||||
// Add audit level enum values for display
|
||||
model.addAttribute("auditLevels", AuditLevel.values());
|
||||
|
||||
// Add audit event types for the dropdown
|
||||
model.addAttribute("auditEventTypes", AuditEventType.values());
|
||||
|
||||
return "audit/dashboard";
|
||||
}
|
||||
|
||||
/** Get audit events data for the dashboard tables. */
|
||||
@GetMapping("/data")
|
||||
@ResponseBody
|
||||
public Map<String, Object> getAuditData(
|
||||
@RequestParam(value = "page", defaultValue = "0") int page,
|
||||
@RequestParam(value = "size", defaultValue = "30") int size,
|
||||
@RequestParam(value = "type", required = false) String type,
|
||||
@RequestParam(value = "principal", required = false) String principal,
|
||||
@RequestParam(value = "startDate", required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||
LocalDate startDate,
|
||||
@RequestParam(value = "endDate", required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||
LocalDate endDate,
|
||||
HttpServletRequest request) {
|
||||
@Operation(summary = "Get audit events data")
|
||||
public AuditDataResponse getAuditData(@ParameterObject AuditDataRequest request) {
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size, Sort.by("timestamp").descending());
|
||||
Pageable pageable =
|
||||
PageRequest.of(
|
||||
request.getPage(), request.getSize(), Sort.by("timestamp").descending());
|
||||
Page<PersistentAuditEvent> events;
|
||||
|
||||
String mode;
|
||||
String type = request.getType();
|
||||
String principal = request.getPrincipal();
|
||||
LocalDate startDate = request.getStartDate();
|
||||
LocalDate endDate = request.getEndDate();
|
||||
|
||||
if (type != null && principal != null && startDate != null && endDate != null) {
|
||||
mode = "principal + type + startDate + endDate";
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
events =
|
||||
auditRepository.findByPrincipalAndTypeAndTimestampBetween(
|
||||
principal, type, start, end, pageable);
|
||||
} else if (type != null && principal != null) {
|
||||
mode = "principal + type";
|
||||
events = auditRepository.findByPrincipalAndType(principal, type, pageable);
|
||||
} else if (type != null && startDate != null && endDate != null) {
|
||||
mode = "type + startDate + endDate";
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
events = auditRepository.findByTypeAndTimestampBetween(type, start, end, pageable);
|
||||
} else if (principal != null && startDate != null && endDate != null) {
|
||||
mode = "principal + startDate + endDate";
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
events =
|
||||
auditRepository.findByPrincipalAndTimestampBetween(
|
||||
principal, start, end, pageable);
|
||||
} else if (startDate != null && endDate != null) {
|
||||
mode = "startDate + endDate";
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
events = auditRepository.findByTimestampBetween(start, end, pageable);
|
||||
} else if (type != null) {
|
||||
mode = "type";
|
||||
events = auditRepository.findByType(type, pageable);
|
||||
} else if (principal != null) {
|
||||
mode = "principal";
|
||||
events = auditRepository.findByPrincipal(principal, pageable);
|
||||
} else {
|
||||
mode = "all";
|
||||
events = auditRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
// Logging
|
||||
List<PersistentAuditEvent> content = events.getContent();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("content", content);
|
||||
response.put("totalPages", events.getTotalPages());
|
||||
response.put("totalElements", events.getTotalElements());
|
||||
response.put("currentPage", events.getNumber());
|
||||
|
||||
return response;
|
||||
return new AuditDataResponse(
|
||||
content, events.getTotalPages(), events.getTotalElements(), events.getNumber());
|
||||
}
|
||||
|
||||
/** Get statistics for charts. */
|
||||
/** Get statistics for charts (last X days). Existing behavior preserved. */
|
||||
@GetMapping("/stats")
|
||||
@ResponseBody
|
||||
public Map<String, Object> getAuditStats(
|
||||
@Operation(summary = "Get audit statistics for the last N days")
|
||||
public AuditStatsResponse getAuditStats(
|
||||
@Schema(description = "Number of days to look back for audit events", example = "7", required = true)
|
||||
@RequestParam(value = "days", defaultValue = "7") int days) {
|
||||
|
||||
// Get events from the last X days
|
||||
@ -180,18 +149,53 @@ public class AuditDashboardController {
|
||||
.format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||
Collectors.counting()));
|
||||
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
stats.put("eventsByType", eventsByType);
|
||||
stats.put("eventsByPrincipal", eventsByPrincipal);
|
||||
stats.put("eventsByDay", eventsByDay);
|
||||
stats.put("totalEvents", events.size());
|
||||
|
||||
return stats;
|
||||
return new AuditStatsResponse(eventsByType, eventsByPrincipal, eventsByDay, events.size());
|
||||
}
|
||||
|
||||
// /** Advanced statistics using repository aggregations, with explicit date range. */
|
||||
// @GetMapping("/stats/range")
|
||||
// @Operation(summary = "Get audit statistics for a date range (aggregated in DB)")
|
||||
// public Map<String, Object> getAuditStatsRange(@ParameterObject AuditDateExportRequest
|
||||
// request) {
|
||||
|
||||
// LocalDate startDate = request.getStartDate();
|
||||
// LocalDate endDate = request.getEndDate();
|
||||
// Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
// Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
|
||||
// Map<String, Long> byType = toStringLongMap(auditRepository.countByTypeBetween(start,
|
||||
// end));
|
||||
// Map<String, Long> byPrincipal =
|
||||
// toStringLongMap(auditRepository.countByPrincipalBetween(start, end));
|
||||
|
||||
// Map<String, Long> byDay = new HashMap<>();
|
||||
// for (Object[] row : auditRepository.histogramByDayBetween(start, end)) {
|
||||
// int y = ((Number) row[0]).intValue();
|
||||
// int m = ((Number) row[1]).intValue();
|
||||
// int d = ((Number) row[2]).intValue();
|
||||
// long count = ((Number) row[3]).longValue();
|
||||
// String key = String.format("%04d-%02d-%02d", y, m, d);
|
||||
// byDay.put(key, count);
|
||||
// }
|
||||
|
||||
// Map<String, Long> byHour = new HashMap<>();
|
||||
// for (Object[] row : auditRepository.histogramByHourBetween(start, end)) {
|
||||
// int hour = ((Number) row[0]).intValue();
|
||||
// long count = ((Number) row[1]).longValue();
|
||||
// byHour.put(String.format("%02d:00", hour), count);
|
||||
// }
|
||||
|
||||
// Map<String, Object> payload = new HashMap<>();
|
||||
// payload.put("byType", byType);
|
||||
// payload.put("byPrincipal", byPrincipal);
|
||||
// payload.put("byDay", byDay);
|
||||
// payload.put("byHour", byHour);
|
||||
// return payload;
|
||||
// }
|
||||
|
||||
/** Get all unique event types from the database for filtering. */
|
||||
@GetMapping("/types")
|
||||
@ResponseBody
|
||||
@Operation(summary = "Get all unique audit event types")
|
||||
public List<String> getAuditTypes() {
|
||||
// Get distinct event types from the database
|
||||
List<String> dbTypes = auditRepository.findDistinctEventTypes();
|
||||
@ -211,49 +215,11 @@ public class AuditDashboardController {
|
||||
}
|
||||
|
||||
/** Export audit data as CSV. */
|
||||
@GetMapping("/export")
|
||||
public ResponseEntity<byte[]> exportAuditData(
|
||||
@RequestParam(value = "type", required = false) String type,
|
||||
@RequestParam(value = "principal", required = false) String principal,
|
||||
@RequestParam(value = "startDate", required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||
LocalDate startDate,
|
||||
@RequestParam(value = "endDate", required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||
LocalDate endDate) {
|
||||
@GetMapping("/export/csv")
|
||||
@Operation(summary = "Export audit data as CSV")
|
||||
public ResponseEntity<byte[]> exportAuditData(@ParameterObject AuditExportRequest request) {
|
||||
|
||||
// Get data with same filtering as getAuditData
|
||||
List<PersistentAuditEvent> events;
|
||||
|
||||
if (type != null && principal != null && startDate != null && endDate != null) {
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
events =
|
||||
auditRepository.findAllByPrincipalAndTypeAndTimestampBetweenForExport(
|
||||
principal, type, start, end);
|
||||
} else if (type != null && principal != null) {
|
||||
events = auditRepository.findAllByPrincipalAndTypeForExport(principal, type);
|
||||
} else if (type != null && startDate != null && endDate != null) {
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
events = auditRepository.findAllByTypeAndTimestampBetweenForExport(type, start, end);
|
||||
} else if (principal != null && startDate != null && endDate != null) {
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
events =
|
||||
auditRepository.findAllByPrincipalAndTimestampBetweenForExport(
|
||||
principal, start, end);
|
||||
} else if (startDate != null && endDate != null) {
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
events = auditRepository.findAllByTimestampBetweenForExport(start, end);
|
||||
} else if (type != null) {
|
||||
events = auditRepository.findByTypeForExport(type);
|
||||
} else if (principal != null) {
|
||||
events = auditRepository.findAllByPrincipalForExport(principal);
|
||||
} else {
|
||||
events = auditRepository.findAll();
|
||||
}
|
||||
List<PersistentAuditEvent> events = getAuditEventsByCriteria(request);
|
||||
|
||||
// Convert to CSV
|
||||
StringBuilder csv = new StringBuilder();
|
||||
@ -281,15 +247,113 @@ public class AuditDashboardController {
|
||||
|
||||
/** Export audit data as JSON. */
|
||||
@GetMapping("/export/json")
|
||||
public ResponseEntity<byte[]> exportAuditDataJson(
|
||||
@RequestParam(value = "type", required = false) String type,
|
||||
@RequestParam(value = "principal", required = false) String principal,
|
||||
@RequestParam(value = "startDate", required = false)
|
||||
@Operation(summary = "Export audit data as JSON")
|
||||
public ResponseEntity<byte[]> exportAuditDataJson(@ParameterObject AuditExportRequest request) {
|
||||
|
||||
List<PersistentAuditEvent> events = getAuditEventsByCriteria(request);
|
||||
|
||||
// Convert to JSON
|
||||
try {
|
||||
byte[] jsonBytes = objectMapper.writeValueAsBytes(events);
|
||||
|
||||
// Set up HTTP headers for download
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setContentDispositionFormData("attachment", "audit_export.json");
|
||||
|
||||
return ResponseEntity.ok().headers(headers).body(jsonBytes);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Error serializing audit events to JSON", e);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
// /** Get all unique principals. */
|
||||
// @GetMapping("/principals")
|
||||
// @Operation(summary = "Get all distinct principals")
|
||||
// public List<String> getPrincipals() {
|
||||
// return auditRepository.findDistinctPrincipals();
|
||||
// }
|
||||
|
||||
// /** Get principals by event type. */
|
||||
// @GetMapping("/types/{type}/principals")
|
||||
// @Operation(summary = "Get distinct principals for a given type")
|
||||
// public List<String> getPrincipalsByType(@PathVariable("type") String type) {
|
||||
// return auditRepository.findDistinctPrincipalsByType(type);
|
||||
// }
|
||||
|
||||
// /** Latest helpers */
|
||||
// @GetMapping("/latest")
|
||||
// @Operation(summary = "Get the latest audit event, optionally filtered by type or principal")
|
||||
// public ResponseEntity<PersistentAuditEvent> getLatest(
|
||||
// @RequestParam(value = "type", required = false) String type,
|
||||
// @RequestParam(value = "principal", required = false) String principal) {
|
||||
// if (type != null) {
|
||||
// return auditRepository
|
||||
// .findTopByTypeOrderByTimestampDesc(type)
|
||||
// .map(ResponseEntity::ok)
|
||||
// .orElse(ResponseEntity.noContent().build());
|
||||
// } else if (principal != null) {
|
||||
// return auditRepository
|
||||
// .findTopByPrincipalOrderByTimestampDesc(principal)
|
||||
// .map(ResponseEntity::ok)
|
||||
// .orElse(ResponseEntity.noContent().build());
|
||||
// }
|
||||
// return auditRepository
|
||||
// .findTopByOrderByTimestampDesc()
|
||||
// .map(ResponseEntity::ok)
|
||||
// .orElse(ResponseEntity.noContent().build());
|
||||
// }
|
||||
|
||||
/** Cleanup endpoints data before a certain date */
|
||||
@DeleteMapping("/cleanup/before")
|
||||
@Operation(
|
||||
summary = "Cleanup audit events before a certain date",
|
||||
description = "Deletes all audit events before the specified date.")
|
||||
public Map<String, Object> cleanupBefore(
|
||||
@RequestParam(value = "date", required = true)
|
||||
@Schema(
|
||||
description = "The cutoff date for cleanup",
|
||||
example = "2025-01-01",
|
||||
format = "date")
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||
LocalDate startDate,
|
||||
@RequestParam(value = "endDate", required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||
LocalDate endDate) {
|
||||
LocalDate date) {
|
||||
if (date != null && !date.isAfter(LocalDate.now())) {
|
||||
Instant cutoff = date.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
int deleted = auditRepository.deleteByTimestampBefore(cutoff);
|
||||
return Map.of("deleted", deleted, "cutoffDate", date.toString());
|
||||
}
|
||||
return Map.of(
|
||||
"error",
|
||||
"Invalid date format. Use ISO date format (YYYY-MM-DD). Date must be in the past.");
|
||||
}
|
||||
|
||||
// // ===== Helpers =====
|
||||
|
||||
// private Map<String, Long> toStringLongMap(List<Object[]> rows) {
|
||||
// Map<String, Long> map = new HashMap<>();
|
||||
// for (Object[] row : rows) {
|
||||
// String key = String.valueOf(row[0]);
|
||||
// long val = ((Number) row[1]).longValue();
|
||||
// map.put(key, val);
|
||||
// }
|
||||
// return map;
|
||||
// }
|
||||
|
||||
/** Helper method to escape CSV fields. */
|
||||
private String escapeCSV(String field) {
|
||||
if (field == null) {
|
||||
return "";
|
||||
}
|
||||
// Replace double quotes with two double quotes and wrap in quotes
|
||||
return "\"" + field.replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
|
||||
private List<PersistentAuditEvent> getAuditEventsByCriteria(AuditExportRequest request) {
|
||||
String type = request.getType();
|
||||
String principal = request.getPrincipal();
|
||||
LocalDate startDate = request.getStartDate();
|
||||
LocalDate endDate = request.getEndDate();
|
||||
|
||||
// Get data with same filtering as getAuditData
|
||||
List<PersistentAuditEvent> events;
|
||||
@ -323,29 +387,6 @@ public class AuditDashboardController {
|
||||
} else {
|
||||
events = auditRepository.findAll();
|
||||
}
|
||||
|
||||
// Convert to JSON
|
||||
try {
|
||||
byte[] jsonBytes = objectMapper.writeValueAsBytes(events);
|
||||
|
||||
// Set up HTTP headers for download
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setContentDispositionFormData("attachment", "audit_export.json");
|
||||
|
||||
return ResponseEntity.ok().headers(headers).body(jsonBytes);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Error serializing audit events to JSON", e);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
/** Helper method to escape CSV fields. */
|
||||
private String escapeCSV(String field) {
|
||||
if (field == null) {
|
||||
return "";
|
||||
}
|
||||
// Replace double quotes with two double quotes and wrap in quotes
|
||||
return "\"" + field.replace("\"", "\"\"") + "\"";
|
||||
return events;
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package stirling.software.proprietary.controller.web;
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.proprietary.audit.AuditEventType;
|
||||
import stirling.software.proprietary.audit.AuditLevel;
|
||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||
|
||||
@Controller
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@RequiredArgsConstructor
|
||||
@EnterpriseEndpoint
|
||||
public class AuditDashboardWebController {
|
||||
private final AuditConfigurationProperties auditConfig;
|
||||
|
||||
/** Display the audit dashboard. */
|
||||
@GetMapping("/audit")
|
||||
@Hidden
|
||||
public String showDashboard(Model model) {
|
||||
model.addAttribute("auditEnabled", auditConfig.isEnabled());
|
||||
model.addAttribute("auditLevel", auditConfig.getAuditLevel());
|
||||
model.addAttribute("auditLevelInt", auditConfig.getLevel());
|
||||
model.addAttribute("retentionDays", auditConfig.getRetentionDays());
|
||||
|
||||
// Add audit level enum values for display
|
||||
model.addAttribute("auditLevels", AuditLevel.values());
|
||||
|
||||
// Add audit event types for the dropdown
|
||||
model.addAttribute("auditEventTypes", AuditEventType.values());
|
||||
|
||||
return "audit/dashboard";
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package stirling.software.proprietary.model.api.audit;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||
|
||||
/** Request object used for querying audit events. */
|
||||
@Data
|
||||
@EnterpriseEndpoint
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class AuditDataRequest extends AuditExportRequest {
|
||||
|
||||
@Schema(description = "Page number for pagination", example = "0", defaultValue = "0")
|
||||
private int page = 0;
|
||||
|
||||
@Schema(description = "Page size for pagination", example = "30", defaultValue = "30")
|
||||
private int size = 30;
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package stirling.software.proprietary.model.api.audit;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
||||
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||
|
||||
/** Response object returned when querying audit data. */
|
||||
@Data
|
||||
@EnterpriseEndpoint
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode
|
||||
public class AuditDataResponse {
|
||||
|
||||
@Schema(description = "List of audit events matching the query")
|
||||
private List<PersistentAuditEvent> content;
|
||||
|
||||
@Schema(description = "Total number of pages available", example = "5")
|
||||
private int totalPages;
|
||||
|
||||
@Schema(description = "Total number of events", example = "150")
|
||||
private long totalElements;
|
||||
|
||||
@Schema(description = "Current page index", example = "0")
|
||||
private int currentPage;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package stirling.software.proprietary.model.api.audit;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||
|
||||
@Data
|
||||
@EnterpriseEndpoint
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode
|
||||
public class AuditDateExportRequest {
|
||||
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||
@Schema(description = "Start date for the export range", example = "2025-01-01")
|
||||
private LocalDate startDate;
|
||||
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||
@Schema(description = "End date for the export range", example = "2025-12-31")
|
||||
private LocalDate endDate;
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package stirling.software.proprietary.model.api.audit;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||
|
||||
/** Request object used for exporting audit data with filters. */
|
||||
@Data
|
||||
@EnterpriseEndpoint
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class AuditExportRequest extends AuditDateExportRequest {
|
||||
|
||||
@Schema(
|
||||
description = "Audit event type to filter by",
|
||||
example = "USER_LOGIN",
|
||||
allowableValues = {
|
||||
"USER_LOGIN",
|
||||
"USER_LOGOUT",
|
||||
"USER_FAILED_LOGIN",
|
||||
"USER_PROFILE_UPDATE",
|
||||
"SETTINGS_CHANGED",
|
||||
"FILE_OPERATION",
|
||||
"PDF_PROCESS",
|
||||
"HTTP_REQUEST"
|
||||
})
|
||||
private String type;
|
||||
|
||||
@Schema(description = "Principal (username) to filter by", example = "admin")
|
||||
private String principal;
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package stirling.software.proprietary.model.api.audit;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||
|
||||
/** Response object for audit statistics. */
|
||||
@Data
|
||||
@EnterpriseEndpoint
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode
|
||||
public class AuditStatsResponse {
|
||||
|
||||
@Schema(description = "Count of events grouped by type")
|
||||
private Map<String, Long> eventsByType;
|
||||
|
||||
@Schema(description = "Count of events grouped by principal")
|
||||
private Map<String, Long> eventsByPrincipal;
|
||||
|
||||
@Schema(description = "Count of events grouped by day")
|
||||
private Map<String, Long> eventsByDay;
|
||||
|
||||
@Schema(description = "Total number of events in the period", example = "42")
|
||||
private int totalEvents;
|
||||
}
|
@ -2,6 +2,7 @@ package stirling.software.proprietary.repository;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@ -19,7 +20,8 @@ public interface PersistentAuditEventRepository extends JpaRepository<Persistent
|
||||
|
||||
// Basic queries
|
||||
@Query(
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%'))")
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%',"
|
||||
+ " :principal, '%'))")
|
||||
Page<PersistentAuditEvent> findByPrincipal(
|
||||
@Param("principal") String principal, Pageable pageable);
|
||||
|
||||
@ -29,12 +31,14 @@ public interface PersistentAuditEventRepository extends JpaRepository<Persistent
|
||||
Instant startDate, Instant endDate, Pageable pageable);
|
||||
|
||||
@Query(
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type")
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%',"
|
||||
+ " :principal, '%')) AND e.type = :type")
|
||||
Page<PersistentAuditEvent> findByPrincipalAndType(
|
||||
@Param("principal") String principal, @Param("type") String type, Pageable pageable);
|
||||
|
||||
@Query(
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.timestamp BETWEEN :startDate AND :endDate")
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%',"
|
||||
+ " :principal, '%')) AND e.timestamp BETWEEN :startDate AND :endDate")
|
||||
Page<PersistentAuditEvent> findByPrincipalAndTimestampBetween(
|
||||
@Param("principal") String principal,
|
||||
@Param("startDate") Instant startDate,
|
||||
@ -45,7 +49,9 @@ public interface PersistentAuditEventRepository extends JpaRepository<Persistent
|
||||
String type, Instant startDate, Instant endDate, Pageable pageable);
|
||||
|
||||
@Query(
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type AND e.timestamp BETWEEN :startDate AND :endDate")
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%',"
|
||||
+ " :principal, '%')) AND e.type = :type AND e.timestamp BETWEEN :startDate AND"
|
||||
+ " :endDate")
|
||||
Page<PersistentAuditEvent> findByPrincipalAndTypeAndTimestampBetween(
|
||||
@Param("principal") String principal,
|
||||
@Param("type") String type,
|
||||
@ -55,7 +61,8 @@ public interface PersistentAuditEventRepository extends JpaRepository<Persistent
|
||||
|
||||
// Non-paged versions for export
|
||||
@Query(
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%'))")
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%',"
|
||||
+ " :principal, '%'))")
|
||||
List<PersistentAuditEvent> findAllByPrincipalForExport(@Param("principal") String principal);
|
||||
|
||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE e.type = :type")
|
||||
@ -69,26 +76,31 @@ public interface PersistentAuditEventRepository extends JpaRepository<Persistent
|
||||
List<PersistentAuditEvent> findByTimestampAfter(@Param("startDate") Instant startDate);
|
||||
|
||||
@Query(
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type")
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%',"
|
||||
+ " :principal, '%')) AND e.type = :type")
|
||||
List<PersistentAuditEvent> findAllByPrincipalAndTypeForExport(
|
||||
@Param("principal") String principal, @Param("type") String type);
|
||||
|
||||
@Query(
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.timestamp BETWEEN :startDate AND :endDate")
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%',"
|
||||
+ " :principal, '%')) AND e.timestamp BETWEEN :startDate AND :endDate")
|
||||
List<PersistentAuditEvent> findAllByPrincipalAndTimestampBetweenForExport(
|
||||
@Param("principal") String principal,
|
||||
@Param("startDate") Instant startDate,
|
||||
@Param("endDate") Instant endDate);
|
||||
|
||||
@Query(
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE e.type = :type AND e.timestamp BETWEEN :startDate AND :endDate")
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE e.type = :type AND e.timestamp BETWEEN"
|
||||
+ " :startDate AND :endDate")
|
||||
List<PersistentAuditEvent> findAllByTypeAndTimestampBetweenForExport(
|
||||
@Param("type") String type,
|
||||
@Param("startDate") Instant startDate,
|
||||
@Param("endDate") Instant endDate);
|
||||
|
||||
@Query(
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type AND e.timestamp BETWEEN :startDate AND :endDate")
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%',"
|
||||
+ " :principal, '%')) AND e.type = :type AND e.timestamp BETWEEN :startDate AND"
|
||||
+ " :endDate")
|
||||
List<PersistentAuditEvent> findAllByPrincipalAndTypeAndTimestampBetweenForExport(
|
||||
@Param("principal") String principal,
|
||||
@Param("type") String type,
|
||||
@ -112,7 +124,51 @@ public interface PersistentAuditEventRepository extends JpaRepository<Persistent
|
||||
@Query("SELECT e.principal, COUNT(e) FROM PersistentAuditEvent e GROUP BY e.principal")
|
||||
List<Object[]> countByPrincipal();
|
||||
|
||||
@Query(
|
||||
"SELECT e.type, COUNT(e) FROM PersistentAuditEvent e WHERE e.timestamp BETWEEN"
|
||||
+ " :startDate AND :endDate GROUP BY e.type")
|
||||
List<Object[]> countByTypeBetween(
|
||||
@Param("startDate") Instant startDate, @Param("endDate") Instant endDate);
|
||||
|
||||
@Query(
|
||||
"SELECT e.principal, COUNT(e) FROM PersistentAuditEvent e WHERE e.timestamp BETWEEN"
|
||||
+ " :startDate AND :endDate GROUP BY e.principal")
|
||||
List<Object[]> countByPrincipalBetween(
|
||||
@Param("startDate") Instant startDate, @Param("endDate") Instant endDate);
|
||||
|
||||
// Portable time-bucketing using YEAR/MONTH/DAY functions (works across most dialects)
|
||||
@Query(
|
||||
"SELECT YEAR(e.timestamp), MONTH(e.timestamp), DAY(e.timestamp), COUNT(e) "
|
||||
+ "FROM PersistentAuditEvent e "
|
||||
+ "WHERE e.timestamp BETWEEN :startDate AND :endDate "
|
||||
+ "GROUP BY YEAR(e.timestamp), MONTH(e.timestamp), DAY(e.timestamp) "
|
||||
+ "ORDER BY YEAR(e.timestamp), MONTH(e.timestamp), DAY(e.timestamp)")
|
||||
List<Object[]> histogramByDayBetween(
|
||||
@Param("startDate") Instant startDate, @Param("endDate") Instant endDate);
|
||||
|
||||
@Query(
|
||||
"SELECT HOUR(e.timestamp), COUNT(e) FROM PersistentAuditEvent e WHERE e.timestamp"
|
||||
+ " BETWEEN :startDate AND :endDate GROUP BY HOUR(e.timestamp) ORDER BY"
|
||||
+ " HOUR(e.timestamp)")
|
||||
List<Object[]> histogramByHourBetween(
|
||||
@Param("startDate") Instant startDate, @Param("endDate") Instant endDate);
|
||||
|
||||
// Get distinct event types for filtering
|
||||
@Query("SELECT DISTINCT e.type FROM PersistentAuditEvent e ORDER BY e.type")
|
||||
List<String> findDistinctEventTypes();
|
||||
|
||||
@Query("SELECT DISTINCT e.principal FROM PersistentAuditEvent e ORDER BY e.principal")
|
||||
List<String> findDistinctPrincipals();
|
||||
|
||||
@Query(
|
||||
"SELECT DISTINCT e.principal FROM PersistentAuditEvent e WHERE e.type = :type ORDER BY"
|
||||
+ " e.principal")
|
||||
List<String> findDistinctPrincipalsByType(@Param("type") String type);
|
||||
|
||||
// Top/Latest helpers & existence checks
|
||||
Optional<PersistentAuditEvent> findTopByOrderByTimestampDesc();
|
||||
|
||||
Optional<PersistentAuditEvent> findTopByPrincipalOrderByTimestampDesc(String principal);
|
||||
|
||||
Optional<PersistentAuditEvent> findTopByTypeOrderByTimestampDesc(String type);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
@ -56,19 +56,16 @@ public class AccountWebController {
|
||||
private final SessionPersistentRegistry sessionPersistentRegistry;
|
||||
// Assuming you have a repository for user operations
|
||||
private final UserRepository userRepository;
|
||||
private final boolean runningEE;
|
||||
private final TeamRepository teamRepository;
|
||||
|
||||
public AccountWebController(
|
||||
ApplicationProperties applicationProperties,
|
||||
SessionPersistentRegistry sessionPersistentRegistry,
|
||||
UserRepository userRepository,
|
||||
TeamRepository teamRepository,
|
||||
@Qualifier("runningEE") boolean runningEE) {
|
||||
TeamRepository teamRepository) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
||||
this.userRepository = userRepository;
|
||||
this.runningEE = runningEE;
|
||||
this.teamRepository = teamRepository;
|
||||
}
|
||||
|
||||
@ -203,12 +200,10 @@ public class AccountWebController {
|
||||
return "login";
|
||||
}
|
||||
|
||||
// @PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
// @GetMapping("/usage")
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@EnterpriseEndpoint
|
||||
@GetMapping("/usage")
|
||||
public String showUsage() {
|
||||
if (!runningEE) {
|
||||
return "error";
|
||||
}
|
||||
return "usage";
|
||||
}
|
||||
|
||||
@ -240,7 +235,7 @@ public class AccountWebController {
|
||||
|
||||
// Also check if user is part of the Internal team
|
||||
if (user.getTeam() != null
|
||||
&& user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||
&& TeamService.INTERNAL_TEAM_NAME.equals(user.getTeam().getName())) {
|
||||
shouldRemove = true;
|
||||
}
|
||||
|
||||
@ -359,11 +354,9 @@ public class AccountWebController {
|
||||
teamRepository.findAll().stream()
|
||||
.filter(
|
||||
team ->
|
||||
!team.getName()
|
||||
.equals(
|
||||
stirling.software.proprietary.security
|
||||
.service.TeamService
|
||||
.INTERNAL_TEAM_NAME))
|
||||
!stirling.software.proprietary.security.service.TeamService
|
||||
.INTERNAL_TEAM_NAME
|
||||
.equals(team.getName()))
|
||||
.toList();
|
||||
model.addAttribute("teams", allTeams);
|
||||
|
||||
|
@ -134,21 +134,21 @@ public class DatabaseConfig {
|
||||
ApplicationProperties.Driver driver =
|
||||
ApplicationProperties.Driver.valueOf(driverName.toUpperCase());
|
||||
|
||||
switch (driver) {
|
||||
return switch (driver) {
|
||||
case H2 -> {
|
||||
log.debug("H2 driver selected");
|
||||
return DatabaseDriver.H2.getDriverClassName();
|
||||
yield DatabaseDriver.H2.getDriverClassName();
|
||||
}
|
||||
case POSTGRESQL -> {
|
||||
log.debug("Postgres driver selected");
|
||||
return DatabaseDriver.POSTGRESQL.getDriverClassName();
|
||||
yield DatabaseDriver.POSTGRESQL.getDriverClassName();
|
||||
}
|
||||
default -> {
|
||||
log.warn("{} driver selected", driverName);
|
||||
throw new UnsupportedProviderException(
|
||||
driverName + " is not currently supported");
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Unknown driver: {}", driverName);
|
||||
throw new UnsupportedProviderException(driverName + " is not currently supported");
|
||||
|
@ -11,7 +11,6 @@ import org.springframework.stereotype.Component;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.proprietary.security.configuration.ee.KeygenLicenseVerifier.License;
|
||||
|
||||
@Slf4j
|
||||
@ -86,12 +85,6 @@ public class LicenseKeyChecker {
|
||||
return keyOrFilePath;
|
||||
}
|
||||
|
||||
public void updateLicenseKey(String newKey) throws IOException {
|
||||
applicationProperties.getPremium().setKey(newKey);
|
||||
GeneralUtils.saveKeyToSettings("EnterpriseEdition.key", newKey);
|
||||
checkLicense();
|
||||
}
|
||||
|
||||
public License getPremiumLicenseEnabledResult() {
|
||||
return premiumEnabledResult;
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@ -81,7 +82,8 @@ public class AdminSettingsController {
|
||||
@Operation(
|
||||
summary = "Get all application settings",
|
||||
description =
|
||||
"Retrieve all current application settings. Use includePending=true to include settings that will take effect after restart. Admin access required.")
|
||||
"Retrieve all current application settings. Use includePending=true to include"
|
||||
+ " settings that will take effect after restart. Admin access required.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(responseCode = "200", description = "Settings retrieved successfully"),
|
||||
@ -95,7 +97,9 @@ public class AdminSettingsController {
|
||||
log.debug("Admin requested all application settings (includePending={})", includePending);
|
||||
|
||||
// Convert ApplicationProperties to Map
|
||||
Map<String, Object> settings = objectMapper.convertValue(applicationProperties, Map.class);
|
||||
Map<String, Object> settings =
|
||||
objectMapper.convertValue(
|
||||
applicationProperties, new TypeReference<Map<String, Object>>() {});
|
||||
|
||||
if (includePending && !pendingChanges.isEmpty()) {
|
||||
// Merge pending changes into the settings map
|
||||
@ -112,7 +116,8 @@ public class AdminSettingsController {
|
||||
@Operation(
|
||||
summary = "Get pending settings changes",
|
||||
description =
|
||||
"Retrieve settings that have been modified but not yet applied (require restart). Admin access required.")
|
||||
"Retrieve settings that have been modified but not yet applied (require"
|
||||
+ " restart). Admin access required.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
@ -137,7 +142,8 @@ public class AdminSettingsController {
|
||||
@Operation(
|
||||
summary = "Update application settings (delta updates)",
|
||||
description =
|
||||
"Update specific application settings using dot notation keys. Only sends changed values. Changes take effect on restart. Admin access required.")
|
||||
"Update specific application settings using dot notation keys. Only sends"
|
||||
+ " changed values. Changes take effect on restart. Admin access required.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(responseCode = "200", description = "Settings updated successfully"),
|
||||
@ -178,7 +184,8 @@ public class AdminSettingsController {
|
||||
|
||||
return ResponseEntity.ok(
|
||||
String.format(
|
||||
"Successfully updated %d setting(s). Changes will take effect on application restart.",
|
||||
"Successfully updated %d setting(s). Changes will take effect on"
|
||||
+ " application restart.",
|
||||
updatedCount));
|
||||
|
||||
} catch (IOException e) {
|
||||
@ -199,7 +206,8 @@ public class AdminSettingsController {
|
||||
@Operation(
|
||||
summary = "Get specific settings section",
|
||||
description =
|
||||
"Retrieve settings for a specific section (e.g., security, system, ui). Admin access required.")
|
||||
"Retrieve settings for a specific section (e.g., security, system, ui). Admin"
|
||||
+ " access required.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
@ -288,7 +296,8 @@ public class AdminSettingsController {
|
||||
String escapedSectionName = HtmlUtils.htmlEscape(sectionName);
|
||||
return ResponseEntity.ok(
|
||||
String.format(
|
||||
"Successfully updated %d setting(s) in section '%s'. Changes will take effect on application restart.",
|
||||
"Successfully updated %d setting(s) in section '%s'. Changes will take"
|
||||
+ " effect on application restart.",
|
||||
updatedCount, escapedSectionName));
|
||||
|
||||
} catch (IOException e) {
|
||||
@ -308,7 +317,8 @@ public class AdminSettingsController {
|
||||
@Operation(
|
||||
summary = "Get specific setting value",
|
||||
description =
|
||||
"Retrieve value for a specific setting key using dot notation. Admin access required.")
|
||||
"Retrieve value for a specific setting key using dot notation. Admin access"
|
||||
+ " required.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
@ -348,7 +358,8 @@ public class AdminSettingsController {
|
||||
@Operation(
|
||||
summary = "Update specific setting value",
|
||||
description =
|
||||
"Update value for a specific setting key using dot notation. Admin access required.")
|
||||
"Update value for a specific setting key using dot notation. Admin access"
|
||||
+ " required.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(responseCode = "200", description = "Setting updated successfully"),
|
||||
@ -376,7 +387,8 @@ public class AdminSettingsController {
|
||||
String escapedKey = HtmlUtils.htmlEscape(key);
|
||||
return ResponseEntity.ok(
|
||||
String.format(
|
||||
"Successfully updated setting '%s'. Changes will take effect on application restart.",
|
||||
"Successfully updated setting '%s'. Changes will take effect on"
|
||||
+ " application restart.",
|
||||
escapedKey));
|
||||
|
||||
} catch (IOException e) {
|
||||
@ -532,7 +544,6 @@ public class AdminSettingsController {
|
||||
* Recursively mask sensitive fields in settings map. Sensitive fields are replaced with a
|
||||
* status indicator showing if they're configured.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> maskSensitiveFields(Map<String, Object> settings) {
|
||||
return maskSensitiveFieldsWithPath(settings, "");
|
||||
}
|
||||
|
@ -7,10 +7,10 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@ -145,7 +145,7 @@ public class DatabaseController {
|
||||
.body(resource);
|
||||
} catch (IOException e) {
|
||||
log.error("Error downloading file: {}", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.SEE_OTHER_303)
|
||||
return ResponseEntity.status(HttpStatus.SEE_OTHER)
|
||||
.location(URI.create("/database?error=downloadFailed"))
|
||||
.build();
|
||||
}
|
||||
|
@ -33,6 +33,9 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||
import stirling.software.proprietary.audit.AuditEventType;
|
||||
import stirling.software.proprietary.audit.AuditLevel;
|
||||
import stirling.software.proprietary.audit.Audited;
|
||||
import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||
import stirling.software.proprietary.security.model.AuthenticationType;
|
||||
@ -82,6 +85,7 @@ public class UserController {
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@PostMapping("/change-username")
|
||||
@Audited(type = AuditEventType.USER_PROFILE_UPDATE, level = AuditLevel.BASIC)
|
||||
public RedirectView changeUsername(
|
||||
Principal principal,
|
||||
@RequestParam(name = "currentPasswordChangeUsername") String currentPassword,
|
||||
@ -125,6 +129,7 @@ public class UserController {
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@PostMapping("/change-password-on-login")
|
||||
@Audited(type = AuditEventType.USER_PROFILE_UPDATE, level = AuditLevel.BASIC)
|
||||
public RedirectView changePasswordOnLogin(
|
||||
Principal principal,
|
||||
@RequestParam(name = "currentPassword") String currentPassword,
|
||||
@ -153,6 +158,7 @@ public class UserController {
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@PostMapping("/change-password")
|
||||
@Audited(type = AuditEventType.USER_PROFILE_UPDATE, level = AuditLevel.BASIC)
|
||||
public RedirectView changePassword(
|
||||
Principal principal,
|
||||
@RequestParam(name = "currentPassword") String currentPassword,
|
||||
|
@ -0,0 +1,101 @@
|
||||
package stirling.software.proprietary.security.controller.api.enterprise;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.FileInfo;
|
||||
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||
import stirling.software.proprietary.security.database.H2SQLCondition;
|
||||
import stirling.software.proprietary.security.service.DatabaseService;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequestMapping("/api/v1/database")
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@EnterpriseEndpoint
|
||||
@Conditional(H2SQLCondition.class)
|
||||
@Tag(name = "Database", description = "Database APIs for backup, import, and management")
|
||||
@RequiredArgsConstructor
|
||||
public class DatabaseControllerEnterprise {
|
||||
|
||||
private final DatabaseService databaseService;
|
||||
|
||||
@Operation(
|
||||
summary = "Delete the last database backup file",
|
||||
description =
|
||||
"Only Enterprise - Deletes the last database backup file from the server.")
|
||||
@DeleteMapping("/deleteLast")
|
||||
public ResponseEntity<?> deleteLastFile() {
|
||||
log.info("Deleting last database backup file...");
|
||||
List<Pair<FileInfo, Boolean>> results = databaseService.deleteLastBackup();
|
||||
return getDeleteAllResults(results);
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Delete all database backup files",
|
||||
description = "Only Enterprise - Deletes all database backup files from the server.")
|
||||
@DeleteMapping("/deleteAll")
|
||||
public ResponseEntity<?> deleteAllFiles() {
|
||||
log.info("Deleting all database backup files...");
|
||||
List<Pair<FileInfo, Boolean>> results = databaseService.deleteAllBackups();
|
||||
return getDeleteAllResults(results);
|
||||
}
|
||||
|
||||
private ResponseEntity<?> getDeleteAllResults(List<Pair<FileInfo, Boolean>> results) {
|
||||
if (results.isEmpty()) {
|
||||
log.info("No backup files found to delete.");
|
||||
return ResponseEntity.ok(new DeleteAllResult(List.of(), List.of(), "noContent"));
|
||||
}
|
||||
|
||||
List<String> deleted =
|
||||
results.stream()
|
||||
.filter(p -> Boolean.TRUE.equals(p.getRight()))
|
||||
.map(p -> p.getLeft().getFileName())
|
||||
.toList();
|
||||
|
||||
List<String> failed =
|
||||
results.stream()
|
||||
.filter(p -> !Boolean.TRUE.equals(p.getRight()))
|
||||
.map(p -> p.getLeft().getFileName())
|
||||
.toList();
|
||||
|
||||
log.info("Deleted backup files: {}", deleted);
|
||||
if (!failed.isEmpty()) {
|
||||
log.warn("Some backup files could not be deleted: {}", failed);
|
||||
return ResponseEntity.status(HttpStatus.MULTI_STATUS) // 207
|
||||
.body(new DeleteAllResult(deleted, failed, "partialFailure"));
|
||||
}
|
||||
DeleteAllResult result = new DeleteAllResult(deleted, failed, "ok");
|
||||
log.debug(
|
||||
"DeleteAllResult: deleted={}, failed={}, status={}",
|
||||
result.deleted,
|
||||
result.failed,
|
||||
result.status);
|
||||
return ResponseEntity.ok(result); // 200
|
||||
}
|
||||
|
||||
private static final class DeleteAllResult {
|
||||
public final List<String> deleted;
|
||||
public final List<String> failed;
|
||||
public final String status;
|
||||
|
||||
public DeleteAllResult(List<String> deleted, List<String> failed, String status) {
|
||||
this.deleted = deleted;
|
||||
this.failed = failed;
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
}
|
@ -8,16 +8,15 @@ public class H2SQLCondition implements Condition {
|
||||
|
||||
@Override
|
||||
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
|
||||
var env = context.getEnvironment();
|
||||
boolean enableCustomDatabase =
|
||||
Boolean.parseBoolean(
|
||||
context.getEnvironment()
|
||||
.getProperty("system.datasource.enableCustomDatabase"));
|
||||
env.getProperty("system.datasource.enableCustomDatabase", Boolean.class, false);
|
||||
|
||||
if (!enableCustomDatabase) {
|
||||
if (enableCustomDatabase) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String dataSourceType = context.getEnvironment().getProperty("system.datasource.type");
|
||||
String dataSourceType = env.getProperty("system.datasource.type", String.class, "");
|
||||
return "h2".equalsIgnoreCase(dataSourceType);
|
||||
}
|
||||
}
|
||||
|
@ -128,7 +128,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
// Check if the authenticated user is disabled and invalidate their session if so
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
|
||||
LoginMethod loginMethod = LoginMethod.UNKNOWN;
|
||||
UserLoginType loginMethod = UserLoginType.UNKNOWN;
|
||||
|
||||
boolean blockRegistration = false;
|
||||
|
||||
@ -137,20 +137,20 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
String username = null;
|
||||
if (principal instanceof UserDetails detailsUser) {
|
||||
username = detailsUser.getUsername();
|
||||
loginMethod = LoginMethod.USERDETAILS;
|
||||
loginMethod = UserLoginType.USERDETAILS;
|
||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||
username = oAuth2User.getName();
|
||||
loginMethod = LoginMethod.OAUTH2USER;
|
||||
loginMethod = UserLoginType.OAUTH2USER;
|
||||
OAUTH2 oAuth = securityProp.getOauth2();
|
||||
blockRegistration = oAuth != null && oAuth.getBlockRegistration();
|
||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||
username = saml2User.name();
|
||||
loginMethod = LoginMethod.SAML2USER;
|
||||
loginMethod = UserLoginType.SAML2USER;
|
||||
SAML2 saml2 = securityProp.getSaml2();
|
||||
blockRegistration = saml2 != null && saml2.getBlockRegistration();
|
||||
} else if (principal instanceof String stringUser) {
|
||||
username = stringUser;
|
||||
loginMethod = LoginMethod.STRINGUSER;
|
||||
loginMethod = UserLoginType.STRINGUSER;
|
||||
}
|
||||
|
||||
// Retrieve all active sessions for the user
|
||||
@ -164,8 +164,8 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
boolean isUserDisabled = userService.isUserDisabled(username);
|
||||
|
||||
boolean notSsoLogin =
|
||||
!LoginMethod.OAUTH2USER.equals(loginMethod)
|
||||
&& !LoginMethod.SAML2USER.equals(loginMethod);
|
||||
!UserLoginType.OAUTH2USER.equals(loginMethod)
|
||||
&& !UserLoginType.SAML2USER.equals(loginMethod);
|
||||
|
||||
// Block user registration if not allowed by configuration
|
||||
if (blockRegistration && !isUserExists) {
|
||||
@ -200,7 +200,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private enum LoginMethod {
|
||||
private enum UserLoginType {
|
||||
USERDETAILS("UserDetails"),
|
||||
OAUTH2USER("OAuth2User"),
|
||||
STRINGUSER("StringUser"),
|
||||
@ -209,7 +209,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private String method;
|
||||
|
||||
LoginMethod(String method) {
|
||||
UserLoginType(String method) {
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
|
@ -16,11 +16,16 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.audit.AuditEventType;
|
||||
import stirling.software.proprietary.audit.AuditLevel;
|
||||
import stirling.software.proprietary.audit.Audited;
|
||||
|
||||
@Slf4j
|
||||
public class CustomOAuth2AuthenticationFailureHandler
|
||||
extends SimpleUrlAuthenticationFailureHandler {
|
||||
|
||||
@Override
|
||||
@Audited(type = AuditEventType.USER_FAILED_LOGIN, level = AuditLevel.BASIC)
|
||||
public void onAuthenticationFailure(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
|
@ -24,6 +24,9 @@ import lombok.RequiredArgsConstructor;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||
import stirling.software.common.util.RequestUriUtils;
|
||||
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.service.JwtServiceInterface;
|
||||
import stirling.software.proprietary.security.service.LoginAttemptService;
|
||||
@ -39,6 +42,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
||||
private final JwtServiceInterface jwtService;
|
||||
|
||||
@Override
|
||||
@Audited(type = AuditEventType.USER_LOGIN, level = AuditLevel.BASIC)
|
||||
public void onAuthenticationSuccess(
|
||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||
throws ServletException, IOException {
|
||||
|
@ -14,11 +14,16 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.audit.AuditEventType;
|
||||
import stirling.software.proprietary.audit.AuditLevel;
|
||||
import stirling.software.proprietary.audit.Audited;
|
||||
|
||||
@Slf4j
|
||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||
public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||
|
||||
@Override
|
||||
@Audited(type = AuditEventType.USER_FAILED_LOGIN, level = AuditLevel.BASIC)
|
||||
public void onAuthenticationFailure(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
|
@ -23,6 +23,9 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||
import stirling.software.common.util.RequestUriUtils;
|
||||
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.service.JwtServiceInterface;
|
||||
import stirling.software.proprietary.security.service.LoginAttemptService;
|
||||
@ -39,6 +42,7 @@ public class CustomSaml2AuthenticationSuccessHandler
|
||||
private final JwtServiceInterface jwtService;
|
||||
|
||||
@Override
|
||||
@Audited(type = AuditEventType.USER_LOGIN, level = AuditLevel.BASIC)
|
||||
public void onAuthenticationSuccess(
|
||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||
throws ServletException, IOException {
|
||||
|
@ -5,6 +5,7 @@ import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
@ -21,6 +22,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.springframework.jdbc.datasource.init.CannotReadScriptException;
|
||||
import org.springframework.jdbc.datasource.init.ScriptException;
|
||||
import org.springframework.stereotype.Service;
|
||||
@ -45,10 +47,39 @@ public class DatabaseService implements DatabaseServiceInterface {
|
||||
|
||||
public DatabaseService(
|
||||
ApplicationProperties.Datasource datasourceProps, DataSource dataSource) {
|
||||
this.BACKUP_DIR =
|
||||
Paths.get(InstallationPathConfig.getConfigPath(), "db", "backup").normalize();
|
||||
this.BACKUP_DIR = Paths.get(InstallationPathConfig.getBackupPath()).normalize();
|
||||
this.datasourceProps = datasourceProps;
|
||||
this.dataSource = dataSource;
|
||||
moveBackupFiles();
|
||||
}
|
||||
|
||||
/** Move all backup files from db/backup to backup/db */
|
||||
@Deprecated(since = "2.0.0", forRemoval = true)
|
||||
private void moveBackupFiles() {
|
||||
Path sourceDir =
|
||||
Paths.get(InstallationPathConfig.getConfigPath(), "db", "backup").normalize();
|
||||
|
||||
if (!Files.exists(sourceDir)) {
|
||||
log.info("Source directory does not exist: {}", sourceDir);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Files.createDirectories(BACKUP_DIR);
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(sourceDir)) {
|
||||
for (Path entry : stream) {
|
||||
if (entry.getFileName().toString().startsWith(BACKUP_PREFIX)
|
||||
&& entry.getFileName().toString().endsWith(SQL_SUFFIX)) {
|
||||
Files.move(
|
||||
entry,
|
||||
BACKUP_DIR.resolve(entry.getFileName()),
|
||||
StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Error moving backup files: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -198,6 +229,46 @@ public class DatabaseService implements DatabaseServiceInterface {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Pair<FileInfo, Boolean>> deleteAllBackups() {
|
||||
List<FileInfo> backupList = this.getBackupList();
|
||||
List<Pair<FileInfo, Boolean>> deletedFiles = new ArrayList<>();
|
||||
|
||||
for (FileInfo backup : backupList) {
|
||||
try {
|
||||
Files.deleteIfExists(Paths.get(backup.getFilePath()));
|
||||
deletedFiles.add(Pair.of(backup, true));
|
||||
} catch (IOException e) {
|
||||
log.error("Error deleting backup file: {}", backup.getFileName(), e);
|
||||
deletedFiles.add(Pair.of(backup, false));
|
||||
}
|
||||
}
|
||||
return deletedFiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Pair<FileInfo, Boolean>> deleteLastBackup() {
|
||||
|
||||
List<FileInfo> backupList = this.getBackupList();
|
||||
List<Pair<FileInfo, Boolean>> deletedFiles = new ArrayList<>();
|
||||
if (!backupList.isEmpty()) {
|
||||
FileInfo lastBackup = backupList.get(backupList.size() - 1);
|
||||
try {
|
||||
Files.deleteIfExists(Paths.get(lastBackup.getFilePath()));
|
||||
deletedFiles.add(Pair.of(lastBackup, true));
|
||||
} catch (IOException e) {
|
||||
log.error("Error deleting last backup file: {}", lastBackup.getFileName(), e);
|
||||
deletedFiles.add(Pair.of(lastBackup, false));
|
||||
}
|
||||
}
|
||||
return deletedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the oldest backup file from the specified list.
|
||||
*
|
||||
* @param filteredBackupList the list of backup files
|
||||
*/
|
||||
private static void deleteOldestBackup(List<FileInfo> filteredBackupList) {
|
||||
try {
|
||||
filteredBackupList.sort(
|
||||
@ -237,6 +308,11 @@ public class DatabaseService implements DatabaseServiceInterface {
|
||||
return version;
|
||||
}
|
||||
|
||||
/*
|
||||
* Checks if the current datasource is H2.
|
||||
*
|
||||
* @return true if the datasource is H2, false otherwise
|
||||
*/
|
||||
private boolean isH2Database() {
|
||||
boolean isTypeH2 =
|
||||
datasourceProps.getType().equalsIgnoreCase(ApplicationProperties.Driver.H2.name());
|
||||
@ -301,6 +377,11 @@ public class DatabaseService implements DatabaseServiceInterface {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a database script.
|
||||
*
|
||||
* @param scriptPath the path to the script file
|
||||
*/
|
||||
private void executeDatabaseScript(Path scriptPath) {
|
||||
if (isH2Database()) {
|
||||
String query = "RUNSCRIPT from ?;";
|
||||
|
@ -3,6 +3,8 @@ package stirling.software.proprietary.security.service;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import stirling.software.common.model.FileInfo;
|
||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||
|
||||
@ -14,4 +16,8 @@ public interface DatabaseServiceInterface {
|
||||
boolean hasBackup();
|
||||
|
||||
List<FileInfo> getBackupList();
|
||||
|
||||
List<Pair<FileInfo, Boolean>> deleteAllBackups();
|
||||
|
||||
List<Pair<FileInfo, Boolean>> deleteLastBackup();
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
@ -53,7 +52,6 @@ public class JwtService implements JwtServiceInterface {
|
||||
private final KeyPersistenceServiceInterface keyPersistenceService;
|
||||
private final boolean v2Enabled;
|
||||
|
||||
@Autowired
|
||||
public JwtService(
|
||||
@Qualifier("v2Enabled") boolean v2Enabled,
|
||||
KeyPersistenceServiceInterface keyPersistenceService) {
|
||||
@ -155,7 +153,8 @@ public class JwtService implements JwtServiceInterface {
|
||||
keyPair = specificKeyPair.get();
|
||||
} else {
|
||||
log.warn(
|
||||
"Key ID {} not found in keystore, token may have been signed with an expired key",
|
||||
"Key ID {} not found in keystore, token may have been signed with an"
|
||||
+ " expired key",
|
||||
keyId);
|
||||
|
||||
if (keyId.equals(keyPersistenceService.getActiveKey().getKeyId())) {
|
||||
|
@ -8,7 +8,10 @@ import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
<<<<<<< HEAD
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
=======
|
||||
>>>>>>> origin
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
@ -30,7 +33,10 @@ public class KeyPairCleanupService {
|
||||
private final KeyPersistenceService keyPersistenceService;
|
||||
private final ApplicationProperties.Security.Jwt jwtProperties;
|
||||
|
||||
<<<<<<< HEAD
|
||||
@Autowired
|
||||
=======
|
||||
>>>>>>> origin
|
||||
public KeyPairCleanupService(
|
||||
KeyPersistenceService keyPersistenceService,
|
||||
ApplicationProperties applicationProperties) {
|
||||
|
@ -1,9 +1,17 @@
|
||||
package stirling.software.proprietary.security.service;
|
||||
|
||||
import java.io.IOException;
|
||||
<<<<<<< HEAD
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
=======
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
>>>>>>> origin
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
@ -20,7 +28,10 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
<<<<<<< HEAD
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
=======
|
||||
>>>>>>> origin
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
@ -43,11 +54,15 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
|
||||
public static final String KEY_SUFFIX = ".key";
|
||||
|
||||
private final ApplicationProperties.Security.Jwt jwtProperties;
|
||||
<<<<<<< HEAD
|
||||
private final CacheManager cacheManager;
|
||||
=======
|
||||
>>>>>>> origin
|
||||
private final Cache verifyingKeyCache;
|
||||
|
||||
private volatile JwtVerificationKey activeKey;
|
||||
|
||||
<<<<<<< HEAD
|
||||
@Autowired
|
||||
public KeyPersistenceService(
|
||||
ApplicationProperties applicationProperties, CacheManager cacheManager) {
|
||||
@ -56,6 +71,42 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
|
||||
this.verifyingKeyCache = cacheManager.getCache("verifyingKeys");
|
||||
}
|
||||
|
||||
=======
|
||||
public KeyPersistenceService(
|
||||
ApplicationProperties applicationProperties, CacheManager cacheManager) {
|
||||
this.jwtProperties = applicationProperties.getSecurity().getJwt();
|
||||
this.verifyingKeyCache = cacheManager.getCache("verifyingKeys");
|
||||
}
|
||||
|
||||
/** Move all key files from db/keys to backup/keys */
|
||||
@Deprecated(since = "2.0.0", forRemoval = true)
|
||||
private void moveKeysToBackup() {
|
||||
Path sourceDir =
|
||||
Paths.get(InstallationPathConfig.getConfigPath(), "db", "keys").normalize();
|
||||
|
||||
if (!Files.exists(sourceDir)) {
|
||||
log.info("Source directory does not exist: {}", sourceDir);
|
||||
return;
|
||||
}
|
||||
|
||||
Path targetDir = Paths.get(InstallationPathConfig.getPrivateKeyPath()).normalize();
|
||||
|
||||
try {
|
||||
Files.createDirectories(targetDir);
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(sourceDir)) {
|
||||
for (Path entry : stream) {
|
||||
Files.move(
|
||||
entry,
|
||||
targetDir.resolve(entry.getFileName()),
|
||||
StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Error moving key files to backup: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
>>>>>>> origin
|
||||
@PostConstruct
|
||||
public void initializeKeystore() {
|
||||
if (!isKeystoreEnabled()) {
|
||||
@ -63,6 +114,10 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
|
||||
}
|
||||
|
||||
try {
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
moveKeysToBackup();
|
||||
>>>>>>> origin
|
||||
ensurePrivateKeyDirectoryExists();
|
||||
loadKeyPair();
|
||||
} catch (Exception e) {
|
||||
@ -159,7 +214,11 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
|
||||
nativeCache.asMap().size());
|
||||
|
||||
return nativeCache.asMap().values().stream()
|
||||
<<<<<<< HEAD
|
||||
.filter(value -> value instanceof JwtVerificationKey)
|
||||
=======
|
||||
.filter(JwtVerificationKey.class::isInstance)
|
||||
>>>>>>> origin
|
||||
.map(value -> (JwtVerificationKey) value)
|
||||
.filter(
|
||||
key -> {
|
||||
@ -233,6 +292,10 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
|
||||
return Base64.getEncoder().encodeToString(publicKey.getEncoded());
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
@Override
|
||||
>>>>>>> origin
|
||||
public PublicKey decodePublicKey(String encodedKey)
|
||||
throws NoSuchAlgorithmException, InvalidKeySpecException {
|
||||
byte[] keyBytes = Base64.getDecoder().decode(encodedKey);
|
||||
|
@ -2,6 +2,7 @@ package stirling.software.proprietary.service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.actuate.audit.AuditEvent;
|
||||
import org.springframework.boot.actuate.audit.AuditEventRepository;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@ -29,8 +30,7 @@ public class AuditService {
|
||||
public AuditService(
|
||||
AuditEventRepository repository,
|
||||
AuditConfigurationProperties auditConfig,
|
||||
@org.springframework.beans.factory.annotation.Qualifier("runningEE")
|
||||
boolean runningEE) {
|
||||
@Qualifier("runningEE") boolean runningEE) {
|
||||
this.repository = repository;
|
||||
this.auditConfig = auditConfig;
|
||||
this.runningEE = runningEE;
|
||||
|
@ -218,7 +218,7 @@ function loadAuditData(targetPage, realPageSize) {
|
||||
showLoading('table-loading');
|
||||
|
||||
// Always request page 0 from server, but with increased page size if needed
|
||||
let url = `/audit/data?page=${requestedPage}&size=${realPageSize}`;
|
||||
let url = `/api/v1/audit/data?page=${requestedPage}&size=${realPageSize}`;
|
||||
|
||||
if (typeFilter) url += `&type=${encodeURIComponent(typeFilter)}`;
|
||||
if (principalFilter) url += `&principal=${encodeURIComponent(principalFilter)}`;
|
||||
@ -302,7 +302,7 @@ function loadStats(days) {
|
||||
showLoading('user-chart-loading');
|
||||
showLoading('time-chart-loading');
|
||||
|
||||
fetchWithCsrf(`/audit/stats?days=${days}`)
|
||||
fetchWithCsrf(`/api/v1/audit/stats?days=${days}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('total-events').textContent = data.totalEvents;
|
||||
@ -328,7 +328,7 @@ function exportAuditData(format) {
|
||||
const startDate = exportStartDateFilter.value;
|
||||
const endDate = exportEndDateFilter.value;
|
||||
|
||||
let url = format === 'json' ? '/audit/export/json?' : '/audit/export?';
|
||||
let url = format === 'json' ? '/api/v1/audit/export/json?' : '/api/v1/audit/export/csv?';
|
||||
|
||||
if (type) url += `&type=${encodeURIComponent(type)}`;
|
||||
if (principal) url += `&principal=${encodeURIComponent(principal)}`;
|
||||
@ -835,7 +835,7 @@ function hideLoading(id) {
|
||||
|
||||
// Load event types from the server for filter dropdowns
|
||||
function loadEventTypes() {
|
||||
fetchWithCsrf('/audit/types')
|
||||
fetchWithCsrf('/api/v1/audit/types')
|
||||
.then(response => response.json())
|
||||
.then(types => {
|
||||
if (!types || types.length === 0) {
|
||||
|
@ -189,7 +189,7 @@ class UserServiceTest {
|
||||
void testSaveUser_WithValidEmail_Success() throws Exception {
|
||||
// Given
|
||||
String emailUsername = "test@example.com";
|
||||
AuthenticationType authType = AuthenticationType.SSO;
|
||||
AuthenticationType authType = AuthenticationType.OAUTH2;
|
||||
|
||||
when(teamRepository.findByName("Default")).thenReturn(Optional.of(mockTeam));
|
||||
when(userRepository.save(any(User.class))).thenReturn(mockUser);
|
||||
|
70
build.gradle
70
build.gradle
@ -2,7 +2,7 @@ plugins {
|
||||
id "java"
|
||||
id "jacoco"
|
||||
id "io.spring.dependency-management" version "1.1.7"
|
||||
id "org.springframework.boot" version "3.5.4"
|
||||
id "org.springframework.boot" version "3.5.5"
|
||||
id "org.springdoc.openapi-gradle-plugin" version "1.9.0"
|
||||
id "io.swagger.swaggerhub" version "1.3.2"
|
||||
id "edu.sc.seis.launch4j" version "4.0.0"
|
||||
@ -21,12 +21,12 @@ import java.nio.file.Files
|
||||
import java.time.Year
|
||||
|
||||
ext {
|
||||
springBootVersion = "3.5.4"
|
||||
springBootVersion = "3.5.5"
|
||||
pdfboxVersion = "3.0.5"
|
||||
imageioVersion = "3.12.0"
|
||||
lombokVersion = "1.18.38"
|
||||
bouncycastleVersion = "1.81"
|
||||
springSecuritySamlVersion = "6.5.2"
|
||||
springSecuritySamlVersion = "6.5.3"
|
||||
openSamlVersion = "4.3.2"
|
||||
commonmarkVersion = "0.25.1"
|
||||
googleJavaFormatVersion = "1.28.0"
|
||||
@ -34,6 +34,13 @@ ext {
|
||||
tempJrePath = null
|
||||
}
|
||||
|
||||
ext.isSecurityDisabled = { ->
|
||||
System.getenv('DOCKER_ENABLE_SECURITY') == 'false' ||
|
||||
System.getenv('DISABLE_ADDITIONAL_FEATURES') == 'true' ||
|
||||
(project.hasProperty('DISABLE_ADDITIONAL_FEATURES') &&
|
||||
System.getProperty('DISABLE_ADDITIONAL_FEATURES') == 'true')
|
||||
}
|
||||
|
||||
jar {
|
||||
enabled = false
|
||||
manifest {
|
||||
@ -67,8 +74,8 @@ allprojects {
|
||||
}
|
||||
|
||||
tasks.register('writeVersion', WriteProperties) {
|
||||
outputFile = layout.projectDirectory.file('app/common/src/main/resources/version.properties')
|
||||
println "Writing version.properties to ${outputFile.path}"
|
||||
destinationFile = layout.projectDirectory.file('app/common/src/main/resources/version.properties')
|
||||
println "Writing version.properties to ${destinationFile.get().asFile.path}"
|
||||
comment = "${new Date()}"
|
||||
property 'version', project.provider { project.version.toString() }
|
||||
}
|
||||
@ -212,53 +219,17 @@ subprojects {
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.encoding = "UTF-8"
|
||||
if (!project.hasProperty("noSpotless")) {
|
||||
dependsOn "spotlessApply"
|
||||
}
|
||||
}
|
||||
gradle.taskGraph.whenReady { graph ->
|
||||
if (project.hasProperty("noSpotless")) {
|
||||
tasks.matching { it.name.startsWith("spotless") }.configureEach {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
dependsOn "spotlessApply"
|
||||
}
|
||||
|
||||
def allProjects = ((subprojects as Set<Project>) + project) as Set<Project>
|
||||
|
||||
licenseReport {
|
||||
projects = [project]
|
||||
projects = allProjects
|
||||
renderers = [new JsonReportRenderer()]
|
||||
allowedLicensesFile = project.layout.projectDirectory.file("app/allowed-licenses.json").asFile
|
||||
outputDir = project.layout.buildDirectory.dir("reports/dependency-license").get().asFile.path
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
if (System.getenv('DOCKER_ENABLE_SECURITY') == 'false' || System.getenv('DISABLE_ADDITIONAL_FEATURES') == 'true'
|
||||
|| (project.hasProperty('DISABLE_ADDITIONAL_FEATURES')
|
||||
&& System.getProperty('DISABLE_ADDITIONAL_FEATURES') == 'true')) {
|
||||
exclude 'stirling/software/proprietary/security/**'
|
||||
}
|
||||
|
||||
if (System.getenv('STIRLING_PDF_DESKTOP_UI') == 'false') {
|
||||
exclude 'stirling/software/SPDF/UI/impl/**'
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
java {
|
||||
if (System.getenv('DOCKER_ENABLE_SECURITY') == 'false' || System.getenv('DISABLE_ADDITIONAL_FEATURES') == 'true'
|
||||
|| (project.hasProperty('DISABLE_ADDITIONAL_FEATURES')
|
||||
&& System.getProperty('DISABLE_ADDITIONAL_FEATURES') == 'true')) {
|
||||
exclude 'stirling/software/proprietary/security/**'
|
||||
}
|
||||
|
||||
if (System.getenv('STIRLING_PDF_DESKTOP_UI') == 'false') {
|
||||
exclude 'stirling/software/SPDF/UI/impl/**'
|
||||
}
|
||||
}
|
||||
}
|
||||
configurations = [ "productionRuntimeClasspath", "runtimeClasspath" ]
|
||||
}
|
||||
|
||||
// Configure the forked spring boot run task to properly delegate to the stirling-pdf module
|
||||
@ -583,9 +554,7 @@ swaggerhubUpload {
|
||||
dependencies {
|
||||
implementation project(':stirling-pdf')
|
||||
implementation project(':common')
|
||||
if (System.getenv('DISABLE_ADDITIONAL_FEATURES') != 'true'
|
||||
|| (project.hasProperty('DISABLE_ADDITIONAL_FEATURES')
|
||||
&& System.getProperty('DISABLE_ADDITIONAL_FEATURES') != 'true')) {
|
||||
if (rootProject.ext.isSecurityDisabled()) {
|
||||
implementation project(':proprietary')
|
||||
}
|
||||
|
||||
@ -600,7 +569,6 @@ tasks.named("test") {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
|
||||
// Make sure all relevant processes depend on writeVersion
|
||||
processResources.dependsOn(writeVersion)
|
||||
|
||||
|
@ -4,110 +4,109 @@
|
||||
#
|
||||
# pip-compile --generate-hashes --output-file='testing\cucumber\requirements.txt' --strip-extras 'testing\cucumber\requirements.in'
|
||||
#
|
||||
behave==1.2.6 \
|
||||
--hash=sha256:b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86 \
|
||||
--hash=sha256:ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c
|
||||
behave==1.3.1 \
|
||||
--hash=sha256:2a1f3a2490242132c4daf0732d9b65c99be6fef1f787f97fd028ea5a402025ff \
|
||||
--hash=sha256:71b2dc00664de83c3aad61c91e5b3051b7b860aa2053e24db4742edecb800d21
|
||||
# via -r testing\cucumber\requirements.in
|
||||
certifi==2025.6.15 \
|
||||
--hash=sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057 \
|
||||
--hash=sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b
|
||||
certifi==2025.8.3 \
|
||||
--hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \
|
||||
--hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5
|
||||
# via requests
|
||||
charset-normalizer==3.4.2 \
|
||||
--hash=sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4 \
|
||||
--hash=sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45 \
|
||||
--hash=sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7 \
|
||||
--hash=sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0 \
|
||||
--hash=sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7 \
|
||||
--hash=sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d \
|
||||
--hash=sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d \
|
||||
--hash=sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0 \
|
||||
--hash=sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184 \
|
||||
--hash=sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db \
|
||||
--hash=sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b \
|
||||
--hash=sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64 \
|
||||
--hash=sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b \
|
||||
--hash=sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8 \
|
||||
--hash=sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff \
|
||||
--hash=sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344 \
|
||||
--hash=sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58 \
|
||||
--hash=sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e \
|
||||
--hash=sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471 \
|
||||
--hash=sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148 \
|
||||
--hash=sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a \
|
||||
--hash=sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836 \
|
||||
--hash=sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e \
|
||||
--hash=sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63 \
|
||||
--hash=sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c \
|
||||
--hash=sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1 \
|
||||
--hash=sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01 \
|
||||
--hash=sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366 \
|
||||
--hash=sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58 \
|
||||
--hash=sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5 \
|
||||
--hash=sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c \
|
||||
--hash=sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2 \
|
||||
--hash=sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a \
|
||||
--hash=sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597 \
|
||||
--hash=sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b \
|
||||
--hash=sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5 \
|
||||
--hash=sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb \
|
||||
--hash=sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f \
|
||||
--hash=sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0 \
|
||||
--hash=sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941 \
|
||||
--hash=sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0 \
|
||||
--hash=sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86 \
|
||||
--hash=sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7 \
|
||||
--hash=sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7 \
|
||||
--hash=sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455 \
|
||||
--hash=sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6 \
|
||||
--hash=sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4 \
|
||||
--hash=sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0 \
|
||||
--hash=sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3 \
|
||||
--hash=sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1 \
|
||||
--hash=sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6 \
|
||||
--hash=sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981 \
|
||||
--hash=sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c \
|
||||
--hash=sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980 \
|
||||
--hash=sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645 \
|
||||
--hash=sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7 \
|
||||
--hash=sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12 \
|
||||
--hash=sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa \
|
||||
--hash=sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd \
|
||||
--hash=sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef \
|
||||
--hash=sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f \
|
||||
--hash=sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2 \
|
||||
--hash=sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d \
|
||||
--hash=sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5 \
|
||||
--hash=sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02 \
|
||||
--hash=sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3 \
|
||||
--hash=sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd \
|
||||
--hash=sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e \
|
||||
--hash=sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214 \
|
||||
--hash=sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd \
|
||||
--hash=sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a \
|
||||
--hash=sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c \
|
||||
--hash=sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681 \
|
||||
--hash=sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba \
|
||||
--hash=sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f \
|
||||
--hash=sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a \
|
||||
--hash=sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28 \
|
||||
--hash=sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691 \
|
||||
--hash=sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82 \
|
||||
--hash=sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a \
|
||||
--hash=sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027 \
|
||||
--hash=sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7 \
|
||||
--hash=sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518 \
|
||||
--hash=sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf \
|
||||
--hash=sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b \
|
||||
--hash=sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9 \
|
||||
--hash=sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544 \
|
||||
--hash=sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da \
|
||||
--hash=sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509 \
|
||||
--hash=sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f \
|
||||
--hash=sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a \
|
||||
--hash=sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f
|
||||
charset-normalizer==3.4.3 \
|
||||
--hash=sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91 \
|
||||
--hash=sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0 \
|
||||
--hash=sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154 \
|
||||
--hash=sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601 \
|
||||
--hash=sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884 \
|
||||
--hash=sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07 \
|
||||
--hash=sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c \
|
||||
--hash=sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64 \
|
||||
--hash=sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe \
|
||||
--hash=sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f \
|
||||
--hash=sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432 \
|
||||
--hash=sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc \
|
||||
--hash=sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa \
|
||||
--hash=sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9 \
|
||||
--hash=sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae \
|
||||
--hash=sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19 \
|
||||
--hash=sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d \
|
||||
--hash=sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e \
|
||||
--hash=sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4 \
|
||||
--hash=sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7 \
|
||||
--hash=sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312 \
|
||||
--hash=sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92 \
|
||||
--hash=sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31 \
|
||||
--hash=sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c \
|
||||
--hash=sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f \
|
||||
--hash=sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99 \
|
||||
--hash=sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b \
|
||||
--hash=sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15 \
|
||||
--hash=sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392 \
|
||||
--hash=sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f \
|
||||
--hash=sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8 \
|
||||
--hash=sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491 \
|
||||
--hash=sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0 \
|
||||
--hash=sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc \
|
||||
--hash=sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0 \
|
||||
--hash=sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f \
|
||||
--hash=sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a \
|
||||
--hash=sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40 \
|
||||
--hash=sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927 \
|
||||
--hash=sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849 \
|
||||
--hash=sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce \
|
||||
--hash=sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14 \
|
||||
--hash=sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05 \
|
||||
--hash=sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c \
|
||||
--hash=sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c \
|
||||
--hash=sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a \
|
||||
--hash=sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc \
|
||||
--hash=sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34 \
|
||||
--hash=sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9 \
|
||||
--hash=sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096 \
|
||||
--hash=sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14 \
|
||||
--hash=sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30 \
|
||||
--hash=sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b \
|
||||
--hash=sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b \
|
||||
--hash=sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942 \
|
||||
--hash=sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db \
|
||||
--hash=sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5 \
|
||||
--hash=sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b \
|
||||
--hash=sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce \
|
||||
--hash=sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669 \
|
||||
--hash=sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0 \
|
||||
--hash=sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018 \
|
||||
--hash=sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93 \
|
||||
--hash=sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe \
|
||||
--hash=sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049 \
|
||||
--hash=sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a \
|
||||
--hash=sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef \
|
||||
--hash=sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2 \
|
||||
--hash=sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca \
|
||||
--hash=sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16 \
|
||||
--hash=sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f \
|
||||
--hash=sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb \
|
||||
--hash=sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1 \
|
||||
--hash=sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557 \
|
||||
--hash=sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37 \
|
||||
--hash=sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7 \
|
||||
--hash=sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72 \
|
||||
--hash=sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c \
|
||||
--hash=sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9
|
||||
# via
|
||||
# reportlab
|
||||
# requests
|
||||
colorama==0.4.6 \
|
||||
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
|
||||
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
|
||||
# via behave
|
||||
cucumber-expressions==18.0.1 \
|
||||
--hash=sha256:86230d503cdda7ef35a1f2072a882d7d57c740aa4c163c82b07f039b6bc60c42 \
|
||||
--hash=sha256:86ce41bf28ee520408416f38022e5a083d815edf04a0bd1dae46d474ca597c60
|
||||
# via behave
|
||||
cucumber-tag-expressions==6.2.0 \
|
||||
--hash=sha256:b60aa2cdbf9ac43e28d9b0e4fd49edf9f09d5d941257d2912f5228f9d166c023 \
|
||||
--hash=sha256:f94404b656831c56a3815da5305ac097003884d2ae64fa51f5f4fad82d97e583
|
||||
# via behave
|
||||
idna==3.10 \
|
||||
--hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
|
||||
--hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
|
||||
@ -118,9 +117,9 @@ parse==1.20.2 \
|
||||
# via
|
||||
# behave
|
||||
# parse-type
|
||||
parse-type==0.6.4 \
|
||||
--hash=sha256:5e1ec10440b000c3f818006033372939e693a9ec0176f446d9303e4db88489a6 \
|
||||
--hash=sha256:83d41144a82d6b8541127bf212dd76c7f01baff680b498ce8a4d052a7a5bce4c
|
||||
parse-type==0.6.6 \
|
||||
--hash=sha256:3ca79bbe71e170dfccc8ec6c341edfd1c2a0fc1e5cfd18330f93af938de2348c \
|
||||
--hash=sha256:513a3784104839770d690e04339a8b4d33439fcd5dd99f2e4580f9fc1097bfb2
|
||||
# via behave
|
||||
pillow==11.3.0 \
|
||||
--hash=sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2 \
|
||||
@ -273,13 +272,13 @@ pycryptodome==3.23.0 \
|
||||
--hash=sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be \
|
||||
--hash=sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7
|
||||
# via -r testing\cucumber\requirements.in
|
||||
pypdf==5.7.0 \
|
||||
--hash=sha256:203379453439f5b68b7a1cd43cdf4c5f7a02b84810cefa7f93a47b350aaaba48 \
|
||||
--hash=sha256:68c92f2e1aae878bab1150e74447f31ab3848b1c0a6f8becae9f0b1904460b6f
|
||||
pypdf==6.0.0 \
|
||||
--hash=sha256:282a99d2cc94a84a3a3159f0d9358c0af53f85b4d28d76ea38b96e9e5ac2a08d \
|
||||
--hash=sha256:56ea60100ce9f11fc3eec4f359da15e9aec3821b036c1f06d2b660d35683abb8
|
||||
# via -r testing\cucumber\requirements.in
|
||||
reportlab==4.4.2 \
|
||||
--hash=sha256:58e11be387457928707c12153b7e41e52533a5da3f587b15ba8f8fd0805c6ee2 \
|
||||
--hash=sha256:fc6283048ddd0781a9db1d671715990e6aa059c8d40ec9baf34294c4bd583a36
|
||||
reportlab==4.4.3 \
|
||||
--hash=sha256:073b0975dab69536acd3251858e6b0524ed3e087e71f1d0d1895acb50acf9c7b \
|
||||
--hash=sha256:df905dc5ec5ddaae91fc9cb3371af863311271d555236410954961c5ee6ee1b5
|
||||
# via -r testing\cucumber\requirements.in
|
||||
requests==2.32.4 \
|
||||
--hash=sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c \
|
||||
@ -291,6 +290,40 @@ six==1.17.0 \
|
||||
# via
|
||||
# behave
|
||||
# parse-type
|
||||
tomli==2.2.1 \
|
||||
--hash=sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6 \
|
||||
--hash=sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd \
|
||||
--hash=sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c \
|
||||
--hash=sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b \
|
||||
--hash=sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8 \
|
||||
--hash=sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6 \
|
||||
--hash=sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77 \
|
||||
--hash=sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff \
|
||||
--hash=sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea \
|
||||
--hash=sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192 \
|
||||
--hash=sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249 \
|
||||
--hash=sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee \
|
||||
--hash=sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4 \
|
||||
--hash=sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98 \
|
||||
--hash=sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8 \
|
||||
--hash=sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4 \
|
||||
--hash=sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281 \
|
||||
--hash=sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744 \
|
||||
--hash=sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69 \
|
||||
--hash=sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13 \
|
||||
--hash=sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140 \
|
||||
--hash=sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e \
|
||||
--hash=sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e \
|
||||
--hash=sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc \
|
||||
--hash=sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff \
|
||||
--hash=sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec \
|
||||
--hash=sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2 \
|
||||
--hash=sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222 \
|
||||
--hash=sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106 \
|
||||
--hash=sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272 \
|
||||
--hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \
|
||||
--hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7
|
||||
# via behave
|
||||
typing-extensions==4.14.1 \
|
||||
--hash=sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36 \
|
||||
--hash=sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76
|
||||
|
Loading…
x
Reference in New Issue
Block a user