Compare commits
786 Commits
feature/ap
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 31e5f59bea | |||
| b6d4c6fb8e | |||
| c1a3899aef | |||
| 8489d15523 | |||
| c9fd397def | |||
| 67a5c6b9ba | |||
| 7f0a7f04cd | |||
| daa563a2b3 | |||
| 74b4146fd3 | |||
| 14cdb2defe | |||
| 08743915f6 | |||
| 97701b5fa3 | |||
| a9f2f1128f | |||
| e9f06843ca | |||
| 46975b8cab | |||
| efe8798188 | |||
| 687cb76ca2 | |||
| c303d2c682 | |||
| 78286540fe | |||
| fd74116911 | |||
| d7bf446a92 | |||
| c6249d3a90 | |||
| 2319698997 | |||
| 6522814666 | |||
| 6d1a60eea5 | |||
| 050c0dcd9d | |||
| 6d7e1ad4b6 | |||
| d9dd2cd1c8 | |||
| 0ef11c9847 | |||
| efb5c59221 | |||
| 06edca9d5a | |||
| 5e65be07dc | |||
| dec1f8338d | |||
| 7d74461b69 | |||
| 1c5ed0048e | |||
| 1aefdebb80 | |||
| 8ad01f0222 | |||
| 2aac0789f6 | |||
| 9a040f0895 | |||
| 18856d96ce | |||
| 11d9545666 | |||
| eaf94ddd6f | |||
| 39cff96b4a | |||
| a7f1081d17 | |||
| 1cd4220674 | |||
| dc1819f777 | |||
| 659a8f9f72 | |||
| 07d593a742 | |||
| d346c22886 | |||
| c7527bdb03 | |||
| 97affc5080 | |||
| 7a5f01247e | |||
| 995f9389f8 | |||
| 562dd5b98d | |||
| 97bc40c6ff | |||
| bb67084f36 | |||
| 597387a66e | |||
| b1fe965cd4 | |||
| ac4ec942c3 | |||
| 9d8f9bd639 | |||
| 9387364f85 | |||
| 7021a7ecca | |||
| 2d2c8855a7 | |||
| 70caeb8d42 | |||
| 6d0d306334 | |||
| f458ec1dd4 | |||
| 5a3078f3de | |||
| deac4ce2ca | |||
| 31e14bb693 | |||
| 9c2769de05 | |||
| 128d0a2e25 | |||
| 6e9e6d65a5 | |||
| 4d364a6ebc | |||
| c0f9d9712b | |||
| d206a00015 | |||
| 03b2783f57 | |||
| 83c809c0e0 | |||
| 1420488ca8 | |||
| 17e63d2024 | |||
| 9dfc66607a | |||
| b452d10db9 | |||
| 1ecc448df1 | |||
| 59d41c995f | |||
| 44a37136d3 | |||
| a996e1bde0 | |||
| 135bcca57c | |||
| 35d61e10c4 | |||
| 8fe115be50 | |||
| 00ce5930b9 | |||
| 9df304aa8b | |||
| 55afbec595 | |||
| 1b7cb2ee20 | |||
| d474e725ff | |||
| 60ec309175 | |||
| d910def81b | |||
| 8a11a1b55f | |||
| 82386e216e | |||
| 9df342de1a | |||
| 8bf5280e6a | |||
| a77863e98c | |||
| c1294cca94 | |||
| 9f119d64f9 | |||
| de820e8d1b | |||
| bb70347581 | |||
| b8f1a60ea4 | |||
| da074ff0a5 | |||
| b6d561ae1e | |||
| 17406830f7 | |||
| 94e2a17fb3 | |||
| 589c788b97 | |||
| 32b66a1375 | |||
| e357aa5d86 | |||
| 89cd709ed2 | |||
| 31a2f8a22c | |||
| f99520d852 | |||
| 431f36725a | |||
| b016511064 | |||
| ce763e350d | |||
| 4f7fa08f76 | |||
| d60da379a4 | |||
| 8e3bdaed2c | |||
| 3f07ddd763 | |||
| 5e7738f1dd | |||
| f9e2bbf77e | |||
| eb20442b86 | |||
| c2051c0b72 | |||
| a17a6f801e | |||
| 64d1fff1b9 | |||
| dbe4b428f7 | |||
| 79d6fbfed8 | |||
| a4ab01fa22 | |||
| c4479f3c76 | |||
| 0b83835926 | |||
| 5caa1238d7 | |||
| 8f3f7d4e83 | |||
| 39cf05979e | |||
| 7a6d7fbef7 | |||
| f03149782d | |||
| bee203f67e | |||
| d5040c825b | |||
| 87e6c24772 | |||
| b0b7c33f05 | |||
| 92480480fc | |||
| d0d7ac23e7 | |||
| a3dafea0c0 | |||
| 84c9b69f2d | |||
| f0dcefa8fa | |||
| 0fa5cbe05b | |||
| 35def3db3b | |||
| f2ca7fc2cc | |||
| d26124809d | |||
| b9623f6c62 | |||
| 4f686375d6 | |||
| 9fa08b6105 | |||
| e8b92c9d9f | |||
| 549fcc0dc5 | |||
| 865c5be52c | |||
| 09cd78d92e | |||
| cc029a2998 | |||
| 98dd17379d | |||
| 797ed2e333 | |||
| c7a226857a | |||
| c3b71dc445 | |||
| bb60d6742c | |||
| c86c1ed215 | |||
| 1590295f6e | |||
| 78ac7a8625 | |||
| 6fa9377397 | |||
| dc86373b2a | |||
| ed534eded7 | |||
| 8e9b5c8eb1 | |||
| c83f28fb9c | |||
| 0a0bd3a418 | |||
| 4407da6616 | |||
| 64d6b63937 | |||
| cd65a2c2dd | |||
| 6793dd0e80 | |||
| 18da7b1eed | |||
| fe4c596dda | |||
| dcdfbadd1a | |||
| c8962b0a6a | |||
| 4088b22bb8 | |||
| 035f165f9f | |||
| e1610ee19c | |||
| b5d2938d5e | |||
| 2b617058b3 | |||
| 5e50c78ab1 | |||
| 22e8e7c486 | |||
| a62cca6e45 | |||
| 83dc15ec9f | |||
| 3320a26ada | |||
| f0e0feeeb9 | |||
| 762b31083e | |||
| 358673cada | |||
| eed49c4745 | |||
| 8eb0c644f3 | |||
| 6f7d8c5ef8 | |||
| ed992d2903 | |||
| aee3467918 | |||
| 087ef47e15 | |||
| aabb419e7d | |||
| 3a7c1f4bb7 | |||
| 36eb81d8e8 | |||
| d4ab2e2dc7 | |||
| 8cd023e224 | |||
| 8788f22a81 | |||
| fd7930b139 | |||
| a68f54be80 | |||
| 038bd7bd26 | |||
| 69a97c0044 | |||
| 26a470fe1e | |||
| 80c791c7e1 | |||
| 05ce600e09 | |||
| c6477f7757 | |||
| 8498af24f9 | |||
| 171a48a411 | |||
| d63c92e36a | |||
| 8929236e85 | |||
| 84e65340b6 | |||
| a1b6fa8911 | |||
| 1ca75a4ed4 | |||
| db7b2e9f46 | |||
| b67fdaee78 | |||
| fd250996b2 | |||
| 797223e976 | |||
| 5961b40349 | |||
| 447dc5a7dc | |||
| 5406e7e3db | |||
| 685a1fd4f3 | |||
| f93f6d14ff | |||
| d15a68285c | |||
| f3233eb19b | |||
| a763a7b230 | |||
| f76fb95fcd | |||
| 5b6faf93f9 | |||
| 88a8010a18 | |||
| 5ceeec7fdd | |||
| 9697b37263 | |||
| 18b667d24b | |||
| c154196090 | |||
| 1306770f2e | |||
| ab063aeb23 | |||
| 36631dc201 | |||
| 4851a50279 | |||
| 03cb227b5d | |||
| 1612d91ac6 | |||
| e27e2b5bc0 | |||
| b18407f64b | |||
| 46e34d4dcc | |||
| 839d865d7c | |||
| 10e6a09640 | |||
| e747b180aa | |||
| 57e281b46f | |||
| 847f2f57a7 | |||
| 23def197c8 | |||
| b9e4395297 | |||
| 0a19621949 | |||
| 162419069b | |||
| b74d5ac34a | |||
| 26b658acf8 | |||
| 25c626be45 | |||
| b038537d1c | |||
| 4b6e461b32 | |||
| dfab716014 | |||
| aa61dfef15 | |||
| 7858823205 | |||
| 4bb4d189e6 | |||
| 9143f7a967 | |||
| ad57a54407 | |||
| fd6ec9b876 | |||
| db5b1b9da3 | |||
| a8c588f41f | |||
| b97fa8a337 | |||
| 2d706fb628 | |||
| 9233c65351 | |||
| 7120453ec8 | |||
| 215d3777db | |||
| 22f45f98ff | |||
| 99aa63f0e1 | |||
| e701fd5543 | |||
| e85f25c21d | |||
| f74fbc3092 | |||
| f29ed77969 | |||
| 31a7c9d012 | |||
| afdf784215 | |||
| 0a35370a81 | |||
| 2cb9417bbf | |||
| 35f1a15fc7 | |||
| 39c062eafc | |||
| 8027155bde | |||
| c6be4c38a6 | |||
| eedb5d45d4 | |||
| 6d6e3c68f5 | |||
| 14727ce6b9 | |||
| 52d354bc9f | |||
| 9732763fc8 | |||
| 4bc61c370c | |||
| 62037febe2 | |||
| fd25dacaee | |||
| 397d82a458 | |||
| 2d86789938 | |||
| f9d304484e | |||
| f0bdefd805 | |||
| b4637ddeca | |||
| fb2de52739 | |||
| 90f496c684 | |||
| f7057bd7cb | |||
| afac2f175e | |||
| 44b7e4cf84 | |||
| 39fd110388 | |||
| da4de58b08 | |||
| 211f1e2638 | |||
| 64ef6c2a8e | |||
| 0ff8299fdc | |||
| 37f8bec215 | |||
| 7b94e0daa0 | |||
| e9752ddcb7 | |||
| 26b85d1892 | |||
| 527e3909e8 | |||
| ce63b9325e | |||
| 77799add6d | |||
| b72ea70242 | |||
| d23d471de4 | |||
| 3ac01a6f00 | |||
| ffb679626e | |||
| c8aa6394f5 | |||
| 1260270c7d | |||
| 97ac1807b0 | |||
| 260c617e35 | |||
| 689b0a12f7 | |||
| dbe25c8a77 | |||
| 7b881be0a7 | |||
| 44241a5010 | |||
| 6202665862 | |||
| 9cf4367521 | |||
| 2dae908074 | |||
| 66590a2627 | |||
| 36c6e5dd2b | |||
| 80624eebfe | |||
| 05fc3b0048 | |||
| 4bfd89d358 | |||
| 4ce59663ad | |||
| 0e42cc45d1 | |||
| a18197b31c | |||
| c3340605f2 | |||
| 0e0e1cf2a2 | |||
| eac42923fd | |||
| 0db8da9810 | |||
| f9a75e8aba | |||
| c567fd1adf | |||
| 71e57d6eb1 | |||
| 7ce5dd0941 | |||
| a860db05d3 | |||
| 1b9d62c69e | |||
| bdc9c8fdde | |||
| 784ef238a2 | |||
| 746ddea4b2 | |||
| d1733b2848 | |||
| 13e6f6dcf2 | |||
| 2e17136f01 | |||
| 19b9369e77 | |||
| c6bc9ce4b0 | |||
| b9a8d5ffd2 | |||
| 1f7a92c8ea | |||
| 899ba89a2b | |||
| 2eafd998c0 | |||
| b0cfc629f9 | |||
| 3da6977daf | |||
| e52c3b7b45 | |||
| 6c56fb7af7 | |||
| dc3d6a5c0b | |||
| 8272192815 | |||
| 1c8782db3c | |||
| cc2bd15658 | |||
| 080e8bdcd9 | |||
| 0ea8a48591 | |||
| d3bb32eb97 | |||
| af2ddba2fa | |||
| 8191970086 | |||
| b948f401d1 | |||
| 5f9ce3f0e7 | |||
| 2c11be5796 | |||
| a1a74565a4 | |||
| 77607608e7 | |||
| 33eb70261d | |||
| a1aefe7bbe | |||
| a929a8d859 | |||
| e2d8dee1a6 | |||
| e7662a304e | |||
| 7efc81235d | |||
| 8efbb4c4a7 | |||
| d3b9fab7ce | |||
| 93378a5a2d | |||
| 5fefdccd17 | |||
| a18f25dc84 | |||
| 8a9369bbaf | |||
| 397eb4651b | |||
| ec15b07f21 | |||
| 2970007d2b | |||
| ee9d5943c9 | |||
| f232a862ac | |||
| 0fcb7dc9f8 | |||
| ccf895c8fb | |||
| 95aa7c9cf5 | |||
| 9ec4fd9ead | |||
| dd33cb6873 | |||
| 4de952967d | |||
| 5b87d02d9e | |||
| 7d08e7ec7c | |||
| 4816bcf024 | |||
| 37dd64e4c4 | |||
| 29af6c75cf | |||
| 591d93d699 | |||
| 5cd00a1ff1 | |||
| fdf34afcd3 | |||
| c1d730ac98 | |||
| 5f73db0a59 | |||
| 54a7e1dae9 | |||
| 33706bb195 | |||
| 9e8055bfee | |||
| 4fefc8c053 | |||
| 2aa398b1f0 | |||
| 91ee36c121 | |||
| dc55a855c3 | |||
| 1f96cce839 | |||
| 8b5968e1da | |||
| 9d2223af3b | |||
| afdd50489f | |||
| b85b01f2d2 | |||
| fdecff66d5 | |||
| e5f8ca953e | |||
| 0d01affb89 | |||
| ee7e463d36 | |||
| ea52e72086 | |||
| 6dd1ca3aad | |||
| 885dcf9b8f | |||
| 545cbe5dd4 | |||
| 74700fcc19 | |||
| 1b76f0fece | |||
| f47f472aaa | |||
| bfac5a92cc | |||
| 23ca040ac0 | |||
| 21b28a0d06 | |||
| 76533287dd | |||
| dbffb288d6 | |||
| 17611a9009 | |||
| 90c126a014 | |||
| cfffe77c93 | |||
| 588d573703 | |||
| 7d342216f8 | |||
| 5184544d6f | |||
| 70f4dac177 | |||
| a7e027177c | |||
| c2972aa015 | |||
| 4cf2ef98d0 | |||
| 1322511cfd | |||
| ed3ca4e6ea | |||
| 75f17f5846 | |||
| f13a5d3c01 | |||
| 6d3fd04232 | |||
| 47a2b051ba | |||
| 40625572e6 | |||
| fde81a3985 | |||
| da8fc64458 | |||
| d535f3d9c5 | |||
| 43750fb8f5 | |||
| 4522ddde8d | |||
| dd4a83043a | |||
| 96765aac84 | |||
| b3e3c72be5 | |||
| d90df7c46f | |||
| 20c1f898a1 | |||
| 666c4ad180 | |||
| eca8b34d88 | |||
| ec024e6fba | |||
| f35d651ffc | |||
| 2aed8052d0 | |||
| 2570785030 | |||
| c7482016ce | |||
| 20ab5a8f08 | |||
| 29c411a23c | |||
| eeb08bc563 | |||
| 316583d108 | |||
| fb278bfdd6 | |||
| 138448212e | |||
| 762ac838b6 | |||
| 93cb9f5e81 | |||
| 4c9725b282 | |||
| a8274dd640 | |||
| 2792b06d79 | |||
| 3c0188fba4 | |||
| 5f65e1a339 | |||
| 709044efef | |||
| bac8ed2be0 | |||
| 7c7bea3666 | |||
| 670fd20e44 | |||
| 338f5ed6f6 | |||
| 53d406ae74 | |||
| 29f836e615 | |||
| 38b7e9b72c | |||
| f2519dc987 | |||
| 8c7f3e13ed | |||
| 0343a8e4a7 | |||
| 57912b60ae | |||
| 2370a2b344 | |||
| f065eb5cb6 | |||
| 3a0161a0bb | |||
| 88f0e8138e | |||
| 7071b3792a | |||
| cef2e672cb | |||
| ec1e98d898 | |||
| 338734db7a | |||
| 411f2a4d8f | |||
| 9ea4dd2ecf | |||
| 903feebcdb | |||
| 2037b27c4c | |||
| beb84339e2 | |||
| df9f964f11 | |||
| 3aea111392 | |||
| 55b7ce7061 | |||
| f826a0e409 | |||
| 00ee7484df | |||
| 6728a541a8 | |||
| c3aed5e9fe | |||
| 2e691df76b | |||
| 89d389c76d | |||
| 7e3eb9522f | |||
| a94077874d | |||
| fee6f5c69d | |||
| 2b4060770a | |||
| da9feee83b | |||
| 0906eb6548 | |||
| 121186a0b9 | |||
| 841fea5103 | |||
| 81e55ea0ec | |||
| 3ee8ec05be | |||
| a5ad7a0ec6 | |||
| 011aa65d19 | |||
| 6e386d95ee | |||
| ef11a5a011 | |||
| d7f3b0156b | |||
| 5936cdb61c | |||
| abb63c293f | |||
| b1699cd10c | |||
| 048c4a398f | |||
| 57143d538e | |||
| 4d3260b2ca | |||
| 0b3ea4811a | |||
| 30c4103212 | |||
| 080ba99b2f | |||
| 7bd586b619 | |||
| 2a5c81ecc3 | |||
| 63b0a1d4c5 | |||
| 885a5b652b | |||
| c19f70031c | |||
| 7c9968063a | |||
| a955932086 | |||
| 8933753fe0 | |||
| 797e66fec3 | |||
| 0d042deaf2 | |||
| 5e767e77a9 | |||
| afff43abd6 | |||
| bcc7d7974e | |||
| 50e8fea3e9 | |||
| 377f4993f8 | |||
| 8be4bafce1 | |||
| bdd0454240 | |||
| f52816da32 | |||
| 7c88dd595d | |||
| 028a9af4aa | |||
| b5daa5a4ba | |||
| 811cf02f16 | |||
| 3b007261d6 | |||
| dfc5b8866a | |||
| d8252fd77f | |||
| 2d518e54ce | |||
| 46c75f14b6 | |||
| 10d800959f | |||
| 0b5da05d42 | |||
| 8f04833b6a | |||
| 61bf838108 | |||
| 6ab8c8c61f | |||
| bedc53df7f | |||
| 91bf29089e | |||
| ca452f9ade | |||
| c47d826f8e | |||
| 3e8112ec0b | |||
| 67d7675e11 | |||
| f32a4300f0 | |||
| 1b30f7e0ea | |||
| 33f5c3d0dd | |||
| 58342532cd | |||
| 06d8a065dc | |||
| ce7c430046 | |||
| 19679875df | |||
| 4c08938403 | |||
| 6d885caedf | |||
| 8f428a5c18 | |||
| 9acc8102e8 | |||
| 116347036f | |||
| c4199c5557 | |||
| 98ed9f6f8e | |||
| 236601fcf6 | |||
| 373483b4eb | |||
| 95536edec5 | |||
| e486c22bfc | |||
| bc407a7c20 | |||
| bc26c37315 | |||
| 74ee34634e | |||
| f1e43c911b | |||
| 8570b73257 | |||
| c97478ba8a | |||
| 6f03e76540 | |||
| 365c51f6c8 | |||
| 953775d9ca | |||
| 683e2d69d0 | |||
| a45be35d6c | |||
| 9a0ea70034 | |||
| d95f84bff7 | |||
| 7cee99e8e0 | |||
| ade9608e72 | |||
| 0dcdcfc829 | |||
| a5b680c273 | |||
| 2d8fe939a3 | |||
| 1f8616e4d6 | |||
| 5d686b28a4 | |||
| c047e36a62 | |||
| 2855967e81 | |||
| 439a52eb0f | |||
| 6a4d1761f3 | |||
| 77a2508c71 | |||
| fd26c90098 | |||
| 4e6eb3191b | |||
| 2cf8f5e7ec | |||
| bc8bc86b74 | |||
| 0b39f08ec7 | |||
| 100accfe34 | |||
| b3b24b08c4 | |||
| 74da043805 | |||
| 8eda3d40c3 | |||
| d74d06a3b0 | |||
| 1797e0c91e | |||
| a23f492621 | |||
| 02384d3404 | |||
| 547390286b | |||
| 596f3a55c1 | |||
| 30b17be43e | |||
| adcb663925 | |||
| e8883cfbf8 | |||
| fd469bdc22 | |||
| 22534b4cbf | |||
| b5c979058f | |||
| 6e751b552f | |||
| 3e3a0f1de7 | |||
| 87783ad99c | |||
| f881b42f7f | |||
| d118f5bce7 | |||
| c12cca2de9 | |||
| 9c290a66ca | |||
| 5d99fef4ed | |||
| 9310c12536 | |||
| e9ad3d8a92 | |||
| d5fdb227dd | |||
| 460d5df27f | |||
| fb4f74d48f | |||
| 52f9b632fb | |||
| af2b0a7faa | |||
| 7de632d9fc | |||
| 6403f371fe | |||
| 717f11e3d5 | |||
| 026473dd59 | |||
| 16b8637192 | |||
| f9e1715de1 | |||
| e045ff3f98 | |||
| fd1d136842 | |||
| a8bcd26cdd | |||
| 9c4c11efc9 | |||
| d405f2aa99 | |||
| 09f57dcce0 | |||
| 3a3ebec2bf | |||
| 6bffbba726 | |||
| 7bb57ded70 | |||
| 36280f0235 | |||
| b068157f87 | |||
| 164e26af91 | |||
| 0157f34949 | |||
| b64cee25d6 | |||
| cdf4e5188a | |||
| 514fae950c | |||
| d64b18c9fb | |||
| 2cb6a59c96 | |||
| c0b6f0c696 | |||
| 146f692a46 | |||
| b7fa26d1e9 | |||
| a1c8fc787e | |||
| ef2275c54d | |||
| 0495ded5c2 | |||
| 1d86258926 | |||
| 8258e7ad29 | |||
| 42bd7c36de | |||
| 0c1fc53331 | |||
| 2b117cdea4 | |||
| 77564c0f46 | |||
| 27e581c40a | |||
| 33a5e0a410 | |||
| 2a9e38fdea | |||
| 71d850531d | |||
| 2fb6db9bd4 | |||
| d0db73624e | |||
| b71d1c300b | |||
| bdb33801c9 | |||
| 7a3e9395e6 | |||
| 8d78de304b | |||
| 9c105565d1 | |||
| 69f360eae8 | |||
| 5e31ea1129 | |||
| 8a13fbfc0d | |||
| 5e64d0ecf6 | |||
| 3b8c867c72 | |||
| e55d30d61b | |||
| 57bceba425 | |||
| 04925de3dc | |||
| 8fa390a5e6 | |||
| a26de022e2 | |||
| 05fd4d35ff | |||
| b44a100bad | |||
| ff5533bee1 | |||
| af07182682 | |||
| 3f04015669 | |||
| b773f767b7 | |||
| 8f49173141 | |||
| 6e2f357f14 | |||
| be68e4f21e | |||
| d532b767fb | |||
| 2c6bf1ad5c | |||
| cf6a71f5e2 | |||
| aed8aa7e8f | |||
| f3ed7d6ea7 | |||
| 1b08f543b0 | |||
| 4b5f6a3b1e | |||
| c791d44ac0 | |||
| 030a331cd5 | |||
| 7719737db7 | |||
| 180b00e8ed | |||
| 7814deb02d | |||
| b3f38d5d98 | |||
| 7db84a060a | |||
| 48de0b0168 | |||
| e2ab4e58fe | |||
| 02bbca628f | |||
| 5816a53b11 | |||
| 4779493fad | |||
| ebe2022164 | |||
| 9faae13e7d | |||
| 196367adc1 | |||
| 96ffc48f84 | |||
| afe27bc8ec | |||
| 7a8dafa49b | |||
| b7ec814231 | |||
| 597cdf4a52 | |||
| a48617db7d | |||
| 506f999920 | |||
| 8fc0370f70 | |||
| 780a1c140b | |||
| bea4db8386 | |||
| 55ba3f154b | |||
| e43cff9762 | |||
| f9011a3dc2 | |||
| 28ce5f929e | |||
| 0bfe3aa264 | |||
| d6cd0df5ec | |||
| 08a4ba52f8 | |||
| e55665e0f3 | |||
| 797e1e10ba | |||
| cb28d1b553 | |||
| 909162d49f | |||
| f987d6e7b6 | |||
| 4a548b2278 | |||
| 14303963b9 | |||
| e79d2d9e91 | |||
| 60c51e6d94 | |||
| c5f004b7af | |||
| 888e265a73 | |||
| fada0b7595 | |||
| c534a7f846 | |||
| 5a3ce23a4b |
21
.cursor/rules/entreprise/00-checklist-nouveau-projet.mdc
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
description: Checklist agent — monter un nouveau projet sur le modèle Progiweb
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Checklist nouveau projet (agent)
|
||||||
|
|
||||||
|
Quand l'utilisateur demande de reproduire le modèle Reader / Inscription / CRM :
|
||||||
|
|
||||||
|
1. Créer la structure `.cursor/rules/{entreprise,projet,ops}/` (copier `entreprise/` depuis un projet déjà aligné).
|
||||||
|
2. Remplir `ops/20-parametres.mdc` (URLs Gitea, Bitbucket, chemins serveur, webhook, SSH).
|
||||||
|
3. Remplir `ops/20-git-deploy-playbook.mdc` (layout git root : `$HOME` vs `public_html`).
|
||||||
|
4. Remplir `projet/` (stack, conventions, préfixe JIRA).
|
||||||
|
5. Windows : remote Gitea, branche `dev`, `.gitignore` secrets / runtime.
|
||||||
|
6. Versionner `deploy_dev.php` au bon endroit (docroot) **avant** d'activer le webhook.
|
||||||
|
7. Serveur : `~/deploy/deploy_dev.sh` (chemins absolus), git init/fetch sur le bon root, tester `curl` webhook.
|
||||||
|
8. Gitea : webhook push → URL `deploy_dev.php` ; miroir Bitbucket **sync à chaque push** + intervalle filet `1h`. Sur le serveur Gitea : `LFS_START_SERVER = false` si pas de Git LFS (sinon `hook declined` / 500).
|
||||||
|
9. Test bout en bout : push `dev` → commit visible sur Gitea, serveur, Bitbucket.
|
||||||
|
10. Secrets uniquement dans 1Password (référencer le titre de l'item dans `ops/20-parametres.mdc`).
|
||||||
|
|
||||||
|
Ne pas inventer Bitbucket SSH sur le serveur : le serveur tire depuis Gitea local (`127.0.0.1:3000`) en général.
|
||||||
42
.cursor/rules/entreprise/00-cout-benefice.mdc
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
description: Progiweb — toujours proposer l’option au meilleur coût-bénéfice (pas l’essai-erreur)
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Coût-bénéfice — qualité d’approche
|
||||||
|
|
||||||
|
Référentiel agent. L’utilisateur veut **toujours coder au meilleur** : proposer d’abord la façon qui minimise le temps total (humain + agent) pour un résultat commercialement fiable.
|
||||||
|
|
||||||
|
## Règle
|
||||||
|
|
||||||
|
Avant d’implémenter (surtout UI print, layout, mapping, calibrage, « ça a l’air bon ») :
|
||||||
|
|
||||||
|
1. **Identifier** si la tâche invite à l’essai-erreur (deviner des mm/%, relire des screenshots, retoucher au feeling).
|
||||||
|
2. **Proposer d’abord** l’option au meilleur coût-bénéfice — même si elle demande 30–60 min de setup.
|
||||||
|
3. **Ne pas** enchaîner des itérations « change un chiffre → redeploie → regarde » comme stratégie principale.
|
||||||
|
4. Si plusieurs options : présenter **brièvement** (1–3) avec coût / bénéfice, **recommander** la meilleure, attendre OK si le setup est plus large que demandé.
|
||||||
|
|
||||||
|
## Préférer (exemples)
|
||||||
|
|
||||||
|
| Anti-pattern (cher) | Meilleure option (souvent) |
|
||||||
|
|---|---|
|
||||||
|
| Deviner positions PDF / CSS au feeling | GUI / outils de calibrage → vérité mesurée (JSON, %) |
|
||||||
|
| 15 essais de mapping colonnes | Preview + template sauvegardé + règles claires |
|
||||||
|
| Hardcoder FR/EN partout | Source unique (Info / config) |
|
||||||
|
| Patch local qui casse au 2ᵉ cas | Modèle extensible minimal (N boîtes, catalogue champs) |
|
||||||
|
| Changer le format d’impression « pour que ça marche chez moi » | **Demander le scénario volume** (1 test vs 2000 en file) avant de coder |
|
||||||
|
|
||||||
|
**Cas vécu MSIN-4485** : calibrateur drag-and-drop des boîtes a coûté **moins** que la série d’essais PDF. Ce genre de levier = défaut à proposer.
|
||||||
|
|
||||||
|
**Impression / papier** : avant de changer Lettre vs demi-feuille vs coupe, poser la question du **jour J** (file d’attente, volume). Ne pas imposer la coupe comme solution « technique » sans accord produit.
|
||||||
|
|
||||||
|
## Ce que ce n’est pas
|
||||||
|
|
||||||
|
- Pas d’over-engineering « au cas où » sans besoin.
|
||||||
|
- Pas de gros refactor hors périmètre.
|
||||||
|
- Le meilleur coût-bénéfice reste **minimal mais juste** : vérité mesurable + extensible léger > génie fragile.
|
||||||
|
|
||||||
|
## À chaque proposition d’approche
|
||||||
|
|
||||||
|
- Dire clairement : « Je recommande X parce que Y (moins d’aller-retours / une calibration / réutilisable). »
|
||||||
|
- Si l’utilisateur pousse l’essai-erreur : rappeler une fois l’alternative, puis suivre s’il insiste.
|
||||||
51
.cursor/rules/entreprise/00-git-modele.mdc
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
description: Progiweb — modèle Git / Gitea / Bitbucket / serveur (entreprise)
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Modèle Git entreprise (Progiweb)
|
||||||
|
|
||||||
|
Référentiel agent. Détails par projet → `ops/20-parametres.mdc` et `ops/20-git-deploy-playbook.mdc`.
|
||||||
|
|
||||||
|
## Push agent — interdit
|
||||||
|
|
||||||
|
L’agent **ne pousse jamais**. L’utilisateur synchronise (voir `entreprise/00-jamais-push.mdc`).
|
||||||
|
|
||||||
|
## Rôles
|
||||||
|
|
||||||
|
| Système | Rôle |
|
||||||
|
|---|---|
|
||||||
|
| **Gitea** (`git.progiweb.ca`) | Source de vérité — push depuis Windows |
|
||||||
|
| **Bitbucket** | Miroir / backup opérationnel hors serveurs |
|
||||||
|
| **Serveur (cPanel)** | Déploiement uniquement — **jamais** de commits sur le serveur |
|
||||||
|
|
||||||
|
## Branches
|
||||||
|
|
||||||
|
- Travail quotidien : **`dev`**
|
||||||
|
- Prod : **`master`** (quand le projet l'utilise ; ne pas inventer le flux prod)
|
||||||
|
- Ne pas pousser `master` sans demande explicite
|
||||||
|
|
||||||
|
**Agent : jamais de `git push`.** L'utilisateur synchronise (voir `00-jamais-push.mdc`).
|
||||||
|
|
||||||
|
## Flux nominal
|
||||||
|
|
||||||
|
```
|
||||||
|
Windows --push--> Gitea(dev) --webhook--> serveur (deploy_dev)
|
||||||
|
|
|
||||||
|
+--miroir push--> Bitbucket(dev)
|
||||||
|
```
|
||||||
|
|
||||||
|
Après un push demandé par l’utilisateur : **commentaire JIRA** sur le ticket (version code + date/heure) — détail projet dans `projet/10-workflow.mdc` (MS1) ou équivalent.
|
||||||
|
## Déploiement (règles absolues)
|
||||||
|
|
||||||
|
- Script shell hors webroot : typiquement `~/deploy/deploy_dev.sh`
|
||||||
|
- Déclencheur versionné dans le **docroot** : `deploy_dev.php` (sinon `git clean -fd` le détruit)
|
||||||
|
- Le script fait : `git fetch` → `git reset --hard FETCH_HEAD` → `git clean` **ciblé** (jamais un clean aveugle sur tout le home cPanel)
|
||||||
|
- Via PHP/Apache, **`HOME` est souvent absent** → chemins **absolus** dans le `.sh`
|
||||||
|
- Auth Gitea Windows : **HTTPS** (`https://git.progiweb.ca/...`) + token (éviter `http://` — GCM « unsafe remote » + dialogues Cancel)
|
||||||
|
- Auth miroir Bitbucket : user `x-token-auth` + Repository Access Token (1Password) — pas le mot de passe web
|
||||||
|
- Miroir Bitbucket : **sync à chaque push** (`sync_on_commit`) + intervalle filet **1h**. Prérequis Gitea : `LFS_START_SERVER = false` si le dépôt n’utilise pas Git LFS — sinon `git cat-file --batch-all-objects` se fait tuer, Gitea décline le push Windows (`hook declined`) et la page du dépôt passe en 500.
|
||||||
|
|
||||||
|
## Nouveau projet
|
||||||
|
|
||||||
|
Suivre `entreprise/00-checklist-nouveau-projet.mdc` et recopier `entreprise/` + remplir `ops/` + `projet/`.
|
||||||
25
.cursor/rules/entreprise/00-jamais-push.mdc
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
description: INTERDIT — l'agent ne pousse jamais (Git / serveur)
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Jamais de push — l'utilisateur synchronise
|
||||||
|
|
||||||
|
Règle absolue. Pas d'exception « pour aider », pas de Sync Cursor à la place de l'utilisateur.
|
||||||
|
|
||||||
|
## Interdit
|
||||||
|
|
||||||
|
- `git push` (toutes formes, tous remotes : Gitea, Bitbucket, origin)
|
||||||
|
- Déclencher `deploy_dev.php`, le webhook, ou `deploy_dev.sh`
|
||||||
|
- Publier / déployer / « sync » vers le serveur
|
||||||
|
|
||||||
|
## Autorisé seulement si l'utilisateur le demande
|
||||||
|
|
||||||
|
- `git commit` **sans** push ensuite
|
||||||
|
- Préparer les fichiers en local ; lui dire quoi synchroniser
|
||||||
|
|
||||||
|
Même s'il dit « pousse », « sync », « déploie » : **ne pas exécuter le push**. Rappeler qu'il synchronise lui-même.
|
||||||
|
|
||||||
|
## Pourquoi (fichiers serveur)
|
||||||
|
|
||||||
|
Le deploy fait `git reset --hard` + `git clean`. Un fichier corrigé **directement sur le serveur** est écrasé au prochain sync. La correction doit être dans Git **avant** que l'utilisateur synchronise — pas après coup sur le serveur.
|
||||||
38
.cursor/rules/entreprise/00-workflow.mdc
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
description: Progiweb — workflow agent (entreprise). Toujours s'y référer.
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Workflow entreprise (Progiweb)
|
||||||
|
|
||||||
|
Référentiel agent — pas une doc humaine. S'appliquer à tous les projets Progiweb montés sur ce modèle.
|
||||||
|
|
||||||
|
## Git push — interdit (l’utilisateur synchronise)
|
||||||
|
|
||||||
|
- **JAMAIS** de `git push` (voir `entreprise/00-jamais-push.mdc`).
|
||||||
|
- Même s’il dit « pousse / sync / déploie » : ne pas exécuter le push ; il synchronise lui-même.
|
||||||
|
- Commit : seulement sur demande explicite, **sans** push ensuite.
|
||||||
|
- Après **son** sync : commentaire JIRA si le playbook projet le demande.
|
||||||
|
|
||||||
|
## Avant de modifier du code
|
||||||
|
|
||||||
|
- Demander le **numéro JIRA** si l'utilisateur ne l'a pas fourni (préfixe selon le projet, voir `projet/`).
|
||||||
|
- Ne pas bloquer une simple question ou une lecture seule.
|
||||||
|
|
||||||
|
## Portée
|
||||||
|
|
||||||
|
- **Changement minimal** : uniquement ce qui est demandé.
|
||||||
|
- Pas de refactor large, renommage massif ou nettoyage opportuniste sans accord explicite.
|
||||||
|
- Cohérence locale (fichier / module) plutôt que modernisation globale.
|
||||||
|
- **Approche** : toujours le meilleur **coût-bénéfice** (voir `entreprise/00-cout-benefice.mdc`) — calibrage / vérité mesurable avant essai-erreur.
|
||||||
|
|
||||||
|
## Incohérences voisines
|
||||||
|
|
||||||
|
1. Signaler brièvement.
|
||||||
|
2. Proposer une correction ciblée.
|
||||||
|
3. Attendre confirmation avant de sortir du périmètre.
|
||||||
|
|
||||||
|
## Fin de tâche
|
||||||
|
|
||||||
|
- Résumer : quoi, où, déploiement / SQL / étapes manuelles restantes.
|
||||||
|
- Ne pas créer de README ou docs hors demande explicite (les règles agent vivent dans `.cursor/rules/`).
|
||||||
36
.cursor/rules/ops/20-git-deploy-playbook.mdc
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
description: MS1 Inscription — playbook Git/deploy (ops)
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Playbook Git / deploy — MS1 Inscription
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
- Le dépôt Git **est** le site (racine web = racine git), modèle historique MS1.
|
||||||
|
- Différent de Reader (git root = `$HOME` avec `ci4app` + `public_html` frères).
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
1. `~/deploy/deploy_dev.sh` : fetch `dev` → reset --hard → clean (adapter les chemins au docroot Inscription).
|
||||||
|
2. `deploy_dev.php` versionné à la racine — obligatoire avant webhook (sinon `git clean` l'efface).
|
||||||
|
3. Chemins absolus dans le `.sh` (PHP sans `HOME`).
|
||||||
|
|
||||||
|
## À vérifier / aligner si besoin
|
||||||
|
|
||||||
|
- Miroir Bitbucket : **sync à chaque push** + intervalle filet `1h`. Ne **pas** décocher `sync_on_commit` pour « réparer » un 500 — la cause était `LFS_START_SERVER = true` (`git cat-file --batch-all-objects` tué). Si la page dépôt est 500 : vérifier LFS encore à `false`, puis `systemctl kill gitea` + `systemctl start gitea` (pas un `restart` qui gèle).
|
||||||
|
- Webhook Gitea → URL HTTPS du `deploy_dev.php` d'environnement dev (asynchrone : le webhook ne doit pas bloquer le hook git).
|
||||||
|
- Remote Windows : **HTTPS** `https://git.progiweb.ca/...` (pas `http://` — GCM affiche un warning unsafe remote et des dialogues Cancel).
|
||||||
|
- Même philosophie entreprise que Reader (voir `entreprise/00-git-modele.mdc`).
|
||||||
|
|
||||||
|
## Push `hook declined` / page Gitea 500
|
||||||
|
|
||||||
|
Ce n’est **pas** Cursor. Gitea tue un process `git` (`GetCommitsInfo` / `cat-file`, `signal: killed`).
|
||||||
|
|
||||||
|
1. Confirmer `LFS_START_SERVER = false` dans `/etc/gitea/app.ini` et `sync_on_commit = 1` pour le miroir du repo (`gitea.push_mirror`, `repo_id` du dépôt).
|
||||||
|
2. `origin` en `https://git.progiweb.ca/...` (`git remote -v`).
|
||||||
|
3. Si la page est encore 500 : `systemctl kill gitea` puis `systemctl start gitea` sur `serveur-prod` (root, SSH 7822). Ne pas en faire un rituel — LFS doit rester à `false`.
|
||||||
|
|
||||||
|
## Après push (agent)
|
||||||
|
|
||||||
|
- Obligatoire : commentaire JIRA version + date/heure — voir `projet/10-workflow.mdc` § « Commentaire JIRA après push ».
|
||||||
41
.cursor/rules/ops/20-parametres.mdc
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
description: MS1 Inscription — paramètres figés (ops)
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Paramètres — MS1 Inscription
|
||||||
|
|
||||||
|
Secrets → 1Password uniquement.
|
||||||
|
|
||||||
|
## Git / remotes
|
||||||
|
|
||||||
|
| Clé | Valeur |
|
||||||
|
|---|---|
|
||||||
|
| Gitea (Windows) | `https://git.progiweb.ca/stephan/ms1inscription-v5.git` |
|
||||||
|
| Branche travail | `dev` |
|
||||||
|
| Branche prod | `master` |
|
||||||
|
| Bitbucket miroir | `https://bitbucket.org/progiwebinc/ms1inscription-v5.git` |
|
||||||
|
|
||||||
|
## Miroir
|
||||||
|
|
||||||
|
| Clé | Valeur |
|
||||||
|
|---|---|
|
||||||
|
| Modèle | **Sync à chaque push** (`sync_on_commit = 1`) + intervalle filet **`1h`**. Prérequis serveur : `/etc/gitea/app.ini` → `LFS_START_SERVER = false` (dépôt sans Git LFS). |
|
||||||
|
| Auth Bitbucket | `x-token-auth` + access token (item 1Password dédié Inscription si distinct) |
|
||||||
|
|
||||||
|
## Déploiement
|
||||||
|
|
||||||
|
| Clé | Valeur |
|
||||||
|
|---|---|
|
||||||
|
| User / home typique | `devinscription` / `/home/devinscription` |
|
||||||
|
| Git root serveur | **Docroot du site** (pas le home entier — layout ≠ Reader) |
|
||||||
|
| Script | `/home/devinscription/deploy/deploy_dev.sh` |
|
||||||
|
| Webhook PHP | `deploy_dev.php` à la **racine du dépôt** (versionné) |
|
||||||
|
| Contenu PHP | `shell_exec('/home/devinscription/deploy/deploy_dev.sh 2>&1')` |
|
||||||
|
|
||||||
|
## SSH / SQLTools
|
||||||
|
|
||||||
|
| Clé | Valeur |
|
||||||
|
|---|---|
|
||||||
|
| Port SSH | **7822** |
|
||||||
|
| Exemple | `.vscode/settings.json.example` |
|
||||||
77
.cursor/rules/projet/10-approbation-dossards.mdc
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
description: Projet MS1 approbation dossards — point de ralliement client (cadrage produit MSIN-4517)
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Projet MSIN-4517 — approbation dossards (point de ralliement)
|
||||||
|
|
||||||
|
JIRA : **MSIN-4517** (sous-tâche de **MSIN-4511** — production de dossard).
|
||||||
|
Parent production : MSIN-4511 ; art Excel / champs : MSIN-4515, MSIN-4469, etc.
|
||||||
|
Doc de cadrage — source de vérité pour ce chantier + référence future côté **CRM** (jalons / dates / points de faim).
|
||||||
|
|
||||||
|
Visuel de référence (maquette v2) : `_maquettes/msin-4517-approbation-visuel-v2.png` (dans le dépôt).
|
||||||
|
|
||||||
|
## Intention
|
||||||
|
|
||||||
|
Remplacer le « Go » ambigu par mail. Le client arrive sur **un écran** et approuve **explicitement** des blocs nommés. Chaque Go = un objet clair (pas « oui » sans savoir sur quoi).
|
||||||
|
|
||||||
|
## Décisions actées
|
||||||
|
|
||||||
|
| Sujet | Décision |
|
||||||
|
|---|---|
|
||||||
|
| Canal design | Le design **vit dans MS1** pour l’approbation (source de vérité du Go). **Dépôt ops préféré** = dossier Google Drive du projet CRM (pas upload MS1 comme flux principal). |
|
||||||
|
| Qui dépose l’art (v1) | **Ops** dépose dans un **répertoire Drive prédéterminé** (par projet CRM / client). MS1 **lit** ce dossier. Upload MS1 = secours seulement si besoin. Imprimeur en dépôt Drive = plus tard possible. |
|
||||||
|
| Lien CRM ↔ Drive | Chaque projet CRM a déjà un lien Google Drive ; on fixe à l’avance le sous-dossier maquettes dossards ; MS1 reçoit / résout ce lien via le projet. |
|
||||||
|
| Granularité art | **Par épreuve** + option **copier sur toutes** (comme les champs Face/Endos) |
|
||||||
|
| Versions art | Historique avec **date + qui a déposé** ; rétention paramétrable backend (défaut **5** dernières) |
|
||||||
|
| Approbation art | Fige **cette** version → **source unique** pour le reste des fonctions dossard |
|
||||||
|
| Nouvelle version après go | **Manuel** : n’invalide pas toute seule ; ops redemande l’approbation |
|
||||||
|
| Blocs v1 | **1. Visuel** + **2. Séquences à imprimer** — pas de Go global |
|
||||||
|
| Bloc Visuel | **Une approbation par épreuve** (liste des épreuves + leur image) |
|
||||||
|
| Bloc séquences | Vue **événement** (liste épreuves + plages + qté), **un** go pour le lot |
|
||||||
|
| Excel / envoi imprimeur | Seulement si **les 2 blocs** sont approuvés |
|
||||||
|
| Champs Face/Endos/Info | **Pas** un 3e bloc client en v1 (préparation ops) |
|
||||||
|
| Écran client | **Lien dédié** (pas le hub promoteur) |
|
||||||
|
| Dates / SLA / faim | **CRM** (projet client) — pas MS1 v1 ; MS1 = preuve visuelle du go |
|
||||||
|
| Code | Pas commencé tant que le **visuel** n’est pas validé avec le client produit |
|
||||||
|
|
||||||
|
## Blocs d’approbation v1
|
||||||
|
|
||||||
|
### 1. Visuel
|
||||||
|
- Objet : version d’art déposée **par épreuve**.
|
||||||
|
- Client voit : **une ligne / carte par épreuve** (image + version + date).
|
||||||
|
- Go = **par épreuve** (« J’approuve le design du 5 km », etc.).
|
||||||
|
- Le bloc Visuel n’est « Approuvé » globalement que quand **toutes** les épreuves concernées le sont.
|
||||||
|
- Effet : version officielle de **cette** épreuve pour le reste dossard.
|
||||||
|
|
||||||
|
### 2. Séquences à imprimer
|
||||||
|
- Objet : plages / quantités qui partent en impression.
|
||||||
|
- Client voit : tableau épreuve → plage → quantité.
|
||||||
|
- Go = **un** go pour le lot événement.
|
||||||
|
- Effet : feu vert séquences ; n’implique pas le go art.
|
||||||
|
|
||||||
|
## Principes UX
|
||||||
|
- Assez simple pour qu’un enfant comprenne : numéros 1 / 2, gros statut, un bouton clair par bloc.
|
||||||
|
- Statuts : **À approuver** / **Approuvé le …**.
|
||||||
|
- Pas de bouton « tout approuver » en v1.
|
||||||
|
|
||||||
|
## Plan livrables (ordre)
|
||||||
|
1. Art par épreuve — dépôt, versions, rétention, copie toutes épreuves
|
||||||
|
2. Écran approbation client — blocs Visuel + Séquences, preuve date
|
||||||
|
3. Garde-fou — pas Excel / envoi imprimeur sans A+B
|
||||||
|
4. Branchement — version art approuvée = source pour le reste dossard
|
||||||
|
5. Plus tard — dépôt imprimeur, dates CRM, autres blocs (champs, Excel…)
|
||||||
|
|
||||||
|
## Lien CRM (plus tard + dépôt Drive)
|
||||||
|
|
||||||
|
- MS1 = **quoi** a été approuvé + **quand** (preuve). **Pas** le radar mail (voir `10-radar-mail-crm.mdc`).
|
||||||
|
- CRM = **échéances**, points de faim, relances projet client + **lien Google Drive** du projet.
|
||||||
|
- Dépôt art préféré : ops met les maquettes dans un **sous-dossier Drive convenu** → MS1 les lit (facile à manipuler, pas besoin d’upload MS1 au quotidien).
|
||||||
|
- Ne pas dupliquer la gestion de délais dans MS1 v1 ; prévoir un ancrage (réf. événement / blocs / dossier Drive) pour le projet CRM.
|
||||||
|
|
||||||
|
## Règles agent
|
||||||
|
- Référencer **MSIN-4517**.
|
||||||
|
- **Pas de code** tant que l’utilisateur n’a pas validé / itéré le **visuel**.
|
||||||
|
- Changement minimal ; ne pas élargir au hub promoteur sans demande.
|
||||||
|
- Quand on touche production dossard / CRM jalons liés : relire cette règle.
|
||||||
|
- SQL : **dev préprod** seulement, puis Navicat + sync outil.
|
||||||
21
.cursor/rules/projet/10-bib-timing.mdc
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
description: Profiling bib MSIN-4546 — comment le réactiver (ne pas réinventer)
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bib timing profil (MSIN-4546)
|
||||||
|
|
||||||
|
Infrastructure **déjà en place** dans `php/inc_fx_promoteur.php` (`fxBibTiming*`). Ne pas recoder un bandeau / microtime custom.
|
||||||
|
|
||||||
|
## Activer
|
||||||
|
|
||||||
|
Sur l’URL compte / module bib : `?bib_timing=1` (ou `&bib_timing=1`).
|
||||||
|
|
||||||
|
## Ce que ça donne
|
||||||
|
|
||||||
|
Bandeau jaune `#bib-timing-report` en bas du HTML bib (Δ ms / cumul / mem + compteurs Info). Coller le bloc dans le chat pour analyser.
|
||||||
|
|
||||||
|
## Règle produit
|
||||||
|
|
||||||
|
Pas d’affichage auto, même en env de développement — **opt-in URL seulement**.
|
||||||
32
.cursor/rules/projet/10-css-js-version-bump.mdc
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
description: Incrémenter _VERSION_CODE après modification CSS/JS servis par Minify
|
||||||
|
globs: css/**/*.css,js/**/*.js,min/groupsConfig.php,inc_footer_scripts.php,inc_header.php
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Cache busting CSS / JS (MS1 inscription)
|
||||||
|
|
||||||
|
Les feuilles de style et scripts du site public passent par Minify :
|
||||||
|
|
||||||
|
- CSS : `/min/g=css&version=v{_VERSION_CODE}` (`inc_header.php`)
|
||||||
|
- JS : `/min/index.php?g=js&version=v{_VERSION_CODE}`
|
||||||
|
|
||||||
|
`_VERSION_CODE` est défini dans **`php/inc_settings.php`** (environnement local/dev).
|
||||||
|
|
||||||
|
## Obligation
|
||||||
|
|
||||||
|
Après **toute modification** d’un fichier inclus dans `min/groupsConfig.php` (ex. `css/style.css`, `css/ms1-doc.css`, JS du groupe `js`) **ou** d’un asset chargé avec `?version=v{_VERSION_CODE}` :
|
||||||
|
|
||||||
|
1. Incrémenter le **dernier segment** de `_VERSION_CODE` dans `php/inc_settings.php`
|
||||||
|
Ex. `4.72.659` → `4.72.660`
|
||||||
|
2. Mettre à jour `_DATE_CODE` à la date du jour (YYYY-MM-DD).
|
||||||
|
|
||||||
|
Ne pas oublier cette étape dans le même changement que le CSS/JS — sinon le navigateur garde l’ancien bundle en cache.
|
||||||
|
|
||||||
|
## Vérification rapide
|
||||||
|
|
||||||
|
Le pied de page affiche : `code (v{_VERSION_CODE})` — utile pour confirmer que la nouvelle version est déployée.
|
||||||
|
|
||||||
|
## Autres environnements
|
||||||
|
|
||||||
|
Fichiers séparés si déploiement ciblé : `php/inc_settingsprod.php`, `inc_settings_baloo.php`, etc. En dev local, c’est **`php/inc_settings.php`** qui compte.
|
||||||
60
.cursor/rules/projet/10-documentation.mdc
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
description: Documentation du code et des changements MS1
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Documentation MS1
|
||||||
|
|
||||||
|
## Commentaires dans le code
|
||||||
|
|
||||||
|
- Documenter la **logique métier non évidente** (règles d'inscription, paiement, permissions, états annulés, etc.).
|
||||||
|
- Pour une nouvelle fonction ou un bloc important : courte description du **pourquoi**, pas seulement du quoi.
|
||||||
|
- Référencer le ticket : `// MSIN-4405` ou bloc PHPDoc `* MSIN-4405 — …`.
|
||||||
|
- Langue : **français** pour les commentaires métier (comme le reste du projet), sauf si le fichier est entièrement en anglais.
|
||||||
|
|
||||||
|
## Libellés UI (table `info`)
|
||||||
|
|
||||||
|
- Tout texte affiché à l’utilisateur (FR/EN) passe par **Info** (`afficheTexte` / `fxInscrGestionT` / clés `info_clef`), **jamais** codé en dur dans le PHP/JS.
|
||||||
|
- Ça inclut aussi : en-têtes de colonnes, compteurs (« 12 inscriptions »), pastilles, `title`/`aria-label`, messages d’invite — **même si `aria-hidden`**.
|
||||||
|
- Interdit : `if ($strLangue === 'fr') { echo '…'; } else { echo '…'; }`, ternaires FR/EN, chaînes collées dans un `echo` HTML.
|
||||||
|
- Nouvelle clé = script SQL `sql/MSIN-xxxx-….sql` (INSERT dans `info`, fr + en) **dans le même lot** que le PHP.
|
||||||
|
- Les fallbacks PHP FR/EN (`fxInscrGestionFallbackTexte`, etc.) = filet de secours uniquement ; **pas** la source de vérité, et ne remplacent **pas** le script SQL.
|
||||||
|
|
||||||
|
Avant de terminer une tâche UI : rechercher dans les fichiers touchés les motifs `=== 'fr'`, `=== 'en'`, `$blnFr ?`, et les libellés français littéraux hors commentaires.
|
||||||
|
## AJAX multi-pages (Info)
|
||||||
|
|
||||||
|
Tout endpoint AJAX qui lit/écrit des libellés Info et peut être appelé depuis plusieurs pages (compte, hub, `/mobile`) doit **forcer le contexte `compte.php`** avant les includes utiles :
|
||||||
|
|
||||||
|
```php
|
||||||
|
define('MS1_…_AJAX', true); // optionnel, pattern existant
|
||||||
|
$_SERVER['PHP_SELF'] = '/compte.php';
|
||||||
|
$_SERVER['SCRIPT_NAME'] = '/compte.php';
|
||||||
|
$vPage = 'compte.php';
|
||||||
|
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
|
||||||
|
```
|
||||||
|
|
||||||
|
Objectif : éviter les clés créées sous `info_prg=ajax_….php` et les libellés manquants selon la page appelante. Références : `ajax_promoteur_hub.php`, `ajax_bib_range.php`, `ajax_inscr_gestion.php`.
|
||||||
|
|
||||||
|
## Scripts SQL
|
||||||
|
|
||||||
|
En-tête obligatoire en tête de fichier :
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- MSIN-xxxx — Titre court
|
||||||
|
-- Contexte / prérequis (scripts à exécuter avant)
|
||||||
|
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
|
||||||
|
```
|
||||||
|
|
||||||
|
- Indiquer l'ordre d'exécution si plusieurs scripts forment une migration.
|
||||||
|
- Préférer les scripts **idempotents** quand c'est possible (pattern déjà utilisé dans `sql/`).
|
||||||
|
- **Ne jamais** documenter / conseiller d’exécuter ces scripts hors **dev préprod** (voir `projet/10-workflow.mdc` — Promotion BD).
|
||||||
|
|
||||||
|
## Résumé des changements (agent)
|
||||||
|
|
||||||
|
À la fin d'une implémentation, fournir :
|
||||||
|
|
||||||
|
1. **Quoi** — comportement ajouté ou corrigé.
|
||||||
|
2. **Où** — fichiers principaux touchés.
|
||||||
|
3. **Déploiement** — SQL **dev préprod seulement**, puis Navicat structure + outil sync pour les autres env ; `_VERSION_CODE` / config / étapes manuelles.
|
||||||
|
|
||||||
|
Ne pas créer de fichiers README ou docs hors demande explicite.
|
||||||
102
.cursor/rules/projet/10-hors-transaction-import.mdc
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
---
|
||||||
|
description: Projet MS1 hors transaction — import / saisie rapide / ChronoTrack (cadrage produit)
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Projet MSIN-4482 — entrée hors transaction (import + terrain)
|
||||||
|
|
||||||
|
JIRA : **MSIN-4482**.
|
||||||
|
Orthographe produit : **ChronoTrack**.
|
||||||
|
Doc de cadrage — source de vérité pour le développement de ce chantier.
|
||||||
|
|
||||||
|
## Intention
|
||||||
|
|
||||||
|
Utiliser la suite MS1 (gestion, ChronoTrack, outils promoteur existants) pour des clients **sans** tunnel d’inscription / paiement en ligne MS1.
|
||||||
|
Les participants arrivent surtout par **import fichier** (puis saisie rapide plus tard) ; ensuite sync ChronoTrack.
|
||||||
|
|
||||||
|
Aussi utile plus tard le **jour J** sur une course **avec** inscription en ligne : correction / ajout rapide **sans** transaction financière.
|
||||||
|
|
||||||
|
## Décisions actées
|
||||||
|
|
||||||
|
| Sujet | Décision |
|
||||||
|
|---|---|
|
||||||
|
| Source terrain | Toujours **`resultats_*`** — **pas de panier / `no_panier`** pour ces lignes |
|
||||||
|
| Type événement | **`inscriptions_types_evenements`** — libellé FR **Sans inscription en ligne** / EN **No online registration** (`sql/MSIN-4482-type-sans-inscription-en-ligne.sql`) ; l’événement pointe via `inscriptions_evenements.te_id` |
|
||||||
|
| Paiement MS1 | Hors scope ; **interdit** le « panier à 0 $ » |
|
||||||
|
| Clé unique | ID du fichier client = **`external_id` ChronoTrack** au push ; même fichier = fallback import CT |
|
||||||
|
| Questions import | **Option C** : matcher ; non matchées → preview + **création confirmée** |
|
||||||
|
| Hub promoteur | **Ne pas toucher pour l’instant** — hub actuel tel quel |
|
||||||
|
| Qui est promoteur | Opérateur MS1 se met **lui-même** promoteur ; droits clients = kits **minimisés via l’existant** |
|
||||||
|
| Mini-hub | **Plus tard**, au fur et à mesure — pas un livrable immédiat |
|
||||||
|
| Waiver | **Hors scope** |
|
||||||
|
| Édition / saisie rapide | Après l’import (phase suivante) |
|
||||||
|
| Finances | Marqueur d’**origine** ; import / saisie rapide exclus des rapports paiement |
|
||||||
|
|
||||||
|
## Origines participant (marqueur)
|
||||||
|
|
||||||
|
| Code | Canal |
|
||||||
|
|---|---|
|
||||||
|
| `inscription_en_ligne` | Flux payant actuel |
|
||||||
|
| `import_fichier` | Bulk modèle fichier |
|
||||||
|
| `saisie_rapide` | Création unitaire / jour J (plus tard) |
|
||||||
|
|
||||||
|
## Phases (réalignées)
|
||||||
|
|
||||||
|
1. **UI import superadm** — dropzone, mapping colonnes + épreuves, templates, bouton **Importer**
|
||||||
|
2. **Écriture `resultats_*`** — upsert clé unique + questions C (sans panier) — en place
|
||||||
|
3. **Mapping pays** (valeurs fichier → `pay_id`) — **étape 2**
|
||||||
|
4. **ChronoTrack** — push avec `par_external_id`
|
||||||
|
5. **Saisie / édition** — fiche rapide (+ jour J)
|
||||||
|
6. **Hub minimisé** — seulement si/quand nécessaire
|
||||||
|
|
||||||
|
## UI import (décisions)
|
||||||
|
|
||||||
|
- Emplacement v1 : **superadm fiche événement** (panneau sous ChronoTrack ; ancre « Import »)
|
||||||
|
- Colonne fichier pour l’épreuve → match vers `epr_id` de l’événement
|
||||||
|
- UX : dropzone ludique + mapping guidé
|
||||||
|
- Template mapping : table `inscriptions_import_templates` (empreinte en-têtes + eve/com)
|
||||||
|
|
||||||
|
## Exemple fichier client (référence)
|
||||||
|
|
||||||
|
Fichier type : export ~Échappée (en-têtes déjà normalisés ; l’original client peut différer).
|
||||||
|
|
||||||
|
| Colonne exemple | Cible MS1 |
|
||||||
|
|---|---|
|
||||||
|
| Attendee # | `par_external_id` (clé unique / CT) |
|
||||||
|
| First Name / Last Name | `par_prenom` / `par_nom` |
|
||||||
|
| Ticket Type | `epr` (valeurs → épreuves MS1) |
|
||||||
|
| No dossard | `no_bib` |
|
||||||
|
| Gender (Male/Female/Other) | `par_sexe` (normaliser à l’écriture) |
|
||||||
|
| Age / Birth Date | `par_age` / `par_naissance` |
|
||||||
|
| Nationalité, urgence, allergies, condition médicale, langue… | questions (option C) |
|
||||||
|
|
||||||
|
**Visibilité liste / CT** : les coquilles `resultats_epreuves_commandees` doivent avoir **`pec_actif = 1`** (sinon absentes de la liste inscriptions et du sync). Les `I{eve}X…` = faux `no_commande` technique, pas un vrai panier.
|
||||||
|
|
||||||
|
~1800 lignes, ~10–12 valeurs distinctes de Ticket Type (distances / jours).
|
||||||
|
|
||||||
|
## Fichiers
|
||||||
|
|
||||||
|
| Rôle | Fichier |
|
||||||
|
|---|---|
|
||||||
|
| SQL templates | `sql/MSIN-4482-import-resultats-templates.sql` |
|
||||||
|
| SQL type eve | `sql/MSIN-4482-type-sans-inscription-en-ligne.sql` |
|
||||||
|
| SQL fix liste | `sql/MSIN-4482-fix-pec-actif-import.sql` |
|
||||||
|
| Logique + UI | `php/inc_fx_import_resultats.php` |
|
||||||
|
| AJAX | `superadm/ajax_import_resultats.php` |
|
||||||
|
| Branche fiche eve | `superadm/php/inc_form.php` |
|
||||||
|
| CSS | `superadm/css/style_blue.css` |
|
||||||
|
|
||||||
|
## Hors scope (maintenant)
|
||||||
|
|
||||||
|
- Hub promoteur / mini-hub
|
||||||
|
- Waiver
|
||||||
|
- Écriture `resultats_*` (prochaine étape après UI)
|
||||||
|
- Import inverse CT → MS1
|
||||||
|
|
||||||
|
## Règles agent
|
||||||
|
|
||||||
|
- Changement minimal ; référencer **MSIN-4482**.
|
||||||
|
- **Pas de panier** pour ces participants — écrire dans `resultats_*`.
|
||||||
|
- **Ne pas modifier le hub promoteur** tant que ce n’est pas redemandé.
|
||||||
|
- Ne pas inventer de waiver pour « signature de dossier ».
|
||||||
|
- SQL : **dev préprod** seulement, puis Navicat + sync outil.
|
||||||
52
.cursor/rules/projet/10-import-mapping-dette.mdc
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
description: MSIN-4520 — dette technique import participants (mapping colonnes) ; à revoir en entier avant d'y retoucher
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Import participants — section à refaire (MSIN-4520)
|
||||||
|
|
||||||
|
**Constat utilisateur (2026-08-14, en pleine course)** : « faut tout revoir cette section-là ».
|
||||||
|
Soirée de patchs empilés (4.73.012 → 4.73.017) sur `php/inc_fx_import_resultats.php`.
|
||||||
|
Le nombre de versions en une soirée **est** le symptôme : on a rustiné au lieu de concevoir.
|
||||||
|
|
||||||
|
**Avant de retoucher cette section : lire cette note en entier et proposer un redesign, pas un patch.**
|
||||||
|
|
||||||
|
## Objectif produit (source de vérité)
|
||||||
|
|
||||||
|
Course **sans inscription en ligne**, fichier client (CSV/Excel) :
|
||||||
|
|
||||||
|
1. Je dépose un fichier → **ça marche**.
|
||||||
|
2. Je le redépose une 2ᵉ fois → **ça écrase** via la **clé unique** (`par_external_id`).
|
||||||
|
3. **Avec ou sans dossard** — le dossard n'est pas obligatoire.
|
||||||
|
4. **Ça n'invente rien** : ni question créée en douce, ni colonne devinée à la place de l'opérateur.
|
||||||
|
|
||||||
|
## Défauts connus (à corriger par conception)
|
||||||
|
|
||||||
|
| # | Défaut | Note |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Le menu déroulant **offre un champ déjà pris** | **Le vrai fix** : un champ MS1 déjà utilisé doit **disparaître du pull-down**. Impossible de choisir deux dossards. |
|
||||||
|
| 2 | Ma « solution » = popup + nettoyage après coup | Jugée **stupide** par l'utilisateur, à raison : on avertit après la faute, et ça vide des sélections par surprise. À remplacer par le point 1. |
|
||||||
|
| 3 | Mapping stocké **champ → colonne** | Structure fragile : deux colonnes sur un champ = écrasement silencieux. Repenser la structure (colonne → champ, ou contrainte forte). |
|
||||||
|
| 4 | Repli par **position** dans `fxImportResultatsBuildColIndex` | Rustiné 3 fois en une soirée (4.73.013/014/017). Logique devenue illisible. À réécrire d'un coup. |
|
||||||
|
| 5 | Profil mémorisé rejoue une **erreur figée** | L'auto-save a mémorisé « Dossard = colonne date » et le rejouait à chaque import. Sanitize ajouté = rustine. |
|
||||||
|
| 6 | **Questions créées sans confirmation** à l'import | `que_tri = 900` = signature des questions auto-créées. L'utilisateur n'avait rien accepté et en a eu. |
|
||||||
|
| 7 | Le **reset** ne supprime pas les questions créées | Il efface participants / pec / réponses, mais `inscriptions_questions` reste pollué. |
|
||||||
|
| 8 | Aucun **aperçu de contrôle** avant écriture | Un dossard qui reçoit une date devrait sauter aux yeux avant l'import, pas se découvrir dans la liste. |
|
||||||
|
|
||||||
|
## Ce qui a été rustiné ce soir (à ne pas prendre pour une base saine)
|
||||||
|
|
||||||
|
- `4.73.012` auto-save du profil — **cause du déraillement**
|
||||||
|
- `4.73.013` mapping strict par nom → dossard **vide**
|
||||||
|
- `4.73.014` repli par position sauf contradiction
|
||||||
|
- `4.73.015` reset purge le profil auto
|
||||||
|
- `4.73.016` sanitize du mapping avant sauvegarde
|
||||||
|
- `4.73.017` interdiction 2 colonnes → 1 champ (UI popup + refus serveur + première colonne gagnante)
|
||||||
|
|
||||||
|
## Règles agent pour cette section
|
||||||
|
|
||||||
|
- **Pas de patch supplémentaire** : proposer un redesign du mapping et le faire valider **avant** de coder.
|
||||||
|
- Priorité 1 = **retirer du pull-down les champs déjà pris** (supprime la classe de bug à la racine).
|
||||||
|
- Ne **jamais** créer de question sans action explicite ; prévoir le nettoyage des questions auto-créées.
|
||||||
|
- Aucune mémorisation de mapping ne doit pouvoir rejouer une association contredite par l'en-tête.
|
||||||
|
- Tester le scénario réel : fichier `Bib / Team Name / Firstname / Lastname / Race / Club / Catégorie`, ~3600 lignes, réimport 2 fois.
|
||||||
|
- Rappel coût-bénéfice : une vérité mesurable (aperçu, contrôle) coûte moins que des essais successifs en production un soir de course.
|
||||||
26
.cursor/rules/projet/10-loader-coureur.mdc
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
description: Loader coureur Ms1 — dès qu’un AJAX / tâche risque d’être longue
|
||||||
|
globs: js/v2/**/*.js,js/ms1-loader.js
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Loader coureur (Ms1Loader) — règle produit bib / v2
|
||||||
|
|
||||||
|
Aussitôt qu’un clic / une tâche JS **risque** d’être long (AJAX deferred, 1er chargement panneau, PDF, batch) → **afficher le bonhomme qui court immédiatement** (`Ms1Loader` / `bibFetch(..., { loader: true })`).
|
||||||
|
|
||||||
|
## Ne pas
|
||||||
|
|
||||||
|
- Partir un `fetch` / travail lourd avec `loader: false` « pour éviter de bloquer » si l’utilisateur attend du feedback.
|
||||||
|
- Attendre la réponse pour seulement ensuite montrer le loader.
|
||||||
|
|
||||||
|
## Pattern bib
|
||||||
|
|
||||||
|
```js
|
||||||
|
bibFetch('/ajax_bib_range.php', { … }, { loader: true });
|
||||||
|
```
|
||||||
|
|
||||||
|
Compteur `bibLoaderDepth` : plusieurs appels concurrents OK.
|
||||||
|
|
||||||
|
## Exceptions rares
|
||||||
|
|
||||||
|
Uniquement si overlay volontairement indésirable **et** feedback local déjà visible (ex. placeholder inline). Documenter le ticket. Sinon = `loader: true`.
|
||||||
38
.cursor/rules/projet/10-php-conventions.mdc
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
description: Conventions PHP legacy MS1 inscription (site + superadm)
|
||||||
|
globs: *.php, php/**/*.php, superadm/**/*.php, ajax*.php, auto_*.php
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Conventions PHP MS1
|
||||||
|
|
||||||
|
Code applicatif legacy (racine, `php/`, `superadm/`). Ne pas imposer ces règles à `v3_ci4/`, `rapports/libs/`, vendors ou librairies tierces.
|
||||||
|
|
||||||
|
## Style à respecter
|
||||||
|
|
||||||
|
- **Suivre le fichier ouvert** : indentation, `array()`, guillemets, style des `if` — ne pas « moderniser » PHP dans un diff parallèle.
|
||||||
|
- **Préfixes de variables** du projet : `$str`, `$int`, `$tab`, `$bln`, `$mem`, `$v…` pour config globale.
|
||||||
|
- **Fonctions métier** : préfixe `fx` (`fxNbItemsPanier`, `inc_fonctions.php`). Réutiliser les helpers existants avant d'en créer.
|
||||||
|
- **Includes** : `php/inc_settings.php`, `php/inc_fonctions.php`, patterns `inc_*` / `inc_fx_*` déjà en place.
|
||||||
|
|
||||||
|
## Qualité
|
||||||
|
|
||||||
|
- Code **propre et lisible** dans la zone modifiée ; pas de code mort ni de `var_dump` laissé en prod.
|
||||||
|
- **Pas de sur-ingénierie** : pas d'abstraction pour un seul appel, pas de classes si le module voisin est procédural.
|
||||||
|
- Sécurité : échapper les sorties HTML, requêtes préparées / fonctions BD existantes — ne pas introduire de concat SQL brute.
|
||||||
|
|
||||||
|
## Version et assets
|
||||||
|
|
||||||
|
- CSS/JS servis par Minify : voir la règle `css-js-version-bump` — incrémenter `_VERSION_CODE` et `_DATE_CODE` dans `php/inc_settings.php`.
|
||||||
|
|
||||||
|
## Exemple de référence ticket
|
||||||
|
|
||||||
|
```php
|
||||||
|
// MSIN-4290
|
||||||
|
define('QR_SECRET_KEY', '…');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MSIN-4405 — Détermine, en LECTURE SEULE, pourquoi une inscription est annulée.
|
||||||
|
*/
|
||||||
|
function fx…()
|
||||||
|
```
|
||||||
88
.cursor/rules/projet/10-radar-mail-crm.mdc
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
description: Radar mail ↔ CRM — porte client (cadrage, pas un tampon métier)
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Radar mail → CRM — deux plans
|
||||||
|
|
||||||
|
Cadrage 2026-08-15, raffiné 2026-08-20. Idée **IDEE-4**. Développement **MSOP-1** (espace MSOP).
|
||||||
|
Pas de code tant que le format du **signal** n’est pas calé sur 2–3 mails réels.
|
||||||
|
|
||||||
|
## Intention
|
||||||
|
|
||||||
|
Aujourd’hui le travail vit dans le **mail** (personne de la semaine) + le **CRM** (Drive déjà mappé au client). On décroche : fil client ↔ imprimeur sans nous, « logo beau » pris pour un Go, relais d’équipe cassé.
|
||||||
|
|
||||||
|
La porte propre : **mail → fiche CRM du client / projet de l’année** et, sur la fiche, **les mails rattachés**. Si c’est un dossard, l’info métier (Drive, lien MS1) est déjà là.
|
||||||
|
|
||||||
|
## Deux plans (ne pas fusionner)
|
||||||
|
|
||||||
|
| Plan | Rôle | Exemple |
|
||||||
|
|---|---|---|
|
||||||
|
| **1. Tampon métier** | Go nommé, daté, dans l’outil qui assume l’erreur | Écran approbation dossards **MSIN-4517** (visuel + séquences) |
|
||||||
|
| **2. Radar** | Lire les mails, rattacher au client, poser un **signal** | Tous types de demandes, pas seulement dossards |
|
||||||
|
|
||||||
|
- L’IA **signale**, elle **ne tamponne pas**. « Le logo est beau » → signal *possible Go visuel*. Le vrai Go reste MS1.
|
||||||
|
- Le Drive client CRM est **déjà là** (`Inscription-Logos-Doss`, etc.). Ne pas remapper.
|
||||||
|
- Pas de boîte mail séparée comme process d’équipe (centaines de tâches). Le canal de travail = **fiche CRM**.
|
||||||
|
- Pas de moteur qui « retrouve tout » par mot-clé comme système. Le radar **colle le fil au client / projet**.
|
||||||
|
|
||||||
|
## Périmètre lecture
|
||||||
|
|
||||||
|
Défaut dès le **jour 1** : **toutes** les boîtes `@ms1timing.com`. Pas une liste blanche. Pas « on commence par `info@` ».
|
||||||
|
Pas `@progiweb.ca`.
|
||||||
|
On **exclut** via une table d’exceptions (modifiable sans recoder). Gmail Watch / Pub/Sub s’abonne à chaque boîte du domaine **sauf** celles en exception.
|
||||||
|
|
||||||
|
| Type | Effet | Exemple v0 |
|
||||||
|
|---|---|---|
|
||||||
|
| **Boîte** | Ne pas ouvrir, pas de Watch | `confirmation@ms1timing.com` (transactionnel inscription) |
|
||||||
|
| **Expéditeur** | Ignorer même s’il atterrit ailleurs | même adresse, si des copies existent |
|
||||||
|
|
||||||
|
Un fil dans plusieurs boîtes = une ligne (`thread_id` Gmail).
|
||||||
|
|
||||||
|
## Porte v0 (produit)
|
||||||
|
|
||||||
|
```
|
||||||
|
Mail ouvert → lien / bouton → fiche CRM du client / projet
|
||||||
|
fiche CRM → fils mail rattachés
|
||||||
|
→ si skill dossards : Drive + lien MS1 (approbation)
|
||||||
|
```
|
||||||
|
|
||||||
|
Le n° dans un code (`DOSS 1842`, `SITE 1842`) = n° **client / projet CRM** (à confirmer si besoin). Code **optionnel** : s’il manque, l’IA propose le client, un humain confirme.
|
||||||
|
|
||||||
|
## Moteur vs skills
|
||||||
|
|
||||||
|
- **Moteur** (stable) : mail → extraire qui / objet / CC / PJ → skill → client CRM → signal + confiance.
|
||||||
|
- **Skill** (ça évolue) : une fiche par type de demande. Ajouter une tâche = une fiche, pas un nouveau projet.
|
||||||
|
|
||||||
|
Skill v0 (seulement deux) :
|
||||||
|
|
||||||
|
| id | Détecte | Code | Action sur la fiche |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `dossards` | dossard, bib, imprimeur, séquence, logo dossard | `DOSS` | Signal + alerte si imprimeur en CC et nous absents + lien MSIN-4517 |
|
||||||
|
| `site` | ouverture site, correction inscription, changement formulaire | `SITE` | Signal / demande CRM (pas de Go MS1) |
|
||||||
|
|
||||||
|
Vérité = **CRM**. On n’invente pas un client depuis un mail. Pas dans le CRM → libellé `Radar/à-mettre-dans-CRM`, on crée dans le CRM, on **relance**. Une correction de libellé est **gardée** (règle adresse → client / projet / skill).
|
||||||
|
|
||||||
|
Un fil Gmail peut avoir **plusieurs skills** (dossards + site dans le même mail) → plusieurs libellés, pas un seul.
|
||||||
|
|
||||||
|
## Signal (contenu minimum)
|
||||||
|
|
||||||
|
Date, de/à/CC, skill, client proposé, résumé 2 phrases, imprimeur en copie ?, « ça ressemble à un oui ? », lien Gmail, lien CRM. Jamais un feu vert d’impression.
|
||||||
|
|
||||||
|
## Évolution
|
||||||
|
|
||||||
|
1. Liste signaux + confirmation (feuille OK 2 semaines si le write CRM n’est pas prêt).
|
||||||
|
2. Écrire le signal **sur la fiche client CRM** (Drive déjà lié).
|
||||||
|
3. Lien inverse : ouvrir le mail → bouton fiche CRM (add-on Gmail ou équivalent).
|
||||||
|
4. Skills 3+ : copier une fiche (TQ, facturation, ChronoTrack…).
|
||||||
|
5. Plus tard : auto-création si confiance haute **et** code présent.
|
||||||
|
|
||||||
|
## Règles agent
|
||||||
|
|
||||||
|
- Relire cette note **et** `10-approbation-dossards.mdc` si on touche dossards + mail/CRM.
|
||||||
|
- Ne pas coder le radar dans MS1. MS1 = tampon (4517) + séquences / Face-Endos. Radar = étage CRM / mail.
|
||||||
|
- Ne pas proposer une boîte `dossards@` comme solution principale (rejeté : trop de tâches, relais hebdo).
|
||||||
|
- Ne pas traiter un mail « oui / logo beau » comme un Go.
|
||||||
|
- Changement minimal ; pas de crawler Gmail maison en v1.
|
||||||
|
- Ticket code : **MSOP-1** (espace MSOP). Idée : **IDEE-4**. Pas de code sous IDEE.
|
||||||
|
- Ne pas proposer un démarrage `info@` seulement : le domaine entier moins exceptions, dès v1.
|
||||||
11
.cursor/rules/projet/10-stack.mdc
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
description: MS1 Inscription — stack et préfixe JIRA (projet)
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Projet MS1 Inscription — stack
|
||||||
|
|
||||||
|
- Stack : PHP legacy (racine / `php/` / `superadm/`) + modules CI4 optionnels (`v3_ci4/`).
|
||||||
|
- Préfixe JIRA : **`MSIN-xxxx`**.
|
||||||
|
- Scripts SQL : `sql/MSIN-xxxx-description-courte.sql`.
|
||||||
|
- Workspace local typique : `C:\dev_v4\ms1inscription4`.
|
||||||
112
.cursor/rules/projet/10-ui-kit-v2.mdc
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
---
|
||||||
|
description: MS1 UI kit v2 — consulter AVANT tout nouvel écran / panneau (surtout choisir des champs)
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# UI kit v2 MS1 — source de vérité visuelle
|
||||||
|
|
||||||
|
**Obligatoire** avant de coder un nouvel écran de **choix de champs / colonnes / ordre** (ou tout panneau outil v2) : relire cette règle.
|
||||||
|
|
||||||
|
Quand l’utilisateur demande « choisir des champs », « colonnes », « ordre d’affichage », « appliquer à toutes » → **même réalité visuelle** = mockup **A**, pas une grille de checkboxes ni ↑↓ années 80.
|
||||||
|
|
||||||
|
## Visuel canonique — mockup A (tous les choosers)
|
||||||
|
|
||||||
|
**Fichier de vérité** : `_maquettes/msin-4512-mockup-A-dual-panel.png`
|
||||||
|
|
||||||
|
| Zone | Comportement |
|
||||||
|
|---|---|
|
||||||
|
| Gauche **Disponibles** | Recherche + **chips / pills** (pas une grille de cases nues) |
|
||||||
|
| Droite **Sélectionnés / Sur le PDF** | Liste ordonnée avec **poignée drag** (SortableJS), pas flèches ↑↓ |
|
||||||
|
| Orientation / options | Segmented control / pills (pas radios « formulaire ») |
|
||||||
|
| Budget / fit | Barre fine (P6) |
|
||||||
|
| Multi-épreuves | Entête (orient/budget) toujours ouvert. Chaque épreuve = même boîte repliable (1re ouverte). Apply-all depuis l’épreuve cliquée. Pas de fusion entête↔1re épreuve. |
|
||||||
|
| Exceptions | (même boîte que toutes les épreuves) |
|
||||||
|
|
||||||
|
### Apply-all (P4) — règle produit
|
||||||
|
|
||||||
|
- Libellé : **Appliquer à toutes les épreuves** (Info).
|
||||||
|
- Source = l’épreuve courante / celle qu’on vient de régler (souvent la 1ʳᵉ), **pas** un panneau maître séparé.
|
||||||
|
- Questions absentes sur une cible = ignorées.
|
||||||
|
- Garde-fous (confirm si déjà custom) = plus tard si besoin.
|
||||||
|
|
||||||
|
### Interdit (retour 80s)
|
||||||
|
|
||||||
|
- Grille dense de checkboxes « Champs de base / Questions »
|
||||||
|
- Boîte « Configuration commune » en plus des épreuves
|
||||||
|
- Monter/descendre comme UI principale d’ordre (drag = défaut)
|
||||||
|
- Réinventer un look différent à chaque ticket « choisir des champs »
|
||||||
|
|
||||||
|
## Code (à aligner sur mockup A)
|
||||||
|
|
||||||
|
| Couche | Fichier |
|
||||||
|
|---|---|
|
||||||
|
| PHP | `php/inc_fx_ms1_ui.php` (`fxMs1UiRender*`) |
|
||||||
|
| CSS | `css/style.css` — `.ms1-ui-*` (variables = escape hatch thème) |
|
||||||
|
| JS | `js/v2/ms1-ui-chooser.js` + **SortableJS** pour le drag |
|
||||||
|
| Mockup | `_maquettes/msin-4512-mockup-A-dual-panel.png` |
|
||||||
|
|
||||||
|
Les helpers actuels (cases / ↑↓) sont un **pont** : tout nouvel écran chooser et le refactor MSIN-4512 doivent viser le dual-panel A, pas le form legacy.
|
||||||
|
|
||||||
|
## Gate
|
||||||
|
|
||||||
|
1. Besoin = P2/P3/P4 ? → ouvrir mockup A, réutiliser le kit.
|
||||||
|
2. Pattern nouveau hors chooser → proposer mockup dans `_maquettes/` → validation → code.
|
||||||
|
3. Métier (PDF, SQL) après le visuel.
|
||||||
|
|
||||||
|
## Escape hatch
|
||||||
|
|
||||||
|
CSS variables sur `.ms1-ui-chooser` (`--ms1-ui-accent`, radius, etc.) — pas forker le composant pour une couleur.
|
||||||
|
|
||||||
|
## Mobile
|
||||||
|
|
||||||
|
Flex-wrap, zones tactiles ~44px, drag SortableJS OK touch ; pas de largeurs fixes cassantes.
|
||||||
|
|
||||||
|
## Chrome hors chooser (inchangé)
|
||||||
|
|
||||||
|
| Surface | Rôle |
|
||||||
|
|---|---|
|
||||||
|
| Fiche `inscr-gestion-fiche-section--*` | Palette terrain |
|
||||||
|
| `#module-bib` `.bib-tool-detail` | Cadre panneau outil |
|
||||||
|
|
||||||
|
## Patterns (catalogue)
|
||||||
|
|
||||||
|
| Id | Besoin | Réalité visuelle |
|
||||||
|
|---|---|---|
|
||||||
|
| P1 | Panneau outil bib | `.bib-tool-detail` |
|
||||||
|
| P2 | Choisir champs | Mockup A — panneau **Disponibles** (chips) |
|
||||||
|
| P3 | Ordre | Mockup A — panneau droit + **drag** SortableJS |
|
||||||
|
| P4 | Appliquer à toutes | Bouton unique ; copie depuis épreuve source |
|
||||||
|
| P5 | Orientation / choix exclusif | Pills / segmented (mockup A) |
|
||||||
|
| P6 | Fit / budget | Barre mm / statut |
|
||||||
|
| P7 | Choix binaire On/Off | **Switch** (`.ms1-ui-switch` / `fxMs1UiRenderSwitch`) — **pas** checkbox |
|
||||||
|
|
||||||
|
### P7 — On/Off (règle produit)
|
||||||
|
|
||||||
|
- Besoin = un seul interrupteur vrai/faux (inclure X, activer Y) → **switch On/Off**.
|
||||||
|
- Interdit : checkbox nue / « crochet form » pour un binaire.
|
||||||
|
- Garder **checkbox / chips** seulement pour **choix multiples** (plusieurs champs / options en parallèle).
|
||||||
|
- Segmented (P5) = 2+ options **exclusives labelisées** (Portrait / Paysage). Switch = un flag, libellé à côté.
|
||||||
|
- Code : `fxMs1UiRenderSwitch()` + CSS `.ms1-ui-switch*`.
|
||||||
|
|
||||||
|
## Marqueur migration
|
||||||
|
|
||||||
|
Root : `fxMs1UiKitRootAttrs()` → `data-ms1-ui-kit="1"` + `data-ms1-ui-screen` + `data-ms1-ui-patterns` + `data-ms1-ui-ticket`.
|
||||||
|
Commentaire : `// MS1-UI-KIT — …`
|
||||||
|
Grep : `data-ms1-ui-kit` / `MS1-UI-KIT`.
|
||||||
|
|
||||||
|
### Inventaire
|
||||||
|
|
||||||
|
| screen | Patterns | Ticket | Statut |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `bib-dist-print` | P2–P7 | MSIN-4512 | **mockup A** + switch P7 include sans no_bib |
|
||||||
|
| `bib-prod-fields` | P2–P4 + side | MSIN-4515 | **mockup A** + pastilles Face/Endos/Info (pas de fit) |
|
||||||
|
| Face/Endos / exports futurs | P2–P4 | — | même mockup A quand on touche |
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- Nouveau chooser sans mockup A
|
||||||
|
- Boîte « config commune » séparée
|
||||||
|
- Checkboxes / ↑↓ comme UI finale
|
||||||
|
- Checkbox pour un On/Off binaire (→ switch P7)
|
||||||
|
- HTML one-off hors `ms1-ui-*`
|
||||||
|
- FR/EN en dur ; JS v2 dans `inc_footer_scripts.php`
|
||||||
36
.cursor/rules/projet/10-v2.mdc
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
description: MS1 modules v2 — bib v4 et gestion inscriptions (hors legacy)
|
||||||
|
globs: js/v2/**,php/inc_v2_assets.php,inc_tableau_gestion_epreuves.php,inc_tableau_inscriptions_gestion.php,ajax_bib_range.php,ajax_inscr_gestion.php,php/inc_fx_promoteur.php,php/inc_fx_inscriptions_gestion.php
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Modules v2 MS1
|
||||||
|
|
||||||
|
## Périmètre v2 (non prod, développement actif)
|
||||||
|
|
||||||
|
- **Bib assignments v4** — `#module-bib`, `fxShowBibTool4`, `ajax_bib_range.php`
|
||||||
|
- **Gestion inscriptions v2** — `#module-inscr-gestion`, `inc_fx_inscriptions_gestion.php`, `ajax_inscr_gestion.php`
|
||||||
|
|
||||||
|
## Règle JS
|
||||||
|
|
||||||
|
- **Ne jamais** ajouter de code v2 dans `inc_footer_scripts.php`.
|
||||||
|
- Nouveau JS v2 → `js/v2/` (fichier existant ou nouveau selon le module).
|
||||||
|
- Enregistrer le script sur la page concernée : `fxV2RegisterScript('bib-assignments')` ou `'inscr-gestion'`.
|
||||||
|
- Config PHP → JS : `data-*` sur le module (bib) ou `window.MS1_V2_INSCR_GESTION` (inscr).
|
||||||
|
- UI chooser : `js/v2/ms1-ui-chooser.js` chargé **avant** `bib-assignments` (auto via `fxV2RenderFooterScripts`).
|
||||||
|
|
||||||
|
## Legacy (ne pas toucher sauf pont minimal)
|
||||||
|
|
||||||
|
- `.btn_bib` jQuery, `fxShowBibTool2/3`, `inc_tableau_dossards.php`
|
||||||
|
|
||||||
|
## Fichiers de référence
|
||||||
|
|
||||||
|
| Rôle | Fichier |
|
||||||
|
|------|---------|
|
||||||
|
| Enregistrement / rendu scripts | `php/inc_v2_assets.php` |
|
||||||
|
| Bib v4 client | `js/v2/bib-assignments.js` |
|
||||||
|
| Inscr gestion client | `js/v2/inscr-gestion.js` |
|
||||||
|
|
||||||
|
## Visuel
|
||||||
|
|
||||||
|
Source de vérité : **`projet/10-ui-kit-v2.mdc`** (palette, boutons, patterns P1–P6, gate avant nouvel écran). Ne pas dupliquer ni inventer une palette ici.
|
||||||
83
.cursor/rules/projet/10-workflow.mdc
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
description: Workflow MS1 — JIRA, validation avant gros changements, code legacy, promotion BD
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Workflow MS1 inscription
|
||||||
|
|
||||||
|
## Numéro JIRA (MSIN-xxxx)
|
||||||
|
|
||||||
|
- Au début d'une tâche de développement (nouvelle feature, correctif, refactor), **demander le numéro JIRA** si l'utilisateur ne l'a pas fourni.
|
||||||
|
- Préfixe courant features : `MSIN-xxxx`. Continuité / ops BD peut aussi être `CON-xxxx`.
|
||||||
|
- Ne pas bloquer une simple question ou une lecture du code — seulement quand on s'apprête à modifier des fichiers.
|
||||||
|
- Référencer le ticket dans le code et les scripts SQL : `// MSIN-4290` ou `-- MSIN-4379 — description`.
|
||||||
|
- Nommer les nouveaux scripts SQL : `sql/MSIN-xxxx-description-courte.sql`.
|
||||||
|
|
||||||
|
## Promotion BD (règle absolue)
|
||||||
|
|
||||||
|
Ordre **non négociable** pour faire passer structure + données vers d’autres environnements :
|
||||||
|
|
||||||
|
1. **Scripts SQL** (`sql/MSIN-*.sql`) → exécutés **uniquement** sur **dev préprod**.
|
||||||
|
2. **Navicat** → copie de **structure** (préprod → autres env : dev prod, client préprod, client prod).
|
||||||
|
3. **Outil soft sync** (`php/sync_static_db.php`, CON-333) → peupler / aligner les **données** (manquants + corrections).
|
||||||
|
|
||||||
|
**Interdit** : demander ou suggérer de « rejouer le SQL » sur prod, préprod client, ou tout env autre que **dev préprod**.
|
||||||
|
|
||||||
|
- Table absente / Erreur lecture ailleurs = d’abord **structure Navicat** depuis préprod, **puis** sync outil.
|
||||||
|
- `info` : jamais de DELETE (auto-création `afficheTexte`). Soft only.
|
||||||
|
- Catalogues (droits, pages, doc…) : pas d’auto-création ; l’alignement données passe par l’outil, pas par un second import SQL.
|
||||||
|
|
||||||
|
## Portée des changements
|
||||||
|
|
||||||
|
- **Changement minimal** : corriger uniquement ce qui est demandé.
|
||||||
|
- Pas de refactor large, renommage massif ou « nettoyage » opportuniste sans accord explicite.
|
||||||
|
- Pour le code legacy (~10 ans), privilégier la cohérence locale (fichier / module touché) plutôt qu'une modernisation globale.
|
||||||
|
- Approche : **meilleur coût-bénéfice** (`entreprise/00-cout-benefice.mdc`) — ex. calibrateur GUI plutôt qu’une série d’essais PDF.
|
||||||
|
|
||||||
|
## Incohérences repérées
|
||||||
|
|
||||||
|
Quand du code voisin contredit le pattern du fichier ou du module :
|
||||||
|
|
||||||
|
1. **Signaler** brièvement l'incohérence à l'utilisateur.
|
||||||
|
2. **Proposer** une correction ciblée ou l'alignement sur le pattern dominant du module.
|
||||||
|
3. **Attendre confirmation** avant de corriger du code hors périmètre de la tâche.
|
||||||
|
|
||||||
|
## Fin de tâche
|
||||||
|
|
||||||
|
- Résumer ce qui a changé (fichiers, comportement).
|
||||||
|
- Pour le SQL : rappeler qu’il s’exécute **seulement en dev préprod**, puis structure Navicat + sync outil pour le reste — **pas** « exécuter ce script en prod ».
|
||||||
|
- Mentionner les étapes manuelles restantes (déploiement code, Navicat, sync soft, cache).
|
||||||
|
|
||||||
|
## Git push — jamais (l’utilisateur synchronise)
|
||||||
|
|
||||||
|
- **JAMAIS** de `git push` (voir `entreprise/00-jamais-push.mdc`).
|
||||||
|
- Même s’il dit « pousse / sync / déploie » : ne pas exécuter le push.
|
||||||
|
- Flux : corriger en local → dire que c’est prêt → **lui** synchronise.
|
||||||
|
- Le webhook serveur ne se déclenche **que** après **son** sync.
|
||||||
|
|
||||||
|
## Commentaire JIRA après sync (obligatoire)
|
||||||
|
|
||||||
|
Quand **l’utilisateur** a synchronisé vers Gitea / `dev`, **poster un commentaire** sur le ticket JIRA de la tâche (`MSIN-xxxx` ou `CON-xxxx`).
|
||||||
|
|
||||||
|
1. Lire `_VERSION_CODE` (et `_DATE_CODE` si présent) dans `php/inc_settings.php`.
|
||||||
|
2. Noter date/heure du push en **America/Toronto** (`YYYY-MM-DD HH:mm`).
|
||||||
|
3. Via MCP Atlassian `addCommentToJiraIssue` :
|
||||||
|
- `cloudId` : `progiwebinc.atlassian.net` (ou UUID via `getAccessibleAtlassianResources`)
|
||||||
|
- `issueIdOrKey` : le ticket de la tâche
|
||||||
|
- `contentFormat` : `markdown`
|
||||||
|
- Corps (format fixe) :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Déployé code v{_VERSION_CODE} — push {YYYY-MM-DD HH:mm} (America/Toronto) — branche {branche} — {_DATE_CODE}
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Déployé code v4.72.883 — push 2026-07-28 21:27 (America/Toronto) — branche dev — 2026-07-28
|
||||||
|
```
|
||||||
|
|
||||||
|
- Si plusieurs tickets touchés : commenter **chaque** ticket listé dans le message de commit / demandé par l’utilisateur.
|
||||||
|
- Si pas de ticket identifiable : **demander** le numéro avant de commenter (ne pas inventer).
|
||||||
|
- Ne pas dupliquer le même commentaire pour le même push (un commentaire par push réussi).
|
||||||
|
- Confirmer brièvement à l’utilisateur que le commentaire JIRA est posté.
|
||||||
7
.gitignore
vendored
@ -7,6 +7,12 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
|
# Composer artifacts (ne jamais committer vendor/)
|
||||||
|
/v3_ci4/vendor/
|
||||||
|
|
||||||
|
# IDE — ne jamais committer les connexions SQL (copier settings.json.example)
|
||||||
|
.vscode/settings.json
|
||||||
|
|
||||||
# Compiled Java class files
|
# Compiled Java class files
|
||||||
*.class
|
*.class
|
||||||
|
|
||||||
@ -68,6 +74,7 @@ Thumbs.db
|
|||||||
/test/
|
/test/
|
||||||
/tpp/
|
/tpp/
|
||||||
/qr/
|
/qr/
|
||||||
|
/output/
|
||||||
/bp/
|
/bp/
|
||||||
/fnq/
|
/fnq/
|
||||||
/images/backgrounds/
|
/images/backgrounds/
|
||||||
|
|||||||
30
.htaccess
@ -1,24 +1,21 @@
|
|||||||
RewriteEngine On
|
RewriteEngine On
|
||||||
RewriteBase /
|
RewriteBase /
|
||||||
|
|
||||||
# ====================================
|
# Redirect www — prod client seulement (en dev le redirect etait commente pour la meme raison)
|
||||||
# CI4 ROUTES PRIORITAIRES
|
RewriteCond %{HTTP_HOST} !^www\. [NC]
|
||||||
# ====================================
|
RewriteCond %{HTTP_HOST} !^preprod\. [NC]
|
||||||
|
RewriteCond %{HTTP_HOST} !\.progiweb\.net$ [NC]
|
||||||
|
RewriteRule ^(.*)$ https://www.%{HTTP_HOST}/$1 [R=302,L]
|
||||||
|
|
||||||
|
# CI4 (dev / v3) — inoffensif en prod si v3 absent
|
||||||
RewriteRule ^test/?$ v3/index.php/test [E=IS_CI4:1,L]
|
RewriteRule ^test/?$ v3/index.php/test [E=IS_CI4:1,L]
|
||||||
RewriteRule ^admin-dashboard/?$ v3/index.php/admin-dashboard [E=IS_CI4:1,L]
|
RewriteRule ^admin-dashboard/?$ v3/index.php/admin-dashboard [E=IS_CI4:1,L]
|
||||||
RewriteRule ^resultats-nouveaux/?$ v3/index.php/resultats-nouveaux [E=IS_CI4:1,L]
|
RewriteRule ^resultats-nouveaux/?$ v3/index.php/resultats-nouveaux [E=IS_CI4:1,L]
|
||||||
|
|
||||||
# ====================================
|
|
||||||
# STOP SI CI4
|
|
||||||
# ====================================
|
|
||||||
|
|
||||||
RewriteCond %{ENV:IS_CI4} =1
|
RewriteCond %{ENV:IS_CI4} =1
|
||||||
RewriteRule ^ - [L]
|
RewriteRule ^ - [L]
|
||||||
|
|
||||||
# ====================================
|
RewriteCond %{SCRIPT_FILENAME} !-d
|
||||||
# LEGACY ROUTES
|
RewriteCond %{SCRIPT_FILENAME} !-f
|
||||||
# ====================================
|
|
||||||
|
|
||||||
RewriteRule ^kc/([\w\-]+)/([\w\-]+)/?$ ./kc.php?param1=$1¶m2=$2 [NC,QSA]
|
RewriteRule ^kc/([\w\-]+)/([\w\-]+)/?$ ./kc.php?param1=$1¶m2=$2 [NC,QSA]
|
||||||
|
|
||||||
@ -28,6 +25,9 @@ RewriteRule ^cart/([\w\-]+)/([\w\-]+)/?$ ./panier.php?lang=en&step=$1&code=$2 [N
|
|||||||
RewriteRule ^compte/?([\w\-]+)?/?$ ./compte.php?lang=fr&code=$1 [NC,QSA]
|
RewriteRule ^compte/?([\w\-]+)?/?$ ./compte.php?lang=fr&code=$1 [NC,QSA]
|
||||||
RewriteRule ^account/?([\w\-]+)?/?$ ./compte.php?lang=en&code=$1 [NC,QSA]
|
RewriteRule ^account/?([\w\-]+)?/?$ ./compte.php?lang=en&code=$1 [NC,QSA]
|
||||||
|
|
||||||
|
RewriteRule ^mobile/?$ ./mobile.php?lang=fr [NC,QSA]
|
||||||
|
RewriteRule ^mobile/en/?$ ./mobile.php?lang=en [NC,QSA]
|
||||||
|
|
||||||
RewriteRule ^reserver/([\w\-]+)/(\d+)/?$ ./reserver.php?lang=fr&code=$1&id=$2 [NC,QSA]
|
RewriteRule ^reserver/([\w\-]+)/(\d+)/?$ ./reserver.php?lang=fr&code=$1&id=$2 [NC,QSA]
|
||||||
RewriteRule ^book/([\w\-]+)/(\d+)/?$ ./reserver.php?lang=en&code=$1&id=$2 [NC,QSA]
|
RewriteRule ^book/([\w\-]+)/(\d+)/?$ ./reserver.php?lang=en&code=$1&id=$2 [NC,QSA]
|
||||||
|
|
||||||
@ -36,6 +36,12 @@ RewriteRule ^donate/([\w\-]+)/([\w\-]+)/?/?([\w\-]+)?/?$ ./dons.php?lang=en&code
|
|||||||
|
|
||||||
RewriteRule ^page/([\w\-]+)/?(fr|en)?/?$ ./index.php?code=$1&lang=$2 [NC,QSA]
|
RewriteRule ^page/([\w\-]+)/?(fr|en)?/?$ ./index.php?code=$1&lang=$2 [NC,QSA]
|
||||||
|
|
||||||
|
RewriteCond %{REQUEST_URI} !^/(rapports_js|superadm)(/|$) [NC]
|
||||||
RewriteRule ^([\w\-]+)/?(fr|en)?/?([\w\-]+)?/?$ ./evenements.php?code=$1&lang=$2&tab=$3 [NC,QSA]
|
RewriteRule ^([\w\-]+)/?(fr|en)?/?([\w\-]+)?/?$ ./evenements.php?code=$1&lang=$2&tab=$3 [NC,QSA]
|
||||||
|
|
||||||
RewriteCond %{REQUEST_URI} !^/(rapports_js|superadm/.*)$
|
# php -- BEGIN cPanel-generated handler, do not edit
|
||||||
|
# Set the "ea-php81" package as the default "PHP" programming language.
|
||||||
|
<IfModule mime_module>
|
||||||
|
AddHandler application/x-httpd-ea-php81___lsphp .php .php8 .phtml
|
||||||
|
</IfModule>
|
||||||
|
# php -- END cPanel-generated handler, do not edit
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
// Copier vers settings.json (gitignored) et renseigner vos identifiants locaux.
|
||||||
"sqltools.connections": [
|
"sqltools.connections": [
|
||||||
{
|
{
|
||||||
"mysqlOptions": {
|
"mysqlOptions": {
|
||||||
@ -11,15 +12,15 @@
|
|||||||
"port": 3306,
|
"port": 3306,
|
||||||
"sshOptions": {
|
"sshOptions": {
|
||||||
"port": 7822,
|
"port": 7822,
|
||||||
"host": "104.254.182.111",
|
"host": "YOUR_SSH_HOST",
|
||||||
"username": "devinscription",
|
"username": "YOUR_SSH_USER",
|
||||||
"password": "M-G7+[NdA34H"
|
"password": "YOUR_SSH_PASSWORD"
|
||||||
},
|
},
|
||||||
"driver": "MariaDB",
|
"driver": "MariaDB",
|
||||||
"name": "astral(dev) ms1inscription",
|
"name": "local dev connection",
|
||||||
"username": "devinscription_bd",
|
"username": "YOUR_DB_USER",
|
||||||
"password": "x$@_v0-l_I((",
|
"password": "YOUR_DB_PASSWORD",
|
||||||
"database": "devinscription_preprod"
|
"database": "YOUR_DATABASE"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
BIN
MSIN-4379-bib_v4-traductions.xlsx
Normal file
623
_maquettes/msin-4511-prep-commande-mockup.html
Normal file
@ -0,0 +1,623 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>MSIN-4511 — Préparation commande dossard / puces</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #eef1f4;
|
||||||
|
--card: #fff;
|
||||||
|
--ink: #1c2430;
|
||||||
|
--muted: #5c6b7a;
|
||||||
|
--line: #d5dde5;
|
||||||
|
--accent: #2f6fed;
|
||||||
|
--endos: #c62828;
|
||||||
|
--ok: #1b7a4a;
|
||||||
|
--warn: #b45309;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { margin: 0; }
|
||||||
|
body {
|
||||||
|
font: 14px/1.45 system-ui, Segoe UI, sans-serif;
|
||||||
|
color: var(--ink);
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
.wrap { max-width: 1080px; margin: 0 auto; padding: 24px 20px 80px; }
|
||||||
|
h1 { font-size: 22px; margin: 0 0 6px; }
|
||||||
|
h2 { font-size: 16px; margin: 0; }
|
||||||
|
p { margin: 0; }
|
||||||
|
.lede { color: var(--muted); margin-bottom: 18px; }
|
||||||
|
nav.tabs { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 18px; }
|
||||||
|
nav.tabs button {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: #fff;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
nav.tabs button.on { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||||
|
.panel { display: none; }
|
||||||
|
.panel.on { display: block; }
|
||||||
|
.box {
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.box.dash { border-style: dashed; color: var(--muted); }
|
||||||
|
.n { color: var(--accent); font-weight: 700; margin-right: 6px; }
|
||||||
|
.row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||||
|
.grow { flex: 1; }
|
||||||
|
.muted { color: var(--muted); font-size: 13px; }
|
||||||
|
.tiny { color: var(--muted); font-size: 12px; }
|
||||||
|
.grid3, .grid2, .grid5 { display: grid; gap: 12px; }
|
||||||
|
.grid3 { grid-template-columns: 1fr 1fr 1fr; }
|
||||||
|
.grid2 { grid-template-columns: 1fr 1fr; }
|
||||||
|
.grid5 { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.grid3, .grid2 { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
.pagebar {
|
||||||
|
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
||||||
|
margin-bottom: 14px; padding-bottom: 12px; border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.fake-nav { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||||
|
.fake-nav span {
|
||||||
|
border: 1px solid var(--line); background: #fff; padding: 6px 10px;
|
||||||
|
border-radius: 6px; font-size: 13px;
|
||||||
|
}
|
||||||
|
.fake-nav span.on { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||||
|
.fake-nav span.off { text-decoration: line-through; color: #9aa6b2; background: #f3f5f7; }
|
||||||
|
.who {
|
||||||
|
display: inline-block; font-size: 11px; font-weight: 700;
|
||||||
|
border-radius: 99px; padding: 1px 7px; margin-left: 4px;
|
||||||
|
border: 1px solid var(--line); color: var(--muted);
|
||||||
|
}
|
||||||
|
.who.auto { border-color: #b7e0c4; color: var(--ok); background: #edf8f0; }
|
||||||
|
.who.edit { border-color: #c9d8f5; color: var(--accent); background: #eef4ff; }
|
||||||
|
.fold-head { cursor: pointer; user-select: none; }
|
||||||
|
.chev { display: inline-block; width: 1.2em; color: var(--muted); font-size: 12px; }
|
||||||
|
.fold-body { margin-top: 12px; }
|
||||||
|
.fold.is-min .fold-body { display: none; }
|
||||||
|
.jalon {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.5fr 160px minmax(120px, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.jalon:last-child { border-bottom: 0; }
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.jalon { grid-template-columns: 1fr; gap: 4px; }
|
||||||
|
}
|
||||||
|
label.field { display: block; font-size: 12px; color: var(--muted); margin-bottom: 4px; }
|
||||||
|
input[type="date"], input[type="search"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
.stat { font-size: 18px; font-weight: 650; }
|
||||||
|
.btn {
|
||||||
|
border: 0;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn.ghost { background: #fff; color: var(--ink); border: 1px solid var(--line); }
|
||||||
|
.sheet {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #f7f9fb;
|
||||||
|
}
|
||||||
|
.epr { border: 1px solid #c9d4e3; border-radius: 8px; padding: 12px; background: #fafbfd; }
|
||||||
|
table.seg { width: 100%; border-collapse: collapse; }
|
||||||
|
table.seg th, table.seg td { text-align: left; padding: 8px 6px; border-bottom: 1px solid var(--line); }
|
||||||
|
table.seg th { font-size: 12px; color: var(--muted); font-weight: 600; }
|
||||||
|
.switch {
|
||||||
|
width: 40px; height: 22px; border-radius: 99px; border: 0; padding: 2px;
|
||||||
|
background: #c5ced6; cursor: pointer; position: relative;
|
||||||
|
}
|
||||||
|
.switch.on { background: var(--accent); }
|
||||||
|
.switch i {
|
||||||
|
display: block; width: 18px; height: 18px; border-radius: 50%;
|
||||||
|
background: #fff; transform: translateX(0);
|
||||||
|
}
|
||||||
|
.switch.on i { transform: translateX(18px); }
|
||||||
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
||||||
|
.chip {
|
||||||
|
border: 1px solid var(--line); border-radius: 99px; padding: 4px 10px;
|
||||||
|
background: #fff; font-size: 12px;
|
||||||
|
}
|
||||||
|
.card-field {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
border: 1px solid var(--line); border-radius: 8px;
|
||||||
|
padding: 8px 10px; margin-bottom: 6px; background: #fff;
|
||||||
|
}
|
||||||
|
.pills { display: flex; gap: 4px; margin-left: auto; }
|
||||||
|
.pill { font-size: 11px; font-weight: 700; border-radius: 99px; padding: 2px 8px; border: 1px solid var(--line); color: var(--muted); }
|
||||||
|
.pill.face.on { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||||
|
.pill.endos.on { background: var(--endos); color: #fff; border-color: var(--endos); }
|
||||||
|
.pill.info.on { background: #fff; color: var(--accent); border-color: var(--accent); }
|
||||||
|
table.data { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||||
|
table.data th, table.data td { text-align: left; padding: 8px; border-bottom: 1px solid var(--line); }
|
||||||
|
table.data th { font-size: 12px; color: var(--muted); }
|
||||||
|
.note { background: #eef4ff; border: 1px solid #c9d8f5; border-radius: 8px; padding: 10px 12px; margin-bottom: 14px; }
|
||||||
|
.warn { background: #fff7ed; border: 1px solid #f0d3a8; border-radius: 8px; padding: 10px 12px; margin: 12px 0; }
|
||||||
|
.ok { background: #edf8f0; border: 1px solid #b7e0c4; border-radius: 8px; padding: 10px 12px; margin-top: 12px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>Préparation commande dossard / puces</h1>
|
||||||
|
<p class="lede">Maquette HTML — scroll normal, interrupteurs cliquables. Pas de code écran MS1. Ouvre ce fichier dans Chrome / Edge.</p>
|
||||||
|
|
||||||
|
<nav class="tabs">
|
||||||
|
<button type="button" class="on" data-tab="ecran">Page production</button>
|
||||||
|
<button type="button" data-tab="ou">Où ça vit</button>
|
||||||
|
<button type="button" data-tab="jalons">Les 5 dates</button>
|
||||||
|
<button type="button" data-tab="dash">Tableau de bord ops</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section id="ecran" class="panel on">
|
||||||
|
<div class="pagebar">
|
||||||
|
<button type="button" class="btn ghost">← Dossards (quantités / assignation)</button>
|
||||||
|
<div>
|
||||||
|
<strong>Commande dossards / puces</strong>
|
||||||
|
<div class="tiny">Demi-marathon Caisse Desjardins · page à part, pas un 4e onglet</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="note">Page dédiée ops. Les interrupteurs des segments mettent à jour les deux fichiers. Cadenas = fichier imprimeur seulement.</div>
|
||||||
|
|
||||||
|
<div class="box fold">
|
||||||
|
<div class="row fold-head">
|
||||||
|
<span class="chev">▼</span>
|
||||||
|
<h2><span class="n">1</span>Dates importantes</h2>
|
||||||
|
</div>
|
||||||
|
<div class="fold-body">
|
||||||
|
<p class="muted" style="margin-bottom:4px">Toutes des jalons (échéances). La colonne réalité se remplira toute seule plus tard, quand l’action aura vraiment eu lieu.</p>
|
||||||
|
<div class="jalon">
|
||||||
|
<div>
|
||||||
|
<strong>Le client les veut</strong>
|
||||||
|
<div class="tiny">Les dossards chez le client pour cette date</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="field">Jalon</label>
|
||||||
|
<input type="date" value="2026-09-07">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="tiny">Réalité <span class="who auto">auto plus tard</span></div>
|
||||||
|
<div>—</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="jalon">
|
||||||
|
<div>
|
||||||
|
<strong>Visuel approuvé</strong>
|
||||||
|
<div class="tiny">Le visuel doit être tamponné pour cette date</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="field">Jalon</label>
|
||||||
|
<input type="date" value="2026-08-20">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="tiny">Réalité <span class="who auto">auto plus tard</span></div>
|
||||||
|
<div>—</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="jalon">
|
||||||
|
<div>
|
||||||
|
<strong>Envoi imprimeur</strong>
|
||||||
|
<div class="tiny">Le fichier dossards doit être parti pour cette date</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="field">Jalon</label>
|
||||||
|
<input type="date" value="2026-08-22">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="tiny">Réalité <span class="who auto">auto plus tard</span></div>
|
||||||
|
<div>—</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="jalon">
|
||||||
|
<div>
|
||||||
|
<strong>Réception de l’imprimeur</strong>
|
||||||
|
<div class="tiny">On veut les dossards imprimés reçus pour cette date</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="field">Jalon</label>
|
||||||
|
<input type="date" value="2026-09-01">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="tiny">Réalité <span class="who auto">auto plus tard</span></div>
|
||||||
|
<div>—</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="jalon">
|
||||||
|
<div>
|
||||||
|
<strong>Envoi puces</strong>
|
||||||
|
<div class="tiny">On veut que les puces soient envoyées pour cette date</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="field">Jalon</label>
|
||||||
|
<input type="date" value="2026-08-25">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="tiny">Réalité <span class="who auto">auto plus tard</span></div>
|
||||||
|
<div>—</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box fold">
|
||||||
|
<div class="row fold-head">
|
||||||
|
<span class="chev">▼</span>
|
||||||
|
<h2><span class="n">2</span>Deux fichiers Excel</h2>
|
||||||
|
</div>
|
||||||
|
<div class="fold-body">
|
||||||
|
<p class="muted" style="margin-bottom:12px">Un fichier par destinataire. Générer remplit la <em>réalité</em> du jalon correspondant, pas l’échéance.</p>
|
||||||
|
<div class="grid2">
|
||||||
|
<div class="sheet">
|
||||||
|
<div class="tiny">Fichier 1 · Imprimeur</div>
|
||||||
|
<strong>commande-dossards.xlsx</strong>
|
||||||
|
<p id="out-doss" style="margin:8px 0">5 km · 1–200</p>
|
||||||
|
<p class="tiny">Face / Endos / Info. Séquences « commande dossards ». Verrouille ces séquences. Réalité = envoi imprimeur.</p>
|
||||||
|
<button type="button" class="btn" style="margin-top:10px" id="btn-doss">Générer le fichier imprimeur</button>
|
||||||
|
</div>
|
||||||
|
<div class="sheet">
|
||||||
|
<div class="tiny">Fichier 2 · Commande puces MS1</div>
|
||||||
|
<strong>commande-puces.xlsx</strong>
|
||||||
|
<p id="out-puces" style="margin:8px 0">5 km · 1–200 · 201–400</p>
|
||||||
|
<p class="tiny">Plages + qté. Même numéro que le dossard. Ne verrouille pas. Réalité = envoi puces.</p>
|
||||||
|
<button type="button" class="btn ghost" style="margin-top:10px" id="btn-puces">Générer le fichier puces</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box fold">
|
||||||
|
<div class="row fold-head">
|
||||||
|
<span class="chev">▼</span>
|
||||||
|
<h2><span class="n">3</span>Par épreuve</h2>
|
||||||
|
</div>
|
||||||
|
<div class="fold-body">
|
||||||
|
<p class="tiny">Segments = quoi commander. Information à imprimer = Face / Endos / Info. Chaque sous-boîte se replie.</p>
|
||||||
|
|
||||||
|
<div class="epr fold" style="margin-top:12px">
|
||||||
|
<div class="row fold-head">
|
||||||
|
<span class="chev">▼</span>
|
||||||
|
<strong>5 km</strong>
|
||||||
|
<span class="tiny">2 séquences</span>
|
||||||
|
</div>
|
||||||
|
<div class="fold-body">
|
||||||
|
|
||||||
|
<div class="box fold" style="margin:0 0 12px; background:#fff">
|
||||||
|
<div class="row fold-head">
|
||||||
|
<span class="chev">▼</span>
|
||||||
|
<strong>Segments de dossard</strong>
|
||||||
|
</div>
|
||||||
|
<div class="fold-body">
|
||||||
|
<table class="seg">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Séquence</th>
|
||||||
|
<th>Commande puces</th>
|
||||||
|
<th>Commande dossards</th>
|
||||||
|
<th>Impression</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>1 à 200</td>
|
||||||
|
<td><button type="button" class="switch on" data-k="s1p" aria-label="Commande puces 1-200"><i></i></button></td>
|
||||||
|
<td><button type="button" class="switch on" data-k="s1d" aria-label="Commande dossards 1-200"><i></i></button></td>
|
||||||
|
<td class="tiny" id="lock-s1">à verrouiller</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>201 à 400</td>
|
||||||
|
<td><button type="button" class="switch on" data-k="s2p" aria-label="Commande puces 201-400"><i></i></button></td>
|
||||||
|
<td><button type="button" class="switch" data-k="s2d" aria-label="Commande dossards 201-400"><i></i></button></td>
|
||||||
|
<td class="tiny" id="lock-s2">libre</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="tiny" style="margin-top:8px">201–400 : puces oui, dossards non = le proprio a déjà le papier. Absent du fichier imprimeur, présent dans le fichier puces.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box fold" style="margin:0; background:#fff">
|
||||||
|
<div class="row fold-head">
|
||||||
|
<span class="chev">▼</span>
|
||||||
|
<strong>Information à imprimer sur le dossard</strong>
|
||||||
|
<span class="grow"></span>
|
||||||
|
<button type="button" class="btn ghost" data-apply>Appliquer à toutes les épreuves</button>
|
||||||
|
</div>
|
||||||
|
<div class="fold-body">
|
||||||
|
<p class="tiny" style="margin-bottom:10px">Copie Face / Endos / Info vers les autres épreuves. Ne copie pas les drapeaux de segments.</p>
|
||||||
|
<div class="grid2">
|
||||||
|
<div class="sheet" style="background:#f7f9fb">
|
||||||
|
<strong>Disponibles</strong>
|
||||||
|
<input type="search" placeholder="Rechercher…" style="margin-top:8px" disabled>
|
||||||
|
<div class="chips">
|
||||||
|
<span class="chip">Prénom</span>
|
||||||
|
<span class="chip">Nom</span>
|
||||||
|
<span class="chip">Ville</span>
|
||||||
|
<span class="chip">Courriel</span>
|
||||||
|
<span class="chip">Pays (code 2 lettres)</span>
|
||||||
|
<span class="chip">Nom d’équipe</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sheet" style="background:#f7f9fb">
|
||||||
|
<strong>Sur l’Excel (ordre)</strong>
|
||||||
|
<div class="card-field" style="margin-top:8px">
|
||||||
|
<span class="tiny">≡</span> N° dossard
|
||||||
|
<span class="pills">
|
||||||
|
<span class="pill face on">Face</span>
|
||||||
|
<span class="pill endos">Endos</span>
|
||||||
|
<span class="pill info">Info</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-field">
|
||||||
|
<span class="tiny">≡</span> Épreuve
|
||||||
|
<span class="pills">
|
||||||
|
<span class="pill face">Face</span>
|
||||||
|
<span class="pill endos">Endos</span>
|
||||||
|
<span class="pill info on">Info</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-field">
|
||||||
|
<span class="tiny">≡</span> Nom contact urgence
|
||||||
|
<span class="pills">
|
||||||
|
<span class="pill face">Face</span>
|
||||||
|
<span class="pill endos on">Endos</span>
|
||||||
|
<span class="pill info">Info</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-field">
|
||||||
|
<span class="tiny">≡</span> Téléphone urgence
|
||||||
|
<span class="pills">
|
||||||
|
<span class="pill face">Face</span>
|
||||||
|
<span class="pill endos on">Endos</span>
|
||||||
|
<span class="pill info">Info</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="epr fold" style="margin-top:10px">
|
||||||
|
<div class="row fold-head">
|
||||||
|
<span class="chev">▼</span>
|
||||||
|
<strong>10 km</strong>
|
||||||
|
<span class="tiny">1 séquence</span>
|
||||||
|
</div>
|
||||||
|
<div class="fold-body">
|
||||||
|
<div class="box fold" style="margin:0 0 12px; background:#fff">
|
||||||
|
<div class="row fold-head">
|
||||||
|
<span class="chev">▼</span>
|
||||||
|
<strong>Segments de dossard</strong>
|
||||||
|
</div>
|
||||||
|
<div class="fold-body">
|
||||||
|
<table class="seg">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Séquence</th>
|
||||||
|
<th>Commande puces</th>
|
||||||
|
<th>Commande dossards</th>
|
||||||
|
<th>Impression</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>500 à 900</td>
|
||||||
|
<td><button type="button" class="switch on" data-k="s3p" aria-label="Commande puces 500-900"><i></i></button></td>
|
||||||
|
<td><button type="button" class="switch on" data-k="s3d" aria-label="Commande dossards 500-900"><i></i></button></td>
|
||||||
|
<td class="tiny" id="lock-s3">à verrouiller</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box fold" style="margin:0; background:#fff">
|
||||||
|
<div class="row fold-head">
|
||||||
|
<span class="chev">▼</span>
|
||||||
|
<strong>Information à imprimer sur le dossard</strong>
|
||||||
|
<span class="grow"></span>
|
||||||
|
<button type="button" class="btn ghost" data-apply>Appliquer à toutes les épreuves</button>
|
||||||
|
</div>
|
||||||
|
<div class="fold-body">
|
||||||
|
<p class="tiny">Même boîte que le 5 km — champs propres à cette épreuve.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box dash fold is-min">
|
||||||
|
<div class="row fold-head">
|
||||||
|
<span class="chev">▶</span>
|
||||||
|
<h2>Plus tard — visuel dossard</h2>
|
||||||
|
</div>
|
||||||
|
<div class="fold-body">
|
||||||
|
<p>Le Go client reste MSIN-4517. Ici, au plus une pastille dans l’entête d’épreuve. Le jalon « visuel approuvé » est l’échéance ; la réalité viendra du tampon 4517.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="ou" class="panel">
|
||||||
|
<h2>Sortir de la barre Quantité / Assignation / Mode spécial</h2>
|
||||||
|
<p class="lede" style="margin-top:8px">Oui : page à part. Ce n’est pas le même métier, pas les mêmes gens, pas le même droit.</p>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<strong>Aujourd’hui</strong>
|
||||||
|
<p class="muted" style="margin:8px 0">4e onglet dans <code>#module-bib</code>, même ligne que la préparation terrain.</p>
|
||||||
|
<div class="fake-nav" style="margin-top:8px">
|
||||||
|
<span>Quantités</span>
|
||||||
|
<span>Assignation</span>
|
||||||
|
<span>Mode spécial</span>
|
||||||
|
<span class="on">Production de dossards</span>
|
||||||
|
</div>
|
||||||
|
<p class="tiny" style="margin-top:8px">Tout le monde qui gère les dossards voit l’onglet. Le catalogue Face/Endos est déjà assez lourd pour qu’on le charge en différé (MSIN-4546 parlait déjà d’« autre page »).</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<strong>Proposition</strong>
|
||||||
|
<p class="muted" style="margin:8px 0">La barre terrain reste terrain. La commande devient une page ops.</p>
|
||||||
|
<div class="fake-nav" style="margin-top:8px">
|
||||||
|
<span class="on">Quantités</span>
|
||||||
|
<span>Assignation</span>
|
||||||
|
<span>Mode spécial</span>
|
||||||
|
<span class="off">Production</span>
|
||||||
|
</div>
|
||||||
|
<p class="muted" style="margin:12px 0 6px">Entrée vers la page dédiée :</p>
|
||||||
|
<ul class="muted">
|
||||||
|
<li>Droit à part (pas <code>dossards.manage</code>). Assigner un dossard ≠ commander chez l’imprimeur.</li>
|
||||||
|
<li>Pour l’instant : nous, administrateurs / ops.</li>
|
||||||
|
<li>Plus tard : le dashboard (file dossards / file puces) ouvre cette page pour une course.</li>
|
||||||
|
<li>Depuis le module dossards : éventuellement un lien discret « Commande » seulement si on a le droit — pas un onglet frère.</li>
|
||||||
|
</ul>
|
||||||
|
<div class="ok">La page lit les séquences de l’événement (déjà là). Elle n’assigne rien. Retour = Quantités / Assignation.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="warn">
|
||||||
|
On ne donne pas cette page au promoteur « dossards » par défaut. Excel imprimeur + commande puces + dates = outil interne. Le client voit l’approbation (MSIN-4517), pas la cuisine.
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="jalons" class="panel">
|
||||||
|
<h2>Les 5 dates = 5 jalons, plus une réalité chacun plus tard</h2>
|
||||||
|
<p class="lede" style="margin-top:8px">Ce qu’on saisit = l’échéance. Ce qui se remplira tout seul = le jour où c’est vraiment arrivé.</p>
|
||||||
|
<div class="box">
|
||||||
|
<table class="data">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Jalon (saisie)</th><th>Sens</th><th>Réalité plus tard</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Le client les veut</td>
|
||||||
|
<td>Les dossards chez le client pour cette date</td>
|
||||||
|
<td>Quand c’est réellement livré / confirmé</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Visuel approuvé</td>
|
||||||
|
<td>Le visuel doit être tamponné pour cette date</td>
|
||||||
|
<td>Date du Go MSIN-4517 (lot complet)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Envoi imprimeur</td>
|
||||||
|
<td>Le fichier dossards doit partir pour cette date</td>
|
||||||
|
<td>Date réelle d’envoi du fichier imprimeur</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Réception de l’imprimeur</td>
|
||||||
|
<td>On veut les dossards imprimés reçus pour cette date</td>
|
||||||
|
<td>Date réelle du colis reçu</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Envoi puces</td>
|
||||||
|
<td>On veut que les puces partent pour cette date</td>
|
||||||
|
<td>Date réelle d’envoi du fichier puces</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="tiny" style="margin-top:10px">Dashboard = jalon vs réalité, pour voir qui est en retard. CRM peut écrire les jalons plus tard, même table.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="dash" class="panel">
|
||||||
|
<h2>Même data, deux lunettes ops</h2>
|
||||||
|
<p class="lede" style="margin-top:8px">Le dashboard n’invente rien. Il filtre les segments et affiche les dates de la commande.</p>
|
||||||
|
<div class="grid2">
|
||||||
|
<div class="box">
|
||||||
|
<strong>File imprimeur</strong>
|
||||||
|
<table class="data" style="margin-top:8px">
|
||||||
|
<thead><tr><th>Course</th><th>Qté</th><th>Client veut</th><th>Visuel</th><th>Envoi</th><th>Réception</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Desjardins 5 km (1–200)</td><td>200</td><td>7 sept</td><td>—</td><td>—</td><td>—</td></tr>
|
||||||
|
<tr><td>Desjardins 10 km</td><td>401</td><td>7 sept</td><td>—</td><td>—</td><td>—</td></tr>
|
||||||
|
<tr><td>Boréalys marathon</td><td>3 000</td><td>1 sept</td><td>20 juil</td><td>12 août</td><td>18 août</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="tiny">Sans le 201–400 (dossards déjà chez le proprio).</p>
|
||||||
|
</div>
|
||||||
|
<div class="box">
|
||||||
|
<strong>File puces</strong>
|
||||||
|
<table class="data" style="margin-top:8px">
|
||||||
|
<thead><tr><th>Course</th><th>Qté</th><th>Client veut</th><th>Envoi puces</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Desjardins 5 km (1–200 + 201–400)</td><td>400</td><td>7 sept</td><td>—</td></tr>
|
||||||
|
<tr><td>Desjardins 10 km</td><td>401</td><td>7 sept</td><td>—</td></tr>
|
||||||
|
<tr><td>Boréalys marathon</td><td>3 000</td><td>1 sept</td><td>12 août</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="tiny">Le 201–400 reste ici. Pas de colonne réception imprimeur.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const state = { s1p: true, s1d: true, s2p: true, s2d: false, s3p: true, s3d: true };
|
||||||
|
|
||||||
|
document.querySelectorAll('nav.tabs button').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
document.querySelectorAll('nav.tabs button').forEach(function (b) { b.classList.remove('on'); });
|
||||||
|
document.querySelectorAll('.panel').forEach(function (p) { p.classList.remove('on'); });
|
||||||
|
btn.classList.add('on');
|
||||||
|
document.getElementById(btn.getAttribute('data-tab')).classList.add('on');
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function refreshExcel() {
|
||||||
|
var doss = [];
|
||||||
|
var puces = [];
|
||||||
|
if (state.s1d) doss.push('5 km · 1–200');
|
||||||
|
if (state.s2d) doss.push('5 km · 201–400');
|
||||||
|
if (state.s3d) doss.push('10 km · 500–900');
|
||||||
|
if (state.s1p) puces.push('5 km · 1–200');
|
||||||
|
if (state.s2p) puces.push('5 km · 201–400');
|
||||||
|
if (state.s3p) puces.push('10 km · 500–900');
|
||||||
|
document.getElementById('out-doss').textContent = doss.length ? doss.join(' · ') : 'Aucune séquence — pas de fichier';
|
||||||
|
document.getElementById('out-puces').textContent = puces.length ? puces.join(' · ') : 'Aucune séquence — pas de fichier';
|
||||||
|
document.getElementById('lock-s1').textContent = state.s1d ? 'à verrouiller' : 'libre';
|
||||||
|
document.getElementById('lock-s2').textContent = state.s2d ? 'à verrouiller' : 'libre';
|
||||||
|
document.getElementById('lock-s3').textContent = state.s3d ? 'à verrouiller' : 'libre';
|
||||||
|
document.getElementById('btn-doss').disabled = !doss.length;
|
||||||
|
document.getElementById('btn-puces').disabled = !puces.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.fold-head').forEach(function (head) {
|
||||||
|
head.addEventListener('click', function (e) {
|
||||||
|
if (e.target.closest('button, input, a, label')) return;
|
||||||
|
var fold = head.closest('.fold');
|
||||||
|
var min = fold.classList.toggle('is-min');
|
||||||
|
var chev = head.querySelector('.chev');
|
||||||
|
if (chev) chev.textContent = min ? '▶' : '▼';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.switch').forEach(function (sw) {
|
||||||
|
sw.addEventListener('click', function () {
|
||||||
|
var k = sw.getAttribute('data-k');
|
||||||
|
state[k] = !state[k];
|
||||||
|
sw.classList.toggle('on', state[k]);
|
||||||
|
refreshExcel();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
_maquettes/msin-4512-mockup-A-dual-panel.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
_maquettes/msin-4512-mockup-B-builder-strip.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
_maquettes/msin-4512-mockup-C-preview-toggles.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
_maquettes/msin-4517-approbation-visuel-v1.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
_maquettes/msin-4517-approbation-visuel-v2.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
359
_maquettes/msin-4579-notes-commande-mockup.html
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>MSIN-4579 — Notes commande + cadenas</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #eef1f4;
|
||||||
|
--card: #fff;
|
||||||
|
--ink: #1c2430;
|
||||||
|
--muted: #5c6b7a;
|
||||||
|
--line: #d5dde5;
|
||||||
|
--accent: #3d5afe;
|
||||||
|
--ok: #1b7a4a;
|
||||||
|
--teal: #1aa37a;
|
||||||
|
--bar: #3d5afe;
|
||||||
|
--yellow: #fff59d;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { margin: 0; }
|
||||||
|
body {
|
||||||
|
font: 14px/1.45 system-ui, Segoe UI, sans-serif;
|
||||||
|
color: var(--ink);
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
.wrap { max-width: 1120px; margin: 0 auto; padding: 24px 20px 80px; }
|
||||||
|
h1 { font-size: 22px; margin: 0 0 6px; }
|
||||||
|
h2 { font-size: 16px; margin: 0; }
|
||||||
|
p { margin: 0; }
|
||||||
|
.lede { color: var(--muted); margin-bottom: 18px; }
|
||||||
|
nav.tabs { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 18px; }
|
||||||
|
nav.tabs button {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: #fff;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
nav.tabs button.on { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||||
|
.panel { display: none; }
|
||||||
|
.panel.on { display: block; }
|
||||||
|
.box {
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.muted { color: var(--muted); font-size: 13px; }
|
||||||
|
.tiny { color: var(--muted); font-size: 12px; }
|
||||||
|
.n { color: var(--accent); font-weight: 700; margin-right: 6px; }
|
||||||
|
.yes {
|
||||||
|
background: #edf8f0;
|
||||||
|
border-left: 4px solid var(--ok);
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||||
|
@media (max-width: 800px) { .grid2 { grid-template-columns: 1fr; } }
|
||||||
|
.epr-bar {
|
||||||
|
background: var(--bar);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-weight: 650;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.epr-panel {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.epr-body { padding: 14px 16px 16px; }
|
||||||
|
table.seg { width: 100%; border-collapse: collapse; }
|
||||||
|
table.seg th, table.seg td {
|
||||||
|
text-align: left;
|
||||||
|
padding: 10px 8px;
|
||||||
|
border-bottom: 1px solid #e8eef4;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
table.seg th { font-size: 12px; color: var(--muted); font-weight: 600; }
|
||||||
|
table.seg td.note { width: 42%; }
|
||||||
|
.cell-sw { display: flex; align-items: center; gap: 8px; }
|
||||||
|
.switch {
|
||||||
|
width: 40px; height: 22px; border-radius: 99px; border: 0; padding: 2px;
|
||||||
|
background: var(--teal); cursor: default; position: relative; flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.switch.off { background: #c5ced6; }
|
||||||
|
.switch i {
|
||||||
|
display: block; width: 18px; height: 18px; border-radius: 50%;
|
||||||
|
background: #fff; transform: translateX(18px);
|
||||||
|
}
|
||||||
|
.switch.off i { transform: translateX(0); }
|
||||||
|
.padlock { width: 14px; height: 16px; color: #6b7785; flex: 0 0 auto; }
|
||||||
|
input.note {
|
||||||
|
width: 100%;
|
||||||
|
padding: 7px 9px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
.sheet {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px;
|
||||||
|
background: #f7f9fb;
|
||||||
|
}
|
||||||
|
label.field { display: block; font-size: 12px; color: var(--muted); margin: 10px 0 4px; }
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 72px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
font: inherit;
|
||||||
|
resize: vertical;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
border: 0;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font: inherit;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.btn.ghost { background: #fff; color: var(--ink); border: 1px solid var(--line); }
|
||||||
|
.actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px; }
|
||||||
|
.ver { font-weight: 650; font-variant-numeric: tabular-nums; }
|
||||||
|
.xl {
|
||||||
|
border: 2px solid #222;
|
||||||
|
background: #fff;
|
||||||
|
font: 12px/1.3 Segoe UI, sans-serif;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.xl table { width: 100%; border-collapse: collapse; }
|
||||||
|
.xl th, .xl td {
|
||||||
|
border: 1px solid #bbb;
|
||||||
|
padding: 6px 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.xl th { background: #f3f3f3; font-weight: 650; }
|
||||||
|
.xl .seq { background: #fff; font-size: 15px; }
|
||||||
|
.xl .puce { font-size: 11px; letter-spacing: .04em; }
|
||||||
|
.xl .yel { background: var(--yellow); font-weight: 650; }
|
||||||
|
.xl .note { text-align: left; min-width: 180px; background: #fffbe6; }
|
||||||
|
.xl .bar { background: #9e9e9e; height: 8px; padding: 0; border-color: #9e9e9e; }
|
||||||
|
.xl .total { font-weight: 700; }
|
||||||
|
.xl .left { text-align: left; }
|
||||||
|
ul.tight { margin: 8px 0 0; padding-left: 18px; color: var(--muted); }
|
||||||
|
ul.tight li { margin: 4px 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>MSIN-4579 — Notes de commande + cadenas</h1>
|
||||||
|
<p class="lede">Proposition visuelle. Pas de code. Continuité MSIN-4577 / parent MSIN-4511.</p>
|
||||||
|
|
||||||
|
<nav class="tabs">
|
||||||
|
<button type="button" class="on" data-tab="ecran">Écran MS1</button>
|
||||||
|
<button type="button" data-tab="excel">Excel (template)</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section id="ecran" class="panel on">
|
||||||
|
<div class="yes">
|
||||||
|
<strong>Deux notes, deux endroits.</strong>
|
||||||
|
Tableau = <em>note par séquence</em> (à la place du Cadenas). Cartes fichier = <em>note globale</em> rattachée à la version qu’on envoie.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box" style="margin-top:14px">
|
||||||
|
<h2><span class="n">3</span>Segments — cadenas à côté du switch, note par séquence</h2>
|
||||||
|
<p class="muted" style="margin:8px 0 12px">Colonne Cadenas enlevée. Petit cadenas seulement si ce switch-là est barré. Le champ Note = une ligne, la même note part dans les deux Excel si la séquence est commandée des deux côtés.</p>
|
||||||
|
<div class="epr-panel">
|
||||||
|
<div class="epr-bar"><span>Segments de dossard</span><span>⌃</span></div>
|
||||||
|
<div class="epr-body">
|
||||||
|
<table class="seg">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Séquence</th>
|
||||||
|
<th>Commande puces</th>
|
||||||
|
<th>Commande dossards</th>
|
||||||
|
<th>Note</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>1 – 2</td>
|
||||||
|
<td>
|
||||||
|
<div class="cell-sw">
|
||||||
|
<button type="button" class="switch" aria-label="Commande puces 1-2, bloquée"><i></i></button>
|
||||||
|
<svg class="padlock" viewBox="0 0 14 16" aria-label="Bloqué"><rect x="2" y="7" width="10" height="8" rx="1.5" fill="currentColor"/><path d="M4 7V5a3 3 0 0 1 6 0v2" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="cell-sw">
|
||||||
|
<button type="button" class="switch" aria-label="Commande dossards 1-2, bloquée"><i></i></button>
|
||||||
|
<svg class="padlock" viewBox="0 0 14 16" aria-label="Bloqué"><rect x="2" y="7" width="10" height="8" rx="1.5" fill="currentColor"/><path d="M4 7V5a3 3 0 0 1 6 0v2" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="note"><input class="note" value="Lot test — 2 dossards"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>2001 – 2500</td>
|
||||||
|
<td>
|
||||||
|
<div class="cell-sw">
|
||||||
|
<button type="button" class="switch" aria-label="Commande puces 2001-2500, bloquée"><i></i></button>
|
||||||
|
<svg class="padlock" viewBox="0 0 14 16" aria-label="Bloqué"><rect x="2" y="7" width="10" height="8" rx="1.5" fill="currentColor"/><path d="M4 7V5a3 3 0 0 1 6 0v2" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="cell-sw">
|
||||||
|
<button type="button" class="switch" aria-label="Commande dossards 2001-2500, bloquée"><i></i></button>
|
||||||
|
<svg class="padlock" viewBox="0 0 14 16" aria-label="Bloqué"><rect x="2" y="7" width="10" height="8" rx="1.5" fill="currentColor"/><path d="M4 7V5a3 3 0 0 1 6 0v2" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="note"><input class="note" value="Livraison entrepôt Longueuil"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>3001 – 3100</td>
|
||||||
|
<td>
|
||||||
|
<div class="cell-sw">
|
||||||
|
<button type="button" class="switch" aria-label="Commande puces 3001-3100"><i></i></button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="cell-sw">
|
||||||
|
<button type="button" class="switch off" aria-label="Commande dossards 3001-3100, off"><i></i></button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="note"><input class="note" value="Dossards déjà chez le proprio"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="tiny" style="margin-top:10px">Ligne 3001–3100 : pas de cadenas (pas bloqué). Switch dossards off = cette séquence n’est pas dans le fichier imprimeur ; la note n’y va pas. Elle va dans le fichier puces, à côté de 3001–3100.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<h2><span class="n">2</span>Note globale — rattachée à la version du fichier</h2>
|
||||||
|
<p class="muted" style="margin:8px 0 12px">Une note pour l’imprimeur, une pour les puces. Pied de page du fichier (le texte « 12 jours ouvrables » devient le défaut éditable). Ce n’est pas la note de séquence.</p>
|
||||||
|
<div class="grid2">
|
||||||
|
<div class="sheet">
|
||||||
|
<div class="tiny">Fichier 1</div>
|
||||||
|
<strong>Fichier imprimeur</strong>
|
||||||
|
<label class="field">Note globale de cette version</label>
|
||||||
|
<textarea>Confirmer le nombre de dossards (12 jours ouvrables avant la date de réception souhaitée).</textarea>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" class="btn">Générer le fichier imprimeur</button>
|
||||||
|
<button type="button" class="btn ghost">Bloquer cette version</button>
|
||||||
|
<button type="button" class="btn ghost">Télécharger</button>
|
||||||
|
<span class="ver">v3</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sheet">
|
||||||
|
<div class="tiny">Fichier 2</div>
|
||||||
|
<strong>Fichier puces MS1</strong>
|
||||||
|
<label class="field">Note globale de cette version</label>
|
||||||
|
<textarea>Confirmer le nombre de puces (12 jours ouvrables avant la date de réception souhaitée).</textarea>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" class="btn">Générer le fichier puces</button>
|
||||||
|
<button type="button" class="btn ghost">Bloquer cette version</button>
|
||||||
|
<button type="button" class="btn ghost">Télécharger</button>
|
||||||
|
<span class="ver">v2</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="excel" class="panel">
|
||||||
|
<h2>Template — colonne Note à côté de chaque séquence</h2>
|
||||||
|
<p class="lede" style="margin-top:8px">Comme sur ton screenshot : après Vérif, une cellule Note par ligne de séquence (colonne Q du gabarit, L–P restent cachées). La note globale reste en pied de page.</p>
|
||||||
|
|
||||||
|
<div class="xl">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="left" rowspan="2">Épreuve</th>
|
||||||
|
<th class="seq" colspan="5">Séquence</th>
|
||||||
|
<th class="seq" rowspan="2">Note</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="puce">QUANTITÉ Puce</th>
|
||||||
|
<th># DÉPART</th>
|
||||||
|
<th># FIN</th>
|
||||||
|
<th>Vérif</th>
|
||||||
|
<th>Vérif</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="left">5 km</td>
|
||||||
|
<td>2</td>
|
||||||
|
<td class="yel">1</td>
|
||||||
|
<td class="yel">2</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td class="note">Lot test — 2 dossards</td>
|
||||||
|
</tr>
|
||||||
|
<tr><td class="bar" colspan="7"></td></tr>
|
||||||
|
<tr>
|
||||||
|
<td class="left">10 km</td>
|
||||||
|
<td>500</td>
|
||||||
|
<td class="yel">2001</td>
|
||||||
|
<td class="yel">2500</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td class="note">Livraison entrepôt Longueuil</td>
|
||||||
|
</tr>
|
||||||
|
<tr><td class="bar" colspan="7"></td></tr>
|
||||||
|
<tr>
|
||||||
|
<td class="left">Elite</td>
|
||||||
|
<td>100</td>
|
||||||
|
<td class="yel">3001</td>
|
||||||
|
<td class="yel">3100</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td class="note">Dossards déjà chez le proprio</td>
|
||||||
|
</tr>
|
||||||
|
<tr><td class="bar" colspan="7"></td></tr>
|
||||||
|
<tr>
|
||||||
|
<td class="left total">TOTAL</td>
|
||||||
|
<td class="total">602</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td class="total">602</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p class="tiny" style="margin-top:10px">Même colonne Note dans le fichier imprimeur (onglet Commande dossards). Une séquence absente d’un fichier (switch off) n’y a pas de ligne, donc pas de note.</p>
|
||||||
|
<ul class="tight">
|
||||||
|
<li><strong>Note de séquence</strong> → cellule à côté de # DÉPART / # FIN (ton screenshot).</li>
|
||||||
|
<li><strong>Note globale</strong> → pied de page du fichier, figée avec la version (v2 / v3).</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.querySelectorAll('nav.tabs button').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
document.querySelectorAll('nav.tabs button').forEach(function (b) { b.classList.remove('on'); });
|
||||||
|
document.querySelectorAll('.panel').forEach(function (p) { p.classList.remove('on'); });
|
||||||
|
btn.classList.add('on');
|
||||||
|
document.getElementById(btn.getAttribute('data-tab')).classList.add('on');
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1372
ajax_bib_range.php
467
ajax_inscr_gestion.php
Normal file
@ -0,0 +1,467 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Actions AJAX reservees a la fiche de gestion des inscriptions (v2).
|
||||||
|
* N'altere pas ajax_promoteur.php ni enregistrer.php (legacy promoteur).
|
||||||
|
*/
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// MSIN-4401 — Traductions toujours au nom de compte.php (jamais ajax_inscr_gestion.php).
|
||||||
|
// Evite les doublons info_prg=ajax_*.php et les libelles manquants selon la page appelante
|
||||||
|
// (hub, compte, /mobile). Meme pattern que ajax_promoteur_hub / ajax_bib_range.
|
||||||
|
define('MS1_INSCR_GESTION_AJAX', true);
|
||||||
|
$_SERVER['PHP_SELF'] = '/compte.php';
|
||||||
|
$_SERVER['SCRIPT_NAME'] = '/compte.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
// Filet de securite : transforme tout fatal (include manquant, fonction
|
||||||
|
// indefinie, parse error...) en reponse JSON exploitable au lieu d'un 500 muet.
|
||||||
|
$GLOBALS['__inscr_gestion_done'] = false;
|
||||||
|
register_shutdown_function(function () {
|
||||||
|
if (!empty($GLOBALS['__inscr_gestion_done'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$err = error_get_last();
|
||||||
|
$strDetail = ($err && in_array($err['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR), true))
|
||||||
|
? ($err['message'] . ' @ ' . $err['file'] . ':' . $err['line'])
|
||||||
|
: 'fatal inconnu';
|
||||||
|
error_log('[ajax_inscr_gestion] ' . $strDetail);
|
||||||
|
if (!headers_sent()) {
|
||||||
|
http_response_code(200);
|
||||||
|
}
|
||||||
|
echo json_encode(array('state' => 'error', 'message' => 'fatal', 'detail' => $strDetail));
|
||||||
|
});
|
||||||
|
|
||||||
|
require_once 'php/inc_fonctions.php';
|
||||||
|
require_once 'php/inc_fx_promoteur.php';
|
||||||
|
require_once 'php/inc_fx_inscriptions_gestion.php';
|
||||||
|
// Necessaire pour fxEveAccesResolveEveIdFromParId() appelee dans le case
|
||||||
|
// 'par_statut_course' : ce fichier n'est sinon charge que paresseusement
|
||||||
|
// (fxInscrGestionLoadEveAcces) APRES ce premier appel -> fatal undefined function.
|
||||||
|
require_once 'php/inc_fx_eve_acces.php';
|
||||||
|
|
||||||
|
$intComId = intval($_SESSION['com_id'] ?? 0);
|
||||||
|
$strAction = trim((string)($_REQUEST['a'] ?? ''));
|
||||||
|
$strLangue = trim((string)($_REQUEST['lang'] ?? 'fr'));
|
||||||
|
if ($strLangue === '' || !in_array($strLangue, array('fr', 'en'), true)) {
|
||||||
|
$strLangue = 'fr';
|
||||||
|
}
|
||||||
|
|
||||||
|
$vPage = 'compte.php';
|
||||||
|
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
|
||||||
|
fxInscrGestionInitTextes($strLangue);
|
||||||
|
|
||||||
|
if ($intComId <= 0) {
|
||||||
|
$GLOBALS['__inscr_gestion_done'] = true;
|
||||||
|
// MSIN-4401 — code session : le client recharge pour afficher le login.
|
||||||
|
echo json_encode(array('state' => 'error', 'code' => 'session', 'message' => 'session'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tabRetour = array('state' => 'error');
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch ($strAction) {
|
||||||
|
case 'par_statut_course':
|
||||||
|
$intParId = intval($_REQUEST['par_id'] ?? 0);
|
||||||
|
$strStatut = trim((string)($_REQUEST['par_statut_course'] ?? ''));
|
||||||
|
$intEveId = fxEveAccesResolveEveIdFromParId($intParId);
|
||||||
|
|
||||||
|
if ($intEveId <= 0 || !fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.statut_edit')) {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tabRetour = fxSetParStatutCourse($intParId, $strStatut, $strLangue);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'refund_form':
|
||||||
|
// Charge le formulaire de remboursement mutualise pour la fiche de gestion (panneau repliable).
|
||||||
|
$strNoPanier = trim((string)($_REQUEST['no_panier'] ?? ''));
|
||||||
|
if ($strNoPanier === '') {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => 'no_panier_manquant');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Meme source que le Super Admin : la commande vient de inscriptions_panier_acheteurs.
|
||||||
|
$sqlCmd = "SELECT *, IF(a.com_id <> 0, (SELECT c.com_login FROM inscriptions_comptes c WHERE a.com_id = c.com_id), '') AS usager"
|
||||||
|
. " FROM inscriptions_panier_acheteurs a, inscriptions_panier_statuts s"
|
||||||
|
. " WHERE s.sta_id = a.sta_id AND a.no_panier = '" . $objDatabase->fxEscape($strNoPanier) . "'";
|
||||||
|
$recCommandes = $objDatabase->fxGetRow($sqlCmd);
|
||||||
|
if (!$recCommandes) {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => 'commande_introuvable');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// L'eve_id de contexte = evenement de la fiche (participant), pas seulement celui du checkout.
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/superadm/php/inc_fx_paypal.php';
|
||||||
|
|
||||||
|
$intEveContext = intval($_REQUEST['eve_id'] ?? 0);
|
||||||
|
if ($intEveContext <= 0) {
|
||||||
|
$intEveContext = intval($recCommandes['eve_id']);
|
||||||
|
}
|
||||||
|
if ($intEveContext <= 0 || !fxInscrGestionCanDo($intComId, $intEveContext, 'inscriptions_gestion.refund')) {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!fxRefundPanierLinkedToEvent($recCommandes['no_panier'], $intEveContext, intval($recCommandes['eve_id']))) {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => 'panier_evenement_incompatible');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strRefundToken = fxRefundEnsureToken();
|
||||||
|
$arrRefundCtx = fxRefundComputeContext($recCommandes, $intEveContext);
|
||||||
|
|
||||||
|
// Auteur = promoteur connecte (alimente l'encart "Ajoute par" du formulaire).
|
||||||
|
$strAuteur = trim((string)($_SESSION['com_info']['com_prenom'] ?? '') . ' '
|
||||||
|
. (string)($_SESSION['com_info']['com_nom'] ?? ''));
|
||||||
|
|
||||||
|
// Markup seul (sans <script>, dernier param false) car injecte via innerHTML :
|
||||||
|
// le comportement (submit AJAX) est gere par le handler delegue dans inc_footer_scripts.php.
|
||||||
|
// NB : le resume de la transaction n'est plus inclus ici (MSIN) ; il est desormais
|
||||||
|
// accessible via le bouton "Afficher la transaction" de la barre Gestion (action transaction_resume).
|
||||||
|
$strDocAside = '';
|
||||||
|
if (function_exists('fxDocRenderRefundPromoteurAside')) {
|
||||||
|
global $vPage;
|
||||||
|
$memPage = $vPage ?? null;
|
||||||
|
$vPage = 'compte.php';
|
||||||
|
$strDocAside = fxDocRenderRefundPromoteurAside();
|
||||||
|
$vPage = $memPage;
|
||||||
|
}
|
||||||
|
$strForm = fxRenderRefundForm($recCommandes, $strRefundToken, $arrRefundCtx, $vDomaine, $strAuteur, null, false, $strDocAside);
|
||||||
|
|
||||||
|
$tabRetour = array('state' => 'ok', 'html' => $strForm);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'bib_ocr':
|
||||||
|
// MSIN-4401 — Lecture numero dossard via Google Cloud Vision.
|
||||||
|
// Image : fichier multipart (prefere) ou base64 POST (legacy).
|
||||||
|
if (!fxInscrGestionBibOcrCloudEnabled()) {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => 'ocr_not_configured');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$strImage = '';
|
||||||
|
if (!empty($_FILES['image']['tmp_name']) && is_uploaded_file($_FILES['image']['tmp_name'])) {
|
||||||
|
if ((int)$_FILES['image']['error'] === UPLOAD_ERR_OK && (int)$_FILES['image']['size'] > 0
|
||||||
|
&& (int)$_FILES['image']['size'] <= 4 * 1024 * 1024) {
|
||||||
|
$strBin = (string)file_get_contents($_FILES['image']['tmp_name']);
|
||||||
|
$strImage = base64_encode($strBin);
|
||||||
|
} else {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => 'image_invalid');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$strImage = (string)($_POST['image'] ?? '');
|
||||||
|
}
|
||||||
|
if ($strImage === '') {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => 'image_invalid');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$arrOcr = fxInscrGestionVisionReadBibNumber($strImage);
|
||||||
|
if (!empty($arrOcr['ok']) && !empty($arrOcr['number'])) {
|
||||||
|
$tabRetour = array(
|
||||||
|
'state' => 'ok',
|
||||||
|
'number' => (string)$arrOcr['number'],
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$strSeen = '';
|
||||||
|
if (!empty($arrOcr['text'])) {
|
||||||
|
$strSeen = preg_replace('/\D+/', '', (string)$arrOcr['text']);
|
||||||
|
}
|
||||||
|
$tabRetour = array(
|
||||||
|
'state' => 'error',
|
||||||
|
'message' => (string)($arrOcr['message'] ?? 'no_number'),
|
||||||
|
'seen' => $strSeen,
|
||||||
|
'detail' => (string)($arrOcr['detail'] ?? ''),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'bib_duplicate_check':
|
||||||
|
// Controle "dossard en double" dans l'evenement (avant enregistrement legacy).
|
||||||
|
// Ne modifie rien : renvoie qui porte deja ce dossard + la politique a appliquer.
|
||||||
|
$intParId = intval($_REQUEST['par_id'] ?? 0);
|
||||||
|
$strNoBib = trim((string)($_REQUEST['no_bib'] ?? ''));
|
||||||
|
$blnTeam = (trim((string)($_REQUEST['type'] ?? '')) === 'no_equipe');
|
||||||
|
|
||||||
|
$intEveId = fxEveAccesResolveEveIdFromParId($intParId);
|
||||||
|
if ($intEveId <= 0 || !fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.bib_edit')) {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strPolicy = fxInscrGestionBibDuplicatePolicy($intEveId);
|
||||||
|
|
||||||
|
if ($strNoBib === '') {
|
||||||
|
$tabRetour = array('state' => 'ok', 'duplicate' => false, 'policy' => $strPolicy, 'who' => array());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commande courante a exclure du controle (un participant ne se "double" pas lui-meme).
|
||||||
|
$intPecOrig = intval($objDatabase->fxGetVar(
|
||||||
|
'SELECT e.pec_id_original FROM resultats_epreuves_commandees e, resultats_participants p'
|
||||||
|
. ' WHERE p.par_id = ' . $intParId . ' AND p.pec_id = e.pec_id_original LIMIT 1'
|
||||||
|
));
|
||||||
|
|
||||||
|
$intCurrentEprId = (int)$objDatabase->fxGetVar(
|
||||||
|
'SELECT epr_id FROM resultats_participants WHERE par_id = ' . $intParId . ' LIMIT 1'
|
||||||
|
);
|
||||||
|
|
||||||
|
$arrDup = fxInscrGestionFindBibDuplicates($intEveId, $strNoBib, $intPecOrig, $blnTeam, $intCurrentEprId, $intParId);
|
||||||
|
$arrWho = array();
|
||||||
|
foreach ($arrDup as $rowDup) {
|
||||||
|
$arrWho[] = array(
|
||||||
|
'name' => fxInscrGestionEsc(fxUnescape($rowDup['par_nom']) . ', ' . fxUnescape($rowDup['par_prenom'])),
|
||||||
|
'epreuve' => fxInscrGestionEsc(fxInscrGestionEpreuveLabel($rowDup['epr_id'], $strLangue)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tabRetour = array(
|
||||||
|
'state' => 'ok',
|
||||||
|
'duplicate' => count($arrWho) > 0,
|
||||||
|
'policy' => $strPolicy,
|
||||||
|
'who' => $arrWho,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'transaction_resume':
|
||||||
|
// Resume de la transaction (meme rendu que facture.php cote promoteur) + detail des
|
||||||
|
// remboursements PayPal, affichable a tout moment via le panneau repliable de la barre Gestion.
|
||||||
|
$strNoPanier = trim((string)($_REQUEST['no_panier'] ?? ''));
|
||||||
|
if ($strNoPanier === '') {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => 'no_panier_manquant');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commande complete (meme source que refund_form / Super Admin) : alimente fxRefundComputeContext.
|
||||||
|
$sqlCmd = "SELECT *, IF(a.com_id <> 0, (SELECT c.com_login FROM inscriptions_comptes c WHERE a.com_id = c.com_id), '') AS usager"
|
||||||
|
. " FROM inscriptions_panier_acheteurs a, inscriptions_panier_statuts s"
|
||||||
|
. " WHERE s.sta_id = a.sta_id AND a.no_panier = '" . $objDatabase->fxEscape($strNoPanier) . "'";
|
||||||
|
$recCommandes = $objDatabase->fxGetRow($sqlCmd);
|
||||||
|
if (!$recCommandes) {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => 'commande_introuvable');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// L'eve_id fait foi cote serveur (jamais le client). Droit granulaire transaction.
|
||||||
|
$intEveId = intval($recCommandes['eve_id']);
|
||||||
|
if ($intEveId <= 0 || !fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.transaction')) {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/inc_fx_panier.php';
|
||||||
|
$strResume = fxShowPanier($strNoPanier, 0, 'web', 1, $strLangue);
|
||||||
|
|
||||||
|
// Sommaire des remboursements PayPal (vue compacte, pas la grande table Super Admin).
|
||||||
|
// N'apparait que s'il existe une capture PayPal exploitable ; sinon rien (les cas
|
||||||
|
// gratuits/non-PayPal sont deja couverts par la facture ci-dessus).
|
||||||
|
$strRefunds = '';
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/superadm/php/inc_fx_paypal.php';
|
||||||
|
$arrRefundCtx = fxRefundComputeContext($recCommandes);
|
||||||
|
if (!empty($arrRefundCtx['ok'])) {
|
||||||
|
// renderPayPalRefundSummaryHtml() renvoie '' s'il n'y a aucun remboursement :
|
||||||
|
// dans ce cas on n'ajoute meme pas le separateur/encadre (rien a montrer).
|
||||||
|
$strSummary = renderPayPalRefundSummaryHtml($arrRefundCtx['paypaldetails']);
|
||||||
|
if (trim($strSummary) !== '') {
|
||||||
|
$strRefunds = '<div style="margin-top:14px;border-top:1px solid #cfe0ff;padding-top:12px;">'
|
||||||
|
. $strSummary
|
||||||
|
. '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conteneur encadre (sans bascule interne : le bouton de la barre Gestion fait office de toggle).
|
||||||
|
$strHtml =
|
||||||
|
'<div class="ms1-transaction-resume"'
|
||||||
|
. ' style="border:1px solid #2d6cdf;border-radius:10px;background:#f4f7fc;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,.12);">'
|
||||||
|
. '<div style="padding:14px;">' . $strResume . $strRefunds . '</div>'
|
||||||
|
. '</div>';
|
||||||
|
|
||||||
|
$tabRetour = array('state' => 'ok', 'html' => $strHtml);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'fiche_audit_history':
|
||||||
|
// MSIN-4464 — derniers changements d'un champ whitelist (popup « ! » fiche).
|
||||||
|
require_once 'php/inc_fx_fiche_audit.php';
|
||||||
|
$intParId = intval($_REQUEST['par_id'] ?? 0);
|
||||||
|
$strField = preg_replace('/[^a-z0-9_]/i', '', (string)($_REQUEST['field'] ?? ''));
|
||||||
|
$intEveId = fxEveAccesResolveEveIdFromParId($intParId);
|
||||||
|
|
||||||
|
if ($intParId <= 0 || $strField === '' || $intEveId <= 0
|
||||||
|
|| !fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.audit_view')) {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrItems = fxFicheAuditGetRecentForField($intParId, $strField);
|
||||||
|
$arrOut = array();
|
||||||
|
if (is_array($arrItems)) {
|
||||||
|
foreach ($arrItems as $row) {
|
||||||
|
if (!is_array($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$arrOut[] = array(
|
||||||
|
'when' => (string)($row['created_at'] ?? ''),
|
||||||
|
'who' => (string)($row['changed_by_label'] ?? ''),
|
||||||
|
'old' => (string)($row['old_value'] ?? ''),
|
||||||
|
'new' => (string)($row['new_value'] ?? ''),
|
||||||
|
'source' => (string)($row['source'] ?? ''),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$tabRetour = array(
|
||||||
|
'state' => 'success',
|
||||||
|
'field' => $strField,
|
||||||
|
'items' => $arrOut,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// MSIN-4521 — édition hors inscription en ligne (fiche verte).
|
||||||
|
case 'hors_tx_profile_save':
|
||||||
|
$intParId = intval($_REQUEST['par_id'] ?? 0);
|
||||||
|
$intEveId = fxEveAccesResolveEveIdFromParId($intParId);
|
||||||
|
if ($intEveId <= 0 || !fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.edit')
|
||||||
|
|| !fxInscrGestionEveIsSansInscriptionEnLigne($intEveId)) {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$tabRetour = fxInscrGestionHorsTxSaveProfile($intParId, $intEveId, array(
|
||||||
|
'par_prenom' => $_REQUEST['par_prenom'] ?? '',
|
||||||
|
'par_nom' => $_REQUEST['par_nom'] ?? '',
|
||||||
|
'par_sexe' => $_REQUEST['par_sexe'] ?? '',
|
||||||
|
'par_naissance' => $_REQUEST['par_naissance'] ?? '',
|
||||||
|
'par_contact_urgence_nom' => $_REQUEST['par_contact_urgence_nom'] ?? '',
|
||||||
|
'par_contact_urgence_telephone' => $_REQUEST['par_contact_urgence_telephone'] ?? '',
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'hors_tx_epreuve_save':
|
||||||
|
$intParId = intval($_REQUEST['par_id'] ?? 0);
|
||||||
|
$intNewEpr = intval($_REQUEST['epr_id'] ?? 0);
|
||||||
|
$blnConfirmed = (intval($_REQUEST['confirm'] ?? 0) === 1);
|
||||||
|
$intEveId = fxEveAccesResolveEveIdFromParId($intParId);
|
||||||
|
if ($intEveId <= 0 || !fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.edit')
|
||||||
|
|| !fxInscrGestionEveIsSansInscriptionEnLigne($intEveId)) {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$tabRetour = fxInscrGestionHorsTxSaveEpreuve($intParId, $intEveId, $intNewEpr, $blnConfirmed);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'hors_tx_questions_save':
|
||||||
|
$intParId = intval($_REQUEST['par_id'] ?? 0);
|
||||||
|
$intEveId = fxEveAccesResolveEveIdFromParId($intParId);
|
||||||
|
if ($intEveId <= 0 || !fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.edit')
|
||||||
|
|| !fxInscrGestionEveIsSansInscriptionEnLigne($intEveId)) {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$arrAnswers = array();
|
||||||
|
if (isset($_REQUEST['answers']) && is_array($_REQUEST['answers'])) {
|
||||||
|
$arrAnswers = $_REQUEST['answers'];
|
||||||
|
} elseif (!empty($_REQUEST['answers_json'])) {
|
||||||
|
$mix = json_decode((string)$_REQUEST['answers_json'], true);
|
||||||
|
if (is_array($mix)) {
|
||||||
|
$arrAnswers = $mix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$tabRetour = fxInscrGestionHorsTxSaveQuestions($intParId, $intEveId, $arrAnswers);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// MSIN-4532 — un seul Enregistrer (identité + épreuve + questions).
|
||||||
|
case 'hors_tx_save_all':
|
||||||
|
$intParId = intval($_REQUEST['par_id'] ?? 0);
|
||||||
|
$intEveId = fxEveAccesResolveEveIdFromParId($intParId);
|
||||||
|
$blnConfirmed = (intval($_REQUEST['confirm'] ?? 0) === 1);
|
||||||
|
if ($intEveId <= 0 || !fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.edit')
|
||||||
|
|| !fxInscrGestionEveIsSansInscriptionEnLigne($intEveId)) {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$arrAnswers = array();
|
||||||
|
if (isset($_REQUEST['answers']) && is_array($_REQUEST['answers'])) {
|
||||||
|
$arrAnswers = $_REQUEST['answers'];
|
||||||
|
} elseif (!empty($_REQUEST['answers_json'])) {
|
||||||
|
$mix = json_decode((string)$_REQUEST['answers_json'], true);
|
||||||
|
if (is_array($mix)) {
|
||||||
|
$arrAnswers = $mix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$tabRetour = fxInscrGestionHorsTxSaveAll(
|
||||||
|
$intParId,
|
||||||
|
$intEveId,
|
||||||
|
array(
|
||||||
|
'par_prenom' => $_REQUEST['par_prenom'] ?? '',
|
||||||
|
'par_nom' => $_REQUEST['par_nom'] ?? '',
|
||||||
|
'par_sexe' => $_REQUEST['par_sexe'] ?? '',
|
||||||
|
'par_naissance' => $_REQUEST['par_naissance'] ?? '',
|
||||||
|
'par_contact_urgence_nom' => $_REQUEST['par_contact_urgence_nom'] ?? '',
|
||||||
|
'par_contact_urgence_telephone' => $_REQUEST['par_contact_urgence_telephone'] ?? '',
|
||||||
|
'epr_id' => $_REQUEST['epr_id'] ?? 0,
|
||||||
|
),
|
||||||
|
$arrAnswers,
|
||||||
|
$blnConfirmed
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// MSIN-4532 — saisie rapide : questions selon épreuve (création).
|
||||||
|
case 'hors_tx_create_questions':
|
||||||
|
$intEveId = intval($_REQUEST['eve_id'] ?? 0);
|
||||||
|
$intEprId = intval($_REQUEST['epr_id'] ?? 0);
|
||||||
|
if ($intEveId <= 0 || !fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.edit')
|
||||||
|
|| !fxInscrGestionEveIsSansInscriptionEnLigne($intEveId)) {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$tabQ = fxInscrGestionHorsTxLoadQuestionsForCreate($intEveId, $intEprId, $strLangue);
|
||||||
|
$tabRetour = array(
|
||||||
|
'state' => 'success',
|
||||||
|
'html' => fxInscrGestionHorsTxRenderCreateQuestionsHtml($tabQ),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// MSIN-4532 — saisie rapide : créer participant.
|
||||||
|
case 'hors_tx_create':
|
||||||
|
$intEveId = intval($_REQUEST['eve_id'] ?? 0);
|
||||||
|
if ($intEveId <= 0 || !fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.edit')
|
||||||
|
|| !fxInscrGestionEveIsSansInscriptionEnLigne($intEveId)) {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$arrAnswers = array();
|
||||||
|
if (isset($_REQUEST['answers']) && is_array($_REQUEST['answers'])) {
|
||||||
|
$arrAnswers = $_REQUEST['answers'];
|
||||||
|
} elseif (!empty($_REQUEST['answers_json'])) {
|
||||||
|
$mix = json_decode((string)$_REQUEST['answers_json'], true);
|
||||||
|
if (is_array($mix)) {
|
||||||
|
$arrAnswers = $mix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$tabRetour = fxInscrGestionHorsTxCreate($intEveId, array(
|
||||||
|
'epr_id' => $_REQUEST['epr_id'] ?? 0,
|
||||||
|
'par_prenom' => $_REQUEST['par_prenom'] ?? '',
|
||||||
|
'par_nom' => $_REQUEST['par_nom'] ?? '',
|
||||||
|
'par_sexe' => $_REQUEST['par_sexe'] ?? '',
|
||||||
|
'par_naissance' => $_REQUEST['par_naissance'] ?? '',
|
||||||
|
'par_contact_urgence_nom' => $_REQUEST['par_contact_urgence_nom'] ?? '',
|
||||||
|
'par_contact_urgence_telephone' => $_REQUEST['par_contact_urgence_telephone'] ?? '',
|
||||||
|
'no_bib' => $_REQUEST['no_bib'] ?? '',
|
||||||
|
), $arrAnswers);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => 'invalid_action');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log('[ajax_inscr_gestion] ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
|
||||||
|
$tabRetour = array(
|
||||||
|
'state' => 'error',
|
||||||
|
'message' => 'exception',
|
||||||
|
'detail' => $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$GLOBALS['__inscr_gestion_done'] = true;
|
||||||
|
echo json_encode($tabRetour);
|
||||||
49
ajax_inscr_gestion_qr.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// MSIN-4401 — Traductions toujours au nom de compte.php (jamais ajax_inscr_gestion_qr.php).
|
||||||
|
define('MS1_INSCR_GESTION_AJAX', true);
|
||||||
|
$_SERVER['PHP_SELF'] = '/compte.php';
|
||||||
|
$_SERVER['SCRIPT_NAME'] = '/compte.php';
|
||||||
|
|
||||||
|
require_once 'php/inc_fonctions.php';
|
||||||
|
require_once 'php/inc_fx_inscriptions_gestion.php';
|
||||||
|
|
||||||
|
global $vDomaine;
|
||||||
|
|
||||||
|
$strLangue = 'fr';
|
||||||
|
if (!empty($_REQUEST['lang']) && in_array($_REQUEST['lang'], array('fr', 'en'), true)) {
|
||||||
|
$strLangue = $_REQUEST['lang'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$vPage = 'compte.php';
|
||||||
|
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
|
||||||
|
fxInscrGestionInitTextes($strLangue);
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
if (!isset($_SESSION['com_id'])) {
|
||||||
|
// MSIN-4401 — session morte : pas un faux "QR invalide".
|
||||||
|
echo json_encode(array('state' => 'error', 'code' => 'session', 'message' => 'session'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$intEveId = 0;
|
||||||
|
if (!empty($_REQUEST['promoteur_eve_id'])) {
|
||||||
|
$intEveId = fxInscrGestionParseEveId($_REQUEST['promoteur_eve_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($intEveId <= 0 || !fxInscrGestionCanScan($_SESSION['com_id'], $intEveId)) {
|
||||||
|
echo json_encode(array('state' => 'error', 'message' => fxInscrGestionT('inscr_gestion_qr_invalid')));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strRaw = '';
|
||||||
|
if (!empty($_REQUEST['qr'])) {
|
||||||
|
$strRaw = $_REQUEST['qr'];
|
||||||
|
} elseif (!empty($_REQUEST['t'])) {
|
||||||
|
$strRaw = $_REQUEST['t'];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(fxInscrGestionResolveQrScan($strRaw, $intEveId, $strLangue));
|
||||||
260
ajax_jira.php
@ -30,7 +30,7 @@ $strAction = $intEvenement = $intNewNoBib = $intEpreuveCommandee = $intEpreuve =
|
|||||||
//$_POST['typedemande']="test sl";
|
//$_POST['typedemande']="test sl";
|
||||||
//$_POST['summary']="sujet";
|
//$_POST['summary']="sujet";
|
||||||
|
|
||||||
$typedemande = (int)($_POST['typedemande'] ?? 4);
|
const JIRA_REQUEST_TYPE_SOUTIEN_TECHNIQUE = '4';
|
||||||
|
|
||||||
if (isset($_POST['request_type'] )) {
|
if (isset($_POST['request_type'] )) {
|
||||||
$strAction = $_POST['request_type'];
|
$strAction = $_POST['request_type'];
|
||||||
@ -40,82 +40,7 @@ $tabRetour = array();
|
|||||||
$tabRetour['success']=false;
|
$tabRetour['success']=false;
|
||||||
switch($strAction) {
|
switch($strAction) {
|
||||||
case 'check_email':
|
case 'check_email':
|
||||||
$result=jira_recherche_email($_POST['email']);
|
$tabRetour = jira_submit_contact_ticket($_POST);
|
||||||
if($result['success']==false){
|
|
||||||
//cree le user
|
|
||||||
$result=jira_creer_user($_POST['email'],$_POST['nom']);
|
|
||||||
if($result['success']==true){
|
|
||||||
$tabRetour['success']=true;
|
|
||||||
$tabRetour['accountId']=$result['statut']['accountId'] ;
|
|
||||||
}else{
|
|
||||||
$tabRetour['success']=false;
|
|
||||||
$tabRetour['accountId']="";
|
|
||||||
};
|
|
||||||
|
|
||||||
}else{
|
|
||||||
//retourne le id
|
|
||||||
$tabRetour['success']=true;
|
|
||||||
$tabRetour['accountId']= $result['jira']['accountId'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if($tabRetour['success']==true){
|
|
||||||
// creer le jira
|
|
||||||
$data['serviceDeskId'] = 2; // Remplacez par l'ID réel de votre Service Desk
|
|
||||||
$data['requestTypeId'] = (string)$typedemande; // Remplacez par l'ID réel du type de demande
|
|
||||||
$data['summary']=$_POST['summary'];
|
|
||||||
$data['description']=$_POST['description'];
|
|
||||||
$data['typedemande']=$_POST['typedemande'];
|
|
||||||
$data['raiseOnBehalfOf']= $_POST['email'] ;
|
|
||||||
|
|
||||||
$tabRetourtemp=jira_create_ticket($data);
|
|
||||||
$tabRetour['success']=true;
|
|
||||||
}else
|
|
||||||
{
|
|
||||||
$tabRetour['success']=false;
|
|
||||||
$tabRetour['accountId']= "";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
break;
|
|
||||||
case 'check_emailold':
|
|
||||||
$result=jira_recherche_email($_POST['email']);
|
|
||||||
if($result['success']==false){
|
|
||||||
//cree le user
|
|
||||||
$result=jira_creer_user($_POST['email'],$_POST['nom']);
|
|
||||||
if($result['success']==true){
|
|
||||||
$tabRetour['success']='success';
|
|
||||||
$tabRetour['accountId']=$result['statut']['accountId'] ;
|
|
||||||
}else{
|
|
||||||
$tabRetour['success']=false;
|
|
||||||
$tabRetour['accountId']="";
|
|
||||||
};
|
|
||||||
|
|
||||||
}else{
|
|
||||||
//retourne le id
|
|
||||||
$tabRetour['success']=true;
|
|
||||||
$tabRetour['accountId']= $result['jira']['accountId'];
|
|
||||||
}
|
|
||||||
if($tabRetour['success']==true){
|
|
||||||
// creer le jira
|
|
||||||
$data['serviceDeskId'] = 2; // Remplacez par l'ID réel de votre Service Desk
|
|
||||||
$data['requestTypeId'] = (string)$typedemande; // Remplacez par l'ID réel du type de demande
|
|
||||||
$data['summary']=utf8_encode($_POST['summary']);
|
|
||||||
$data['description']=utf8_encode($_POST['description']);
|
|
||||||
$data['typedemande']=utf8_encode($_POST['typedemande']);
|
|
||||||
$data['raiseOnBehalfOf']= $_POST['email'] ;
|
|
||||||
|
|
||||||
$tabRetourtemp=jira_create_ticket($data);
|
|
||||||
$tabRetour['success']=true;
|
|
||||||
}else
|
|
||||||
{
|
|
||||||
$tabRetour['success']=false;
|
|
||||||
$tabRetour['accountId']= "";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'no_equipe':
|
case 'no_equipe':
|
||||||
case 'no_bib':
|
case 'no_bib':
|
||||||
@ -129,6 +54,149 @@ header('Content-Type: application/json; charset=utf-8');
|
|||||||
echo json_encode($tabRetour);
|
echo json_encode($tabRetour);
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
|
function jira_get_type_demande_row(array $post)
|
||||||
|
{
|
||||||
|
global $objDatabase;
|
||||||
|
|
||||||
|
if (empty($post['typedemande']) || !isset($objDatabase)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sqlCol = "SELECT info_texte, info_option2, info_option3 FROM info WHERE info_actif = 1 AND info_clef = 'type_de_demande' AND info_option2 = '" . $objDatabase->fxEscape($post['typedemande']) . "' LIMIT 1";
|
||||||
|
$row = $objDatabase->fxGetResults($sqlCol);
|
||||||
|
|
||||||
|
return !empty($row) ? reset($row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jira_resolve_request_type_id(array $post)
|
||||||
|
{
|
||||||
|
$row = jira_get_type_demande_row($post);
|
||||||
|
if (!empty($row['info_texte']) && fxJiraIsAutreDemande($row['info_texte'])) {
|
||||||
|
return (string)($post['typedemande'] ?? JIRA_REQUEST_TYPE_SOUTIEN_TECHNIQUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return JIRA_REQUEST_TYPE_SOUTIEN_TECHNIQUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jira_resolve_categorie_interne(array $post)
|
||||||
|
{
|
||||||
|
$strCategorie = '';
|
||||||
|
|
||||||
|
if (isset($post['categorie_interne'])) {
|
||||||
|
$strCategorie = trim((string)$post['categorie_interne']);
|
||||||
|
} else {
|
||||||
|
$row = jira_get_type_demande_row($post);
|
||||||
|
if (!empty($row['info_option3'])) {
|
||||||
|
$strCategorie = trim((string)$row['info_option3']);
|
||||||
|
} elseif (!empty($row['info_texte'])) {
|
||||||
|
$strCategorie = fxJiraMapCategorieInterne($row['info_texte']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seul « Vente » est géré pour l'instant ; les autres choix n'envoient pas de catégorie interne.
|
||||||
|
return ($strCategorie === 'Vente') ? 'Vente' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function jira_log_contact_error($context, $detail = '')
|
||||||
|
{
|
||||||
|
$line = 'jira contact [' . $context . ']';
|
||||||
|
if ($detail !== '') {
|
||||||
|
$line .= ': ' . (is_string($detail) ? $detail : json_encode($detail, JSON_UNESCAPED_UNICODE));
|
||||||
|
}
|
||||||
|
error_log($line);
|
||||||
|
}
|
||||||
|
|
||||||
|
function jira_wait_for_account_id($email, $maxAttempts = 4)
|
||||||
|
{
|
||||||
|
for ($i = 0; $i < $maxAttempts; $i++) {
|
||||||
|
if ($i > 0) {
|
||||||
|
usleep(500000);
|
||||||
|
}
|
||||||
|
$result = jira_recherche_email($email);
|
||||||
|
if (!empty($result['success']) && !empty($result['jira']['accountId'])) {
|
||||||
|
return $result['jira']['accountId'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function jira_submit_contact_ticket(array $post)
|
||||||
|
{
|
||||||
|
$tabRetour = array('success' => false, 'accountId' => '');
|
||||||
|
$email = trim((string)($post['email'] ?? ''));
|
||||||
|
$nom = trim((string)($post['nom'] ?? ''));
|
||||||
|
$userJustCreated = false;
|
||||||
|
|
||||||
|
$result = jira_recherche_email($email);
|
||||||
|
$accountId = !empty($result['success']) ? ($result['jira']['accountId'] ?? '') : '';
|
||||||
|
|
||||||
|
if ($accountId === '') {
|
||||||
|
$result = jira_creer_user($email, $nom);
|
||||||
|
if (empty($result['success'])) {
|
||||||
|
jira_log_contact_error('user_creation', $result['statut'] ?? '');
|
||||||
|
return $tabRetour;
|
||||||
|
}
|
||||||
|
$userJustCreated = true;
|
||||||
|
if (is_array($result['statut'])) {
|
||||||
|
$accountId = $result['statut']['accountId'] ?? '';
|
||||||
|
}
|
||||||
|
if ($accountId === '') {
|
||||||
|
$accountId = jira_wait_for_account_id($email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$tabRetour['accountId'] = $accountId;
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'serviceDeskId' => 2,
|
||||||
|
'requestTypeId' => jira_resolve_request_type_id($post),
|
||||||
|
'summary' => $post['summary'] ?? '',
|
||||||
|
'description' => $post['description'] ?? '',
|
||||||
|
'categorie_interne' => jira_resolve_categorie_interne($post),
|
||||||
|
'raiseOnBehalfOf' => $accountId !== '' ? $accountId : $email,
|
||||||
|
);
|
||||||
|
|
||||||
|
$ticket = jira_create_ticket($data);
|
||||||
|
if (empty($ticket['success']) && ($userJustCreated || $accountId === '')) {
|
||||||
|
$accountId = jira_wait_for_account_id($email, 3);
|
||||||
|
if ($accountId !== '') {
|
||||||
|
$tabRetour['accountId'] = $accountId;
|
||||||
|
$data['raiseOnBehalfOf'] = $accountId;
|
||||||
|
$ticket = jira_create_ticket($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($ticket['success'])) {
|
||||||
|
jira_log_contact_error('ticket_creation', $ticket['statut'] ?? '');
|
||||||
|
return $tabRetour;
|
||||||
|
}
|
||||||
|
|
||||||
|
$categorieInterne = $data['categorie_interne'] ?? '';
|
||||||
|
if ($categorieInterne === '') {
|
||||||
|
$tabRetour['success'] = true;
|
||||||
|
return $tabRetour;
|
||||||
|
}
|
||||||
|
|
||||||
|
$issueKey = '';
|
||||||
|
if (is_array($ticket['statut']) && !empty($ticket['statut']['issueKey'])) {
|
||||||
|
$issueKey = $ticket['statut']['issueKey'];
|
||||||
|
}
|
||||||
|
if ($issueKey === '') {
|
||||||
|
jira_log_contact_error('categorie_interne', 'issueKey manquant après création');
|
||||||
|
return $tabRetour;
|
||||||
|
}
|
||||||
|
|
||||||
|
$update = jira_set_categorie_interne($issueKey, $categorieInterne);
|
||||||
|
if (empty($update['success'])) {
|
||||||
|
jira_log_contact_error('categorie_interne', $update['statut'] ?? '');
|
||||||
|
return $tabRetour;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tabRetour['success'] = true;
|
||||||
|
return $tabRetour;
|
||||||
|
}
|
||||||
|
|
||||||
function jira_recherche_email($param)
|
function jira_recherche_email($param)
|
||||||
{
|
{
|
||||||
// recherche user
|
// recherche user
|
||||||
@ -263,6 +331,43 @@ function jira_creer_user($email,$displayName)
|
|||||||
return($sortie);
|
return($sortie);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function jira_set_categorie_interne($issueKey, $categorieInterne)
|
||||||
|
{
|
||||||
|
$jiraDomain = 'https://progiwebinc.atlassian.net';
|
||||||
|
$userEmail = 'stephan@progiweb.ca';
|
||||||
|
$apiToken = 'ATATT3xFfGF0Z91qESp8j7uO783qK0E4BWsY8rhPWEmexSa4Rw3c21Spg6hYIkmVgy6Vwzb34RxqFl3sR00Kby_DuY1NdcUJyfPQYZ0_JbeEjAk9ybFgNY8-MBSABMCxzaDk4szTbWupfjxHT8sJxJMvSNxgHPds5FxXaGo2DnZICZu5dXuvWbk=F73E15AE';
|
||||||
|
|
||||||
|
$payload = json_encode(array(
|
||||||
|
'fields' => array(
|
||||||
|
'customfield_11179' => array(
|
||||||
|
'value' => $categorieInterne,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
$ch = curl_init($jiraDomain . '/rest/api/3/issue/' . rawurlencode($issueKey));
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
|
||||||
|
'Authorization: Basic ' . base64_encode($userEmail . ':' . $apiToken),
|
||||||
|
'Content-Type: application/json',
|
||||||
|
));
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
$sortie = array('success' => false, 'statut' => '');
|
||||||
|
if ($httpcode === 204) {
|
||||||
|
$sortie['success'] = true;
|
||||||
|
return $sortie;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sortie['statut'] = ($response !== false && $response !== '') ? $response : ('HTTP ' . $httpcode);
|
||||||
|
return $sortie;
|
||||||
|
}
|
||||||
|
|
||||||
function jira_create_ticket( $param)
|
function jira_create_ticket( $param)
|
||||||
{
|
{
|
||||||
|
|
||||||
@ -278,7 +383,6 @@ function jira_create_ticket( $param)
|
|||||||
'requestFieldValues' => [
|
'requestFieldValues' => [
|
||||||
'summary' => $param['summary'],
|
'summary' => $param['summary'],
|
||||||
'description' => $param['description'],
|
'description' => $param['description'],
|
||||||
'customfield_11143' => $param['typedemande'],
|
|
||||||
],
|
],
|
||||||
'raiseOnBehalfOf' => $param['raiseOnBehalfOf'], // Remplacez par l'accountId du client (facultatif)
|
'raiseOnBehalfOf' => $param['raiseOnBehalfOf'], // Remplacez par l'accountId du client (facultatif)
|
||||||
] ;
|
] ;
|
||||||
|
|||||||
@ -1,13 +1,22 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
|
// MSIN-4468 — Les libellés AJAX appartiennent toujours au contexte compte.php.
|
||||||
|
define('MS1_PROMOTEUR_AJAX', true);
|
||||||
|
$_SERVER['PHP_SELF'] = '/compte.php';
|
||||||
|
$_SERVER['SCRIPT_NAME'] = '/compte.php';
|
||||||
|
$vPage = 'compte.php';
|
||||||
|
|
||||||
require_once "php/inc_fonctions.php";
|
require_once "php/inc_fonctions.php";
|
||||||
include_once("php/inc_fx_modifierinscriptions.php");
|
include_once("php/inc_fx_modifierinscriptions.php");
|
||||||
require_once "php/inc_fx_promoteur.php";
|
require_once "php/inc_fx_promoteur.php";
|
||||||
require_once "php/inc_fx_messages.php";
|
require_once "php/inc_fx_messages.php";
|
||||||
|
require_once "php/inc_fx_eve_acces.php";
|
||||||
|
|
||||||
global $vDomaine;
|
global $vDomaine;
|
||||||
$strAction = $intEvenement = $intNewNoBib = $intEpreuveCommandee = $intEpreuve = $intPartipcipant = $intQuantite = $strLangue = '';
|
$strAction = $intEvenement = $intNewNoBib = $intEpreuveCommandee = $intEpreuve = $intPartipcipant = $intQuantite = $strStatutCourse = '';
|
||||||
|
$strLangue = $_SESSION['lang'] ?? 'fr';
|
||||||
|
$strQuantiteTotale = '';
|
||||||
|
|
||||||
if (isset($_REQUEST['a'])) {
|
if (isset($_REQUEST['a'])) {
|
||||||
$strAction = $_REQUEST['a'];
|
$strAction = $_REQUEST['a'];
|
||||||
@ -30,23 +39,72 @@ if (isset($_REQUEST['par_id'])) {
|
|||||||
if (isset($_REQUEST['qte'])) {
|
if (isset($_REQUEST['qte'])) {
|
||||||
$intQuantite = $_REQUEST['qte'];
|
$intQuantite = $_REQUEST['qte'];
|
||||||
}
|
}
|
||||||
if (isset($_REQUEST['lang'])) {
|
if (isset($_REQUEST['total'])) {
|
||||||
|
$strQuantiteTotale = trim((string)$_REQUEST['total']);
|
||||||
|
}
|
||||||
|
if (isset($_REQUEST['lang']) && in_array($_REQUEST['lang'], array('fr', 'en'), true)) {
|
||||||
$strLangue = $_REQUEST['lang'];
|
$strLangue = $_REQUEST['lang'];
|
||||||
}
|
}
|
||||||
|
if (isset($_REQUEST['par_statut_course'])) {
|
||||||
|
$strStatutCourse = $_REQUEST['par_statut_course'];
|
||||||
|
}
|
||||||
|
|
||||||
$vPage = 'compte.php';
|
|
||||||
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
|
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
|
||||||
|
|
||||||
$tabRetour = array();
|
$tabRetour = array();
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
switch($strAction) {
|
switch($strAction) {
|
||||||
case 'qte':
|
case 'qte':
|
||||||
$tabRetour = fxUpdateQuantites($intEpreuve, $intQuantite, $strLangue);
|
$tabRetour = fxUpdateQuantites($intEpreuve, $intQuantite, $strLangue);
|
||||||
break;
|
break;
|
||||||
|
case 'qte_total':
|
||||||
|
// MSIN-4468 — Nouvelle convention V4; l'action legacy qte demeure inchangée.
|
||||||
|
if (!fxBibRequirePromoteurSession()) {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => fxBibMsg('bib_v4_ajax_session_invalid'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!fxBibValidateCsrf($_POST['csrf_token'] ?? '')) {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => fxBibMsg('bib_v4_ajax_unauthorized'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$intEveId = fxBibResolveEveIdFromRequest();
|
||||||
|
$blnCanEditQte = !empty($_SESSION['usa_id'])
|
||||||
|
|| (!empty($_SESSION['com_id'])
|
||||||
|
&& $intEveId > 0
|
||||||
|
&& fxEveAccesHasPermission($_SESSION['com_id'], $intEveId, 'epreuves.edit_qte'));
|
||||||
|
if (!$blnCanEditQte) {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => fxBibMsg('bib_v4_ajax_unauthorized'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($strQuantiteTotale === ''
|
||||||
|
|| strlen($strQuantiteTotale) > 7
|
||||||
|
|| !ctype_digit($strQuantiteTotale)) {
|
||||||
|
$tabRetour = array('state' => 'error', 'message' => afficheTexte('promoteur_bad_qte_update', 0));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tabRetour = fxUpdateQuantiteTotale($intEpreuve, intval($strQuantiteTotale));
|
||||||
|
if (($tabRetour['state'] ?? 'error') !== 'success') {
|
||||||
|
if (($tabRetour['error_code'] ?? '') === 'total_below_registrations') {
|
||||||
|
$tabRetour['message'] = fxBibMsg(
|
||||||
|
'bib_v5_qte_total_min',
|
||||||
|
intval($tabRetour['qte_inscrits'] ?? 0)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$tabRetour['message'] = afficheTexte('promoteur_bad_qte_update', 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'no_equipe':
|
case 'no_equipe':
|
||||||
case 'no_bib':
|
case 'no_bib':
|
||||||
$tabRetour = fxSetBibManuel($strAction, $intEvenement, $intNewNoBib, $intEpreuveCommandee, $intEpreuve, $intPartipcipant, $strLangue);
|
$tabRetour = fxSetBibManuel($strAction, $intEvenement, $intNewNoBib, $intEpreuveCommandee, $intEpreuve, $intPartipcipant, $strLangue);
|
||||||
break;
|
break;
|
||||||
|
case 'par_statut_course':
|
||||||
|
$tabRetour = fxSetParStatutCourse($intPartipcipant, $strStatutCourse, $strLangue);
|
||||||
|
break;
|
||||||
case 'teammates':
|
case 'teammates':
|
||||||
$tabRetour = fxShowTeammates($intEvenement, $intEpreuve, $intEpreuveCommandee);
|
$tabRetour = fxShowTeammates($intEvenement, $intEpreuve, $intEpreuveCommandee);
|
||||||
break;
|
break;
|
||||||
|
|||||||
71
ajax_promoteur_hub.php
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// Traductions toujours au nom de compte.php (jamais ajax_promoteur_hub.php).
|
||||||
|
define('MS1_PROMOTEUR_HUB_AJAX', true);
|
||||||
|
$_SERVER['PHP_SELF'] = '/compte.php';
|
||||||
|
$_SERVER['SCRIPT_NAME'] = '/compte.php';
|
||||||
|
|
||||||
|
require_once __DIR__ . '/php/inc_fonctions.php';
|
||||||
|
require_once __DIR__ . '/php/inc_fx_promoteur_hub.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
$strAction = $_REQUEST['a'] ?? '';
|
||||||
|
$strLangue = $_REQUEST['lang'] ?? ($_SESSION['lang'] ?? 'fr');
|
||||||
|
if (!in_array($strLangue, array('fr', 'en'), true)) {
|
||||||
|
$strLangue = 'fr';
|
||||||
|
}
|
||||||
|
$vPage = 'compte.php';
|
||||||
|
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
|
||||||
|
$intEveId = intval($_REQUEST['eve_id'] ?? 0);
|
||||||
|
|
||||||
|
if (empty($_SESSION['com_id'])) {
|
||||||
|
// MSIN-4401 — Le JS doit renvoyer au login, pas afficher un message mort.
|
||||||
|
echo json_encode(array(
|
||||||
|
'ok' => false,
|
||||||
|
'code' => 'session',
|
||||||
|
'msg' => fxPromoteurHubTexte('promoteur_hub_ajax_session', 0),
|
||||||
|
));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$intComId = intval($_SESSION['com_id']);
|
||||||
|
|
||||||
|
if (!in_array($strAction, array('event_stats', 'event_links'), true) || $intEveId <= 0) {
|
||||||
|
echo json_encode(array('ok' => false, 'msg' => fxPromoteurHubTexte('promoteur_hub_ajax_invalid', 0)));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fxPromoteurHubComHasEvent($intComId, $intEveId)) {
|
||||||
|
echo json_encode(array('ok' => false, 'msg' => fxPromoteurHubTexte('promoteur_hub_ajax_denied', 0)));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($strAction === 'event_stats') {
|
||||||
|
if (!fxPromoteurHubCanViewDashboard($intComId, $intEveId)) {
|
||||||
|
echo json_encode(array('ok' => false, 'msg' => fxPromoteurHubTexte('promoteur_hub_ajax_denied', 0)));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrBundle = fxPromoteurHubRenderEventStatsBundle($intEveId, $strLangue);
|
||||||
|
|
||||||
|
echo json_encode(array(
|
||||||
|
'ok' => true,
|
||||||
|
'flash' => $arrBundle['flash'],
|
||||||
|
'html' => $arrBundle['html'],
|
||||||
|
));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrItem = fxPromoteurHubGetEventItemForLinks($intComId, $intEveId);
|
||||||
|
if ($arrItem === null) {
|
||||||
|
echo json_encode(array('ok' => false, 'msg' => fxPromoteurHubTexte('promoteur_hub_ajax_not_found', 0)));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(array(
|
||||||
|
'ok' => true,
|
||||||
|
'html' => fxPromoteurHubRenderEventPanelContent($arrItem, $strLangue),
|
||||||
|
));
|
||||||
1
assets/dossard_templates/.gitkeep
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
BIN
assets/dossard_templates/default.jpg
Normal file
|
After Width: | Height: | Size: 219 KiB |
27
assets/dossard_templates/default.layout.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"page": "8.5x5.5",
|
||||||
|
"boxes": [
|
||||||
|
{
|
||||||
|
"id": "bib",
|
||||||
|
"field": "no_bib",
|
||||||
|
"align": "C",
|
||||||
|
"font_max": 220,
|
||||||
|
"left": 0.2,
|
||||||
|
"right": 0.78,
|
||||||
|
"top": 0.36,
|
||||||
|
"bottom": 0.66
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "prenom",
|
||||||
|
"field": "par_prenom",
|
||||||
|
"align": "L",
|
||||||
|
"font_max": 28,
|
||||||
|
"left": 0.32,
|
||||||
|
"right": 0.68,
|
||||||
|
"top": 0.72,
|
||||||
|
"bottom": 0.84
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"updated_at": "2026-07-25T00:00:00+00:00"
|
||||||
|
}
|
||||||
BIN
assets/dossard_templates/mamie-joe-papi-fan.jpg
Normal file
|
After Width: | Height: | Size: 219 KiB |
50
auto_chronotrack_sync.php
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* MSIN-4328 — Cron sync auto ChronoTrack (différentiel).
|
||||||
|
*
|
||||||
|
* Même pattern que auto_liste_attente.php :
|
||||||
|
* /usr/bin/curl -s https://VOTRE-DOMAINE/auto_chronotrack_sync.php > /dev/null 2>&1
|
||||||
|
*
|
||||||
|
* Minute=* Hour=* … → le script décide selon fréquence/fin de chaque event.
|
||||||
|
*/
|
||||||
|
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
ini_set('log_errors', 1);
|
||||||
|
ini_set('error_log', __DIR__ . '/log/php_log.txt');
|
||||||
|
|
||||||
|
@set_time_limit(300);
|
||||||
|
|
||||||
|
$strLockPath = rtrim(sys_get_temp_dir(), '/\\') . DIRECTORY_SEPARATOR . 'ms1_chronotrack_auto_sync.lock';
|
||||||
|
$fpLock = @fopen($strLockPath, 'c');
|
||||||
|
if ($fpLock === false || !flock($fpLock, LOCK_EX | LOCK_NB)) {
|
||||||
|
if (is_resource($fpLock)) {
|
||||||
|
fclose($fpLock);
|
||||||
|
}
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo 'busy';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
require_once __DIR__ . '/php/inc_fonctions.php';
|
||||||
|
require_once __DIR__ . '/php/chronotrack_api/inc_bootstrap.php';
|
||||||
|
|
||||||
|
$arrResult = fxChronotrackApiSyncRunAutoCron();
|
||||||
|
$strLine = 'auto_chronotrack_sync [' . date('Y-m-d H:i:s') . '] '
|
||||||
|
. ($arrResult['message'] ?? 'done');
|
||||||
|
// Même style que liste d’attente : une ligne dans log/php_log.txt
|
||||||
|
error_log($strLine);
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo $strLine . "\n";
|
||||||
|
} catch (Throwable $ex) {
|
||||||
|
$strErr = 'auto_chronotrack_sync ERROR [' . date('Y-m-d H:i:s') . '] ' . $ex->getMessage();
|
||||||
|
error_log($strErr);
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo $strErr . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
flock($fpLock, LOCK_UN);
|
||||||
|
fclose($fpLock);
|
||||||
59
bib_commande.php
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* MSIN-4577 — Page ops : préparation commande dossards / puces.
|
||||||
|
* Chrome compte (Info via compte.php). Pas un onglet du module dossards.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once('php/inc_start_time.php');
|
||||||
|
require_once('php/inc_fonctions.php');
|
||||||
|
require_once('php/inc_fx_messages.php');
|
||||||
|
require_once('php/inc_v2_assets.php');
|
||||||
|
require_once('php/inc_fx_eve_acces.php');
|
||||||
|
require_once('php/inc_fx_promoteur.php');
|
||||||
|
|
||||||
|
global $vDomaine, $vPage, $strLangue;
|
||||||
|
|
||||||
|
$_SERVER['PHP_SELF'] = '/compte.php';
|
||||||
|
$_SERVER['SCRIPT_NAME'] = '/compte.php';
|
||||||
|
|
||||||
|
$strLangue = $_GET['lang'] ?? $_SESSION['lang'] ?? 'fr';
|
||||||
|
if (!in_array($strLangue, ['fr', 'en'], true)) {
|
||||||
|
$strLangue = 'fr';
|
||||||
|
}
|
||||||
|
$_SESSION['lang'] = $strLangue;
|
||||||
|
|
||||||
|
$vPage = 'compte.php';
|
||||||
|
$footer_script = false;
|
||||||
|
$strCode = 'bib_commande';
|
||||||
|
$strPage = ($strLangue === 'en') ? 'account' : 'compte';
|
||||||
|
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
|
||||||
|
|
||||||
|
$intEveId = 0;
|
||||||
|
if (!empty($_GET['promoteur_eve_id'])) {
|
||||||
|
$intEveId = (int)base64_decode(urldecode((string)$_GET['promoteur_eve_id']));
|
||||||
|
}
|
||||||
|
|
||||||
|
$strLienFr = '/bib_commande.php';
|
||||||
|
$strLienEn = '/bib_commande.php';
|
||||||
|
$blnHideCompteTitle = true;
|
||||||
|
|
||||||
|
if ($intEveId <= 0 || !fxBibOpsUserCanAccessCommande($intEveId)) {
|
||||||
|
header('Location: ' . $vDomaine . '/' . $strPage);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strMetaTitle = fxBibTexte('bib_v5_cmd_page_title', 0);
|
||||||
|
$strMetaDescription = $strMetaTitle;
|
||||||
|
fxV2RegisterScript('bib-commande');
|
||||||
|
|
||||||
|
require_once('inc_header.php');
|
||||||
|
?>
|
||||||
|
<div id="page">
|
||||||
|
<div id="main">
|
||||||
|
<div class="container bg-white p-3 p-xl-5">
|
||||||
|
<?php echo fxBibCommandeRenderPage($intEveId, $strLangue); ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
require_once('inc_footer.php');
|
||||||
49
bib_dist_print.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
// MSIN-4433 — Génération PDF distribution des dossards (POST + session promoteur).
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$_SERVER['PHP_SELF'] = '/compte.php';
|
||||||
|
$_SERVER['SCRIPT_NAME'] = '/compte.php';
|
||||||
|
|
||||||
|
require_once('php/inc_fonctions.php');
|
||||||
|
require_once('php/inc_fx_promoteur.php');
|
||||||
|
|
||||||
|
$strLangue = $_SESSION['lang'] ?? 'fr';
|
||||||
|
$reqLang = $_REQUEST['lang'] ?? $_REQUEST['lng'] ?? '';
|
||||||
|
if (!empty($reqLang) && in_array($reqLang, ['fr', 'en'], true)) {
|
||||||
|
$strLangue = $reqLang;
|
||||||
|
}
|
||||||
|
$vPage = 'compte.php';
|
||||||
|
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$int_eve_id = (int)($_POST['eve_id'] ?? 0);
|
||||||
|
$_REQUEST['eve_id'] = $int_eve_id;
|
||||||
|
$_POST['eve_id'] = $int_eve_id;
|
||||||
|
|
||||||
|
fxBibRequireDistPrintAccess();
|
||||||
|
|
||||||
|
$tabSelections = fxBibParseDistPrintSelectionsFromPost($_POST);
|
||||||
|
$tabIncludeSans = fxBibParseDistPrintIncludeSansFromPost($_POST);
|
||||||
|
$strOrient = fxBibDistPrintNormalizeOrient($_POST['dist_print_orient'] ?? 'P');
|
||||||
|
$tabSave = fxBibSaveDistPrintSelections($int_eve_id, $tabSelections, $strLangue, $strOrient, $tabIncludeSans);
|
||||||
|
|
||||||
|
if (!$tabSave['success']) {
|
||||||
|
http_response_code(400);
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo $tabSave['message'] ?? 'Invalid selection';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strPrintedBy = fxBibGetCurrentPromoteurDisplayName();
|
||||||
|
$intComId = (int)($_SESSION['com_id'] ?? 0);
|
||||||
|
fxBibSaveDistPrintMeta($int_eve_id, $intComId, $strPrintedBy);
|
||||||
|
|
||||||
|
// MSIN-4512 — PDF de CE run lit le POST (indépendant de la colonne BD).
|
||||||
|
fxBibOutputDistPrintPdf($int_eve_id, $strLangue, $strPrintedBy, $tabIncludeSans, $strOrient);
|
||||||
|
exit;
|
||||||
53
bib_ops.php
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* MSIN-4578 — Tableau de bord temporaire gestionnaire de dossards (ops MS1).
|
||||||
|
* Chrome compte (Info via compte.php). Pas le hub promoteur.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once('php/inc_start_time.php');
|
||||||
|
require_once('php/inc_fonctions.php');
|
||||||
|
require_once('php/inc_fx_messages.php');
|
||||||
|
require_once('php/inc_v2_assets.php');
|
||||||
|
require_once('php/inc_fx_eve_acces.php');
|
||||||
|
require_once('php/inc_fx_promoteur.php');
|
||||||
|
|
||||||
|
global $vDomaine, $vPage, $strLangue;
|
||||||
|
|
||||||
|
$_SERVER['PHP_SELF'] = '/compte.php';
|
||||||
|
$_SERVER['SCRIPT_NAME'] = '/compte.php';
|
||||||
|
|
||||||
|
$strLangue = $_GET['lang'] ?? $_SESSION['lang'] ?? 'fr';
|
||||||
|
if (!in_array($strLangue, ['fr', 'en'], true)) {
|
||||||
|
$strLangue = 'fr';
|
||||||
|
}
|
||||||
|
$_SESSION['lang'] = $strLangue;
|
||||||
|
|
||||||
|
$vPage = 'compte.php';
|
||||||
|
$footer_script = false;
|
||||||
|
$strCode = 'bib_ops';
|
||||||
|
$strPage = ($strLangue === 'en') ? 'account' : 'compte';
|
||||||
|
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
|
||||||
|
|
||||||
|
$strLienFr = '/bib_ops.php';
|
||||||
|
$strLienEn = '/bib_ops.php';
|
||||||
|
$blnHideCompteTitle = true;
|
||||||
|
|
||||||
|
if (!fxBibOpsUserCanAccessGlobal()) {
|
||||||
|
header('Location: ' . $vDomaine . '/' . $strPage);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strMetaTitle = fxBibTexte('bib_v5_ops_page_title', 0);
|
||||||
|
$strMetaDescription = $strMetaTitle;
|
||||||
|
|
||||||
|
require_once('inc_header.php');
|
||||||
|
?>
|
||||||
|
<div id="page">
|
||||||
|
<div id="main">
|
||||||
|
<div class="container bg-white p-3 p-xl-5">
|
||||||
|
<?php echo fxBibOpsRenderDashboard($strLangue); ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
require_once('inc_footer.php');
|
||||||
205
bib_production_excel.php
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
<?php
|
||||||
|
// MSIN-4469 — Génération et retéléchargement du fichier officiel de production.
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$_SERVER['PHP_SELF'] = '/compte.php';
|
||||||
|
$_SERVER['SCRIPT_NAME'] = '/compte.php';
|
||||||
|
|
||||||
|
require_once('php/inc_fonctions.php');
|
||||||
|
require_once('php/inc_fx_promoteur.php');
|
||||||
|
require_once('php/inc_fx_eve_acces.php');
|
||||||
|
|
||||||
|
$strLangue = $_SESSION['lang'] ?? 'fr';
|
||||||
|
$strReqLang = $_REQUEST['lang'] ?? '';
|
||||||
|
if (in_array($strReqLang, ['fr', 'en'], true)) {
|
||||||
|
$strLangue = $strReqLang;
|
||||||
|
}
|
||||||
|
$vPage = 'compte.php';
|
||||||
|
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$intEveId = (int)($_POST['eve_id'] ?? 0);
|
||||||
|
$strAction = (string)($_POST['action'] ?? '');
|
||||||
|
$strKind = fxBibProductionNormalizeKind((string)($_POST['kind'] ?? 'dossards'));
|
||||||
|
$blnReplace = ((string)($_POST['replace'] ?? '') === '1');
|
||||||
|
|
||||||
|
fxBibRequireDistPrintAccess();
|
||||||
|
if (!fxBibOpsUserCanAccessCommande($intEveId)) {
|
||||||
|
http_response_code(403);
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo fxBibMsg('bib_v4_ajax_unauthorized');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strCsrf = (string)($_POST['csrf_token'] ?? '');
|
||||||
|
if (!fxBibValidateCsrf($strCsrf)) {
|
||||||
|
http_response_code(403);
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo fxBibMsg('bib_v4_ajax_unauthorized');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($strAction === 'generate') {
|
||||||
|
// MSIN-4516 — Filet timeout (cPanel 60s) pendant génération Excel imprimeur.
|
||||||
|
if (function_exists('set_time_limit')) {
|
||||||
|
@set_time_limit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MSIN-4515 — Mémoriser Face/Endos/Info avant génération (pas si le fichier imprimeur est déjà bloqué).
|
||||||
|
$tabFieldSelections = fxBibProductionParseFieldsFromPost($_POST);
|
||||||
|
if (!empty($tabFieldSelections) && !fxBibProductionEventIsLocked($intEveId)) {
|
||||||
|
$tabSaveFields = fxBibProductionSaveFields($intEveId, $tabFieldSelections, $strLangue);
|
||||||
|
if (empty($tabSaveFields['success'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
$strMsg = (string)($tabSaveFields['message'] ?? fxBibMsg('bib_v5_production_generation_error'));
|
||||||
|
echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>Erreur</title></head><body>';
|
||||||
|
echo '<p>' . htmlspecialchars($strMsg, ENT_QUOTES, 'UTF-8') . '</p>';
|
||||||
|
echo '<p><a href="javascript:window.close()">Fermer</a></p>';
|
||||||
|
echo '</body></html>';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$tabResult = fxBibProductionCreateOfficialOrder($intEveId, $strLangue, $strKind, $blnReplace);
|
||||||
|
if (empty($tabResult['success']) || empty($tabResult['order'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
$strMsg = (string)($tabResult['message'] ?? fxBibMsg('bib_v5_production_generation_error'));
|
||||||
|
echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>Erreur</title></head><body>';
|
||||||
|
echo '<p>' . htmlspecialchars($strMsg, ENT_QUOTES, 'UTF-8') . '</p>';
|
||||||
|
echo '<p><a href="javascript:window.close()">Fermer</a></p>';
|
||||||
|
echo '</body></html>';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$tabOrder = $tabResult['order'];
|
||||||
|
} elseif ($strAction === 'download') {
|
||||||
|
if (function_exists('set_time_limit')) {
|
||||||
|
@set_time_limit(0);
|
||||||
|
}
|
||||||
|
$intOrderId = (int)($_POST['order_id'] ?? 0);
|
||||||
|
$tabOrder = fxBibProductionGetOrder($intOrderId, $intEveId);
|
||||||
|
if (!$tabOrder || (int)($tabOrder['bpo_active_key'] ?? 0) !== 1) {
|
||||||
|
http_response_code(404);
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo fxBibMsg('bib_v5_production_file_missing');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
// MSIN-4516 — Si coincé en generating (timeout), reprendre puis servir le fichier.
|
||||||
|
if ((string)($tabOrder['bpo_status'] ?? '') === 'generating') {
|
||||||
|
$tabResume = fxBibProductionResumeGeneratingOrder($tabOrder, $strLangue);
|
||||||
|
if (empty($tabResume['success']) || empty($tabResume['order'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
$strMsg = (string)($tabResume['message'] ?? fxBibMsg('bib_v5_production_generation_error'));
|
||||||
|
echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>Erreur</title></head><body>';
|
||||||
|
echo '<p>' . htmlspecialchars($strMsg, ENT_QUOTES, 'UTF-8') . '</p>';
|
||||||
|
echo '<p><a href="javascript:window.close()">Fermer</a></p>';
|
||||||
|
echo '</body></html>';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$tabOrder = $tabResume['order'];
|
||||||
|
} elseif ((string)($tabOrder['bpo_status'] ?? '') === 'ready'
|
||||||
|
&& !fxBibProductionOrderIsFrozen($tabOrder)
|
||||||
|
) {
|
||||||
|
// MSIN-4577 — Brouillon : le fichier suit les interrupteurs actuels.
|
||||||
|
$tabRebuild = fxBibProductionRebuildActiveOrderWorkbook(
|
||||||
|
$intEveId,
|
||||||
|
$strLangue,
|
||||||
|
$strKind,
|
||||||
|
$tabOrder,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
if (empty($tabRebuild['success']) || empty($tabRebuild['order'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
$strMsg = (string)($tabRebuild['message'] ?? fxBibMsg('bib_v5_production_generation_error'));
|
||||||
|
echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>Erreur</title></head><body>';
|
||||||
|
echo '<p>' . htmlspecialchars($strMsg, ENT_QUOTES, 'UTF-8') . '</p>';
|
||||||
|
echo '<p><a href="javascript:window.close()">Fermer</a></p>';
|
||||||
|
echo '</body></html>';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$tabOrder = $tabRebuild['order'];
|
||||||
|
} elseif (
|
||||||
|
in_array((string)($tabOrder['bpo_status'] ?? ''), ['ready', 'locked'], true)
|
||||||
|
&& fxBibProductionOrderNeedsSplitRebuild($tabOrder)
|
||||||
|
) {
|
||||||
|
// MSIN-4577 — Ancien fichier mixte : recréer sans changer le cadenas.
|
||||||
|
$tabRebuild = fxBibProductionRebuildActiveOrderWorkbook(
|
||||||
|
$intEveId,
|
||||||
|
$strLangue,
|
||||||
|
$strKind,
|
||||||
|
$tabOrder,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
if (empty($tabRebuild['success']) || empty($tabRebuild['order'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
$strMsg = (string)($tabRebuild['message'] ?? fxBibMsg('bib_v5_production_generation_error'));
|
||||||
|
echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>Erreur</title></head><body>';
|
||||||
|
echo '<p>' . htmlspecialchars($strMsg, ENT_QUOTES, 'UTF-8') . '</p>';
|
||||||
|
echo '<p><a href="javascript:window.close()">Fermer</a></p>';
|
||||||
|
echo '</body></html>';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$tabOrder = $tabRebuild['order'];
|
||||||
|
}
|
||||||
|
} elseif ($strAction === 'lock_file') {
|
||||||
|
$tabResult = fxBibProductionLockOfficialOrder($intEveId, $strKind);
|
||||||
|
if (empty($tabResult['success'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
$strMsg = (string)($tabResult['message'] ?? fxBibMsg('bib_v5_production_generation_error'));
|
||||||
|
echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>Erreur</title></head><body>';
|
||||||
|
echo '<p>' . htmlspecialchars($strMsg, ENT_QUOTES, 'UTF-8') . '</p>';
|
||||||
|
echo '<p><a href="javascript:window.close()">Fermer</a></p>';
|
||||||
|
echo '</body></html>';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (function_exists('fxBibCommandePageUrl')) {
|
||||||
|
header('Location: ' . fxBibCommandePageUrl($intEveId, $strLangue));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
header('Location: ' . (($strLangue === 'en') ? '/account' : '/compte'));
|
||||||
|
exit;
|
||||||
|
} elseif ($strAction === 'development_unlock') {
|
||||||
|
$tabResult = fxBibProductionDevelopmentUnlock($intEveId, $strKind);
|
||||||
|
if (empty($tabResult['success'])) {
|
||||||
|
http_response_code(403);
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo $tabResult['message'] ?? fxBibMsg('bib_v4_ajax_unauthorized');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strRedirect = (string)($_POST['redirect'] ?? '');
|
||||||
|
if ($strRedirect === 'commande' && function_exists('fxBibCommandePageUrl')) {
|
||||||
|
header('Location: ' . fxBibCommandePageUrl($intEveId, $strLangue));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strAccountPage = $strLangue === 'en' ? '/account' : '/compte';
|
||||||
|
header(
|
||||||
|
'Location: ' . $strAccountPage
|
||||||
|
. '/inc_tableau_gestion_epreuves?promoteur_eve_id='
|
||||||
|
. urlencode(base64_encode($intEveId))
|
||||||
|
);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
http_response_code(400);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fxBibProductionStreamOrder($tabOrder)) {
|
||||||
|
http_response_code(404);
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo fxBibMsg('bib_v5_production_file_missing');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
exit;
|
||||||
180
compte.php
@ -21,6 +21,10 @@ require_once('php/inc_logs_course.php');
|
|||||||
require_once('superadm/php/inc_fx_excell.php');
|
require_once('superadm/php/inc_fx_excell.php');
|
||||||
|
|
||||||
require_once('php/inc_fx_liste_attente.php');
|
require_once('php/inc_fx_liste_attente.php');
|
||||||
|
require_once('php/inc_fx_inscriptions_gestion.php');
|
||||||
|
require_once('php/inc_v2_assets.php');
|
||||||
|
require_once('php/inc_fx_eve_acces.php');
|
||||||
|
require_once('php/inc_fx_promoteur_hub.php');
|
||||||
global $objDatabase, $vPage, $vDomaine;
|
global $objDatabase, $vPage, $vDomaine;
|
||||||
// vérifier les paramètres
|
// vérifier les paramètres
|
||||||
|
|
||||||
@ -51,15 +55,40 @@ else
|
|||||||
if (!empty($_GET['code']))
|
if (!empty($_GET['code']))
|
||||||
$strCode = 'compte_' . $_GET['code'];
|
$strCode = 'compte_' . $_GET['code'];
|
||||||
else {
|
else {
|
||||||
if (isset($_SESSION['com_id']))
|
if (isset($_SESSION['com_id'])) {
|
||||||
$strCode = "compte_accueil";
|
// Compte v2 : atterrissage hub (Mes événements). Sinon accueil participant.
|
||||||
else
|
if (fxPromoteurHubShouldUse($_SESSION['com_id'])) {
|
||||||
$strCode = "compte_login";
|
$strCode = 'compte_promoteur_hub';
|
||||||
|
} else {
|
||||||
|
$strCode = 'compte_accueil';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$strCode = 'compte_login';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($strCode === 'compte_promoteur_hub' && (!isset($_SESSION['com_id']) || !fxPromoteurHubIsAvailable($_SESSION['com_id']))) {
|
||||||
|
header('Location: ' . $vDomaine . '/' . ($strLangue === 'fr' ? 'compte' : 'account'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ancien slug renomme (retrait du nom "mobile") : redirection 301 vers le nouveau slug "gestion".
|
||||||
|
// Conserve les parametres utiles (promoteur_eve_id, pec_id, lng...) pour ne pas casser favoris/QR.
|
||||||
|
if (($_GET['code'] ?? '') === 'inc_tableau_inscriptions_mobile') {
|
||||||
|
$strNewPath = ($strLangue === 'en') ? '/account/inc_tableau_inscriptions_gestion' : '/compte/inc_tableau_inscriptions_gestion';
|
||||||
|
parse_str($_SERVER['QUERY_STRING'] ?? '', $arrOldQs);
|
||||||
|
unset($arrOldQs['code'], $arrOldQs['lang']);
|
||||||
|
$strQsClean = http_build_query($arrOldQs);
|
||||||
|
header('Location: ' . $vDomaine . $strNewPath . ($strQsClean !== '' ? '?' . $strQsClean : ''), true, 301);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$recPage = fxGetPage($strCode);
|
$recPage = fxGetPage($strCode);
|
||||||
|
|
||||||
if (count($recPage) < 1) {
|
// SM-9493 — affichecarte n'a pas de ligne CMS. Depuis v4.72.707 (ce7c4300, 2026-06-27)
|
||||||
header('Location: index.php');
|
// fxGetPage([]) + Location: index.php relatif → 404. Ne pas court-circuiter le PDF plus haut.
|
||||||
|
if (count($recPage) < 1 && $strCode !== 'compte_affichecarte') {
|
||||||
|
header('Location: ' . $vDomaine . '/' . ($strLangue === 'en' ? 'account' : 'compte'));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,16 +101,17 @@ if (!empty($_GET['a']) && $_GET['code'] != 'bib_assignment') {
|
|||||||
$strCode = $_GET['a'];
|
$strCode = $_GET['a'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// empêcher d'accéder à une page où il faut être loggé et qu'on ne l'est pas
|
// Page protégée sans session : sauver la cible puis formulaire de login (MSIN-4339).
|
||||||
if (($strCode != "compte_login" && $strCode != "compte_motdepasse" && $strCode != "compte_nouveau" && $strCode != "compte_reset" && !isset($_SESSION['com_id'])) || (($strCode == "compte_login" || $strCode == "compte_motdepasse" || $strCode == "compte_nouveau" || $strCode == "compte_reset") && isset($_SESSION['com_id']))) {
|
if ($strCode != 'compte_login' && $strCode != 'compte_motdepasse' && $strCode != 'compte_nouveau' && $strCode != 'compte_reset' && !isset($_SESSION['com_id'])) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// MSIN-4339 Sauvegarder l'URL demandée
|
|
||||||
$_SESSION['redirect_after_login'] = $_SERVER['REQUEST_URI'];
|
$_SESSION['redirect_after_login'] = $_SERVER['REQUEST_URI'];
|
||||||
|
|
||||||
header('Location: ' . $vDomaine . '/' . $strPage);
|
header('Location: ' . $vDomaine . '/' . $strPage);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Déjà connecté sur login / mot de passe / inscription : accueil compte, sans polluer redirect_after_login.
|
||||||
|
// MSIN-4401 — compte_reset exclu : géré dans fxShowFormReset (conflit autre session).
|
||||||
|
if (($strCode == 'compte_login' || $strCode == 'compte_motdepasse' || $strCode == 'compte_nouveau') && isset($_SESSION['com_id'])) {
|
||||||
|
header('Location: ' . $vDomaine . '/' . $strPage);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,7 +256,6 @@ if (!empty($_POST['form_action'])) {
|
|||||||
default:
|
default:
|
||||||
switch ($strAction) {
|
switch ($strAction) {
|
||||||
case 'login':
|
case 'login':
|
||||||
|
|
||||||
fxLoginCompte($_POST, $strLangue);
|
fxLoginCompte($_POST, $strLangue);
|
||||||
|
|
||||||
|
|
||||||
@ -308,10 +337,26 @@ if (isset($_GET['promoteur_eve_id'])) {
|
|||||||
|
|
||||||
$intEveId = base64_decode(urldecode($_GET['promoteur_eve_id']));
|
$intEveId = base64_decode(urldecode($_GET['promoteur_eve_id']));
|
||||||
$intEveIdbase = $intEveId;
|
$intEveIdbase = $intEveId;
|
||||||
$blnIsPromoteur = fxIsPromoteur( $_SESSION['com_id'],$intEveIdbase );
|
if ($strCode === 'compte_inc_tableau_inscriptions_gestion') {
|
||||||
|
if (!fxInscrGestionCanAccess($_SESSION['com_id'], $intEveIdbase)) {
|
||||||
|
header('Location: ' . $vDomaine . '/' . $strPage);
|
||||||
if (!$blnIsPromoteur) {
|
exit;
|
||||||
|
}
|
||||||
|
} elseif ($strCode === 'compte_inc_tableau_gestion_epreuves') {
|
||||||
|
if (empty($_SESSION['usa_id'])
|
||||||
|
&& !fxEveAccesHasAnyPermission($_SESSION['com_id'] ?? 0, $intEveIdbase, array('dossards.manage', 'epreuves.edit_qte'))) {
|
||||||
|
header('Location: ' . $vDomaine . '/' . $strPage);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} elseif ($strCode === 'compte_inc_tableau_eve_acces') {
|
||||||
|
require_once('php/inc_fx_eve_acces.php');
|
||||||
|
if (!fxEveAccesCanTeamInvite($_SESSION['com_id'] ?? 0, $intEveIdbase)) {
|
||||||
|
header('Location: ' . $vDomaine . '/' . $strPage);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} elseif (empty($_SESSION['usa_id'])
|
||||||
|
&& !fxIsPromoteur($_SESSION['com_id'], $intEveIdbase)
|
||||||
|
&& !fxEveAccesComHasV2ForEvent($_SESSION['com_id'], $intEveIdbase)) {
|
||||||
header('Location: ' . $vDomaine . '/' . $strPage);
|
header('Location: ' . $vDomaine . '/' . $strPage);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@ -334,6 +379,69 @@ $strMetaTitle = fxRemoveHtml(fxUnescape($recPage['pag_titre_' . $strLangue]));
|
|||||||
$strMetaDescription = fxRemoveHtml(fxUnescape($recPage['pag_description_' . $strLangue]));
|
$strMetaDescription = fxRemoveHtml(fxUnescape($recPage['pag_description_' . $strLangue]));
|
||||||
$strMetaKeywords = fxRemoveHtml(fxUnescape($recPage['pag_keywords_' . $strLangue]));
|
$strMetaKeywords = fxRemoveHtml(fxUnescape($recPage['pag_keywords_' . $strLangue]));
|
||||||
$blnBoutonRetour = true;
|
$blnBoutonRetour = true;
|
||||||
|
$blnHideCompteTitle = false;
|
||||||
|
|
||||||
|
// MSIN-4401 — POST invite/revoke AVANT le HTML (sinon header Location → page blanche).
|
||||||
|
if ($strCode === 'compte_inc_tableau_eve_acces'
|
||||||
|
&& ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST'
|
||||||
|
&& isset($_GET['promoteur_eve_id'])) {
|
||||||
|
require_once('php/inc_fx_eve_acces_promoteur.php');
|
||||||
|
$intEveIdPost = intval(base64_decode(urldecode($_GET['promoteur_eve_id'])));
|
||||||
|
$intActorPost = intval($_SESSION['com_id'] ?? 0);
|
||||||
|
if (!empty($_SESSION['usa_id'])) {
|
||||||
|
$intActorPost = intval($_SESSION['usa_id']);
|
||||||
|
}
|
||||||
|
$strLanguePost = ($strLangue === 'en') ? 'en' : 'fr';
|
||||||
|
$strBasePathPost = ($strLanguePost === 'en') ? '/account' : '/compte';
|
||||||
|
$strSelfUrlPost = $vDomaine . $strBasePathPost . '/inc_tableau_eve_acces?promoteur_eve_id='
|
||||||
|
. urlencode(base64_encode($intEveIdPost)) . '&lng=' . $strLanguePost;
|
||||||
|
|
||||||
|
if ($intEveIdPost > 0 && fxEveAccesCanTeamInvite($intActorPost, $intEveIdPost)) {
|
||||||
|
$strEveAccesAction = (string)($_POST['eve_acces_action'] ?? '');
|
||||||
|
if ($strEveAccesAction === 'invite') {
|
||||||
|
$arrEveAccesResult = fxEveAccesPromoteurInvite($intEveIdPost, $intActorPost, $_POST, $strLanguePost);
|
||||||
|
if (($arrEveAccesResult['state'] ?? '') === 'success') {
|
||||||
|
header('Location: ' . $strSelfUrlPost . '&ok=' . urlencode($arrEveAccesResult['message']));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
header('Location: ' . $strSelfUrlPost . '&err=' . urlencode(
|
||||||
|
$arrEveAccesResult['message'] ?? fxEveAccesPromoteurT('eve_acces_promoteur_err_generic', $strLanguePost)
|
||||||
|
));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($strEveAccesAction === 'revoke') {
|
||||||
|
$arrEveAccesResult = fxEveAccesPromoteurRevoke(intval($_POST['ea_id'] ?? 0), $intActorPost, $intEveIdPost);
|
||||||
|
if (($arrEveAccesResult['state'] ?? '') === 'success') {
|
||||||
|
header('Location: ' . $strSelfUrlPost . '&ok=' . urlencode(
|
||||||
|
fxEveAccesPromoteurT('eve_acces_promoteur_ok_revoked', $strLanguePost)
|
||||||
|
));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
header('Location: ' . $strSelfUrlPost . '&err=' . urlencode(
|
||||||
|
fxEveAccesPromoteurT('eve_acces_promoteur_err_revoke', $strLanguePost)
|
||||||
|
));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($strEveAccesAction === 'resend') {
|
||||||
|
$arrEveAccesResult = fxEveAccesPromoteurResendMail(
|
||||||
|
intval($_POST['ea_id'] ?? 0),
|
||||||
|
$intActorPost,
|
||||||
|
$intEveIdPost,
|
||||||
|
$strLanguePost
|
||||||
|
);
|
||||||
|
if (($arrEveAccesResult['state'] ?? '') === 'success') {
|
||||||
|
header('Location: ' . $strSelfUrlPost . '&ok=' . urlencode(
|
||||||
|
$arrEveAccesResult['message'] ?? fxEveAccesPromoteurT('eve_acces_promoteur_ok_resent', $strLanguePost)
|
||||||
|
));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
header('Location: ' . $strSelfUrlPost . '&err=' . urlencode(
|
||||||
|
$arrEveAccesResult['message'] ?? fxEveAccesPromoteurT('eve_acces_promoteur_err_resend', $strLanguePost)
|
||||||
|
));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
require_once("inc_header.php");
|
require_once("inc_header.php");
|
||||||
|
|
||||||
@ -343,7 +451,7 @@ require_once("inc_header.php");
|
|||||||
<div class="container bg-white p-3 p-xl-5">
|
<div class="container bg-white p-3 p-xl-5">
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
if (isset($recPage['pag_titre_' . $strLangue]) != '') {
|
if (isset($recPage['pag_titre_' . $strLangue]) != '' && empty($blnHideCompteTitle)) {
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
|
||||||
@ -360,7 +468,7 @@ require_once("inc_header.php");
|
|||||||
}
|
}
|
||||||
if (isset($_SESSION['msg'])) {
|
if (isset($_SESSION['msg'])) {
|
||||||
echo fxMessage($_SESSION['msg'], '', $strLangue);
|
echo fxMessage($_SESSION['msg'], '', $strLangue);
|
||||||
} else {
|
} else if (empty($blnHideCompteTitle)) {
|
||||||
?>
|
?>
|
||||||
<br>
|
<br>
|
||||||
<?php
|
<?php
|
||||||
@ -465,9 +573,13 @@ require_once("inc_header.php");
|
|||||||
case 'compte_annuler_inscription':
|
case 'compte_annuler_inscription':
|
||||||
include_once('annuler_inscription.php');
|
include_once('annuler_inscription.php');
|
||||||
break;
|
break;
|
||||||
|
case 'compte_promoteur_hub':
|
||||||
|
$blnBoutonRetour = false;
|
||||||
|
echo fxShowPromoteurHub($_SESSION['com_id'], $strLangue);
|
||||||
|
break;
|
||||||
case 'compte_accueil':
|
case 'compte_accueil':
|
||||||
//MSIN-4371
|
//MSIN-4371
|
||||||
if (isset($_SESSION['usa_id'])) { ?>
|
if (fxEveAccesCanQrTest($_SESSION['com_id'])) { ?>
|
||||||
|
|
||||||
<a href="qr_test.php" target="_blank" class="btn btn-primary">
|
<a href="qr_test.php" target="_blank" class="btn btn-primary">
|
||||||
QR Test
|
QR Test
|
||||||
@ -688,19 +800,18 @@ require_once("inc_header.php");
|
|||||||
$footer_script = true;
|
$footer_script = true;
|
||||||
include("inc_tableau_rabais.php");
|
include("inc_tableau_rabais.php");
|
||||||
break;
|
break;
|
||||||
// MSIN-3679
|
|
||||||
case 'compte_inc_tableau_remboursement':
|
|
||||||
$blnBoutonRetour = false;
|
|
||||||
$footer_script = true;
|
|
||||||
|
|
||||||
include("inc_tableau_remboursement.php");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'compte_inc_tableau_inscriptions':
|
case 'compte_inc_tableau_inscriptions':
|
||||||
$blnBoutonRetour = false;
|
$blnBoutonRetour = false;
|
||||||
$footer_script = true;
|
$footer_script = true;
|
||||||
include("inc_tableau_inscriptions.php");
|
include("inc_tableau_inscriptions.php");
|
||||||
break;
|
break;
|
||||||
|
case 'compte_inc_tableau_inscriptions_gestion':
|
||||||
|
$blnBoutonRetour = false;
|
||||||
|
$footer_script = true;
|
||||||
|
$blnHideCompteTitle = true;
|
||||||
|
fxInscrGestionInitTextes($strLangue);
|
||||||
|
include("inc_tableau_inscriptions_gestion.php");
|
||||||
|
break;
|
||||||
case 'compte_inc_tableau_mail_entraineurs':
|
case 'compte_inc_tableau_mail_entraineurs':
|
||||||
$blnBoutonRetour = false;
|
$blnBoutonRetour = false;
|
||||||
$footer_script = true;
|
$footer_script = true;
|
||||||
@ -720,6 +831,17 @@ require_once("inc_header.php");
|
|||||||
$blnBoutonRetour = false;
|
$blnBoutonRetour = false;
|
||||||
include("inc_tableau_epreuves.php");
|
include("inc_tableau_epreuves.php");
|
||||||
break;
|
break;
|
||||||
|
case 'compte_inc_tableau_gestion_epreuves':
|
||||||
|
$blnBoutonRetour = false;
|
||||||
|
include("inc_tableau_gestion_epreuves.php");
|
||||||
|
break;
|
||||||
|
case 'compte_inc_tableau_eve_acces':
|
||||||
|
// MSIN-4401
|
||||||
|
$blnBoutonRetour = false;
|
||||||
|
$footer_script = true;
|
||||||
|
$blnHideCompteTitle = true;
|
||||||
|
include("inc_tableau_eve_acces.php");
|
||||||
|
break;
|
||||||
|
|
||||||
case 'compte_inc_tableau_liste_attente':
|
case 'compte_inc_tableau_liste_attente':
|
||||||
|
|
||||||
|
|||||||
324
css/ms1-doc.css
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
body.ms1-doc-open {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MS1 — Module documentation (overlay paginé).
|
||||||
|
* Le bouton ? réutilise .btn-aide-bib (style.css) — pas de style rond dupliqué ici.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.btn-aide-bib.ms1-doc-trigger {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overlay plein écran */
|
||||||
|
.ms1-doc-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 10050;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-overlay[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Panneau central — plus large pour le guide */
|
||||||
|
.ms1-doc-panel {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 840px;
|
||||||
|
max-height: 92vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #212529;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-subtitle {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #6c757d;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-close {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-close:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 20px 22px;
|
||||||
|
min-height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-page[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typographie contenu HTML admin */
|
||||||
|
.ms1-doc-format,
|
||||||
|
.ms1-doc-content {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-format h2,
|
||||||
|
.ms1-doc-format h3,
|
||||||
|
.ms1-doc-content h2,
|
||||||
|
.ms1-doc-content h3 {
|
||||||
|
margin: 14px 0 8px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-format h2:first-child,
|
||||||
|
.ms1-doc-format h3:first-child,
|
||||||
|
.ms1-doc-content h2:first-child,
|
||||||
|
.ms1-doc-content h3:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-format p,
|
||||||
|
.ms1-doc-content p {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-format ol,
|
||||||
|
.ms1-doc-format ul,
|
||||||
|
.ms1-doc-content ol,
|
||||||
|
.ms1-doc-content ul {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
padding-left: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-format li,
|
||||||
|
.ms1-doc-content li {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-format blockquote,
|
||||||
|
.ms1-doc-content blockquote {
|
||||||
|
margin: 12px 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-left: 3px solid #007bff;
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-format table,
|
||||||
|
.ms1-doc-content table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-format th,
|
||||||
|
.ms1-doc-format td,
|
||||||
|
.ms1-doc-content th,
|
||||||
|
.ms1-doc-content td {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
padding: 6px 8px;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-format th,
|
||||||
|
.ms1-doc-content th {
|
||||||
|
background: #f1f3f5;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-format strong,
|
||||||
|
.ms1-doc-content strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bloc pédagogique par bouton / contrôle */
|
||||||
|
.ms1-doc-bouton {
|
||||||
|
margin: 16px 0;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-bouton h3,
|
||||||
|
.ms1-doc-bouton h4 {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2f5fd0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-bouton dl {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-bouton dt {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-bouton dt:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-bouton dd {
|
||||||
|
margin: 2px 0 0;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pied — navigation */
|
||||||
|
.ms1-doc-footer {
|
||||||
|
padding: 10px 14px 12px;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-nav-dots {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-dot {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
border: 2px solid #dee2e6;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
color: #495057;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-dot.is-active,
|
||||||
|
.ms1-doc-dot:hover {
|
||||||
|
border-color: #007bff;
|
||||||
|
background: #e7f1ff;
|
||||||
|
color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-nav-arrows {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-prev,
|
||||||
|
.ms1-doc-next {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border: 1px solid #6c757d;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
color: #495057;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-prev:hover:not(:disabled),
|
||||||
|
.ms1-doc-next:hover:not(:disabled) {
|
||||||
|
background: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-prev:disabled,
|
||||||
|
.ms1-doc-next:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-counter {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #6c757d;
|
||||||
|
min-width: 48px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Métadonnées JSON — jamais affichées */
|
||||||
|
script.ms1-doc-meta {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.ms1-doc-overlay {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-panel {
|
||||||
|
max-height: 95vh;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-doc-dot {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
100
css/ms1-loader.css
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* MS1 — Indicateur de chargement (coureur animé).
|
||||||
|
* Reutilisable en overlay plein ecran (#preloader) ou inline (panneaux AJAX).
|
||||||
|
*/
|
||||||
|
.ms1-loader {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-loader__runner {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
object-fit: contain;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-loader__label {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-loader--inline {
|
||||||
|
padding: 1.25rem 1rem;
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-loader--inline .ms1-loader__runner {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-loader--inline .ms1-loader__label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
#preloader #spinner.ms1-loader--overlay {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
|
||||||
|
padding: 1.25rem 1.75rem;
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
margin: 0;
|
||||||
|
width: auto;
|
||||||
|
min-width: 200px;
|
||||||
|
font-size: inherit;
|
||||||
|
color: inherit;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#preloader #spinner.ms1-loader--overlay .ms1-loader__runner {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#preloader #spinner.ms1-loader--overlay #preloader_text,
|
||||||
|
#preloader #spinner.ms1-loader--overlay .ms1-loader__label {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-loader__progress {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 220px;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-loader__progress-track {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: #e8e8e8;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-loader__progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 0;
|
||||||
|
background: linear-gradient(90deg, #2e7d32, #43a047);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms1-loader__progress-pct {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
6498
css/style.css
95
docs/chronotrack/README.md
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
# ChronoTrack Live — docs de référence (MS1)
|
||||||
|
|
||||||
|
**Source officielle** : https://api.chronotrack.com/dev/docs
|
||||||
|
**Résultats (champs pays / province)** : https://api.chronotrack.com/dev/docs/ctlive-results
|
||||||
|
**Meta entry** : snapshot local `api-meta-entry.json`
|
||||||
|
|
||||||
|
## Fichiers
|
||||||
|
|
||||||
|
| Fichier | Contenu |
|
||||||
|
|---|---|
|
||||||
|
| `api-dev-docs.md` | Copie Overview `/dev/docs` |
|
||||||
|
| `api-meta-entry.json` | Colonnes meta `entry` |
|
||||||
|
| `README.md` | Ce fichier |
|
||||||
|
|
||||||
|
## ID unique envoyé à ChronoTrack (MSIN-4482)
|
||||||
|
|
||||||
|
| Origine MS1 | `external_id` CT |
|
||||||
|
|---|---|
|
||||||
|
| Import fichier / saisie rapide | `par_external_id` (ID du fichier client) |
|
||||||
|
| Inscription en ligne | `par_id_original` (ID inventé MS1) |
|
||||||
|
|
||||||
|
Si l’entry CT existe déjà sous l’ancien ID MS1, le PUT (via `ct_entry_id`) pose l’ID fichier — pas de doublon.
|
||||||
|
|
||||||
|
## Calibrage validé (2026-08-11) — event 93163
|
||||||
|
|
||||||
|
Sources : **Sonde lecture pays** (GET entry) + export `confirmedentries` CSV.
|
||||||
|
|
||||||
|
Quand Athlete Info UI = Canada + Quebec, CT stocke :
|
||||||
|
|
||||||
|
| Champ API / export | Valeur |
|
||||||
|
|---|---|
|
||||||
|
| `location_country` / `COUNTRY_CODE` | **`CA`** (ISO-2) |
|
||||||
|
| `location_region` / `REGION_CODE` | **`QC`** |
|
||||||
|
| `country_name` / `COUNTRY_NAME` | `Canada` |
|
||||||
|
| `REGION_NAME` (export) | `Quebec` |
|
||||||
|
|
||||||
|
**Pas** `CAN` (ISO-3) dans `location_country`.
|
||||||
|
Export massif : 1578/1580 sans `COUNTRY_CODE` avant fix — seule Gabrielle-Ann (manuel) + 1 FR avaient un pays.
|
||||||
|
|
||||||
|
## Relecture liste entry (vérif contenu)
|
||||||
|
|
||||||
|
L’écriture accepte souvent `bib` / `first_name` / `external_id`.
|
||||||
|
La **liste** meta renvoie plutôt :
|
||||||
|
|
||||||
|
| Payload écriture | Clé lecture CT |
|
||||||
|
|---|---|
|
||||||
|
| `external_id` | `entry_external_id` |
|
||||||
|
| `bib` | `entry_bib` |
|
||||||
|
| `birthdate` | `athlete_birthdate` |
|
||||||
|
| `first_name` / `last_name` / `sex` | `athlete_first_name` / `athlete_last_name` / `athlete_sex` |
|
||||||
|
|
||||||
|
`fx_chronotrack_verify.php` mappe ces alias — sinon faux écarts « CT ∅ ».
|
||||||
|
|
||||||
|
**Règle vérif (tous les champs)** :
|
||||||
|
- Liste CT avec `columns=` (identité, adresse, bib, statut, `entry_check_in`).
|
||||||
|
- Clé absente de la réponse API → **pas un écart**.
|
||||||
|
- Questions (`entry_note_*`) : envoyées, jamais comparées (Custom Import UI seulement).
|
||||||
|
|
||||||
|
**Écriture pays/région** : seulement `location_country` / `location_region` / `country_name`
|
||||||
|
(pas `country_code` / `state_code` — collision possible → QC dans le pays).
|
||||||
|
Si province **canadienne** (ex. `QC`) et pays MS1 ≠ `CA` → **province non envoyée** + log `skip_region`
|
||||||
|
(pas de réparation auto du pays ; pas de faux écart à la vérif).
|
||||||
|
|
||||||
|
**Push** (règle produit MSIN-4574 — **correctitude > vitesse**) :
|
||||||
|
- entry déjà dans CT (`entry_id`) → **PUT** unitaire (écrase pays / région / bib)
|
||||||
|
- création seulement → **POST** bulk
|
||||||
|
- **Jamais** sacrifier des champs pour aller plus vite ; optimiser la vitesse **après** 0 écart
|
||||||
|
- **Vérifier contenu** = validation seulement (pas un filet opérationnel)
|
||||||
|
|
||||||
|
## Fichiers code (module)
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
|---|---|
|
||||||
|
| `fx_chronotrack_payload.php` | Source de vérité **écriture** entry (pays, bib, sexe, notes) |
|
||||||
|
| `fx_chronotrack_push.php` | Push HTTP, lots, job, conflits dossard |
|
||||||
|
| `fx_chronotrack_read.php` | Lecture entries CT, index, comptage |
|
||||||
|
| `fx_chronotrack_lifecycle.php` | Cancel / restore / WITHDRAWN / orphelins |
|
||||||
|
| `fx_chronotrack_cron.php` | Auto-push / cron |
|
||||||
|
| `fx_chronotrack_probe.php` | Sondes Diagnostic (support) |
|
||||||
|
| `fx_chronotrack_sync.php` | Classify, preview, logs |
|
||||||
|
| `fx_chronotrack_verify.php` | **Vérifier contenu** — payload MS1 vs entries CT (écarts) |
|
||||||
|
| UI Opération | Envoyer / Tout renvoyer / **Vérifier contenu** / logs |
|
||||||
|
| UI Diagnostic | Sondes + **Orphelins CT** (hors éligibles) |
|
||||||
|
|
||||||
|
| MS1 BD | Champ CT |
|
||||||
|
|---|---|
|
||||||
|
| `pay_id` → `inscriptions_pays.pay_iso` | `location_country` + `country_code` = **ISO-2** (`CA`) |
|
||||||
|
| `pay_id` → `pay_nom_en` | `country_name` (`Canada`) |
|
||||||
|
| `pro_id` → `inscriptions_provinces.pro_iso` | `location_region` + `state_code` = **`QC`** |
|
||||||
|
| `pay_iso3` / map ISO-2→3 | référence / logs seulement (pas Athlete Info) |
|
||||||
|
|
||||||
|
## Docs résultats CTLIVE
|
||||||
|
|
||||||
|
La page résultats mentionne `country_code` « three character ISO-3166 ».
|
||||||
|
Pour **Athlete Info / entry / export confirmed**, la vérité mesurée = **ISO-2**. On suit la calibrage entry.
|
||||||
318
docs/chronotrack/api-dev-docs.md
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
Source URL: https://api.chronotrack.com/dev/docs
|
||||||
|
Title: ChronoTrack Live - ChronoTrack
|
||||||
|
|
||||||
|
* Home
|
||||||
|
* Projects
|
||||||
|
* Docs
|
||||||
|
|
||||||
|
Sign-In Sign-Up
|
||||||
|
|
||||||
|
API Version: **v1.3**
|
||||||
|
|
||||||
|
NOTICE This is a non-production environment used for testing. Any changes you make to data will be lost. Please do not use for production services.
|
||||||
|
|
||||||
|
# API Docs Test the API Right Here!
|
||||||
|
|
||||||
|
* Overview
|
||||||
|
* Terminology
|
||||||
|
* Endpoints
|
||||||
|
* Security
|
||||||
|
* Formats
|
||||||
|
* Requests
|
||||||
|
* Rate Limiting
|
||||||
|
* Responses
|
||||||
|
* Character Encoding
|
||||||
|
* Data Types
|
||||||
|
* Resources
|
||||||
|
|
||||||
|
The API hews as closely as possible to the REST architectural style. The API is organized around a set of `resources` that populate the world of endurance racing, like events, races, race entries, etc. With the API, your application can perform operations on these resources through the HTTP protocol. You can use the documentation below to learn about the API. In particular, you can test out API calls right here on this page to see exactly how API calls work and how to incorporate them into your application.
|
||||||
|
|
||||||
|
## Terminology
|
||||||
|
|
||||||
|
A `resource` is some object of interest targeted by the API. In the world of endurance racing this includes events, races, athletes, timing devices, and others. We'll refer to a specific resource sometimes as a `resource entity` to distinguish that specific entity from all entities of that `resource type`. A `resource collection` is a number of resource entities of the same type.
|
||||||
|
|
||||||
|
The ChronoTrack Live Open Platform API is comprised of `operations` on these resources. All operations are executed by sending HTTP requests to one of our API endpoints. An API `endpoint` is a URI to which a API request is directed.
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
All API requests should be sent to one of the following two endpoints:
|
||||||
|
|
||||||
|
| Endpoint Type | Endpoint URL |
|
||||||
|
| ------------- | ------------------------------------ |
|
||||||
|
| Production | https://api.chronotrack.com/api |
|
||||||
|
| Testing | https://api-test.chronotrack.com/api |
|
||||||
|
|
||||||
|
The production endpoint should only be used in live, production code and never for testing. The production endpoint is backed by the ChronoTrack Live production database, which means all changes to data via the production API will be immediately visible to ChronoTrack Live users.
|
||||||
|
|
||||||
|
The testing endpoint is backed by a separate testing database that is refreshed every night with a copy of the production database. This means any API activity that changes data via the testing endpoint will have those changes disappear the next day. Please note the QA environment is not 100% guaranteed up at all times as it is a test environment.
|
||||||
|
|
||||||
|
**Note:** Both endpoints are secured by the `https` (TLS/SSL) protocol. Any requests sent via the non-secure `http` protocol will be redirected to the secure endpoint.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
All requests to our production API must be through the secure HTTPS protocol. Any requests that come in on a non-secure port will be redirected to our TLS/SSL secured service.
|
||||||
|
|
||||||
|
All requests must also be authenticated. First, a request must originate from an application that is registered and approved by ChronoTrack. Second, the application must authenticate the ChronoTrack Live user on whose behalf it is making requests to our system.
|
||||||
|
|
||||||
|
### Supported Authorization Methods
|
||||||
|
|
||||||
|
We support four authentication methods, each suited for a different kind of application environment. For applications that have easy access to the user's browser, like a web or mobile application, we support the **OAuth 2.0 Web Flow**. For applications without browser access, like native desktop/laptop applications, we provide two alternatives. If the environment your application operates in has the capacity to securely store your users' ChronoTrack Live account credentials, then you can use **HTTP Basic** authentication. If you can't, or prefer not to, locally store your users' credentials, we support the **OAuth 2.0 Password Flow**.
|
||||||
|
|
||||||
|
Finally, for certain testing environments where data security requirements are minimal, we support a **simple authentication scheme** where you can pass a user id and password as parameters to the API call. All our methods are described in detail below.
|
||||||
|
|
||||||
|
#### OAuth2 Web Flow
|
||||||
|
|
||||||
|
Web applications can acquire an OAuth2 access token for a ChronoTrack Live user by following these steps:
|
||||||
|
|
||||||
|
1. Obtain a ChronoTrack Live developer account here. Creating a developer account will also register your client application.
|
||||||
|
2. Your application will be assigned a `client_id` and a `client_secret`. The client\_id is public information, but you should take care to safeguard your client\_secret. During the process of registering your application with us, you will be asked to provide a `redirect_uri`, that is, a URL within your web application where users will be redirected after granting your application permission to access their ChronoTrack Live resources. An example redirect\_uri is:
|
||||||
|
https://mywebapp.com/oauth2
|
||||||
|
3. When the user first invokes a function in your application requiring access to their ChronoTrack Live resources, you should redirect the user to our authorization end-point:
|
||||||
|
GET /oauth2/authorize?response_type=code
|
||||||
|
Make sure to add a Basic HTTP `Authorization` header to this request using your `client_id` and `client_secret` as the username and password. This is required to verify the authorization request is coming from your application. See the specific instructions in the HTTP Basic Authentication section for the mechanics of how to construct the `Authorization` header.
|
||||||
|
This will redirect your user to our authentication service. They will have to login as they normally would to ChronoTrack Live. They then will be presented with a screen that allows them to grant your application permission to access their ChronoTrack Live resources.
|
||||||
|
4. Upon authorizing your application, the user will be redirected back to your application's specified redirect\_uri:
|
||||||
|
https://mywebapp.com/oauth2?code=bef10603874b
|
||||||
|
The `code` parameter appended to your redirect uri is a short-lived (10 minutes), single-use authorization code that your application can convert into a longer-lived access token.
|
||||||
|
5. To obtain an `access token`, your application should make a GET API call to our token endpoint:
|
||||||
|
GET /oauth2/token?grant_type=authorization_code&code=bef10603874b
|
||||||
|
passing in the code parameter returned to your application in the last step and making sure to add an `Authorization` header that provides Basic HTTP authentication for your client app (using your `client_id` as the username and your `client_secret` as the password).
|
||||||
|
This API token endpoint will return a json formatted object containing an OAuth 2.0 access token that allows your application to access the user's ChronoTrack Live resources for a lengthy period of time (usually 72 hours).
|
||||||
|
{
|
||||||
|
"access_token": "2YotnFZFEjr1zCsicMWpAA",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires": "1343309220",
|
||||||
|
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA"
|
||||||
|
}
|
||||||
|
You also will receive a `refresh token` that you can send to our token endpoint to receive another access token, good for another 72 hours of use. The refresh token is valid for one year or until the user revokes your application's access to their resources.
|
||||||
|
You send a refresh token request like this:
|
||||||
|
GET /oauth2/token?grant_type=refresh_token&refresh_token=597262138b23
|
||||||
|
making sure to again provide Basic HTTP authentication for your client app (using your client\_id as the username and your client\_secret as the password). This will send you another access token that you can use for another 72 hours. It will also return a new refresh token that you can use to extend your access.
|
||||||
|
6. Finally, with a fresh access token, you can make API requests, like getting a list of events the user has access to:
|
||||||
|
GET /api/event
|
||||||
|
making sure to pass in an `Authorization` header with the access token like so:
|
||||||
|
Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA
|
||||||
|
|
||||||
|
#### OAuth2 Password Flow
|
||||||
|
|
||||||
|
For applications where the OAuth2 Web Flow cannot be used (for example, native applications without a browser layer), an OAuth access token can be acquired by making a single call to our token endpoint and passing the end-user’s ChronoTrack Live account credentials as the `username` and `password` parameters with a `grant_type` of `password`.
|
||||||
|
|
||||||
|
GET /oauth2/token?grant_type=password&username=joesmith%40someplace.com&password=some_password
|
||||||
|
|
||||||
|
**Note:** In order for this to work you must pass in an `Authorization` header using HTTP Basic Auth where the username is your `client_id` and the password is your `client_secret`. Follow the instructions under the "HTTP Basic Auth" tab for how to construct this header.
|
||||||
|
|
||||||
|
**Note:** that users need only enter their username and password once for the application to authenticate. **Applications using the password flow SHOULD NOT store the user's credentials**.
|
||||||
|
|
||||||
|
#### HTTP Basic Authentication
|
||||||
|
|
||||||
|
This method involves passing in the resource owner's ChronoTrack Live account username and password directly, according to the HTTP Basic Authentication protocol. You simply construct an `Authorization` header as follows:
|
||||||
|
|
||||||
|
1. Obtain the resource owner's ChronoTrack Live account credentials. For example, you could provide a dialog through your application asking for their ChronoTrack Live account username and password requesting their permission to use those credentials for accessing their account data on their behalf.
|
||||||
|
2. Concatenate their username and password with a colon (:). For example, if their username is "zooba" and their password is "tooba", you would construct the string `zooba:tooba`.
|
||||||
|
3. Now base64 encode that concatenated string to produce: `em9vYmE6dG9vYmE=`.
|
||||||
|
4. Finally, add an authorization header to your request like this:
|
||||||
|
Authorization: Basic em9vYmE6dG9vYmE=
|
||||||
|
|
||||||
|
**WARNING!** This method can be less secure than using one of the OAuth2 methods, and as such we do not recommend you use it. However, we still provide support for this authentication method because some applications may find it simpler to use.
|
||||||
|
|
||||||
|
There are two risks in using this method. The first risk lies in sending a request over a non-TLS/SSL connection. While we automatically redirect all non-secure requests to our TLS/SSL port, an initial non-secure request risks exposing your user's ChronoTrack Live account credentials to anyone sophisticated enough to be sniffing network packets, allowing man-in-the-middle attacks. This risk can be completely mitigated by sending all requests over TLS/SSL.
|
||||||
|
|
||||||
|
A second risk is that your application has to ask for and store the user's ChronoTrack Live credentials. You then become responsible for safeguarding their password and making sure it is not exposed via an insecure storage medium, which adds complexity to your application security aparatus. The only way to mitigate this risk is to not use this method and instead use one of the OAuth2 methods.
|
||||||
|
|
||||||
|
#### Test Auth Scheme
|
||||||
|
|
||||||
|
In certain situations, like when using our docs here to test the API, security requirements are minimal and using oauth or basic authentication is infeasible. For these special cases we support a simple method of passing in the ChronoTrack Live resource owner credentials via parameters. For instance, if you have a test account for a resource owner called `test-user@myorg.com` with password `12345`, then you could make authenticated requests to our API on behalf of that test user:
|
||||||
|
|
||||||
|
GET /api/event?client_id=abcdefgh&user_id=test-user%40myorg.com&user_pass=12345
|
||||||
|
|
||||||
|
To prevent exposing the password, you should SHA1-encode the password:
|
||||||
|
|
||||||
|
GET /api/event?client_id=abcdefgh&user_id=test-user%40myorg.com&user_pass=8cb2237d0679ca88db6464eac60da96345513964
|
||||||
|
|
||||||
|
**WARNING!** We provide this authorization method for convenience and testing. We strongly recommend you do not use this in a production environment. If you do, at least make sure you only do so over TLS/SSL and SHA1-encode the password.
|
||||||
|
|
||||||
|
## Formats
|
||||||
|
|
||||||
|
The following table describes the formats used for the content of API requests and responses. The first column contains a short `format code` for each format, while the second column contains the standard Internet Assigned Numbers Authority MIME type for the format.
|
||||||
|
|
||||||
|
| Format Code | MIME Type | Description |
|
||||||
|
| ----------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| json | application/json | _Javascript Object Notation_. See <http://www.json.org/> for details. |
|
||||||
|
| xml | application/xml | _Extensible Markup Language_. See <http://www.w3.org/XML/> for details. |
|
||||||
|
| yaml | application/yaml | _Yet Another Markup Language_. See <http://www.yaml.org/> for details. |
|
||||||
|
| csv | text/csv | _Comma Separated Values_. A tabular plain-text format. Columns within each row are delimited by a comma character (","). Columns that contain literal commas are escaped by double quotes. Rows are delimited by a carriage-return/line-feed ("\\r\\n") sequence. See <http://tools.ietf.org/html/rfc4180> for more details. |
|
||||||
|
| tsv | text/tab-delimited-values | _Tab Separated Values_. A tabular plain-text format. Columns within each row are delimited by a tab character ("\\t"). Rows are delimited by a carriage-return/line-feed ("\\r\\n") sequence. See <http://www.iana.org/assignments/media-types/text/tab-separated-values> for more details. |
|
||||||
|
|
||||||
|
## Requests
|
||||||
|
|
||||||
|
API requests to our servers are just standard HTTP requests. As with all HTTP requests, a request to our API always contains a method which denotes the operation to be done, always has a URL (specifying the resource to operate on), usually has some parameters you send to qualify your request, and sometimes those parameters are specified in the URL itself, sometimes in the request headers and sometimes in the request body (formatted as JSON, XML, or CSV). We'll look at each of these request components in this section.
|
||||||
|
|
||||||
|
### Request Methods
|
||||||
|
|
||||||
|
| HTTP Method | Example Requests |
|
||||||
|
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| GET | This method is used to retrieve either a list of resource entities or a single resource entity. GET /api/eventReturn a list of all events the user has access to GET /api/event/123Return a single event specified by its resource ID GET /api/event/123/entryReturn a list of all entries (registrants) in event 123 |
|
||||||
|
| PUT | This method is used to update a single existing resource entity. PUT /api/entry/123456Update an existing entry (body is a single entry entity) |
|
||||||
|
| POST | This method is used to create or update one or more resource entities. POST /api/event/123/entryCreate new and/or update existing entries in event 123 (body is one or more entry entities) |
|
||||||
|
| DELETE | This method is used to delete an existing resource entity. DELETE /api/entry/123456Delete an existing entry by specifying its resource ID |
|
||||||
|
|
||||||
|
### Request URL
|
||||||
|
|
||||||
|
The URL path specifies the resource or resource collection the request operates on.
|
||||||
|
|
||||||
|
/api/{resource-name}.{format}/{resource-id}/{sub-resource-name}?{url-params}
|
||||||
|
|
||||||
|
| URL Component | Required | Meaning |
|
||||||
|
| ----------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| /api/ | Yes | All API request URLs begin with this prefix. |
|
||||||
|
| resource-name | Yes | All API request URLs must contain the name of the resource type to operate on. |
|
||||||
|
| format | No | The format of the response that you prefer. May be one of format codes specified in the [formats section](#formats). |
|
||||||
|
| resource-id | No | The identifier for a specific resource. If you are interested in getting or setting the properties of a specific resource or getting a collection of sub-resources belonging to a specific resource, you must specify this URL component in your request to identify the resource. |
|
||||||
|
| sub-resource-name | No | For requests where you want the collection of resources that belong to a specific resource (like all the entries in a given event, for instance), you use this URL component to name the type of sub-resource you want. |
|
||||||
|
| url-params | No | Ampersand (&) separated list of request parameters, URL-encoded as per [RFC 1738](http://www.ietf.org/rfc/rfc1738.txt). |
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
|
||||||
|
Most API requests will have one or more parameters that define some context for the request. For instance, all requests must specify the `client_id` parameter, which identifies the application the request originates from. The request parameters may be specified in several different places, including the URL path, the URL query, the request header and the request body. We'll consider each in turn.
|
||||||
|
|
||||||
|
#### URL Path Parameters
|
||||||
|
|
||||||
|
Two optional, but significant, parameters are commonly specified in the path of a URL request. First, the `resource_id` of the resource being operated on. Second, the desired `format` of the response. The following example illustrates the use of both of the parameters to request a particular event's properties in xml format:
|
||||||
|
|
||||||
|
GET /api/event.xml/123
|
||||||
|
|
||||||
|
#### URL Query Parameters
|
||||||
|
|
||||||
|
The query portion of a URL is the list of parameters that comes after the question mark following the URL path. If more than one parameter is specified, it must be separated from the others by an ampersand (&). The following URL requests the details for event 123 in xml format for the client application with id `abcdef`:
|
||||||
|
|
||||||
|
GET /api/event.xml/123?client_id=abcdef
|
||||||
|
|
||||||
|
Instead of specifying the format of the request in the URL path, you could instead specify it as a query parameter:
|
||||||
|
|
||||||
|
GET /api/event/123?client_id=abcdef&format=xml
|
||||||
|
|
||||||
|
#### Header Parameters
|
||||||
|
|
||||||
|
Request headers can also be used to specify context for a request. The following table provides a list of headers you can use with API requests.
|
||||||
|
|
||||||
|
| Header | Meaning |
|
||||||
|
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Accept | This is an alternative to specifying the desired format of the response in the URL path or query. The value should be set to one of the MIME types specified in the [formats section](#formats). Defaults to application/json. |
|
||||||
|
| Accept-Encoding | If you have the capacity to uncompress gzipped or deflated content, set this header to either "gzip" or "default" to trigger automatic compression of the response content to save network bandwidth. |
|
||||||
|
| Accept-Language | The languages you'd prefer the response to be sent back using. See [RFC 1766](http://www.ietf.org/rfc/rfc1766.txt) for more details. |
|
||||||
|
| Authorization | Authorization for the request. The value here depends on what kind of authorization method you are using. See the [security section](#security) for details on the setting. |
|
||||||
|
| X-Http-Method-Override | Specify this header to override the value of the HTTP method for a request. This should only be used if your application wants to submit a PUT or DELETE request but your HTTP library only supports GET and POST. You can tell your library to use the POST method (instead of PUT) or the GET method (instead of DELETE) and set then this header to PUT or DELETE. |
|
||||||
|
| Content-Type | The MIME type of the content body of your request. Must be set to one of the MIME types listed in the [formats section](#formats). Defaults to application/json. |
|
||||||
|
| Content-Length | The size (in bytes, not characters) of the request body. Usually this will be set by your HTTP library and does not need to be set manually. |
|
||||||
|
| Date | The [RFC 822](http://www.ietf.org/rfc/rfc0822.txt) date-time on the client when the request is sent. This should be set by your HTTP library. |
|
||||||
|
|
||||||
|
#### Body Parameters
|
||||||
|
|
||||||
|
| Method | Expected Content |
|
||||||
|
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| GET | No content sent with request. |
|
||||||
|
| POST | One or more properly encoded entities containing the fields of each new resource to create or existing resource to update. In the case of updating resources, you must provide either the resource id or some other defined identifier to allow our server to find the existing resource. |
|
||||||
|
| PUT | A properly encoded (possibly partial) entity containing the data items of the specified resource to be updated. You only need to provide those data items that require updating. The id of the resource being updated is specified as part of the URL path of the request. |
|
||||||
|
| DELETE | No content sent with request. |
|
||||||
|
|
||||||
|
When sending a body in your request that is not formatted as json, remember to set the `Content-Type` header appropriately so our API server can decode it.
|
||||||
|
|
||||||
|
#### Common API Request Parameters
|
||||||
|
|
||||||
|
The following table enumerates some commonly used request parameters and where they are usually included in the request.
|
||||||
|
|
||||||
|
| Parameter | Placement | Comment |
|
||||||
|
| ---------------- | ----------- | ------- |
|
||||||
|
| format | Path, Query | |
|
||||||
|
| client\_id | Query | |
|
||||||
|
| page | Query | |
|
||||||
|
| last\_id | Query | |
|
||||||
|
| modified\_after | Query | |
|
||||||
|
| modified\_before | Query | |
|
||||||
|
| columns | Query | |
|
||||||
|
| user\_id | Query | |
|
||||||
|
| user\_pass | Query | |
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
When 1500 concurrent connections, from the same IP address, are reached within a 60 second time frame, the user will be blacklisted for the remaining duration of that time frame and will receive the 503 over rate error.
|
||||||
|
|
||||||
|
## Responses
|
||||||
|
|
||||||
|
An API response from our servers has several salient characteristics worth drawing attention to. First, each response has a numeric HTTP code that indicates success or failure. Second, a response usually contains a body formatted as JSON, XML, or CSV that contains the data you requested. Third, for many requests that return multiple entities (like a list of all events your user has access to, for instance), there are headers in the response that contain useful information on the size of the total result set and what portion of that total result set the current body of the request contains. We'll elaborate on each of these in turn.
|
||||||
|
|
||||||
|
### Response Codes
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Success | |
|
||||||
|
| 200 | Success With Content. The request succeeded and content was returned. The content of the response body depends on the type of request. A GET on a single resource will return the entity corresponding to that resource. A GET on a resource collection will return one or more entities. A POST will return a data structure for each requested entity indicating either that the resource was created or updated. The responses will correspond one for one with the order of entities in the request. |
|
||||||
|
| 202 | Accepted. The request has been accepted for processing, but the processing has not been completed. The response to the initial request provides a url that can be used to get further information about the status of processing. This allows our server to accept a request that might take some time to complete without requiring your connection persist until that process is done. |
|
||||||
|
| Client Errors | |
|
||||||
|
| 400 | Bad Request. The request could not be understood by the server due to malformed syntax. The client SHOULD NOT repeat the request without modifications. Details of the error are provided in the body of the response. |
|
||||||
|
| 401 | Unauthenticated. The request requires authentication (of the ChronoTrack Live user) and either the credentials provided are not a match, or no credentials were provided. Details of the error are provided in the body of the response. |
|
||||||
|
| 403 | Forbidden. The server understood the request, but is refusing to fulfill it. Authorization will not help and the request SHOULD NOT be repeated. More details are in the body of the response. Usually this is caused by going over usage limits for a request type by an application. |
|
||||||
|
| 404 | Not Found. The server could not find any resources requested by the client. |
|
||||||
|
| Server Errors | |
|
||||||
|
| 500 | Internal Server Error. The server encountered an unexpected condition which prevented it from fulfilling the request. |
|
||||||
|
| 501 | Not Implemented. The server does not support the functionality required to fulfill the request. |
|
||||||
|
| 503 | Server Unavailable. The server is currently unable to handle the request due to a temporary overloading or maintenance of the server. The implication is that this is a temporary condition which will be alleviated after some delay. If known, the length of the delay may be indicated in a Retry-After header. If no Retry-After is given, the client should handle the response as it would for a 500 response. |
|
||||||
|
| 503 over rate | This 'IP', 'IP range' or 'email address' has been blacklisted, please contact esupport@chronotrack.com or call (812) 759-7877 to work with our support staff to resolve this issue. |
|
||||||
|
|
||||||
|
### Response Bodies
|
||||||
|
|
||||||
|
For responses that return content (basically all responses except PUT and DELETE) the format of the response body can be set to one of several alternatives.
|
||||||
|
|
||||||
|
You may specify the response format you want in one of two ways. First, you can add an `Accept` header to your request with the appropriate MIME type for your preferred format. Second, you can add an extension to the resource name in the URL (for example, to request a list of all accessible events in XML format, you could use `GET /api/event.xml`. If neither of these methods is used, the response format defaults to JSON. The proper extension is just the lower-case form of the name in the table above.
|
||||||
|
|
||||||
|
### Response Headers
|
||||||
|
|
||||||
|
| Header | Meaning |
|
||||||
|
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Standard HTTP Headers | |
|
||||||
|
| Content-Type | The MIME type of the response body. |
|
||||||
|
| Content-Length | The size (in bytes, not characters) of the response body. |
|
||||||
|
| Content-Encoding | If the request indicates it can accept compression via the Accept-Encoding header, our API server will compress the response body to save network bandwidth. In this case, the Content-Encoding response header contains the compression method used, either "gzip" (see [RFC 1952](http://www.ietf.org/rfc/rfc1952.txt)) or "deflate" (see [RFC 1951](http://www.ietf.org/rfc/rfc1951.txt)) depending on which algorithm was requested. |
|
||||||
|
| Transfer-Encoding | If set, it will be set to "chunked". The chunked encoding modifies the response body in order to transfer it as a series of chunks, each with its own size indicator, followed by an optional trailer containing entity-header fields. This allows dynamically produced content to be transferred along with the information necessary for the recipient to verify that it has received the full message. |
|
||||||
|
| Content-Language | The language used in the response represented as in [RFC 1766](http://www.ietf.org/rfc/rfc1766.txt) using ISO-639 language abbreviations and, optionally, ISO-3166 country codes to construct the language-tag. |
|
||||||
|
| Date | The date and time on the server when the response was sent. This is in [RFC 822](http://www.w3.org/Protocols/rfc822/#z28) date-time format, for example: "Tue, 04 Dec 2012 05:44:04 GMT." |
|
||||||
|
| WWW-Authenticate | The WWW-Authenticate response header field is included only in 401 (Unauthorized) response messages. The field value consists of at least one challenge that indicates the authentication scheme and parameters applicable to the request URI. For more details see [RFC 2617](http://tools.ietf.org/html/rfc2617). |
|
||||||
|
| Link | For resource collection requests, provides a list of links to first, last, previous and next pages for use in pagination interfaces in the client application. This various relation types (first, prev, next, last) are defined in [RFC 5988](http://tools.ietf.org/html/rfc5988). |
|
||||||
|
| Custom ChronoTrack Live Headers | |
|
||||||
|
| X-Ctlive-Api-Version | Provides the current version number of the API. |
|
||||||
|
| X-Ctlive-Row-Count | For resource collection requests, provides the total number of entities in the collection (of which only a portion may be returned in the current response). |
|
||||||
|
| X-Ctlive-Current-Rows | For resource collection requests, provides the current range of rows contained in the response body in the format min-max (so on page 1 of a response with a page size of 10, this header's value would be "1-10", page 2 would be "11-20", and so on). |
|
||||||
|
| X-Ctlive-Page-Count | For resource collection requests, provides the total number of pages, given the current page size. |
|
||||||
|
| X-Ctlive-Page-Size | For resource collection requests, provides the number of entities returned in a single "page" or results. |
|
||||||
|
| X-Ctlive-Current-Page | For resource collection requests, provides the current page of results within the total number of pages contained in the collection, given the page size setting. |
|
||||||
|
|
||||||
|
## Character Encoding
|
||||||
|
|
||||||
|
We use **UTF-8 encoding** through and through to guarantee proper encoding of all international character sets. All data you send to an API endpoint is assumed to be, and therefore must be, encoded in UTF-8\. All data transmitted from an API endpoint is encoded in UTF-8.
|
||||||
|
|
||||||
|
## Data Types
|
||||||
|
|
||||||
|
The API uses a pretty standard collection of types for all data that it processes, as described below.
|
||||||
|
|
||||||
|
| Type | Description |
|
||||||
|
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| resource\_id | A non-semantic (numeric) identifier associated with a particular resource. This identifier is guaranteed to be unique within the collection of all resources of that resource type. Resource identifiers are assigned by ChronoTrack Live and will not change over the life cycle of a resource. Resources may also have one or more semantically significant identifiers assigned by event and timer related organizations. |
|
||||||
|
| string | A UTF-8 encoded character string without any line breaks, and rarely longer than 250 characters. |
|
||||||
|
| text | A UTF-8 encoded character string that may contain line breaks and is likely to be longer than 250 characters. |
|
||||||
|
| boolean | May take either the value 0, meaning false, or 1, meaning true. |
|
||||||
|
| integer | An integral number (a number without any fractional part). |
|
||||||
|
| float | A floating point number (a number that may have a fractional part). |
|
||||||
|
| enum | Several data items in the API may take on a value drawn from a small enumeration of possible strings. All enumerated values are represented as strings. |
|
||||||
|
| date | Dates (without a time) are represented in ISO-8601 standard format, namely YYYY-MM-DD. |
|
||||||
|
| epoch\_time | Datetimes are represented as unix epoch times, namely the number of seconds since midnight January 1, 1970 UTC. Information on how to convert unix epoch times to other date/time formats can be found at <http://epochconverter.com>. |
|
||||||
|
|
||||||
|
## Resources (Live Testing Sandbox)
|
||||||
|
|
||||||
|
This section describes the the list of available resource types as well as the operations available for each. Click on the resource names below to expand the documentation for each. Each operation can be tested live right here on this web page to get a better sense of how the API works.
|
||||||
|
|
||||||
|
For each API operation below, you must provide a valid application `client_id` as well as administrative credentials from a valid ChronoTrack Live account that has access to at least one event. If you sign-in with your developer credentials and revisit this page, these values will be filled in automatically for you. Otherwise, common test credentials will be used that have limited access to a few test events.
|
||||||
|
|
||||||
|
Loading resources...
|
||||||
|
|
||||||
|
* ©2009-2026 ChronoTrack. All rights reserved.
|
||||||
|
* License
|
||||||
1
docs/chronotrack/api-meta-entry.json
Normal file
49
dossard_print.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* MSIN-4485 — Endpoint PDF dossard (1 participant).
|
||||||
|
* Isolé du tunnel / panier ; droit inscriptions_gestion.dossard_print.
|
||||||
|
*/
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$_SERVER['PHP_SELF'] = '/compte.php';
|
||||||
|
$_SERVER['SCRIPT_NAME'] = '/compte.php';
|
||||||
|
|
||||||
|
require_once __DIR__ . '/php/inc_fonctions.php';
|
||||||
|
require_once __DIR__ . '/php/inc_fx_dossard_print.php';
|
||||||
|
|
||||||
|
$strLangue = $_SESSION['lang'] ?? 'fr';
|
||||||
|
$reqLang = $_REQUEST['lang'] ?? $_REQUEST['lng'] ?? '';
|
||||||
|
if (!empty($reqLang) && in_array($reqLang, array('fr', 'en'), true)) {
|
||||||
|
$strLangue = $reqLang;
|
||||||
|
}
|
||||||
|
$vPage = 'compte.php';
|
||||||
|
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
|
||||||
|
|
||||||
|
$intParId = intval($_GET['par_id'] ?? $_POST['par_id'] ?? 0);
|
||||||
|
$intEveId = intval($_GET['eve_id'] ?? $_POST['eve_id'] ?? 0);
|
||||||
|
$intComId = intval($_SESSION['com_id'] ?? 0);
|
||||||
|
|
||||||
|
if ($intComId <= 0 || $intParId <= 0 || $intEveId <= 0) {
|
||||||
|
http_response_code(403);
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo 'Accès refusé.';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fxDossardPrintCan($intComId, $intEveId)) {
|
||||||
|
http_response_code(403);
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo 'Droit insuffisant.';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrRow = fxDossardPrintLoadParticipant($intParId, $intEveId);
|
||||||
|
if ($arrRow === null) {
|
||||||
|
http_response_code(404);
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo 'Participant introuvable.';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
fxDossardPrintOutputPdf($arrRow);
|
||||||
21
fpdi2/src/FpdfTpl.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class FpdfTpl
|
||||||
|
*
|
||||||
|
* This class adds a templating feature to FPDF.
|
||||||
|
*/
|
||||||
|
class FpdfTpl extends \FPDF
|
||||||
|
{
|
||||||
|
use FpdfTplTrait;
|
||||||
|
}
|
||||||
473
fpdi2/src/FpdfTplTrait.php
Normal file
@ -0,0 +1,473 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trait FpdfTplTrait
|
||||||
|
*
|
||||||
|
* This trait adds a templating feature to FPDF and tFPDF.
|
||||||
|
*/
|
||||||
|
trait FpdfTplTrait
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Data of all created templates.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $templates = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The template id for the currently created template.
|
||||||
|
*
|
||||||
|
* @var null|int
|
||||||
|
*/
|
||||||
|
protected $currentTemplateId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A counter for template ids.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $templateId = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the page format of the current page.
|
||||||
|
*
|
||||||
|
* @param array $size An array with two values defining the size.
|
||||||
|
* @param string $orientation "L" for landscape, "P" for portrait.
|
||||||
|
* @throws \BadMethodCallException
|
||||||
|
*/
|
||||||
|
public function setPageFormat($size, $orientation)
|
||||||
|
{
|
||||||
|
if ($this->currentTemplateId !== null) {
|
||||||
|
throw new \BadMethodCallException('The page format cannot be changed when writing to a template.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\in_array($orientation, ['P', 'L'], true)) {
|
||||||
|
throw new \InvalidArgumentException(\sprintf(
|
||||||
|
'Invalid page orientation "%s"! Only "P" and "L" are allowed!',
|
||||||
|
$orientation
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$size = $this->_getpagesize($size);
|
||||||
|
|
||||||
|
if (
|
||||||
|
$orientation != $this->CurOrientation
|
||||||
|
|| $size[0] != $this->CurPageSize[0]
|
||||||
|
|| $size[1] != $this->CurPageSize[1]
|
||||||
|
) {
|
||||||
|
// New size or orientation
|
||||||
|
if ($orientation === 'P') {
|
||||||
|
$this->w = $size[0];
|
||||||
|
$this->h = $size[1];
|
||||||
|
} else {
|
||||||
|
$this->w = $size[1];
|
||||||
|
$this->h = $size[0];
|
||||||
|
}
|
||||||
|
$this->wPt = $this->w * $this->k;
|
||||||
|
$this->hPt = $this->h * $this->k;
|
||||||
|
$this->PageBreakTrigger = $this->h - $this->bMargin;
|
||||||
|
$this->CurOrientation = $orientation;
|
||||||
|
$this->CurPageSize = $size;
|
||||||
|
|
||||||
|
$this->PageInfo[$this->page]['size'] = array($this->wPt, $this->hPt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a template onto the page or another template.
|
||||||
|
*
|
||||||
|
* Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
|
||||||
|
* aspect ratio.
|
||||||
|
*
|
||||||
|
* @param mixed $tpl The template id
|
||||||
|
* @param array|float|int $x The abscissa of upper-left corner. Alternatively you could use an assoc array
|
||||||
|
* with the keys "x", "y", "width", "height", "adjustPageSize".
|
||||||
|
* @param float|int $y The ordinate of upper-left corner.
|
||||||
|
* @param float|int|null $width The width.
|
||||||
|
* @param float|int|null $height The height.
|
||||||
|
* @param bool $adjustPageSize
|
||||||
|
* @return array The size
|
||||||
|
* @see FpdfTplTrait::getTemplateSize()
|
||||||
|
*/
|
||||||
|
public function useTemplate($tpl, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false)
|
||||||
|
{
|
||||||
|
if (!isset($this->templates[$tpl])) {
|
||||||
|
throw new \InvalidArgumentException('Template does not exist!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\is_array($x)) {
|
||||||
|
unset($x['tpl']);
|
||||||
|
\extract($x, EXTR_IF_EXISTS);
|
||||||
|
/** @noinspection NotOptimalIfConditionsInspection */
|
||||||
|
/** @phpstan-ignore function.alreadyNarrowedType */
|
||||||
|
if (\is_array($x)) {
|
||||||
|
$x = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$template = $this->templates[$tpl];
|
||||||
|
|
||||||
|
$originalSize = $this->getTemplateSize($tpl);
|
||||||
|
$newSize = $this->getTemplateSize($tpl, $width, $height);
|
||||||
|
if ($adjustPageSize) {
|
||||||
|
$this->setPageFormat($newSize, $newSize['orientation']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_out(
|
||||||
|
// reset standard values, translate and scale
|
||||||
|
\sprintf(
|
||||||
|
'q 0 J 1 w 0 j 0 G 0 g %.4F 0 0 %.4F %.4F %.4F cm /%s Do Q',
|
||||||
|
($newSize['width'] / $originalSize['width']),
|
||||||
|
($newSize['height'] / $originalSize['height']),
|
||||||
|
$x * $this->k,
|
||||||
|
($this->h - $y - $newSize['height']) * $this->k,
|
||||||
|
$template['id']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $newSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of a template.
|
||||||
|
*
|
||||||
|
* Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
|
||||||
|
* aspect ratio.
|
||||||
|
*
|
||||||
|
* @param mixed $tpl The template id
|
||||||
|
* @param float|int|null $width The width.
|
||||||
|
* @param float|int|null $height The height.
|
||||||
|
* @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P)
|
||||||
|
*/
|
||||||
|
public function getTemplateSize($tpl, $width = null, $height = null)
|
||||||
|
{
|
||||||
|
if (!isset($this->templates[$tpl])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($width === null && $height === null) {
|
||||||
|
$width = $this->templates[$tpl]['width'];
|
||||||
|
$height = $this->templates[$tpl]['height'];
|
||||||
|
} elseif ($width === null) {
|
||||||
|
$width = $height * $this->templates[$tpl]['width'] / $this->templates[$tpl]['height'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($height === null) {
|
||||||
|
$height = $width * $this->templates[$tpl]['height'] / $this->templates[$tpl]['width'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($height <= 0. || $width <= 0.) {
|
||||||
|
throw new \InvalidArgumentException('Width or height parameter needs to be larger than zero.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'width' => $width,
|
||||||
|
'height' => $height,
|
||||||
|
0 => $width,
|
||||||
|
1 => $height,
|
||||||
|
'orientation' => $width > $height ? 'L' : 'P'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begins a new template.
|
||||||
|
*
|
||||||
|
* @param float|int|null $width The width of the template. If null, the current page width is used.
|
||||||
|
* @param float|int|null $height The height of the template. If null, the current page height is used.
|
||||||
|
* @param bool $groupXObject Define the form XObject as a group XObject to support transparency (if used).
|
||||||
|
* @return int A template identifier.
|
||||||
|
*/
|
||||||
|
public function beginTemplate($width = null, $height = null, $groupXObject = false)
|
||||||
|
{
|
||||||
|
if ($width === null) {
|
||||||
|
$width = $this->w;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($height === null) {
|
||||||
|
$height = $this->h;
|
||||||
|
}
|
||||||
|
|
||||||
|
$templateId = $this->getNextTemplateId();
|
||||||
|
|
||||||
|
// initiate buffer with current state of FPDF
|
||||||
|
$buffer = "2 J\n"
|
||||||
|
. \sprintf('%.2F w', $this->LineWidth * $this->k) . "\n";
|
||||||
|
|
||||||
|
if ($this->FontFamily) {
|
||||||
|
$buffer .= \sprintf("BT /F%d %.2F Tf ET\n", $this->CurrentFont['i'], $this->FontSizePt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->DrawColor !== '0 G') {
|
||||||
|
$buffer .= $this->DrawColor . "\n";
|
||||||
|
}
|
||||||
|
if ($this->FillColor !== '0 g') {
|
||||||
|
$buffer .= $this->FillColor . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($groupXObject && \version_compare('1.4', $this->PDFVersion, '>')) {
|
||||||
|
$this->PDFVersion = '1.4';
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->templates[$templateId] = [
|
||||||
|
'objectNumber' => null,
|
||||||
|
'id' => 'TPL' . $templateId,
|
||||||
|
'buffer' => $buffer,
|
||||||
|
'width' => $width,
|
||||||
|
'height' => $height,
|
||||||
|
'groupXObject' => $groupXObject,
|
||||||
|
'state' => [
|
||||||
|
'x' => $this->x,
|
||||||
|
'y' => $this->y,
|
||||||
|
'AutoPageBreak' => $this->AutoPageBreak,
|
||||||
|
'bMargin' => $this->bMargin,
|
||||||
|
'tMargin' => $this->tMargin,
|
||||||
|
'lMargin' => $this->lMargin,
|
||||||
|
'rMargin' => $this->rMargin,
|
||||||
|
'h' => $this->h,
|
||||||
|
'hPt' => $this->hPt,
|
||||||
|
'w' => $this->w,
|
||||||
|
'wPt' => $this->wPt,
|
||||||
|
'FontFamily' => $this->FontFamily,
|
||||||
|
'FontStyle' => $this->FontStyle,
|
||||||
|
'FontSizePt' => $this->FontSizePt,
|
||||||
|
'FontSize' => $this->FontSize,
|
||||||
|
'underline' => $this->underline,
|
||||||
|
'TextColor' => $this->TextColor,
|
||||||
|
'DrawColor' => $this->DrawColor,
|
||||||
|
'FillColor' => $this->FillColor,
|
||||||
|
'ColorFlag' => $this->ColorFlag
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->SetAutoPageBreak(false);
|
||||||
|
$this->currentTemplateId = $templateId;
|
||||||
|
|
||||||
|
$this->h = $height;
|
||||||
|
$this->hPt = $height * $this->k;
|
||||||
|
$this->w = $width;
|
||||||
|
$this->wPt = $width * $this->k;
|
||||||
|
|
||||||
|
$this->SetXY($this->lMargin, $this->tMargin);
|
||||||
|
$this->SetRightMargin($this->w - $width + $this->rMargin);
|
||||||
|
|
||||||
|
return $templateId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ends a template.
|
||||||
|
*
|
||||||
|
* @return bool|int|null A template identifier.
|
||||||
|
*/
|
||||||
|
public function endTemplate()
|
||||||
|
{
|
||||||
|
if ($this->currentTemplateId === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$templateId = $this->currentTemplateId;
|
||||||
|
$template = $this->templates[$templateId];
|
||||||
|
|
||||||
|
$state = $template['state'];
|
||||||
|
$this->SetXY($state['x'], $state['y']);
|
||||||
|
$this->tMargin = $state['tMargin'];
|
||||||
|
$this->lMargin = $state['lMargin'];
|
||||||
|
$this->rMargin = $state['rMargin'];
|
||||||
|
$this->h = $state['h'];
|
||||||
|
$this->hPt = $state['hPt'];
|
||||||
|
$this->w = $state['w'];
|
||||||
|
$this->wPt = $state['wPt'];
|
||||||
|
$this->SetAutoPageBreak($state['AutoPageBreak'], $state['bMargin']);
|
||||||
|
|
||||||
|
$this->FontFamily = $state['FontFamily'];
|
||||||
|
$this->FontStyle = $state['FontStyle'];
|
||||||
|
$this->FontSizePt = $state['FontSizePt'];
|
||||||
|
$this->FontSize = $state['FontSize'];
|
||||||
|
|
||||||
|
$this->TextColor = $state['TextColor'];
|
||||||
|
$this->DrawColor = $state['DrawColor'];
|
||||||
|
$this->FillColor = $state['FillColor'];
|
||||||
|
$this->ColorFlag = $state['ColorFlag'];
|
||||||
|
|
||||||
|
$this->underline = $state['underline'];
|
||||||
|
|
||||||
|
$fontKey = $this->FontFamily . $this->FontStyle;
|
||||||
|
if ($fontKey) {
|
||||||
|
$this->CurrentFont = $this->fonts[$fontKey];
|
||||||
|
} else {
|
||||||
|
unset($this->CurrentFont);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->currentTemplateId = null;
|
||||||
|
|
||||||
|
return $templateId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the next template id.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
protected function getNextTemplateId()
|
||||||
|
{
|
||||||
|
return $this->templateId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* overwritten FPDF methods: */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
public function AddPage($orientation = '', $size = '', $rotation = 0)
|
||||||
|
{
|
||||||
|
if ($this->currentTemplateId !== null) {
|
||||||
|
throw new \BadMethodCallException('Pages cannot be added when writing to a template.');
|
||||||
|
}
|
||||||
|
parent::AddPage($orientation, $size, $rotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
public function Link($x, $y, $w, $h, $link)
|
||||||
|
{
|
||||||
|
if ($this->currentTemplateId !== null) {
|
||||||
|
throw new \BadMethodCallException('Links cannot be set when writing to a template.');
|
||||||
|
}
|
||||||
|
parent::Link($x, $y, $w, $h, $link);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
public function SetLink($link, $y = 0, $page = -1)
|
||||||
|
{
|
||||||
|
if ($this->currentTemplateId !== null) {
|
||||||
|
throw new \BadMethodCallException('Links cannot be set when writing to a template.');
|
||||||
|
}
|
||||||
|
return parent::SetLink($link, $y, $page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
public function SetDrawColor($r, $g = null, $b = null)
|
||||||
|
{
|
||||||
|
parent::SetDrawColor($r, $g, $b);
|
||||||
|
if ($this->page === 0 && $this->currentTemplateId !== null) {
|
||||||
|
$this->_out($this->DrawColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
public function SetFillColor($r, $g = null, $b = null)
|
||||||
|
{
|
||||||
|
parent::SetFillColor($r, $g, $b);
|
||||||
|
if ($this->page === 0 && $this->currentTemplateId !== null) {
|
||||||
|
$this->_out($this->FillColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
public function SetLineWidth($width)
|
||||||
|
{
|
||||||
|
parent::SetLineWidth($width);
|
||||||
|
if ($this->page === 0 && $this->currentTemplateId !== null) {
|
||||||
|
$this->_out(\sprintf('%.2F w', $width * $this->k));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
public function SetFont($family, $style = '', $size = 0)
|
||||||
|
{
|
||||||
|
parent::SetFont($family, $style, $size);
|
||||||
|
if ($this->page === 0 && $this->currentTemplateId !== null) {
|
||||||
|
$this->_out(\sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
public function SetFontSize($size)
|
||||||
|
{
|
||||||
|
parent::SetFontSize($size);
|
||||||
|
if ($this->page === 0 && $this->currentTemplateId !== null) {
|
||||||
|
$this->_out(sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _putimages()
|
||||||
|
{
|
||||||
|
parent::_putimages();
|
||||||
|
|
||||||
|
foreach ($this->templates as $key => $template) {
|
||||||
|
$this->_newobj();
|
||||||
|
$this->templates[$key]['objectNumber'] = $this->n;
|
||||||
|
|
||||||
|
$this->_put('<</Type /XObject /Subtype /Form /FormType 1');
|
||||||
|
$this->_put(\sprintf(
|
||||||
|
'/BBox[0 0 %.2F %.2F]',
|
||||||
|
$template['width'] * $this->k,
|
||||||
|
$template['height'] * $this->k
|
||||||
|
));
|
||||||
|
$this->_put('/Resources 2 0 R'); // default resources dictionary of FPDF
|
||||||
|
|
||||||
|
if ($this->compress) {
|
||||||
|
$buffer = \gzcompress($template['buffer']);
|
||||||
|
$this->_put('/Filter/FlateDecode');
|
||||||
|
} else {
|
||||||
|
$buffer = $template['buffer'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_put('/Length ' . \strlen($buffer));
|
||||||
|
|
||||||
|
if ($template['groupXObject']) {
|
||||||
|
$this->_put('/Group <</Type/Group/S/Transparency>>');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_put('>>');
|
||||||
|
$this->_putstream($buffer);
|
||||||
|
$this->_put('endobj');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected function _putxobjectdict()
|
||||||
|
{
|
||||||
|
foreach ($this->templates as $key => $template) {
|
||||||
|
$this->_put('/' . $template['id'] . ' ' . $template['objectNumber'] . ' 0 R');
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::_putxobjectdict();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
public function _out($s)
|
||||||
|
{
|
||||||
|
if ($this->currentTemplateId !== null) {
|
||||||
|
$this->templates[$this->currentTemplateId]['buffer'] .= $s . "\n";
|
||||||
|
} else {
|
||||||
|
parent::_out($s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
193
fpdi2/src/FpdfTrait.php
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParserException;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfNull;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This trait is used for the implementation of FPDI in FPDF and tFPDF.
|
||||||
|
*/
|
||||||
|
trait FpdfTrait
|
||||||
|
{
|
||||||
|
protected function _enddoc()
|
||||||
|
{
|
||||||
|
parent::_enddoc();
|
||||||
|
$this->cleanUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws an imported page or a template onto the page or another template.
|
||||||
|
*
|
||||||
|
* Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
|
||||||
|
* aspect ratio.
|
||||||
|
*
|
||||||
|
* @param mixed $tpl The template id
|
||||||
|
* @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array
|
||||||
|
* with the keys "x", "y", "width", "height", "adjustPageSize".
|
||||||
|
* @param float|int $y The ordinate of upper-left corner.
|
||||||
|
* @param float|int|null $width The width.
|
||||||
|
* @param float|int|null $height The height.
|
||||||
|
* @param bool $adjustPageSize
|
||||||
|
* @return array The size
|
||||||
|
* @see Fpdi::getTemplateSize()
|
||||||
|
*/
|
||||||
|
public function useTemplate($tpl, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false)
|
||||||
|
{
|
||||||
|
if (isset($this->importedPages[$tpl])) {
|
||||||
|
$size = $this->useImportedPage($tpl, $x, $y, $width, $height, $adjustPageSize);
|
||||||
|
if ($this->currentTemplateId !== null) {
|
||||||
|
$this->templates[$this->currentTemplateId]['resources']['templates']['importedPages'][$tpl] = $tpl;
|
||||||
|
}
|
||||||
|
return $size;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::useTemplate($tpl, $x, $y, $width, $height, $adjustPageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of an imported page or template.
|
||||||
|
*
|
||||||
|
* Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
|
||||||
|
* aspect ratio.
|
||||||
|
*
|
||||||
|
* @param mixed $tpl The template id
|
||||||
|
* @param float|int|null $width The width.
|
||||||
|
* @param float|int|null $height The height.
|
||||||
|
* @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P)
|
||||||
|
*/
|
||||||
|
public function getTemplateSize($tpl, $width = null, $height = null)
|
||||||
|
{
|
||||||
|
$size = parent::getTemplateSize($tpl, $width, $height);
|
||||||
|
if ($size === false) {
|
||||||
|
return $this->getImportedPageSize($tpl, $width, $height);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
* @throws PdfParserException
|
||||||
|
*/
|
||||||
|
protected function _putimages()
|
||||||
|
{
|
||||||
|
$this->currentReaderId = null;
|
||||||
|
parent::_putimages();
|
||||||
|
|
||||||
|
foreach ($this->importedPages as $key => $pageData) {
|
||||||
|
$this->_newobj();
|
||||||
|
$this->importedPages[$key]['objectNumber'] = $this->n;
|
||||||
|
$this->currentReaderId = $pageData['readerId'];
|
||||||
|
$this->writePdfType($pageData['stream']);
|
||||||
|
$this->_put('endobj');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (\array_keys($this->readers) as $readerId) {
|
||||||
|
$parser = $this->getPdfReader($readerId)->getParser();
|
||||||
|
$this->currentReaderId = $readerId;
|
||||||
|
|
||||||
|
while (($objectNumber = \array_pop($this->objectsToCopy[$readerId])) !== null) {
|
||||||
|
try {
|
||||||
|
$object = $parser->getIndirectObject($objectNumber);
|
||||||
|
} catch (CrossReferenceException $e) {
|
||||||
|
if ($e->getCode() === CrossReferenceException::OBJECT_NOT_FOUND) {
|
||||||
|
$object = PdfIndirectObject::create($objectNumber, 0, new PdfNull());
|
||||||
|
} else {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->writePdfType($object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->currentReaderId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected function _putxobjectdict()
|
||||||
|
{
|
||||||
|
foreach ($this->importedPages as $pageData) {
|
||||||
|
$this->_put('/' . $pageData['id'] . ' ' . $pageData['objectNumber'] . ' 0 R');
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::_putxobjectdict();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $n
|
||||||
|
* @return void
|
||||||
|
* @throws PdfParser\Type\PdfTypeException
|
||||||
|
*/
|
||||||
|
protected function _putlinks($n)
|
||||||
|
{
|
||||||
|
foreach ($this->PageLinks[$n] as $pl) {
|
||||||
|
$this->_newobj();
|
||||||
|
$rect = \sprintf('%.2F %.2F %.2F %.2F', $pl[0], $pl[1], $pl[0] + $pl[2], $pl[1] - $pl[3]);
|
||||||
|
$this->_put('<</Type /Annot /Subtype /Link /Rect [' . $rect . ']', false);
|
||||||
|
if (is_string($pl[4])) {
|
||||||
|
if (isset($pl['importedLink'])) {
|
||||||
|
$this->_put('/A <</S /URI /URI (' . $this->_escape($pl[4]) . ')>>');
|
||||||
|
$values = $pl['importedLink']['pdfObject']->value;
|
||||||
|
|
||||||
|
foreach ($values as $name => $entry) {
|
||||||
|
$this->_put('/' . $name . ' ', false);
|
||||||
|
$this->writePdfType($entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($pl['quadPoints'])) {
|
||||||
|
$s = '/QuadPoints[';
|
||||||
|
foreach ($pl['quadPoints'] as $value) {
|
||||||
|
$s .= \sprintf('%.2F ', $value);
|
||||||
|
}
|
||||||
|
$s .= ']';
|
||||||
|
$this->_put($s);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->_put('/A <</S /URI /URI ' . $this->_textstring($pl[4]) . '>>');
|
||||||
|
$this->_put('/Border [0 0 0]', false);
|
||||||
|
}
|
||||||
|
$this->_put('>>');
|
||||||
|
} else {
|
||||||
|
$this->_put('/Border [0 0 0] ', false);
|
||||||
|
$l = $this->links[$pl[4]];
|
||||||
|
if (isset($this->PageInfo[$l[0]]['size'])) {
|
||||||
|
$h = $this->PageInfo[$l[0]]['size'][1];
|
||||||
|
} else {
|
||||||
|
$h = ($this->DefOrientation === 'P')
|
||||||
|
? $this->DefPageSize[1] * $this->k
|
||||||
|
: $this->DefPageSize[0] * $this->k;
|
||||||
|
}
|
||||||
|
$this->_put(\sprintf(
|
||||||
|
'/Dest [%d 0 R /XYZ 0 %.2F null]>>',
|
||||||
|
$this->PageInfo[$l[0]]['n'],
|
||||||
|
$h - $l[1] * $this->k
|
||||||
|
));
|
||||||
|
}
|
||||||
|
$this->_put('endobj');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _put($s, $newLine = true)
|
||||||
|
{
|
||||||
|
if ($newLine) {
|
||||||
|
$this->buffer .= $s . "\n";
|
||||||
|
} else {
|
||||||
|
$this->buffer .= $s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
fpdi2/src/Fpdi.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParserException;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Fpdi
|
||||||
|
*
|
||||||
|
* This class let you import pages of existing PDF documents into a reusable structure for FPDF.
|
||||||
|
*/
|
||||||
|
class Fpdi extends FpdfTpl
|
||||||
|
{
|
||||||
|
use FpdiTrait;
|
||||||
|
use FpdfTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FPDI version
|
||||||
|
*
|
||||||
|
* @string
|
||||||
|
*/
|
||||||
|
const VERSION = '2.6.8';
|
||||||
|
}
|
||||||
18
fpdi2/src/FpdiException.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base exception class for the FPDI package.
|
||||||
|
*/
|
||||||
|
class FpdiException extends \Exception
|
||||||
|
{
|
||||||
|
}
|
||||||
665
fpdi2/src/FpdiTrait.php
Normal file
@ -0,0 +1,665 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
|
||||||
|
use setasign\Fpdi\PdfParser\Filter\FilterException;
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParser;
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParserException;
|
||||||
|
use setasign\Fpdi\PdfParser\StreamReader;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfArray;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfBoolean;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfDictionary;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfHexString;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfIndirectObjectReference;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfName;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfNull;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfNumeric;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfStream;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfString;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfToken;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfType;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfTypeException;
|
||||||
|
use setasign\Fpdi\PdfReader\DataStructure\Rectangle;
|
||||||
|
use setasign\Fpdi\PdfReader\PageBoundaries;
|
||||||
|
use setasign\Fpdi\PdfReader\PdfReader;
|
||||||
|
use setasign\Fpdi\PdfReader\PdfReaderException;
|
||||||
|
use /* This namespace/class is used by the commercial FPDI PDF-Parser add-on. */
|
||||||
|
/** @noinspection PhpUndefinedClassInspection */
|
||||||
|
/** @noinspection PhpUndefinedNamespaceInspection */
|
||||||
|
setasign\FpdiPdfParser\PdfParser\PdfParser as FpdiPdfParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The FpdiTrait
|
||||||
|
*
|
||||||
|
* This trait offers the core functionalities of FPDI. By passing them to a trait we can reuse it with e.g. TCPDF in a
|
||||||
|
* very easy way.
|
||||||
|
*/
|
||||||
|
trait FpdiTrait
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The pdf reader instances.
|
||||||
|
*
|
||||||
|
* @var PdfReader[]
|
||||||
|
*/
|
||||||
|
protected $readers = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances created internally.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $createdReaders = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current reader id.
|
||||||
|
*
|
||||||
|
* @var string|null
|
||||||
|
*/
|
||||||
|
protected $currentReaderId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data of all imported pages.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $importedPages = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map from object numbers of imported objects to new assigned object numbers by FPDF.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $objectMap = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array with information about objects, which needs to be copied to the resulting document.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $objectsToCopy = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release resources and file handles.
|
||||||
|
*
|
||||||
|
* This method is called internally when the document is created successfully. By default it only cleans up
|
||||||
|
* stream reader instances which were created internally.
|
||||||
|
*
|
||||||
|
* @param bool $allReaders
|
||||||
|
*/
|
||||||
|
public function cleanUp($allReaders = false)
|
||||||
|
{
|
||||||
|
$readers = $allReaders ? array_keys($this->readers) : $this->createdReaders;
|
||||||
|
foreach ($readers as $id) {
|
||||||
|
$this->readers[$id]->getParser()->getStreamReader()->cleanUp();
|
||||||
|
unset($this->readers[$id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->createdReaders = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the minimal PDF version.
|
||||||
|
*
|
||||||
|
* @param string $pdfVersion
|
||||||
|
*/
|
||||||
|
protected function setMinPdfVersion($pdfVersion)
|
||||||
|
{
|
||||||
|
if (\version_compare($pdfVersion, $this->PDFVersion, '>')) {
|
||||||
|
$this->PDFVersion = $pdfVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @noinspection PhpUndefinedClassInspection */
|
||||||
|
/**
|
||||||
|
* Get a new pdf parser instance.
|
||||||
|
*
|
||||||
|
* @param StreamReader $streamReader
|
||||||
|
* @param array $parserParams Individual parameters passed to the parser instance.
|
||||||
|
* @return PdfParser|FpdiPdfParser
|
||||||
|
*/
|
||||||
|
protected function getPdfParserInstance(
|
||||||
|
StreamReader $streamReader,
|
||||||
|
#[\SensitiveParameter]
|
||||||
|
array $parserParams = []
|
||||||
|
) {
|
||||||
|
// note: if you get an exception here - turn off errors/warnings on not found classes for your autoloader.
|
||||||
|
// psr-4 (https://www.php-fig.org/psr/psr-4/) says: Autoloader implementations MUST NOT throw
|
||||||
|
// exceptions, MUST NOT raise errors of any level, and SHOULD NOT return a value.
|
||||||
|
/** @noinspection PhpUndefinedClassInspection */
|
||||||
|
if (\class_exists(FpdiPdfParser::class)) {
|
||||||
|
/** @noinspection PhpUndefinedClassInspection */
|
||||||
|
return new FpdiPdfParser($streamReader, $parserParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PdfParser($streamReader);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an unique reader id by the $file parameter.
|
||||||
|
*
|
||||||
|
* @param string|resource|PdfReader|StreamReader $file An open file descriptor, a path to a file, a PdfReader
|
||||||
|
* instance or a StreamReader instance.
|
||||||
|
* @param array $parserParams Individual parameters passed to the parser instance.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getPdfReaderId(
|
||||||
|
$file,
|
||||||
|
#[\SensitiveParameter]
|
||||||
|
array $parserParams = []
|
||||||
|
) {
|
||||||
|
if (\is_resource($file)) {
|
||||||
|
$id = (string) $file;
|
||||||
|
} elseif (\is_string($file)) {
|
||||||
|
$id = \realpath($file);
|
||||||
|
if ($id === false) {
|
||||||
|
$id = $file;
|
||||||
|
}
|
||||||
|
} elseif (\is_object($file)) {
|
||||||
|
$id = \spl_object_hash($file);
|
||||||
|
} else {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
\sprintf('Invalid type in $file parameter (%s)', \gettype($file))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = \md5($id . '|' . \print_r($parserParams, true));
|
||||||
|
|
||||||
|
if (isset($this->readers[$id])) {
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\is_resource($file)) {
|
||||||
|
$streamReader = new StreamReader($file);
|
||||||
|
} elseif (\is_string($file)) {
|
||||||
|
$streamReader = StreamReader::createByFile($file);
|
||||||
|
$this->createdReaders[] = $id;
|
||||||
|
} else {
|
||||||
|
$streamReader = $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reader = new PdfReader($this->getPdfParserInstance($streamReader, $parserParams));
|
||||||
|
$this->readers[$id] = $reader;
|
||||||
|
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a pdf reader instance by its id.
|
||||||
|
*
|
||||||
|
* @param string $id
|
||||||
|
* @return PdfReader
|
||||||
|
*/
|
||||||
|
protected function getPdfReader($id)
|
||||||
|
{
|
||||||
|
if (isset($this->readers[$id])) {
|
||||||
|
return $this->readers[$id];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
\sprintf('No pdf reader with the given id (%s) exists.', $id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the source PDF file.
|
||||||
|
*
|
||||||
|
* @param string|resource|StreamReader $file Path to the file or a stream resource or a StreamReader instance.
|
||||||
|
* @return int The page count of the PDF document.
|
||||||
|
* @throws PdfParserException
|
||||||
|
*/
|
||||||
|
public function setSourceFile($file)
|
||||||
|
{
|
||||||
|
return $this->setSourceFileWithParserParams($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the source PDF file with parameters which are passed to the parser instance.
|
||||||
|
*
|
||||||
|
* This method allows us to pass e.g. authentication information to the parser instance.
|
||||||
|
*
|
||||||
|
* @param string|resource|StreamReader $file Path to the file or a stream resource or a StreamReader instance.
|
||||||
|
* @param array $parserParams Individual parameters passed to the parser instance.
|
||||||
|
* @return int The page count of the PDF document.
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
* @throws PdfParserException
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
public function setSourceFileWithParserParams(
|
||||||
|
$file,
|
||||||
|
#[\SensitiveParameter]
|
||||||
|
array $parserParams = []
|
||||||
|
) {
|
||||||
|
$this->currentReaderId = $this->getPdfReaderId($file, $parserParams);
|
||||||
|
$this->objectsToCopy[$this->currentReaderId] = [];
|
||||||
|
|
||||||
|
$reader = $this->getPdfReader($this->currentReaderId);
|
||||||
|
$this->setMinPdfVersion($reader->getPdfVersion());
|
||||||
|
|
||||||
|
return $reader->getPageCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports a page.
|
||||||
|
*
|
||||||
|
* @param int $pageNumber The page number.
|
||||||
|
* @param string $box The page boundary to import. Default set to PageBoundaries::CROP_BOX.
|
||||||
|
* @param bool $groupXObject Define the form XObject as a group XObject to support transparency (if used).
|
||||||
|
* @param bool $importExternalLinks Define whether external links are imported or not.
|
||||||
|
* @return string A unique string identifying the imported page.
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
* @throws FilterException
|
||||||
|
* @throws PdfParserException
|
||||||
|
* @throws PdfTypeException
|
||||||
|
* @throws PdfReaderException
|
||||||
|
* @see PageBoundaries
|
||||||
|
*/
|
||||||
|
public function importPage(
|
||||||
|
$pageNumber,
|
||||||
|
$box = PageBoundaries::CROP_BOX,
|
||||||
|
$groupXObject = true,
|
||||||
|
$importExternalLinks = false
|
||||||
|
) {
|
||||||
|
if ($this->currentReaderId === null) {
|
||||||
|
throw new \BadMethodCallException('No reader initiated. Call setSourceFile() first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageId = $this->currentReaderId;
|
||||||
|
|
||||||
|
$pageNumber = (int)$pageNumber;
|
||||||
|
$pageId .= '|' . $pageNumber . '|' . ($groupXObject ? '1' : '0') . '|' . ($importExternalLinks ? '1' : '0');
|
||||||
|
|
||||||
|
// for backwards compatibility with FPDI 1
|
||||||
|
$box = \ltrim($box, '/');
|
||||||
|
if (!PageBoundaries::isValidName($box)) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
\sprintf('Box name is invalid: "%s"', $box)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageId .= '|' . $box;
|
||||||
|
|
||||||
|
if (isset($this->importedPages[$pageId])) {
|
||||||
|
return $pageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reader = $this->getPdfReader($this->currentReaderId);
|
||||||
|
$page = $reader->getPage($pageNumber);
|
||||||
|
|
||||||
|
$bbox = $page->getBoundary($box);
|
||||||
|
if ($bbox === false) {
|
||||||
|
throw new PdfReaderException(
|
||||||
|
\sprintf("Page doesn't have a boundary box (%s).", $box),
|
||||||
|
PdfReaderException::MISSING_DATA
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dict = new PdfDictionary();
|
||||||
|
$dict->value['Type'] = PdfName::create('XObject');
|
||||||
|
$dict->value['Subtype'] = PdfName::create('Form');
|
||||||
|
$dict->value['FormType'] = PdfNumeric::create(1);
|
||||||
|
$dict->value['BBox'] = $bbox->toPdfArray();
|
||||||
|
|
||||||
|
if ($groupXObject) {
|
||||||
|
$this->setMinPdfVersion('1.4');
|
||||||
|
$dict->value['Group'] = PdfDictionary::create([
|
||||||
|
'Type' => PdfName::create('Group'),
|
||||||
|
'S' => PdfName::create('Transparency')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$resources = $page->getAttribute('Resources');
|
||||||
|
if ($resources !== null) {
|
||||||
|
$dict->value['Resources'] = $resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
[$width, $height] = $page->getWidthAndHeight($box);
|
||||||
|
|
||||||
|
$a = 1;
|
||||||
|
$b = 0;
|
||||||
|
$c = 0;
|
||||||
|
$d = 1;
|
||||||
|
$e = -$bbox->getLlx();
|
||||||
|
$f = -$bbox->getLly();
|
||||||
|
|
||||||
|
$rotation = $page->getRotation();
|
||||||
|
|
||||||
|
if ($rotation !== 0) {
|
||||||
|
$rotation *= -1;
|
||||||
|
$angle = $rotation * M_PI / 180;
|
||||||
|
$a = \cos($angle);
|
||||||
|
$b = \sin($angle);
|
||||||
|
$c = -$b;
|
||||||
|
$d = $a;
|
||||||
|
|
||||||
|
switch ($rotation) {
|
||||||
|
case -90:
|
||||||
|
$e = -$bbox->getLly();
|
||||||
|
$f = $bbox->getUrx();
|
||||||
|
break;
|
||||||
|
case -180:
|
||||||
|
$e = $bbox->getUrx();
|
||||||
|
$f = $bbox->getUry();
|
||||||
|
break;
|
||||||
|
case -270:
|
||||||
|
$e = $bbox->getUry();
|
||||||
|
$f = -$bbox->getLlx();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we need to rotate/translate
|
||||||
|
if ($a != 1 || $b != 0 || $c != 0 || $d != 1 || $e != 0 || $f != 0) {
|
||||||
|
$dict->value['Matrix'] = PdfArray::create([
|
||||||
|
PdfNumeric::create($a), PdfNumeric::create($b), PdfNumeric::create($c),
|
||||||
|
PdfNumeric::create($d), PdfNumeric::create($e), PdfNumeric::create($f)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to use the existing content stream
|
||||||
|
$pageDict = $page->getPageDictionary();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$contentsObject = PdfType::resolve(PdfDictionary::get($pageDict, 'Contents'), $reader->getParser(), true);
|
||||||
|
$contents = PdfType::resolve($contentsObject, $reader->getParser());
|
||||||
|
|
||||||
|
// just copy the stream reference if it is only a single stream
|
||||||
|
if (
|
||||||
|
($contentsIsStream = ($contents instanceof PdfStream))
|
||||||
|
|| ($contents instanceof PdfArray && \count($contents->value) === 1)
|
||||||
|
) {
|
||||||
|
if ($contentsIsStream) {
|
||||||
|
/**
|
||||||
|
* @var PdfIndirectObject $contentsObject
|
||||||
|
*/
|
||||||
|
$stream = $contents;
|
||||||
|
} else {
|
||||||
|
$stream = PdfType::resolve($contents->value[0], $reader->getParser());
|
||||||
|
}
|
||||||
|
|
||||||
|
$filter = PdfDictionary::get($stream->value, 'Filter');
|
||||||
|
if (!$filter instanceof PdfNull) {
|
||||||
|
$dict->value['Filter'] = $filter;
|
||||||
|
}
|
||||||
|
$length = PdfType::resolve(PdfDictionary::get($stream->value, 'Length'), $reader->getParser());
|
||||||
|
$dict->value['Length'] = $length;
|
||||||
|
$stream->value = $dict;
|
||||||
|
// otherwise extract it from the array and re-compress the whole stream
|
||||||
|
} else {
|
||||||
|
$streamContent = $this->compress
|
||||||
|
? \gzcompress($page->getContentStream())
|
||||||
|
: $page->getContentStream();
|
||||||
|
|
||||||
|
$dict->value['Length'] = PdfNumeric::create(\strlen($streamContent));
|
||||||
|
if ($this->compress) {
|
||||||
|
$dict->value['Filter'] = PdfName::create('FlateDecode');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stream = PdfStream::create($dict, $streamContent);
|
||||||
|
}
|
||||||
|
// Catch faulty pages and use an empty content stream
|
||||||
|
} catch (FpdiException $e) {
|
||||||
|
$dict->value['Length'] = PdfNumeric::create(0);
|
||||||
|
$stream = PdfStream::create($dict, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
$externalLinks = [];
|
||||||
|
if ($importExternalLinks) {
|
||||||
|
$externalLinks = $page->getExternalLinks($box);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->importedPages[$pageId] = [
|
||||||
|
'objectNumber' => null,
|
||||||
|
'readerId' => $this->currentReaderId,
|
||||||
|
'id' => 'TPL' . $this->getNextTemplateId(),
|
||||||
|
'width' => $width / $this->k,
|
||||||
|
'height' => $height / $this->k,
|
||||||
|
'stream' => $stream,
|
||||||
|
'externalLinks' => $externalLinks
|
||||||
|
];
|
||||||
|
|
||||||
|
return $pageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws an imported page onto the page.
|
||||||
|
*
|
||||||
|
* Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
|
||||||
|
* aspect ratio.
|
||||||
|
*
|
||||||
|
* @param mixed $pageId The page id
|
||||||
|
* @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array
|
||||||
|
* with the keys "x", "y", "width", "height", "adjustPageSize".
|
||||||
|
* @param float|int $y The ordinate of upper-left corner.
|
||||||
|
* @param float|int|null $width The width.
|
||||||
|
* @param float|int|null $height The height.
|
||||||
|
* @param bool $adjustPageSize
|
||||||
|
* @return array The size.
|
||||||
|
* @see Fpdi::getTemplateSize()
|
||||||
|
*/
|
||||||
|
public function useImportedPage($pageId, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false)
|
||||||
|
{
|
||||||
|
if (\is_array($x)) {
|
||||||
|
/** @noinspection OffsetOperationsInspection */
|
||||||
|
unset($x['pageId']);
|
||||||
|
\extract($x, EXTR_IF_EXISTS);
|
||||||
|
/** @noinspection NotOptimalIfConditionsInspection */
|
||||||
|
/** @phpstan-ignore function.alreadyNarrowedType */
|
||||||
|
if (\is_array($x)) {
|
||||||
|
$x = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($this->importedPages[$pageId])) {
|
||||||
|
throw new \InvalidArgumentException('Imported page does not exist!');
|
||||||
|
}
|
||||||
|
|
||||||
|
$importedPage = $this->importedPages[$pageId];
|
||||||
|
|
||||||
|
$originalSize = $this->getTemplateSize($pageId);
|
||||||
|
$newSize = $this->getTemplateSize($pageId, $width, $height);
|
||||||
|
if ($adjustPageSize) {
|
||||||
|
$this->setPageFormat($newSize, $newSize['orientation']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scaleX = ($newSize['width'] / $originalSize['width']);
|
||||||
|
$scaleY = ($newSize['height'] / $originalSize['height']);
|
||||||
|
$xPt = $x * $this->k;
|
||||||
|
$yPt = $y * $this->k;
|
||||||
|
$newHeightPt = $newSize['height'] * $this->k;
|
||||||
|
|
||||||
|
$this->_out(
|
||||||
|
// reset standard values, translate and scale
|
||||||
|
\sprintf(
|
||||||
|
'q 0 J 1 w 0 j 0 G 0 g %.4F 0 0 %.4F %.4F %.4F cm /%s Do Q',
|
||||||
|
$scaleX,
|
||||||
|
$scaleY,
|
||||||
|
$xPt,
|
||||||
|
$this->hPt - $yPt - $newHeightPt,
|
||||||
|
$importedPage['id']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (count($importedPage['externalLinks']) > 0) {
|
||||||
|
foreach ($importedPage['externalLinks'] as $externalLink) {
|
||||||
|
// mPDF uses also 'externalLinks' but doesn't come with a rect-value
|
||||||
|
if (!isset($externalLink['rect'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Rectangle $rect */
|
||||||
|
$rect = $externalLink['rect'];
|
||||||
|
$this->Link(
|
||||||
|
$x + $rect->getLlx() / $this->k * $scaleX,
|
||||||
|
$y + $newSize['height'] - ($rect->getLly() + $rect->getHeight()) / $this->k * $scaleY,
|
||||||
|
$rect->getWidth() / $this->k * $scaleX,
|
||||||
|
$rect->getHeight() / $this->k * $scaleY,
|
||||||
|
$externalLink['uri']
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->adjustLastLink($externalLink, $xPt, $scaleX, $yPt, $newHeightPt, $scaleY, $importedPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $newSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will add additional data to the last created link/annotation.
|
||||||
|
*
|
||||||
|
* It is separated because TCPDF uses its own logic to handle link annotations.
|
||||||
|
* This method is overwritten in the TCPDF implementation.
|
||||||
|
*
|
||||||
|
* @param array $externalLink
|
||||||
|
* @param float|int $xPt
|
||||||
|
* @param float|int $scaleX
|
||||||
|
* @param float|int $yPt
|
||||||
|
* @param float|int $newHeightPt
|
||||||
|
* @param float|int $scaleY
|
||||||
|
* @param array $importedPage
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function adjustLastLink($externalLink, $xPt, $scaleX, $yPt, $newHeightPt, $scaleY, $importedPage)
|
||||||
|
{
|
||||||
|
// let's create a relation of the newly created link to the data of the external link
|
||||||
|
$lastLink = \count($this->PageLinks[$this->page]);
|
||||||
|
$this->PageLinks[$this->page][$lastLink - 1]['importedLink'] = $externalLink;
|
||||||
|
if (\count($externalLink['quadPoints']) > 0) {
|
||||||
|
$quadPoints = [];
|
||||||
|
for ($i = 0, $n = \count($externalLink['quadPoints']); $i < $n; $i += 2) {
|
||||||
|
$quadPoints[] = $xPt + $externalLink['quadPoints'][$i] * $scaleX;
|
||||||
|
$quadPoints[] = $this->hPt - $yPt - $newHeightPt + $externalLink['quadPoints'][$i + 1] * $scaleY;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->PageLinks[$this->page][$lastLink - 1]['quadPoints'] = $quadPoints;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of an imported page.
|
||||||
|
*
|
||||||
|
* Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
|
||||||
|
* aspect ratio.
|
||||||
|
*
|
||||||
|
* @param mixed $tpl The template id
|
||||||
|
* @param float|int|null $width The width.
|
||||||
|
* @param float|int|null $height The height.
|
||||||
|
* @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P)
|
||||||
|
*/
|
||||||
|
public function getImportedPageSize($tpl, $width = null, $height = null)
|
||||||
|
{
|
||||||
|
if (isset($this->importedPages[$tpl])) {
|
||||||
|
$importedPage = $this->importedPages[$tpl];
|
||||||
|
|
||||||
|
if ($width === null && $height === null) {
|
||||||
|
$width = $importedPage['width'];
|
||||||
|
$height = $importedPage['height'];
|
||||||
|
} elseif ($width === null) {
|
||||||
|
$width = $height * $importedPage['width'] / $importedPage['height'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($height === null) {
|
||||||
|
$height = $width * $importedPage['height'] / $importedPage['width'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($height <= 0. || $width <= 0.) {
|
||||||
|
throw new \InvalidArgumentException('Width or height parameter needs to be larger than zero.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'width' => $width,
|
||||||
|
'height' => $height,
|
||||||
|
0 => $width,
|
||||||
|
1 => $height,
|
||||||
|
'orientation' => $width > $height ? 'L' : 'P'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a PdfType object to the resulting buffer.
|
||||||
|
*
|
||||||
|
* @param PdfType $value
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
protected function writePdfType(PdfType $value)
|
||||||
|
{
|
||||||
|
if ($value instanceof PdfNumeric) {
|
||||||
|
if (\is_int($value->value)) {
|
||||||
|
$this->_put($value->value . ' ', false);
|
||||||
|
} else {
|
||||||
|
$this->_put(\rtrim(\rtrim(\sprintf('%.5F', $value->value), '0'), '.') . ' ', false);
|
||||||
|
}
|
||||||
|
} elseif ($value instanceof PdfName) {
|
||||||
|
$this->_put('/' . $value->value . ' ', false);
|
||||||
|
} elseif ($value instanceof PdfString) {
|
||||||
|
$this->_put('(' . $value->value . ')', false);
|
||||||
|
} elseif ($value instanceof PdfHexString) {
|
||||||
|
$this->_put('<' . $value->value . '>', false);
|
||||||
|
} elseif ($value instanceof PdfBoolean) {
|
||||||
|
$this->_put($value->value ? 'true ' : 'false ', false);
|
||||||
|
} elseif ($value instanceof PdfArray) {
|
||||||
|
$this->_put('[', false);
|
||||||
|
foreach ($value->value as $entry) {
|
||||||
|
$this->writePdfType($entry);
|
||||||
|
}
|
||||||
|
$this->_put(']');
|
||||||
|
} elseif ($value instanceof PdfDictionary) {
|
||||||
|
$this->_put('<<', false);
|
||||||
|
foreach ($value->value as $name => $entry) {
|
||||||
|
$this->_put('/' . $name . ' ', false);
|
||||||
|
$this->writePdfType($entry);
|
||||||
|
}
|
||||||
|
$this->_put('>>');
|
||||||
|
} elseif ($value instanceof PdfToken) {
|
||||||
|
$this->_put($value->value);
|
||||||
|
} elseif ($value instanceof PdfNull) {
|
||||||
|
$this->_put('null ', false);
|
||||||
|
} elseif ($value instanceof PdfStream) {
|
||||||
|
$this->writePdfType($value->value);
|
||||||
|
$this->_put('stream');
|
||||||
|
$this->_put($value->getStream());
|
||||||
|
$this->_put('endstream');
|
||||||
|
} elseif ($value instanceof PdfIndirectObjectReference) {
|
||||||
|
if (!isset($this->objectMap[$this->currentReaderId])) {
|
||||||
|
$this->objectMap[$this->currentReaderId] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($this->objectMap[$this->currentReaderId][$value->value])) {
|
||||||
|
$this->objectMap[$this->currentReaderId][$value->value] = ++$this->n;
|
||||||
|
$this->objectsToCopy[$this->currentReaderId][] = $value->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_put($this->objectMap[$this->currentReaderId][$value->value] . ' 0 R ', false);
|
||||||
|
} elseif ($value instanceof PdfIndirectObject) {
|
||||||
|
$n = $this->objectMap[$this->currentReaderId][$value->objectNumber];
|
||||||
|
$this->_newobj($n);
|
||||||
|
$this->writePdfType($value->value);
|
||||||
|
|
||||||
|
// add newline before "endobj" for all objects in view to PDF/A conformance
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
($value->value instanceof PdfArray) ||
|
||||||
|
($value->value instanceof PdfDictionary) ||
|
||||||
|
($value->value instanceof PdfToken) ||
|
||||||
|
($value->value instanceof PdfStream)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
$this->_put("\n", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_put('endobj');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
97
fpdi2/src/GraphicsState.php
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi;
|
||||||
|
|
||||||
|
use setasign\Fpdi\Math\Matrix;
|
||||||
|
use setasign\Fpdi\Math\Vector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple graphic state class which holds the current transformation matrix.
|
||||||
|
*/
|
||||||
|
class GraphicsState
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Matrix
|
||||||
|
*/
|
||||||
|
protected $ctm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Matrix|null $ctm
|
||||||
|
*/
|
||||||
|
public function __construct(?Matrix $ctm = null)
|
||||||
|
{
|
||||||
|
if ($ctm === null) {
|
||||||
|
$ctm = new Matrix();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->ctm = $ctm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Matrix $matrix
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function add(Matrix $matrix)
|
||||||
|
{
|
||||||
|
$this->ctm = $matrix->multiply($this->ctm);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int|float $x
|
||||||
|
* @param int|float $y
|
||||||
|
* @param int|float $angle
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function rotate($x, $y, $angle)
|
||||||
|
{
|
||||||
|
if (\abs($angle) < 1e-5) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$angle = \deg2rad($angle);
|
||||||
|
$c = \cos($angle);
|
||||||
|
$s = \sin($angle);
|
||||||
|
|
||||||
|
$this->add(new Matrix($c, $s, -$s, $c, $x, $y));
|
||||||
|
|
||||||
|
return $this->translate(-$x, -$y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int|float $shiftX
|
||||||
|
* @param int|float $shiftY
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function translate($shiftX, $shiftY)
|
||||||
|
{
|
||||||
|
return $this->add(new Matrix(1, 0, 0, 1, $shiftX, $shiftY));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int|float $scaleX
|
||||||
|
* @param int|float $scaleY
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function scale($scaleX, $scaleY)
|
||||||
|
{
|
||||||
|
return $this->add(new Matrix($scaleX, 0, 0, $scaleY, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Vector $vector
|
||||||
|
* @return Vector
|
||||||
|
*/
|
||||||
|
public function toUserSpace(Vector $vector)
|
||||||
|
{
|
||||||
|
return $vector->multiplyWithMatrix($this->ctm);
|
||||||
|
}
|
||||||
|
}
|
||||||
116
fpdi2/src/Math/Matrix.php
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\Math;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple 2D-Matrix class
|
||||||
|
*/
|
||||||
|
class Matrix
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var float
|
||||||
|
*/
|
||||||
|
protected $a;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var float
|
||||||
|
*/
|
||||||
|
protected $b;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var float
|
||||||
|
*/
|
||||||
|
protected $c;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var float
|
||||||
|
*/
|
||||||
|
protected $d;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var float
|
||||||
|
*/
|
||||||
|
protected $e;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var float
|
||||||
|
*/
|
||||||
|
protected $f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int|float $a
|
||||||
|
* @param int|float $b
|
||||||
|
* @param int|float $c
|
||||||
|
* @param int|float $d
|
||||||
|
* @param int|float $e
|
||||||
|
* @param int|float $f
|
||||||
|
*/
|
||||||
|
public function __construct($a = 1, $b = 0, $c = 0, $d = 1, $e = 0, $f = 0)
|
||||||
|
{
|
||||||
|
$this->a = (float)$a;
|
||||||
|
$this->b = (float)$b;
|
||||||
|
$this->c = (float)$c;
|
||||||
|
$this->d = (float)$d;
|
||||||
|
$this->e = (float)$e;
|
||||||
|
$this->f = (float)$f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return float[]
|
||||||
|
*/
|
||||||
|
public function getValues()
|
||||||
|
{
|
||||||
|
return [$this->a, $this->b, $this->c, $this->d, $this->e, $this->f];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Matrix $by
|
||||||
|
* @return Matrix
|
||||||
|
*/
|
||||||
|
public function multiply(self $by)
|
||||||
|
{
|
||||||
|
$a =
|
||||||
|
$this->a * $by->a
|
||||||
|
+ $this->b * $by->c
|
||||||
|
//+ 0 * $by->e
|
||||||
|
;
|
||||||
|
|
||||||
|
$b =
|
||||||
|
$this->a * $by->b
|
||||||
|
+ $this->b * $by->d
|
||||||
|
//+ 0 * $by->f
|
||||||
|
;
|
||||||
|
|
||||||
|
$c =
|
||||||
|
$this->c * $by->a
|
||||||
|
+ $this->d * $by->c
|
||||||
|
//+ 0 * $by->e
|
||||||
|
;
|
||||||
|
|
||||||
|
$d =
|
||||||
|
$this->c * $by->b
|
||||||
|
+ $this->d * $by->d
|
||||||
|
//+ 0 * $by->f
|
||||||
|
;
|
||||||
|
|
||||||
|
$e =
|
||||||
|
$this->e * $by->a
|
||||||
|
+ $this->f * $by->c
|
||||||
|
+ /*1 * */$by->e;
|
||||||
|
|
||||||
|
$f =
|
||||||
|
$this->e * $by->b
|
||||||
|
+ $this->f * $by->d
|
||||||
|
+ /*1 * */$by->f;
|
||||||
|
|
||||||
|
return new self($a, $b, $c, $d, $e, $f);
|
||||||
|
}
|
||||||
|
}
|
||||||
66
fpdi2/src/Math/Vector.php
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\Math;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple 2D-Vector class
|
||||||
|
*/
|
||||||
|
class Vector
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var float
|
||||||
|
*/
|
||||||
|
protected $x;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var float
|
||||||
|
*/
|
||||||
|
protected $y;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int|float $x
|
||||||
|
* @param int|float $y
|
||||||
|
*/
|
||||||
|
public function __construct($x = .0, $y = .0)
|
||||||
|
{
|
||||||
|
$this->x = (float)$x;
|
||||||
|
$this->y = (float)$y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
public function getX()
|
||||||
|
{
|
||||||
|
return $this->x;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
public function getY()
|
||||||
|
{
|
||||||
|
return $this->y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Matrix $matrix
|
||||||
|
* @return Vector
|
||||||
|
*/
|
||||||
|
public function multiplyWithMatrix(Matrix $matrix)
|
||||||
|
{
|
||||||
|
[$a, $b, $c, $d, $e, $f] = $matrix->getValues();
|
||||||
|
$x = $a * $this->x + $c * $this->y + $e;
|
||||||
|
$y = $b * $this->x + $d * $this->y + $f;
|
||||||
|
|
||||||
|
return new self($x, $y);
|
||||||
|
}
|
||||||
|
}
|
||||||
93
fpdi2/src/PdfParser/CrossReference/AbstractReader.php
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\CrossReference;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParser;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfDictionary;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfToken;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfTypeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class for cross-reference reader classes.
|
||||||
|
*/
|
||||||
|
abstract class AbstractReader
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var PdfParser
|
||||||
|
*/
|
||||||
|
protected $parser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var PdfDictionary
|
||||||
|
*/
|
||||||
|
protected $trailer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AbstractReader constructor.
|
||||||
|
*
|
||||||
|
* @param PdfParser $parser
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
*/
|
||||||
|
public function __construct(PdfParser $parser)
|
||||||
|
{
|
||||||
|
$this->parser = $parser;
|
||||||
|
$this->readTrailer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the trailer dictionary.
|
||||||
|
*
|
||||||
|
* @return PdfDictionary
|
||||||
|
*/
|
||||||
|
public function getTrailer()
|
||||||
|
{
|
||||||
|
return $this->trailer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the trailer dictionary.
|
||||||
|
*
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
*/
|
||||||
|
protected function readTrailer()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$trailerKeyword = $this->parser->readValue(null, PdfToken::class);
|
||||||
|
if ($trailerKeyword->value !== 'trailer') {
|
||||||
|
throw new CrossReferenceException(
|
||||||
|
\sprintf(
|
||||||
|
'Unexpected end of cross reference. "trailer"-keyword expected, got: %s.',
|
||||||
|
$trailerKeyword->value
|
||||||
|
),
|
||||||
|
CrossReferenceException::UNEXPECTED_END
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (PdfTypeException $e) {
|
||||||
|
throw new CrossReferenceException(
|
||||||
|
'Unexpected end of cross reference. "trailer"-keyword expected, got an invalid object type.',
|
||||||
|
CrossReferenceException::UNEXPECTED_END,
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$trailer = $this->parser->readValue(null, PdfDictionary::class);
|
||||||
|
} catch (PdfTypeException $e) {
|
||||||
|
throw new CrossReferenceException(
|
||||||
|
'Unexpected end of cross reference. Trailer not found.',
|
||||||
|
CrossReferenceException::UNEXPECTED_END,
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->trailer = $trailer;
|
||||||
|
}
|
||||||
|
}
|
||||||
337
fpdi2/src/PdfParser/CrossReference/CrossReference.php
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\CrossReference;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParser;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfDictionary;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfNumeric;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfStream;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfToken;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfTypeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class CrossReference
|
||||||
|
*
|
||||||
|
* This class processes the standard cross reference of a PDF document.
|
||||||
|
*/
|
||||||
|
class CrossReference
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The byte length in which the "startxref" keyword should be searched.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public static $trailerSearchLength = 5500;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $fileHeaderOffset = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var PdfParser
|
||||||
|
*/
|
||||||
|
protected $parser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ReaderInterface[]
|
||||||
|
*/
|
||||||
|
protected $readers = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CrossReference constructor.
|
||||||
|
*
|
||||||
|
* @param PdfParser $parser
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
public function __construct(PdfParser $parser, $fileHeaderOffset = 0)
|
||||||
|
{
|
||||||
|
// clear the token stack, if the parser instance is re-used
|
||||||
|
$parser->getTokenizer()->clearStack();
|
||||||
|
|
||||||
|
$this->parser = $parser;
|
||||||
|
$this->fileHeaderOffset = $fileHeaderOffset;
|
||||||
|
|
||||||
|
$offset = $this->findStartXref();
|
||||||
|
$reader = null;
|
||||||
|
$offsets = [$offset];
|
||||||
|
/** @noinspection TypeUnsafeComparisonInspection */
|
||||||
|
while ($offset != false) { // By doing an unsafe comparsion we ignore faulty references to byte offset 0
|
||||||
|
try {
|
||||||
|
$reader = $this->readXref($offset + $this->fileHeaderOffset);
|
||||||
|
} catch (CrossReferenceException $e) {
|
||||||
|
// sometimes the file header offset is part of the byte offsets, so let's retry by resetting it to zero.
|
||||||
|
if ($e->getCode() === CrossReferenceException::INVALID_DATA && $this->fileHeaderOffset !== 0) {
|
||||||
|
$this->fileHeaderOffset = 0;
|
||||||
|
$reader = $this->readXref($offset);
|
||||||
|
} else {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$trailer = $reader->getTrailer();
|
||||||
|
$this->checkForEncryption($trailer);
|
||||||
|
$this->readers[] = $reader;
|
||||||
|
|
||||||
|
if (isset($trailer->value['Prev'])) {
|
||||||
|
$offset = $trailer->value['Prev']->value;
|
||||||
|
if (\in_array($offset, $offsets, true)) {
|
||||||
|
throw new CrossReferenceException(
|
||||||
|
'Cross-references includes cyclic structure.',
|
||||||
|
CrossReferenceException::CYCLIC_STRUCTURE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$offsets[] = $offset;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fix faulty sub-section header
|
||||||
|
if ($reader instanceof FixedReader) {
|
||||||
|
/**
|
||||||
|
* @var FixedReader $reader
|
||||||
|
*/
|
||||||
|
$reader->fixFaultySubSectionShift();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($reader === null) {
|
||||||
|
throw new CrossReferenceException('No cross-reference found.', CrossReferenceException::NO_XREF_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of the cross reference.
|
||||||
|
*
|
||||||
|
* @return integer
|
||||||
|
*/
|
||||||
|
public function getSize()
|
||||||
|
{
|
||||||
|
return $this->getTrailer()->value['Size']->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the trailer dictionary.
|
||||||
|
*
|
||||||
|
* @return PdfDictionary
|
||||||
|
*/
|
||||||
|
public function getTrailer()
|
||||||
|
{
|
||||||
|
return $this->readers[0]->getTrailer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cross reference readser instances.
|
||||||
|
*
|
||||||
|
* @return ReaderInterface[]
|
||||||
|
*/
|
||||||
|
public function getReaders()
|
||||||
|
{
|
||||||
|
return $this->readers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the offset by an object number.
|
||||||
|
*
|
||||||
|
* @param int $objectNumber
|
||||||
|
* @return integer|bool
|
||||||
|
*/
|
||||||
|
public function getOffsetFor($objectNumber)
|
||||||
|
{
|
||||||
|
foreach ($this->getReaders() as $reader) {
|
||||||
|
$offset = $reader->getOffsetFor($objectNumber);
|
||||||
|
if ($offset !== false) {
|
||||||
|
return $offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an indirect object by its object number.
|
||||||
|
*
|
||||||
|
* @param int $objectNumber
|
||||||
|
* @return PdfIndirectObject
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
*/
|
||||||
|
public function getIndirectObject($objectNumber)
|
||||||
|
{
|
||||||
|
$offset = $this->getOffsetFor($objectNumber);
|
||||||
|
if ($offset === false) {
|
||||||
|
throw new CrossReferenceException(
|
||||||
|
\sprintf('Object (id:%s) not found.', $objectNumber),
|
||||||
|
CrossReferenceException::OBJECT_NOT_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$parser = $this->parser;
|
||||||
|
|
||||||
|
$parser->getTokenizer()->clearStack();
|
||||||
|
$parser->getStreamReader()->reset($offset + $this->fileHeaderOffset);
|
||||||
|
|
||||||
|
try {
|
||||||
|
/** @var PdfIndirectObject $object */
|
||||||
|
$object = $parser->readValue(null, PdfIndirectObject::class);
|
||||||
|
} catch (PdfTypeException $e) {
|
||||||
|
throw new CrossReferenceException(
|
||||||
|
\sprintf('Object (id:%s) not found at location (%s).', $objectNumber, $offset),
|
||||||
|
CrossReferenceException::OBJECT_NOT_FOUND,
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($object->objectNumber !== $objectNumber) {
|
||||||
|
throw new CrossReferenceException(
|
||||||
|
\sprintf('Wrong object found, got %s while %s was expected.', $object->objectNumber, $objectNumber),
|
||||||
|
CrossReferenceException::OBJECT_NOT_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the cross-reference table at a given offset.
|
||||||
|
*
|
||||||
|
* Internally the method will try to evaluate the best reader for this cross-reference.
|
||||||
|
*
|
||||||
|
* @param int $offset
|
||||||
|
* @return ReaderInterface
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
protected function readXref($offset)
|
||||||
|
{
|
||||||
|
$this->parser->getStreamReader()->reset($offset);
|
||||||
|
$this->parser->getTokenizer()->clearStack();
|
||||||
|
$initValue = $this->parser->readValue();
|
||||||
|
|
||||||
|
return $this->initReaderInstance($initValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a cross-reference reader instance.
|
||||||
|
*
|
||||||
|
* @param PdfToken|PdfIndirectObject $initValue
|
||||||
|
* @return ReaderInterface|bool
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
protected function initReaderInstance($initValue)
|
||||||
|
{
|
||||||
|
$position = $this->parser->getStreamReader()->getPosition()
|
||||||
|
+ $this->parser->getStreamReader()->getOffset() + $this->fileHeaderOffset;
|
||||||
|
|
||||||
|
if ($initValue instanceof PdfToken && $initValue->value === 'xref') {
|
||||||
|
try {
|
||||||
|
return new FixedReader($this->parser);
|
||||||
|
} catch (CrossReferenceException $e) {
|
||||||
|
$this->parser->getStreamReader()->reset($position);
|
||||||
|
$this->parser->getTokenizer()->clearStack();
|
||||||
|
|
||||||
|
return new LineReader($this->parser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($initValue instanceof PdfIndirectObject) {
|
||||||
|
try {
|
||||||
|
$stream = PdfStream::ensure($initValue->value);
|
||||||
|
} catch (PdfTypeException $e) {
|
||||||
|
throw new CrossReferenceException(
|
||||||
|
'Invalid object type at xref reference offset.',
|
||||||
|
CrossReferenceException::INVALID_DATA,
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = PdfDictionary::get($stream->value, 'Type');
|
||||||
|
if ($type->value !== 'XRef') {
|
||||||
|
throw new CrossReferenceException(
|
||||||
|
'The xref position points to an incorrect object type.',
|
||||||
|
CrossReferenceException::INVALID_DATA
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->checkForEncryption($stream->value);
|
||||||
|
|
||||||
|
throw new CrossReferenceException(
|
||||||
|
'This PDF document probably uses a compression technique which is not supported by the ' .
|
||||||
|
'free parser shipped with FPDI. (See https://www.setasign.com/fpdi-pdf-parser for more details)',
|
||||||
|
CrossReferenceException::COMPRESSED_XREF
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new CrossReferenceException(
|
||||||
|
'The xref position points to an incorrect object type.',
|
||||||
|
CrossReferenceException::INVALID_DATA
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for encryption.
|
||||||
|
*
|
||||||
|
* @param PdfDictionary $dictionary
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
*/
|
||||||
|
protected function checkForEncryption(PdfDictionary $dictionary)
|
||||||
|
{
|
||||||
|
if (isset($dictionary->value['Encrypt'])) {
|
||||||
|
throw new CrossReferenceException(
|
||||||
|
'This PDF document is encrypted and cannot be processed with FPDI.',
|
||||||
|
CrossReferenceException::ENCRYPTED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the start position for the first cross-reference.
|
||||||
|
*
|
||||||
|
* @return int The byte-offset position of the first cross-reference.
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
*/
|
||||||
|
protected function findStartXref()
|
||||||
|
{
|
||||||
|
$reader = $this->parser->getStreamReader();
|
||||||
|
$reader->reset(-self::$trailerSearchLength, self::$trailerSearchLength);
|
||||||
|
|
||||||
|
$buffer = $reader->getBuffer(false);
|
||||||
|
$pos = \strrpos($buffer, 'startxref');
|
||||||
|
$addOffset = 9;
|
||||||
|
if ($pos === false) {
|
||||||
|
// Some corrupted documents uses startref, instead of startxref
|
||||||
|
$pos = \strrpos($buffer, 'startref');
|
||||||
|
if ($pos === false) {
|
||||||
|
throw new CrossReferenceException(
|
||||||
|
'Unable to find pointer to xref table',
|
||||||
|
CrossReferenceException::NO_STARTXREF_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$addOffset = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reader->setOffset($pos + $addOffset);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$value = $this->parser->readValue(null, PdfNumeric::class);
|
||||||
|
} catch (PdfTypeException $e) {
|
||||||
|
throw new CrossReferenceException(
|
||||||
|
'Invalid data after startxref keyword.',
|
||||||
|
CrossReferenceException::INVALID_DATA,
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value->value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\CrossReference;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParserException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception used by the CrossReference and Reader classes.
|
||||||
|
*/
|
||||||
|
class CrossReferenceException extends PdfParserException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const INVALID_DATA = 0x0101;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const XREF_MISSING = 0x0102;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const ENTRIES_TOO_LARGE = 0x0103;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const ENTRIES_TOO_SHORT = 0x0104;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const NO_ENTRIES = 0x0105;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const NO_TRAILER_FOUND = 0x0106;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const NO_STARTXREF_FOUND = 0x0107;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const NO_XREF_FOUND = 0x0108;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const UNEXPECTED_END = 0x0109;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const OBJECT_NOT_FOUND = 0x010A;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const COMPRESSED_XREF = 0x010B;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const ENCRYPTED = 0x010C;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const CYCLIC_STRUCTURE = 0x010D;
|
||||||
|
}
|
||||||
200
fpdi2/src/PdfParser/CrossReference/FixedReader.php
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\CrossReference;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParser;
|
||||||
|
use setasign\Fpdi\PdfParser\StreamReader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class FixedReader
|
||||||
|
*
|
||||||
|
* This reader allows a very less overhead parsing of single entries of the cross-reference, because the main entries
|
||||||
|
* are only read when needed and not in a single run.
|
||||||
|
*/
|
||||||
|
class FixedReader extends AbstractReader implements ReaderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var StreamReader
|
||||||
|
*/
|
||||||
|
protected $reader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data of subsections.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $subSections;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FixedReader constructor.
|
||||||
|
*
|
||||||
|
* @param PdfParser $parser
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
*/
|
||||||
|
public function __construct(PdfParser $parser)
|
||||||
|
{
|
||||||
|
$this->reader = $parser->getStreamReader();
|
||||||
|
$this->read();
|
||||||
|
parent::__construct($parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all subsection data.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getSubSections()
|
||||||
|
{
|
||||||
|
return $this->subSections;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
* @return int|false
|
||||||
|
*/
|
||||||
|
public function getOffsetFor($objectNumber)
|
||||||
|
{
|
||||||
|
foreach ($this->subSections as $offset => [$startObject, $objectCount]) {
|
||||||
|
/**
|
||||||
|
* @var int $startObject
|
||||||
|
* @var int $objectCount
|
||||||
|
*/
|
||||||
|
if ($objectNumber >= $startObject && $objectNumber < ($startObject + $objectCount)) {
|
||||||
|
$position = $offset + 20 * ($objectNumber - $startObject);
|
||||||
|
$this->reader->ensure($position, 20);
|
||||||
|
$line = $this->reader->readBytes(20);
|
||||||
|
if ($line === false || $line[17] === 'f') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) \substr($line, 0, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the cross-reference.
|
||||||
|
*
|
||||||
|
* This reader will only read the subsections in this method. The offsets were resolved individually by this
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
*/
|
||||||
|
protected function read()
|
||||||
|
{
|
||||||
|
$subSections = [];
|
||||||
|
|
||||||
|
$startObject = $entryCount = $lastLineStart = null;
|
||||||
|
$validityChecked = false;
|
||||||
|
while (($line = $this->reader->readLine(20)) !== false) {
|
||||||
|
if (\strpos($line, 'trailer') !== false) {
|
||||||
|
$this->reader->reset($lastLineStart);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// jump over if line content doesn't match the expected string
|
||||||
|
if (\sscanf($line, '%d %d', $startObject, $entryCount) !== 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$oldPosition = $this->reader->getPosition();
|
||||||
|
$position = $oldPosition + $this->reader->getOffset();
|
||||||
|
|
||||||
|
if (!$validityChecked && $entryCount > 0) {
|
||||||
|
$nextLine = $this->reader->readBytes(21);
|
||||||
|
/* Check the next line for maximum of 20 bytes and not longer
|
||||||
|
* By catching 21 bytes and trimming the length should be still 21.
|
||||||
|
*/
|
||||||
|
if (\strlen(\trim($nextLine)) !== 21) {
|
||||||
|
throw new CrossReferenceException(
|
||||||
|
'Cross-reference entries are larger than 20 bytes.',
|
||||||
|
CrossReferenceException::ENTRIES_TOO_LARGE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check for less than 20 bytes: cut the line to 20 bytes and trim; have to result in exactly 18 bytes.
|
||||||
|
* If it would have less bytes the substring would get the first bytes of the next line which would
|
||||||
|
* evaluate to a 20 bytes long string after trimming.
|
||||||
|
*/
|
||||||
|
if (\strlen(\trim(\substr($nextLine, 0, 20))) !== 18) {
|
||||||
|
throw new CrossReferenceException(
|
||||||
|
'Cross-reference entries are less than 20 bytes.',
|
||||||
|
CrossReferenceException::ENTRIES_TOO_SHORT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validityChecked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subSections[$position] = [$startObject, $entryCount];
|
||||||
|
|
||||||
|
$lastLineStart = $position + $entryCount * 20;
|
||||||
|
$this->reader->reset($lastLineStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset after the last correct parsed line
|
||||||
|
$this->reader->reset($lastLineStart);
|
||||||
|
|
||||||
|
if (\count($subSections) === 0) {
|
||||||
|
throw new CrossReferenceException(
|
||||||
|
'No entries found in cross-reference.',
|
||||||
|
CrossReferenceException::NO_ENTRIES
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->subSections = $subSections;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixes an invalid object number shift.
|
||||||
|
*
|
||||||
|
* This method can be used to repair documents with an invalid subsection header:
|
||||||
|
*
|
||||||
|
* <code>
|
||||||
|
* xref
|
||||||
|
* 1 7
|
||||||
|
* 0000000000 65535 f
|
||||||
|
* 0000000009 00000 n
|
||||||
|
* 0000412075 00000 n
|
||||||
|
* 0000412172 00000 n
|
||||||
|
* 0000412359 00000 n
|
||||||
|
* 0000412417 00000 n
|
||||||
|
* 0000412468 00000 n
|
||||||
|
* </code>
|
||||||
|
*
|
||||||
|
* It shall only be called on the first table.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function fixFaultySubSectionShift()
|
||||||
|
{
|
||||||
|
$subSections = $this->getSubSections();
|
||||||
|
if (\count($subSections) > 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subSection = \current($subSections);
|
||||||
|
if ($subSection[0] != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->getOffsetFor(1) === false) {
|
||||||
|
foreach ($subSections as $offset => [$startObject, $objectCount]) {
|
||||||
|
$this->subSections[$offset] = [$startObject - 1, $objectCount];
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
168
fpdi2/src/PdfParser/CrossReference/LineReader.php
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\CrossReference;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParser;
|
||||||
|
use setasign\Fpdi\PdfParser\StreamReader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class LineReader
|
||||||
|
*
|
||||||
|
* This reader class read all cross-reference entries in a single run.
|
||||||
|
* It supports reading cross-references with e.g. invalid data (e.g. entries with a length < or > 20 bytes).
|
||||||
|
*/
|
||||||
|
class LineReader extends AbstractReader implements ReaderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The object offsets.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $offsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LineReader constructor.
|
||||||
|
*
|
||||||
|
* @param PdfParser $parser
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
*/
|
||||||
|
public function __construct(PdfParser $parser)
|
||||||
|
{
|
||||||
|
$this->read($this->extract($parser->getStreamReader()));
|
||||||
|
parent::__construct($parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
* @return int|false
|
||||||
|
*/
|
||||||
|
public function getOffsetFor($objectNumber)
|
||||||
|
{
|
||||||
|
if (isset($this->offsets[$objectNumber])) {
|
||||||
|
return $this->offsets[$objectNumber][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all found offsets.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getOffsets()
|
||||||
|
{
|
||||||
|
return $this->offsets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the cross reference data from the stream reader.
|
||||||
|
*
|
||||||
|
* @param StreamReader $reader
|
||||||
|
* @return string
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
*/
|
||||||
|
protected function extract(StreamReader $reader)
|
||||||
|
{
|
||||||
|
$bytesPerCycle = 100;
|
||||||
|
$reader->reset(null, $bytesPerCycle);
|
||||||
|
|
||||||
|
$cycles = 0;
|
||||||
|
do {
|
||||||
|
// 6 = length of "trailer" - 1
|
||||||
|
$pos = \max(($bytesPerCycle * $cycles) - 6, 0);
|
||||||
|
$trailerPos = \strpos($reader->getBuffer(false), 'trailer', $pos);
|
||||||
|
$cycles++;
|
||||||
|
} while ($trailerPos === false && $reader->increaseLength($bytesPerCycle) !== false);
|
||||||
|
|
||||||
|
if ($trailerPos === false) {
|
||||||
|
throw new CrossReferenceException(
|
||||||
|
'Unexpected end of cross reference. "trailer"-keyword not found.',
|
||||||
|
CrossReferenceException::NO_TRAILER_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$xrefContent = \substr($reader->getBuffer(false), 0, $trailerPos);
|
||||||
|
$reader->reset($reader->getPosition() + $trailerPos);
|
||||||
|
|
||||||
|
return $xrefContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the cross-reference entries.
|
||||||
|
*
|
||||||
|
* @param string $xrefContent
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
*/
|
||||||
|
protected function read($xrefContent)
|
||||||
|
{
|
||||||
|
// get eol markers in the first 100 bytes
|
||||||
|
\preg_match_all("/(\r\n|\n|\r)/", \substr($xrefContent, 0, 100), $m);
|
||||||
|
|
||||||
|
if (\count($m[0]) === 0) {
|
||||||
|
throw new CrossReferenceException(
|
||||||
|
'No data found in cross-reference.',
|
||||||
|
CrossReferenceException::INVALID_DATA
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// count(array_count_values()) is faster then count(array_unique())
|
||||||
|
// @see https://github.com/symfony/symfony/pull/23731
|
||||||
|
// can be reverted for php7.2
|
||||||
|
$differentLineEndings = \count(\array_count_values($m[0]));
|
||||||
|
if ($differentLineEndings > 1) {
|
||||||
|
$lines = \preg_split("/(\r\n|\n|\r)/", $xrefContent, -1, PREG_SPLIT_NO_EMPTY);
|
||||||
|
} else {
|
||||||
|
$lines = \explode($m[0][0], $xrefContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($differentLineEndings, $m);
|
||||||
|
if (!\is_array($lines)) {
|
||||||
|
$this->offsets = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$start = 0;
|
||||||
|
$offsets = [];
|
||||||
|
|
||||||
|
// trim all lines and remove empty lines
|
||||||
|
$lines = \array_filter(\array_map('\trim', $lines));
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$pieces = \explode(' ', $line);
|
||||||
|
|
||||||
|
switch (\count($pieces)) {
|
||||||
|
case 2:
|
||||||
|
$start = (int) $pieces[0];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
switch ($pieces[2]) {
|
||||||
|
case 'n':
|
||||||
|
$offsets[$start] = [(int) $pieces[0], (int) $pieces[1]];
|
||||||
|
$start++;
|
||||||
|
break 2;
|
||||||
|
case 'f':
|
||||||
|
$start++;
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
// fall through if pieces doesn't match
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new CrossReferenceException(
|
||||||
|
\sprintf('Unexpected data in xref table (%s)', \implode(' ', $pieces)),
|
||||||
|
CrossReferenceException::INVALID_DATA
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->offsets = $offsets;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
fpdi2/src/PdfParser/CrossReference/ReaderInterface.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\CrossReference;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfDictionary;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ReaderInterface for cross-reference readers.
|
||||||
|
*/
|
||||||
|
interface ReaderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get an offset by an object number.
|
||||||
|
*
|
||||||
|
* @param int $objectNumber
|
||||||
|
* @return int|bool False if the offset was not found.
|
||||||
|
*/
|
||||||
|
public function getOffsetFor($objectNumber);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the trailer related to this cross reference.
|
||||||
|
*
|
||||||
|
* @return PdfDictionary
|
||||||
|
*/
|
||||||
|
public function getTrailer();
|
||||||
|
}
|
||||||
102
fpdi2/src/PdfParser/Filter/Ascii85.php
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Filter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for handling ASCII base-85 encoded data
|
||||||
|
*/
|
||||||
|
class Ascii85 implements FilterInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Decode ASCII85 encoded string.
|
||||||
|
*
|
||||||
|
* @param string $data The input string
|
||||||
|
* @return string
|
||||||
|
* @throws Ascii85Exception
|
||||||
|
*/
|
||||||
|
public function decode($data)
|
||||||
|
{
|
||||||
|
$out = '';
|
||||||
|
$state = 0;
|
||||||
|
$chn = null;
|
||||||
|
|
||||||
|
$data = \preg_replace('/\s/', '', $data);
|
||||||
|
|
||||||
|
$l = \strlen($data);
|
||||||
|
|
||||||
|
/** @noinspection ForeachInvariantsInspection */
|
||||||
|
for ($k = 0; $k < $l; ++$k) {
|
||||||
|
$ch = \ord($data[$k]) & 0xff;
|
||||||
|
|
||||||
|
//Start <~
|
||||||
|
if ($k === 0 && $ch === 60 && isset($data[$k + 1]) && (\ord($data[$k + 1]) & 0xFF) === 126) {
|
||||||
|
$k++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
//End ~>
|
||||||
|
if ($ch === 126 && isset($data[$k + 1]) && (\ord($data[$k + 1]) & 0xFF) === 62) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ch === 122 /* z */ && $state === 0) {
|
||||||
|
$out .= "\x00\x00\x00\x00";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ch < 33 /* ! */ || $ch > 117 /* u */) {
|
||||||
|
throw new Ascii85Exception(
|
||||||
|
'Illegal character found while ASCII85 decode.',
|
||||||
|
Ascii85Exception::ILLEGAL_CHAR_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$chn[$state] = $ch - 33;/* ! */
|
||||||
|
$state++;
|
||||||
|
|
||||||
|
if ($state === 5) {
|
||||||
|
$state = 0;
|
||||||
|
$r = 0;
|
||||||
|
for ($j = 0; $j < 5; ++$j) {
|
||||||
|
/** @noinspection UnnecessaryCastingInspection */
|
||||||
|
$r = (int)($r * 85 + $chn[$j]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$out .= \chr(($r >> 24) & 255)
|
||||||
|
. \chr(($r >> 16) & 255)
|
||||||
|
. \chr(($r >> 8) & 255)
|
||||||
|
. \chr($r & 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($state === 1) {
|
||||||
|
throw new Ascii85Exception(
|
||||||
|
'Illegal length while ASCII85 decode.',
|
||||||
|
Ascii85Exception::ILLEGAL_LENGTH
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($state === 2) {
|
||||||
|
$r = $chn[0] * 85 * 85 * 85 * 85 + ($chn[1] + 1) * 85 * 85 * 85;
|
||||||
|
$out .= \chr(($r >> 24) & 255);
|
||||||
|
} elseif ($state === 3) {
|
||||||
|
$r = $chn[0] * 85 * 85 * 85 * 85 + $chn[1] * 85 * 85 * 85 + ($chn[2] + 1) * 85 * 85;
|
||||||
|
$out .= \chr(($r >> 24) & 255);
|
||||||
|
$out .= \chr(($r >> 16) & 255);
|
||||||
|
} elseif ($state === 4) {
|
||||||
|
$r = $chn[0] * 85 * 85 * 85 * 85 + $chn[1] * 85 * 85 * 85 + $chn[2] * 85 * 85 + ($chn[3] + 1) * 85;
|
||||||
|
$out .= \chr(($r >> 24) & 255);
|
||||||
|
$out .= \chr(($r >> 16) & 255);
|
||||||
|
$out .= \chr(($r >> 8) & 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
fpdi2/src/PdfParser/Filter/Ascii85Exception.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Filter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for Ascii85 filter class
|
||||||
|
*/
|
||||||
|
class Ascii85Exception extends FilterException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var integer
|
||||||
|
*/
|
||||||
|
const ILLEGAL_CHAR_FOUND = 0x0301;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var integer
|
||||||
|
*/
|
||||||
|
const ILLEGAL_LENGTH = 0x0302;
|
||||||
|
}
|
||||||
47
fpdi2/src/PdfParser/Filter/AsciiHex.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Filter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for handling ASCII hexadecimal encoded data
|
||||||
|
*/
|
||||||
|
class AsciiHex implements FilterInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Converts an ASCII hexadecimal encoded string into its binary representation.
|
||||||
|
*
|
||||||
|
* @param string $data The input string
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function decode($data)
|
||||||
|
{
|
||||||
|
$data = \preg_replace('/[^0-9A-Fa-f]/', '', \rtrim($data, '>'));
|
||||||
|
if ((\strlen($data) % 2) === 1) {
|
||||||
|
$data .= '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
return \pack('H*', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string into ASCII hexadecimal representation.
|
||||||
|
*
|
||||||
|
* @param string $data The input string
|
||||||
|
* @param boolean $leaveEOD
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function encode($data, $leaveEOD = false)
|
||||||
|
{
|
||||||
|
$t = \unpack('H*', $data);
|
||||||
|
return \current($t)
|
||||||
|
. ($leaveEOD ? '' : '>');
|
||||||
|
}
|
||||||
|
}
|
||||||
23
fpdi2/src/PdfParser/Filter/FilterException.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Filter;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParserException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for filters
|
||||||
|
*/
|
||||||
|
class FilterException extends PdfParserException
|
||||||
|
{
|
||||||
|
const UNSUPPORTED_FILTER = 0x0201;
|
||||||
|
|
||||||
|
const NOT_IMPLEMENTED = 0x0202;
|
||||||
|
}
|
||||||
25
fpdi2/src/PdfParser/Filter/FilterInterface.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Filter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for filters
|
||||||
|
*/
|
||||||
|
interface FilterInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Decode a string.
|
||||||
|
*
|
||||||
|
* @param string $data The input string
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function decode($data);
|
||||||
|
}
|
||||||
77
fpdi2/src/PdfParser/Filter/Flate.php
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Filter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for handling zlib/deflate encoded data
|
||||||
|
*/
|
||||||
|
class Flate implements FilterInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Checks whether the zlib extension is loaded.
|
||||||
|
*
|
||||||
|
* Used for testing purpose.
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
protected function extensionLoaded()
|
||||||
|
{
|
||||||
|
return \extension_loaded('zlib');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a flate compressed string.
|
||||||
|
*
|
||||||
|
* @param string|false $data The input string
|
||||||
|
* @return string
|
||||||
|
* @throws FlateException
|
||||||
|
*/
|
||||||
|
public function decode($data)
|
||||||
|
{
|
||||||
|
if ($this->extensionLoaded()) {
|
||||||
|
$oData = $data;
|
||||||
|
$data = (($data !== '') ? @\gzuncompress($data) : '');
|
||||||
|
if ($data === false) {
|
||||||
|
// let's try if the checksum is CRC32
|
||||||
|
$fh = fopen('php://temp', 'w+b');
|
||||||
|
fwrite($fh, "\x1f\x8b\x08\x00\x00\x00\x00\x00" . $oData);
|
||||||
|
// "window" == 31 -> 16 + (8 to 15): Uses the low 4 bits of the value as the window size logarithm.
|
||||||
|
// The input must include a gzip header and trailer (via 16).
|
||||||
|
stream_filter_append($fh, 'zlib.inflate', STREAM_FILTER_READ, ['window' => 31]);
|
||||||
|
fseek($fh, 0);
|
||||||
|
$data = @stream_get_contents($fh);
|
||||||
|
fclose($fh);
|
||||||
|
|
||||||
|
if ($data) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try this fallback (remove the zlib stream header)
|
||||||
|
$data = @(gzinflate(substr($oData, 2)));
|
||||||
|
|
||||||
|
if ($data === false) {
|
||||||
|
throw new FlateException(
|
||||||
|
'Error while decompressing stream.',
|
||||||
|
FlateException::DECOMPRESS_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new FlateException(
|
||||||
|
'To handle FlateDecode filter, enable zlib support in PHP.',
|
||||||
|
FlateException::NO_ZLIB
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
fpdi2/src/PdfParser/Filter/FlateException.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Filter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for flate filter class
|
||||||
|
*/
|
||||||
|
class FlateException extends FilterException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var integer
|
||||||
|
*/
|
||||||
|
const NO_ZLIB = 0x0401;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var integer
|
||||||
|
*/
|
||||||
|
const DECOMPRESS_ERROR = 0x0402;
|
||||||
|
}
|
||||||
178
fpdi2/src/PdfParser/Filter/Lzw.php
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Filter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for handling LZW encoded data
|
||||||
|
*/
|
||||||
|
class Lzw implements FilterInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var null|string
|
||||||
|
*/
|
||||||
|
protected $data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $sTable = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $dataLength = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $tIdx;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $bitsToGet = 9;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $bytePointer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $nextData = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $nextBits = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $andTable = [511, 1023, 2047, 4095];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to decode LZW compressed data.
|
||||||
|
*
|
||||||
|
* @param string $data The compressed data
|
||||||
|
* @return string The uncompressed data
|
||||||
|
* @throws LzwException
|
||||||
|
*/
|
||||||
|
public function decode($data)
|
||||||
|
{
|
||||||
|
if ($data[0] === "\x00" && $data[1] === "\x01") {
|
||||||
|
throw new LzwException(
|
||||||
|
'LZW flavour not supported.',
|
||||||
|
LzwException::LZW_FLAVOUR_NOT_SUPPORTED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->initsTable();
|
||||||
|
|
||||||
|
$this->data = $data;
|
||||||
|
$this->dataLength = \strlen($data);
|
||||||
|
|
||||||
|
// Initialize pointers
|
||||||
|
$this->bytePointer = 0;
|
||||||
|
|
||||||
|
$this->nextData = 0;
|
||||||
|
$this->nextBits = 0;
|
||||||
|
|
||||||
|
$prevCode = 0;
|
||||||
|
|
||||||
|
$uncompData = '';
|
||||||
|
|
||||||
|
while (($code = $this->getNextCode()) !== 257) {
|
||||||
|
if ($code === 256) {
|
||||||
|
$this->initsTable();
|
||||||
|
} elseif ($prevCode === 256) {
|
||||||
|
$uncompData .= $this->sTable[$code];
|
||||||
|
} elseif ($code < $this->tIdx) {
|
||||||
|
$string = $this->sTable[$code];
|
||||||
|
$uncompData .= $string;
|
||||||
|
|
||||||
|
$this->addStringToTable($this->sTable[$prevCode], $string[0]);
|
||||||
|
} else {
|
||||||
|
$string = $this->sTable[$prevCode];
|
||||||
|
$string .= $string[0];
|
||||||
|
$uncompData .= $string;
|
||||||
|
|
||||||
|
$this->addStringToTable($string);
|
||||||
|
}
|
||||||
|
$prevCode = $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $uncompData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the string table.
|
||||||
|
*/
|
||||||
|
protected function initsTable()
|
||||||
|
{
|
||||||
|
$this->sTable = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < 256; $i++) {
|
||||||
|
$this->sTable[$i] = \chr($i);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->tIdx = 258;
|
||||||
|
$this->bitsToGet = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new string to the string table.
|
||||||
|
*
|
||||||
|
* @param string $oldString
|
||||||
|
* @param string $newString
|
||||||
|
*/
|
||||||
|
protected function addStringToTable($oldString, $newString = '')
|
||||||
|
{
|
||||||
|
$string = $oldString . $newString;
|
||||||
|
|
||||||
|
// Add this new String to the table
|
||||||
|
$this->sTable[$this->tIdx++] = $string;
|
||||||
|
|
||||||
|
if ($this->tIdx === 511) {
|
||||||
|
$this->bitsToGet = 10;
|
||||||
|
} elseif ($this->tIdx === 1023) {
|
||||||
|
$this->bitsToGet = 11;
|
||||||
|
} elseif ($this->tIdx === 2047) {
|
||||||
|
$this->bitsToGet = 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next 9, 10, 11 or 12 bits.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
protected function getNextCode()
|
||||||
|
{
|
||||||
|
if ($this->bytePointer === $this->dataLength) {
|
||||||
|
return 257;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->nextData = ($this->nextData << 8) | (\ord($this->data[$this->bytePointer++]) & 0xff);
|
||||||
|
$this->nextBits += 8;
|
||||||
|
|
||||||
|
if ($this->nextBits < $this->bitsToGet) {
|
||||||
|
$this->nextData = ($this->nextData << 8) | (\ord($this->data[$this->bytePointer++]) & 0xff);
|
||||||
|
$this->nextBits += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = ($this->nextData >> ($this->nextBits - $this->bitsToGet)) & $this->andTable[$this->bitsToGet - 9];
|
||||||
|
$this->nextBits -= $this->bitsToGet;
|
||||||
|
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
fpdi2/src/PdfParser/Filter/LzwException.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Filter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for LZW filter class
|
||||||
|
*/
|
||||||
|
class LzwException extends FilterException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var integer
|
||||||
|
*/
|
||||||
|
const LZW_FLAVOUR_NOT_SUPPORTED = 0x0501;
|
||||||
|
}
|
||||||
436
fpdi2/src/PdfParser/PdfParser.php
Normal file
@ -0,0 +1,436 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\CrossReference\CrossReference;
|
||||||
|
use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfArray;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfBoolean;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfDictionary;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfHexString;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfIndirectObjectReference;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfName;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfNull;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfNumeric;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfStream;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfString;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfToken;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfType;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfTypeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A PDF parser class
|
||||||
|
*/
|
||||||
|
class PdfParser
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var StreamReader
|
||||||
|
*/
|
||||||
|
protected $streamReader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Tokenizer
|
||||||
|
*/
|
||||||
|
protected $tokenizer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The file header.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $fileHeader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The offset to the file header.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $fileHeaderOffset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var CrossReference|null
|
||||||
|
*/
|
||||||
|
protected $xref;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All read objects.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $objects = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PdfParser constructor.
|
||||||
|
*
|
||||||
|
* @param StreamReader $streamReader
|
||||||
|
*/
|
||||||
|
public function __construct(StreamReader $streamReader)
|
||||||
|
{
|
||||||
|
$this->streamReader = $streamReader;
|
||||||
|
$this->tokenizer = new Tokenizer($streamReader);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes cycled references.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function cleanUp()
|
||||||
|
{
|
||||||
|
$this->xref = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the stream reader instance.
|
||||||
|
*
|
||||||
|
* @return StreamReader
|
||||||
|
*/
|
||||||
|
public function getStreamReader()
|
||||||
|
{
|
||||||
|
return $this->streamReader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the tokenizer instance.
|
||||||
|
*
|
||||||
|
* @return Tokenizer
|
||||||
|
*/
|
||||||
|
public function getTokenizer()
|
||||||
|
{
|
||||||
|
return $this->tokenizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the file header.
|
||||||
|
*
|
||||||
|
* @throws PdfParserException
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
protected function resolveFileHeader()
|
||||||
|
{
|
||||||
|
if ($this->fileHeader) {
|
||||||
|
return $this->fileHeaderOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->streamReader->reset(0);
|
||||||
|
$maxIterations = 1000;
|
||||||
|
while (true) {
|
||||||
|
$buffer = $this->streamReader->getBuffer(false);
|
||||||
|
$offset = \strpos($buffer, '%PDF-');
|
||||||
|
if ($offset === false) {
|
||||||
|
if (!$this->streamReader->increaseLength(100) || (--$maxIterations === 0)) {
|
||||||
|
throw new PdfParserException(
|
||||||
|
'Unable to find PDF file header.',
|
||||||
|
PdfParserException::FILE_HEADER_NOT_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->fileHeaderOffset = $offset;
|
||||||
|
$this->streamReader->setOffset($offset);
|
||||||
|
|
||||||
|
$this->fileHeader = \trim($this->streamReader->readLine());
|
||||||
|
return $this->fileHeaderOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cross-reference instance.
|
||||||
|
*
|
||||||
|
* @return CrossReference
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
* @throws PdfParserException
|
||||||
|
*/
|
||||||
|
public function getCrossReference()
|
||||||
|
{
|
||||||
|
if ($this->xref === null) {
|
||||||
|
$this->xref = new CrossReference($this, $this->resolveFileHeader());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->xref;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the PDF version.
|
||||||
|
*
|
||||||
|
* @return int[] An array of major and minor version.
|
||||||
|
* @throws PdfParserException
|
||||||
|
*/
|
||||||
|
public function getPdfVersion()
|
||||||
|
{
|
||||||
|
$this->resolveFileHeader();
|
||||||
|
|
||||||
|
if (\preg_match('/%PDF-(\d)\.(\d)/', $this->fileHeader, $result) === 0) {
|
||||||
|
throw new PdfParserException(
|
||||||
|
'Unable to extract PDF version from file header.',
|
||||||
|
PdfParserException::PDF_VERSION_NOT_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
[, $major, $minor] = $result;
|
||||||
|
|
||||||
|
$catalog = $this->getCatalog();
|
||||||
|
if (isset($catalog->value['Version'])) {
|
||||||
|
$versionParts = \explode(
|
||||||
|
'.',
|
||||||
|
PdfName::unescape(PdfType::resolve($catalog->value['Version'], $this)->value)
|
||||||
|
);
|
||||||
|
if (count($versionParts) === 2) {
|
||||||
|
[$major, $minor] = $versionParts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [(int) $major, (int) $minor];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the catalog dictionary.
|
||||||
|
*
|
||||||
|
* @return PdfDictionary
|
||||||
|
* @throws Type\PdfTypeException
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
* @throws PdfParserException
|
||||||
|
*/
|
||||||
|
public function getCatalog()
|
||||||
|
{
|
||||||
|
$trailer = $this->getCrossReference()->getTrailer();
|
||||||
|
|
||||||
|
$catalog = PdfType::resolve(PdfDictionary::get($trailer, 'Root'), $this);
|
||||||
|
|
||||||
|
return PdfDictionary::ensure($catalog);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an indirect object by its object number.
|
||||||
|
*
|
||||||
|
* @param int $objectNumber
|
||||||
|
* @param bool $cache
|
||||||
|
* @return PdfIndirectObject
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
* @throws PdfParserException
|
||||||
|
*/
|
||||||
|
public function getIndirectObject($objectNumber, $cache = false)
|
||||||
|
{
|
||||||
|
$objectNumber = (int) $objectNumber;
|
||||||
|
if (isset($this->objects[$objectNumber])) {
|
||||||
|
return $this->objects[$objectNumber];
|
||||||
|
}
|
||||||
|
|
||||||
|
$object = $this->getCrossReference()->getIndirectObject($objectNumber);
|
||||||
|
|
||||||
|
if ($cache) {
|
||||||
|
$this->objects[$objectNumber] = $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a PDF value.
|
||||||
|
*
|
||||||
|
* @param null|bool|string $token
|
||||||
|
* @param null|string $expectedType
|
||||||
|
* @return false|PdfArray|PdfBoolean|PdfDictionary|PdfHexString|PdfIndirectObject|PdfIndirectObjectReference|PdfName|PdfNull|PdfNumeric|PdfStream|PdfString|PdfToken
|
||||||
|
* @throws Type\PdfTypeException
|
||||||
|
*/
|
||||||
|
public function readValue($token = null, $expectedType = null)
|
||||||
|
{
|
||||||
|
if ($token === null) {
|
||||||
|
$token = $this->tokenizer->getNextToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($token === false) {
|
||||||
|
if ($expectedType !== null) {
|
||||||
|
throw new Type\PdfTypeException('Got unexpected token type.', Type\PdfTypeException::INVALID_DATA_TYPE);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($token) {
|
||||||
|
case '(':
|
||||||
|
$this->ensureExpectedType($token, $expectedType);
|
||||||
|
return $this->parsePdfString();
|
||||||
|
|
||||||
|
case '<':
|
||||||
|
if ($this->streamReader->getByte() === '<') {
|
||||||
|
$this->ensureExpectedType('<<', $expectedType);
|
||||||
|
$this->streamReader->addOffset(1);
|
||||||
|
return $this->parsePdfDictionary();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->ensureExpectedType($token, $expectedType);
|
||||||
|
return $this->parsePdfHexString();
|
||||||
|
|
||||||
|
case '/':
|
||||||
|
$this->ensureExpectedType($token, $expectedType);
|
||||||
|
return $this->parsePdfName();
|
||||||
|
|
||||||
|
case '[':
|
||||||
|
$this->ensureExpectedType($token, $expectedType);
|
||||||
|
return $this->parsePdfArray();
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (\is_numeric($token)) {
|
||||||
|
$token2 = $this->tokenizer->getNextToken();
|
||||||
|
if ($token2 !== false) {
|
||||||
|
if (\is_numeric($token2)) {
|
||||||
|
$token3 = $this->tokenizer->getNextToken();
|
||||||
|
if ($token3 === 'obj') {
|
||||||
|
if ($expectedType !== null && $expectedType !== PdfIndirectObject::class) {
|
||||||
|
throw new Type\PdfTypeException(
|
||||||
|
'Got unexpected token type.',
|
||||||
|
Type\PdfTypeException::INVALID_DATA_TYPE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->parsePdfIndirectObject((int) $token, (int) $token2);
|
||||||
|
} elseif ($token3 === 'R') {
|
||||||
|
if (
|
||||||
|
$expectedType !== null &&
|
||||||
|
$expectedType !== PdfIndirectObjectReference::class
|
||||||
|
) {
|
||||||
|
throw new Type\PdfTypeException(
|
||||||
|
'Got unexpected token type.',
|
||||||
|
Type\PdfTypeException::INVALID_DATA_TYPE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PdfIndirectObjectReference::create((int) $token, (int) $token2);
|
||||||
|
} elseif ($token3 !== false) {
|
||||||
|
$this->tokenizer->pushStack($token3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->tokenizer->pushStack($token2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($expectedType !== null && $expectedType !== PdfNumeric::class) {
|
||||||
|
throw new Type\PdfTypeException(
|
||||||
|
'Got unexpected token type.',
|
||||||
|
Type\PdfTypeException::INVALID_DATA_TYPE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return PdfNumeric::create($token + 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($token === 'true' || $token === 'false') {
|
||||||
|
$this->ensureExpectedType($token, $expectedType);
|
||||||
|
return PdfBoolean::create($token === 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($token === 'null') {
|
||||||
|
$this->ensureExpectedType($token, $expectedType);
|
||||||
|
return new PdfNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($expectedType !== null && $expectedType !== PdfToken::class) {
|
||||||
|
throw new Type\PdfTypeException(
|
||||||
|
'Got unexpected token type.',
|
||||||
|
Type\PdfTypeException::INVALID_DATA_TYPE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = new PdfToken();
|
||||||
|
$v->value = $token;
|
||||||
|
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return PdfString
|
||||||
|
*/
|
||||||
|
protected function parsePdfString()
|
||||||
|
{
|
||||||
|
return PdfString::parse($this->streamReader);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return false|PdfHexString
|
||||||
|
*/
|
||||||
|
protected function parsePdfHexString()
|
||||||
|
{
|
||||||
|
return PdfHexString::parse($this->streamReader);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool|PdfDictionary
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
protected function parsePdfDictionary()
|
||||||
|
{
|
||||||
|
return PdfDictionary::parse($this->tokenizer, $this->streamReader, $this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return PdfName
|
||||||
|
*/
|
||||||
|
protected function parsePdfName()
|
||||||
|
{
|
||||||
|
return PdfName::parse($this->tokenizer, $this->streamReader);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return false|PdfArray
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
protected function parsePdfArray()
|
||||||
|
{
|
||||||
|
return PdfArray::parse($this->tokenizer, $this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $objectNumber
|
||||||
|
* @param int $generationNumber
|
||||||
|
* @return false|PdfIndirectObject
|
||||||
|
* @throws Type\PdfTypeException
|
||||||
|
*/
|
||||||
|
protected function parsePdfIndirectObject($objectNumber, $generationNumber)
|
||||||
|
{
|
||||||
|
return PdfIndirectObject::parse(
|
||||||
|
$objectNumber,
|
||||||
|
$generationNumber,
|
||||||
|
$this,
|
||||||
|
$this->tokenizer,
|
||||||
|
$this->streamReader
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the token will evaluate to an expected object type (or not).
|
||||||
|
*
|
||||||
|
* @param string $token
|
||||||
|
* @param string|null $expectedType
|
||||||
|
* @return bool
|
||||||
|
* @throws Type\PdfTypeException
|
||||||
|
*/
|
||||||
|
protected function ensureExpectedType($token, $expectedType)
|
||||||
|
{
|
||||||
|
static $mapping = [
|
||||||
|
'(' => PdfString::class,
|
||||||
|
'<' => PdfHexString::class,
|
||||||
|
'<<' => PdfDictionary::class,
|
||||||
|
'/' => PdfName::class,
|
||||||
|
'[' => PdfArray::class,
|
||||||
|
'true' => PdfBoolean::class,
|
||||||
|
'false' => PdfBoolean::class,
|
||||||
|
'null' => PdfNull::class
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($expectedType === null || $mapping[$token] === $expectedType) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Type\PdfTypeException('Got unexpected token type.', Type\PdfTypeException::INVALID_DATA_TYPE);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
fpdi2/src/PdfParser/PdfParserException.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser;
|
||||||
|
|
||||||
|
use setasign\Fpdi\FpdiException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for the pdf parser class
|
||||||
|
*/
|
||||||
|
class PdfParserException extends FpdiException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const NOT_IMPLEMENTED = 0x0001;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const IMPLEMENTED_IN_FPDI_PDF_PARSER = 0x0002;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const INVALID_DATA_TYPE = 0x0003;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const FILE_HEADER_NOT_FOUND = 0x0004;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const PDF_VERSION_NOT_FOUND = 0x0005;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const INVALID_DATA_SIZE = 0x0006;
|
||||||
|
}
|
||||||
481
fpdi2/src/PdfParser/StreamReader.php
Normal file
@ -0,0 +1,481 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A stream reader class
|
||||||
|
*/
|
||||||
|
class StreamReader
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a stream reader instance by a string value.
|
||||||
|
*
|
||||||
|
* @param string $content
|
||||||
|
* @param int $maxMemory
|
||||||
|
* @return StreamReader
|
||||||
|
*/
|
||||||
|
public static function createByString($content, $maxMemory = 2097152)
|
||||||
|
{
|
||||||
|
$h = \fopen('php://temp/maxmemory:' . ((int) $maxMemory), 'r+b');
|
||||||
|
\fwrite($h, $content);
|
||||||
|
\rewind($h);
|
||||||
|
|
||||||
|
return new self($h, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a stream reader instance by a filename.
|
||||||
|
*
|
||||||
|
* @param string $filename
|
||||||
|
* @return StreamReader
|
||||||
|
*/
|
||||||
|
public static function createByFile($filename)
|
||||||
|
{
|
||||||
|
$h = \fopen($filename, 'rb');
|
||||||
|
return new self($h, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines whether the stream should be closed when the stream reader instance is deconstructed or not.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
protected $closeStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The stream resource.
|
||||||
|
*
|
||||||
|
* @var resource
|
||||||
|
*/
|
||||||
|
protected $stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The byte-offset position in the stream.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $position;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The byte-offset position in the buffer.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $offset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The buffer length.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $bufferLength;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total length of the stream.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $totalLength;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The buffer.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $buffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StreamReader constructor.
|
||||||
|
*
|
||||||
|
* @param resource $stream
|
||||||
|
* @param bool $closeStream Defines whether to close the stream resource if the instance is destructed or not.
|
||||||
|
*/
|
||||||
|
public function __construct($stream, $closeStream = false)
|
||||||
|
{
|
||||||
|
if (!\is_resource($stream)) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
'No stream given.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$metaData = \stream_get_meta_data($stream);
|
||||||
|
if (!$metaData['seekable']) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
'Given stream is not seekable!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fseek($stream, 0) === -1) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
'Given stream is not seekable!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stream = $stream;
|
||||||
|
$this->closeStream = $closeStream;
|
||||||
|
$this->reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The destructor.
|
||||||
|
*/
|
||||||
|
public function __destruct()
|
||||||
|
{
|
||||||
|
$this->cleanUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the file handle.
|
||||||
|
*/
|
||||||
|
public function cleanUp()
|
||||||
|
{
|
||||||
|
if ($this->closeStream && is_resource($this->stream)) {
|
||||||
|
\fclose($this->stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the byte length of the buffer.
|
||||||
|
*
|
||||||
|
* @param bool $atOffset
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getBufferLength($atOffset = false)
|
||||||
|
{
|
||||||
|
if ($atOffset === false) {
|
||||||
|
return $this->bufferLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->bufferLength - $this->offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current position in the stream.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getPosition()
|
||||||
|
{
|
||||||
|
return $this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current buffer.
|
||||||
|
*
|
||||||
|
* @param bool $atOffset
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getBuffer($atOffset = true)
|
||||||
|
{
|
||||||
|
if ($atOffset === false) {
|
||||||
|
return $this->buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
$string = \substr($this->buffer, $this->offset);
|
||||||
|
|
||||||
|
return (string) $string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a byte at a specific position in the buffer.
|
||||||
|
*
|
||||||
|
* If the position is invalid the method will return false.
|
||||||
|
*
|
||||||
|
* If the $position parameter is set to null the value of $this->offset will be used.
|
||||||
|
*
|
||||||
|
* @param int|null $position
|
||||||
|
* @return string|bool
|
||||||
|
*/
|
||||||
|
public function getByte($position = null)
|
||||||
|
{
|
||||||
|
$position = (int) ($position !== null ? $position : $this->offset);
|
||||||
|
if (
|
||||||
|
$position >= $this->bufferLength
|
||||||
|
&& (!$this->increaseLength() || $position >= $this->bufferLength)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->buffer[$position];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a byte at a specific position, and set the offset to the next byte position.
|
||||||
|
*
|
||||||
|
* If the position is invalid the method will return false.
|
||||||
|
*
|
||||||
|
* If the $position parameter is set to null the value of $this->offset will be used.
|
||||||
|
*
|
||||||
|
* @param int|null $position
|
||||||
|
* @return string|bool
|
||||||
|
*/
|
||||||
|
public function readByte($position = null)
|
||||||
|
{
|
||||||
|
if ($position !== null) {
|
||||||
|
$position = (int) $position;
|
||||||
|
// check if needed bytes are available in the current buffer
|
||||||
|
if (!($position >= $this->position && $position < $this->position + $this->bufferLength)) {
|
||||||
|
$this->reset($position);
|
||||||
|
$offset = $this->offset;
|
||||||
|
} else {
|
||||||
|
$offset = $position - $this->position;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$offset = $this->offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
$offset >= $this->bufferLength
|
||||||
|
&& ((!$this->increaseLength()) || $offset >= $this->bufferLength)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->offset = $offset + 1;
|
||||||
|
return $this->buffer[$offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read bytes from the current or a specific offset position and set the internal pointer to the next byte.
|
||||||
|
*
|
||||||
|
* If the position is invalid the method will return false.
|
||||||
|
*
|
||||||
|
* If the $position parameter is set to null the value of $this->offset will be used.
|
||||||
|
*
|
||||||
|
* @param int $length
|
||||||
|
* @param int|null $position
|
||||||
|
* @return string|false
|
||||||
|
*/
|
||||||
|
public function readBytes($length, $position = null)
|
||||||
|
{
|
||||||
|
$length = (int) $length;
|
||||||
|
if ($position !== null) {
|
||||||
|
// check if needed bytes are available in the current buffer
|
||||||
|
if (!($position >= $this->position && $position < $this->position + $this->bufferLength)) {
|
||||||
|
$this->reset($position, $length);
|
||||||
|
$offset = $this->offset;
|
||||||
|
} else {
|
||||||
|
$offset = $position - $this->position;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$offset = $this->offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
($offset + $length) > $this->bufferLength
|
||||||
|
&& ((!$this->increaseLength($length)) || ($offset + $length) > $this->bufferLength)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bytes = \substr($this->buffer, $offset, $length);
|
||||||
|
$this->offset = $offset + $length;
|
||||||
|
|
||||||
|
return $bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a line from the current position.
|
||||||
|
*
|
||||||
|
* @param int $length
|
||||||
|
* @return string|bool
|
||||||
|
*/
|
||||||
|
public function readLine($length = 1024)
|
||||||
|
{
|
||||||
|
if ($this->ensureContent() === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$line = '';
|
||||||
|
while ($this->ensureContent()) {
|
||||||
|
$char = $this->readByte();
|
||||||
|
|
||||||
|
if ($char === "\n") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($char === "\r") {
|
||||||
|
if ($this->getByte() === "\n") {
|
||||||
|
$this->addOffset(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$line .= $char;
|
||||||
|
|
||||||
|
if (\strlen($line) >= $length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $line;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the offset position in the current buffer.
|
||||||
|
*
|
||||||
|
* @param int $offset
|
||||||
|
*/
|
||||||
|
public function setOffset($offset)
|
||||||
|
{
|
||||||
|
if ($offset > $this->bufferLength || $offset < 0) {
|
||||||
|
throw new \OutOfRangeException(
|
||||||
|
\sprintf('Offset (%s) out of range (length: %s)', $offset, $this->bufferLength)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->offset = (int) $offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current offset in the current buffer.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getOffset()
|
||||||
|
{
|
||||||
|
return $this->offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an offset to the current offset.
|
||||||
|
*
|
||||||
|
* @param int $offset
|
||||||
|
*/
|
||||||
|
public function addOffset($offset)
|
||||||
|
{
|
||||||
|
$this->setOffset($this->offset + $offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sure that there is at least one character beyond the current offset in the buffer.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function ensureContent()
|
||||||
|
{
|
||||||
|
while ($this->offset >= $this->bufferLength) {
|
||||||
|
if (!$this->increaseLength()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the stream.
|
||||||
|
*
|
||||||
|
* @return resource
|
||||||
|
*/
|
||||||
|
public function getStream()
|
||||||
|
{
|
||||||
|
return $this->stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the total available length.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getTotalLength()
|
||||||
|
{
|
||||||
|
if ($this->totalLength === null) {
|
||||||
|
$stat = \fstat($this->stream);
|
||||||
|
$this->totalLength = $stat['size'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->totalLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the buffer to a position and re-read the buffer with the given length.
|
||||||
|
*
|
||||||
|
* If the $pos parameter is negative the start buffer position will be the $pos'th position from
|
||||||
|
* the end of the file.
|
||||||
|
*
|
||||||
|
* If the $pos parameter is negative and the absolute value is bigger then the totalLength of
|
||||||
|
* the file $pos will set to zero.
|
||||||
|
*
|
||||||
|
* @param int|null $pos Start position of the new buffer
|
||||||
|
* @param int $length Length of the new buffer. Mustn't be negative
|
||||||
|
*/
|
||||||
|
public function reset($pos = 0, $length = 200)
|
||||||
|
{
|
||||||
|
if ($pos === null) {
|
||||||
|
$pos = $this->position + $this->offset;
|
||||||
|
} elseif ($pos < 0) {
|
||||||
|
$pos = \max(0, $this->getTotalLength() + $pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
\fseek($this->stream, $pos);
|
||||||
|
|
||||||
|
$this->position = $pos;
|
||||||
|
$this->offset = 0;
|
||||||
|
if ($length > 0) {
|
||||||
|
$this->buffer = (string) \fread($this->stream, $length);
|
||||||
|
} else {
|
||||||
|
$this->buffer = '';
|
||||||
|
}
|
||||||
|
$this->bufferLength = \strlen($this->buffer);
|
||||||
|
|
||||||
|
// If a stream wrapper is in use it is possible that
|
||||||
|
// length values > 8096 will be ignored, so use the
|
||||||
|
// increaseLength()-method to correct that behavior
|
||||||
|
if ($this->bufferLength < $length && $this->increaseLength($length - $this->bufferLength)) {
|
||||||
|
// increaseLength parameter is $minLength, so cut to have only the required bytes in the buffer
|
||||||
|
$this->buffer = (string) \substr($this->buffer, 0, $length);
|
||||||
|
$this->bufferLength = \strlen($this->buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures bytes in the buffer with a specific length and location in the file.
|
||||||
|
*
|
||||||
|
* @param int $pos
|
||||||
|
* @param int $length
|
||||||
|
* @see reset()
|
||||||
|
*/
|
||||||
|
public function ensure($pos, $length)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
$pos >= $this->position
|
||||||
|
&& $pos < ($this->position + $this->bufferLength)
|
||||||
|
&& ($this->position + $this->bufferLength) >= ($pos + $length)
|
||||||
|
) {
|
||||||
|
$this->offset = $pos - $this->position;
|
||||||
|
} else {
|
||||||
|
$this->reset($pos, $length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forcefully read more data into the buffer.
|
||||||
|
*
|
||||||
|
* @param int $minLength
|
||||||
|
* @return bool Returns false if the stream reaches the end
|
||||||
|
*/
|
||||||
|
public function increaseLength($minLength = 100)
|
||||||
|
{
|
||||||
|
$length = \max($minLength, 100);
|
||||||
|
|
||||||
|
if (\feof($this->stream) || $this->getTotalLength() === $this->position + $this->bufferLength) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$newLength = $this->bufferLength + $length;
|
||||||
|
do {
|
||||||
|
$this->buffer .= \fread($this->stream, $newLength - $this->bufferLength);
|
||||||
|
$this->bufferLength = \strlen($this->buffer);
|
||||||
|
} while (($this->bufferLength !== $newLength) && !\feof($this->stream));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
151
fpdi2/src/PdfParser/Tokenizer.php
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A tokenizer class.
|
||||||
|
*/
|
||||||
|
class Tokenizer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var StreamReader
|
||||||
|
*/
|
||||||
|
protected $streamReader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A token stack.
|
||||||
|
*
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
protected $stack = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tokenizer constructor.
|
||||||
|
*
|
||||||
|
* @param StreamReader $streamReader
|
||||||
|
*/
|
||||||
|
public function __construct(StreamReader $streamReader)
|
||||||
|
{
|
||||||
|
$this->streamReader = $streamReader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the stream reader instance.
|
||||||
|
*
|
||||||
|
* @return StreamReader
|
||||||
|
*/
|
||||||
|
public function getStreamReader()
|
||||||
|
{
|
||||||
|
return $this->streamReader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the token stack.
|
||||||
|
*/
|
||||||
|
public function clearStack()
|
||||||
|
{
|
||||||
|
$this->stack = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push a token onto the stack.
|
||||||
|
*
|
||||||
|
* @param string $token
|
||||||
|
*/
|
||||||
|
public function pushStack($token)
|
||||||
|
{
|
||||||
|
$this->stack[] = $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get next token.
|
||||||
|
*
|
||||||
|
* @return false|string
|
||||||
|
*/
|
||||||
|
public function getNextToken()
|
||||||
|
{
|
||||||
|
$token = \array_pop($this->stack);
|
||||||
|
if ($token !== null) {
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($byte = $this->streamReader->readByte()) === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\in_array($byte, ["\x20", "\x0A", "\x0D", "\x0C", "\x09", "\x00"], true)) {
|
||||||
|
if ($this->leapWhiteSpaces() === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$byte = $this->streamReader->readByte();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($byte) {
|
||||||
|
case '/':
|
||||||
|
case '[':
|
||||||
|
case ']':
|
||||||
|
case '(':
|
||||||
|
case ')':
|
||||||
|
case '{':
|
||||||
|
case '}':
|
||||||
|
case '<':
|
||||||
|
case '>':
|
||||||
|
return $byte;
|
||||||
|
case '%':
|
||||||
|
$this->streamReader->readLine();
|
||||||
|
return $this->getNextToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This way is faster than checking single bytes.
|
||||||
|
*/
|
||||||
|
$bufferOffset = $this->streamReader->getOffset();
|
||||||
|
do {
|
||||||
|
$lastBuffer = $this->streamReader->getBuffer(false);
|
||||||
|
$pos = \strcspn(
|
||||||
|
$lastBuffer,
|
||||||
|
"\x00\x09\x0A\x0C\x0D\x20()<>[]{}/%",
|
||||||
|
$bufferOffset
|
||||||
|
);
|
||||||
|
} while (
|
||||||
|
// Break the loop if a delimiter or white space char is matched
|
||||||
|
// in the current buffer or increase the buffers length
|
||||||
|
$bufferOffset + $pos === \strlen($lastBuffer)
|
||||||
|
&& $this->streamReader->increaseLength()
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = \substr($lastBuffer, $bufferOffset - 1, $pos + 1);
|
||||||
|
$this->streamReader->setOffset($bufferOffset + $pos);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leap white spaces.
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function leapWhiteSpaces()
|
||||||
|
{
|
||||||
|
do {
|
||||||
|
if (!$this->streamReader->ensureContent()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$buffer = $this->streamReader->getBuffer(false);
|
||||||
|
$matches = \strspn($buffer, "\x20\x0A\x0C\x0D\x09\x00", $this->streamReader->getOffset());
|
||||||
|
if ($matches > 0) {
|
||||||
|
$this->streamReader->addOffset($matches);
|
||||||
|
}
|
||||||
|
} while ($this->streamReader->getOffset() >= $this->streamReader->getBufferLength());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
85
fpdi2/src/PdfParser/Type/PdfArray.php
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Type;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParser;
|
||||||
|
use setasign\Fpdi\PdfParser\Tokenizer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a PDF array object
|
||||||
|
*
|
||||||
|
* @property array $value The value of the PDF type.
|
||||||
|
*/
|
||||||
|
class PdfArray extends PdfType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Parses an array of the passed tokenizer and parser.
|
||||||
|
*
|
||||||
|
* @param Tokenizer $tokenizer
|
||||||
|
* @param PdfParser $parser
|
||||||
|
* @return false|self
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
public static function parse(Tokenizer $tokenizer, PdfParser $parser)
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
|
||||||
|
// Recurse into this function until we reach the end of the array.
|
||||||
|
while (($token = $tokenizer->getNextToken()) !== ']') {
|
||||||
|
if ($token === false || ($value = $parser->readValue($token)) === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = new self();
|
||||||
|
$v->value = $result;
|
||||||
|
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to create an instance.
|
||||||
|
*
|
||||||
|
* @param PdfType[] $values
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function create(array $values = [])
|
||||||
|
{
|
||||||
|
$v = new self();
|
||||||
|
$v->value = $values;
|
||||||
|
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the passed array is a PdfArray instance with a (optional) specific size.
|
||||||
|
*
|
||||||
|
* @param mixed $array
|
||||||
|
* @param null|int $size
|
||||||
|
* @return self
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
public static function ensure($array, $size = null)
|
||||||
|
{
|
||||||
|
$result = PdfType::ensureType(self::class, $array, 'Array value expected.');
|
||||||
|
|
||||||
|
if ($size !== null && \count($array->value) !== $size) {
|
||||||
|
throw new PdfTypeException(
|
||||||
|
\sprintf('Array with %s entries expected.', $size),
|
||||||
|
PdfTypeException::INVALID_DATA_SIZE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
42
fpdi2/src/PdfParser/Type/PdfBoolean.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a boolean PDF object
|
||||||
|
*/
|
||||||
|
class PdfBoolean extends PdfType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Helper method to create an instance.
|
||||||
|
*
|
||||||
|
* @param bool $value
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function create($value)
|
||||||
|
{
|
||||||
|
$v = new self();
|
||||||
|
$v->value = (bool) $value;
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the passed value is a PdfBoolean instance.
|
||||||
|
*
|
||||||
|
* @param mixed $value
|
||||||
|
* @return self
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
public static function ensure($value)
|
||||||
|
{
|
||||||
|
return PdfType::ensureType(self::class, $value, 'Boolean value expected.');
|
||||||
|
}
|
||||||
|
}
|
||||||
131
fpdi2/src/PdfParser/Type/PdfDictionary.php
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Type;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParser;
|
||||||
|
use setasign\Fpdi\PdfParser\StreamReader;
|
||||||
|
use setasign\Fpdi\PdfParser\Tokenizer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a PDF dictionary object
|
||||||
|
*/
|
||||||
|
class PdfDictionary extends PdfType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Parses a dictionary of the passed tokenizer, stream-reader and parser.
|
||||||
|
*
|
||||||
|
* @param Tokenizer $tokenizer
|
||||||
|
* @param StreamReader $streamReader
|
||||||
|
* @param PdfParser $parser
|
||||||
|
* @return bool|self
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
public static function parse(Tokenizer $tokenizer, StreamReader $streamReader, PdfParser $parser)
|
||||||
|
{
|
||||||
|
$entries = [];
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
$token = $tokenizer->getNextToken();
|
||||||
|
if ($token === '>' && $streamReader->getByte() === '>') {
|
||||||
|
$streamReader->addOffset(1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = $parser->readValue($token);
|
||||||
|
if ($key === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure the first value to be a Name object
|
||||||
|
if (!($key instanceof PdfName)) {
|
||||||
|
$lastToken = null;
|
||||||
|
// ignore all other entries and search for the closing brackets
|
||||||
|
while (($token = $tokenizer->getNextToken()) !== '>' || $lastToken !== '>') {
|
||||||
|
if ($token === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$lastToken = $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$value = $parser->readValue();
|
||||||
|
if ($value === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof PdfNull) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// catch missing value
|
||||||
|
if ($value instanceof PdfToken && $value->value === '>' && $streamReader->getByte() === '>') {
|
||||||
|
$streamReader->addOffset(1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$entries[$key->value] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = new self();
|
||||||
|
$v->value = $entries;
|
||||||
|
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to create an instance.
|
||||||
|
*
|
||||||
|
* @param PdfType[] $entries The keys are the name entries of the dictionary.
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function create(array $entries = [])
|
||||||
|
{
|
||||||
|
$v = new self();
|
||||||
|
$v->value = $entries;
|
||||||
|
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a value by its key from a dictionary or a default value.
|
||||||
|
*
|
||||||
|
* @param mixed $dictionary
|
||||||
|
* @param string $key
|
||||||
|
* @param PdfType|null $default
|
||||||
|
* @return PdfNull|PdfType
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
public static function get($dictionary, $key, ?PdfType $default = null)
|
||||||
|
{
|
||||||
|
$dictionary = self::ensure($dictionary);
|
||||||
|
|
||||||
|
if (isset($dictionary->value[$key])) {
|
||||||
|
return $dictionary->value[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $default ?? new PdfNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the passed value is a PdfDictionary instance.
|
||||||
|
*
|
||||||
|
* @param mixed $dictionary
|
||||||
|
* @return self
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
public static function ensure($dictionary)
|
||||||
|
{
|
||||||
|
return PdfType::ensureType(self::class, $dictionary, 'Dictionary value expected.');
|
||||||
|
}
|
||||||
|
}
|
||||||
77
fpdi2/src/PdfParser/Type/PdfHexString.php
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Type;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\StreamReader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a hexadecimal encoded PDF string object
|
||||||
|
*/
|
||||||
|
class PdfHexString extends PdfType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Parses a hexadecimal string object from the stream reader.
|
||||||
|
*
|
||||||
|
* @param StreamReader $streamReader
|
||||||
|
* @return false|self
|
||||||
|
*/
|
||||||
|
public static function parse(StreamReader $streamReader)
|
||||||
|
{
|
||||||
|
$bufferOffset = $streamReader->getOffset();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
$buffer = $streamReader->getBuffer(false);
|
||||||
|
$pos = \strpos($buffer, '>', $bufferOffset);
|
||||||
|
if ($pos === false) {
|
||||||
|
if (!$streamReader->increaseLength()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = \substr($buffer, $bufferOffset, $pos - $bufferOffset);
|
||||||
|
$streamReader->setOffset($pos + 1);
|
||||||
|
|
||||||
|
$v = new self();
|
||||||
|
$v->value = $result;
|
||||||
|
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to create an instance.
|
||||||
|
*
|
||||||
|
* @param string $string The hex encoded string.
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function create($string)
|
||||||
|
{
|
||||||
|
$v = new self();
|
||||||
|
$v->value = $string;
|
||||||
|
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the passed value is a PdfHexString instance.
|
||||||
|
*
|
||||||
|
* @param mixed $hexString
|
||||||
|
* @return self
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
public static function ensure($hexString)
|
||||||
|
{
|
||||||
|
return PdfType::ensureType(self::class, $hexString, 'Hex string value expected.');
|
||||||
|
}
|
||||||
|
}
|
||||||
103
fpdi2/src/PdfParser/Type/PdfIndirectObject.php
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Type;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParser;
|
||||||
|
use setasign\Fpdi\PdfParser\StreamReader;
|
||||||
|
use setasign\Fpdi\PdfParser\Tokenizer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing an indirect object
|
||||||
|
*/
|
||||||
|
class PdfIndirectObject extends PdfType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Parses an indirect object from a tokenizer, parser and stream-reader.
|
||||||
|
*
|
||||||
|
* @param int $objectNumber
|
||||||
|
* @param int $objectGenerationNumber
|
||||||
|
* @param PdfParser $parser
|
||||||
|
* @param Tokenizer $tokenizer
|
||||||
|
* @param StreamReader $reader
|
||||||
|
* @return self|false
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
public static function parse(
|
||||||
|
$objectNumber,
|
||||||
|
$objectGenerationNumber,
|
||||||
|
PdfParser $parser,
|
||||||
|
Tokenizer $tokenizer,
|
||||||
|
StreamReader $reader
|
||||||
|
) {
|
||||||
|
$value = $parser->readValue();
|
||||||
|
if ($value === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nextToken = $tokenizer->getNextToken();
|
||||||
|
if ($nextToken === 'stream') {
|
||||||
|
$value = PdfStream::parse($value, $reader, $parser);
|
||||||
|
} elseif ($nextToken !== false) {
|
||||||
|
$tokenizer->pushStack($nextToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = new self();
|
||||||
|
$v->objectNumber = (int) $objectNumber;
|
||||||
|
$v->generationNumber = (int) $objectGenerationNumber;
|
||||||
|
$v->value = $value;
|
||||||
|
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to create an instance.
|
||||||
|
*
|
||||||
|
* @param int $objectNumber
|
||||||
|
* @param int $generationNumber
|
||||||
|
* @param PdfType $value
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function create($objectNumber, $generationNumber, PdfType $value)
|
||||||
|
{
|
||||||
|
$v = new self();
|
||||||
|
$v->objectNumber = (int) $objectNumber;
|
||||||
|
$v->generationNumber = (int) $generationNumber;
|
||||||
|
$v->value = $value;
|
||||||
|
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the passed value is a PdfIndirectObject instance.
|
||||||
|
*
|
||||||
|
* @param mixed $indirectObject
|
||||||
|
* @return self
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
public static function ensure($indirectObject)
|
||||||
|
{
|
||||||
|
return PdfType::ensureType(self::class, $indirectObject, 'Indirect object expected.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object number.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $objectNumber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The generation number.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $generationNumber;
|
||||||
|
}
|
||||||
52
fpdi2/src/PdfParser/Type/PdfIndirectObjectReference.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing an indirect object reference
|
||||||
|
*/
|
||||||
|
class PdfIndirectObjectReference extends PdfType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Helper method to create an instance.
|
||||||
|
*
|
||||||
|
* @param int $objectNumber
|
||||||
|
* @param int $generationNumber
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function create($objectNumber, $generationNumber)
|
||||||
|
{
|
||||||
|
$v = new self();
|
||||||
|
$v->value = (int) $objectNumber;
|
||||||
|
$v->generationNumber = (int) $generationNumber;
|
||||||
|
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the passed value is a PdfIndirectObject instance.
|
||||||
|
*
|
||||||
|
* @param mixed $value
|
||||||
|
* @return self
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
public static function ensure($value)
|
||||||
|
{
|
||||||
|
return PdfType::ensureType(self::class, $value, 'Indirect reference value expected.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The generation number.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $generationNumber;
|
||||||
|
}
|
||||||
82
fpdi2/src/PdfParser/Type/PdfName.php
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Type;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\StreamReader;
|
||||||
|
use setasign\Fpdi\PdfParser\Tokenizer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a PDF name object
|
||||||
|
*/
|
||||||
|
class PdfName extends PdfType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Parses a name object from the passed tokenizer and stream-reader.
|
||||||
|
*
|
||||||
|
* @param Tokenizer $tokenizer
|
||||||
|
* @param StreamReader $streamReader
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function parse(Tokenizer $tokenizer, StreamReader $streamReader)
|
||||||
|
{
|
||||||
|
$v = new self();
|
||||||
|
if (\strspn($streamReader->getByte(), "\x00\x09\x0A\x0C\x0D\x20()<>[]{}/%") === 0) {
|
||||||
|
$v->value = (string) $tokenizer->getNextToken();
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
$v->value = '';
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unescapes a name string.
|
||||||
|
*
|
||||||
|
* @param string $value
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function unescape($value)
|
||||||
|
{
|
||||||
|
if (\strpos($value, '#') === false) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return \preg_replace_callback('/#([a-fA-F\d]{2})/', function ($matches) {
|
||||||
|
return \chr(\hexdec($matches[1]));
|
||||||
|
}, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to create an instance.
|
||||||
|
*
|
||||||
|
* @param string $string
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function create($string)
|
||||||
|
{
|
||||||
|
$v = new self();
|
||||||
|
$v->value = $string;
|
||||||
|
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the passed value is a PdfName instance.
|
||||||
|
*
|
||||||
|
* @param mixed $name
|
||||||
|
* @return self
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
public static function ensure($name)
|
||||||
|
{
|
||||||
|
return PdfType::ensureType(self::class, $name, 'Name value expected.');
|
||||||
|
}
|
||||||
|
}
|
||||||
19
fpdi2/src/PdfParser/Type/PdfNull.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a PDF null object
|
||||||
|
*/
|
||||||
|
class PdfNull extends PdfType
|
||||||
|
{
|
||||||
|
// empty body
|
||||||
|
}
|
||||||
43
fpdi2/src/PdfParser/Type/PdfNumeric.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a numeric PDF object
|
||||||
|
*/
|
||||||
|
class PdfNumeric extends PdfType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Helper method to create an instance.
|
||||||
|
*
|
||||||
|
* @param int|float $value
|
||||||
|
* @return PdfNumeric
|
||||||
|
*/
|
||||||
|
public static function create($value)
|
||||||
|
{
|
||||||
|
$v = new self();
|
||||||
|
$v->value = $value + 0;
|
||||||
|
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the passed value is a PdfNumeric instance.
|
||||||
|
*
|
||||||
|
* @param mixed $value
|
||||||
|
* @return self
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
public static function ensure($value)
|
||||||
|
{
|
||||||
|
return PdfType::ensureType(self::class, $value, 'Numeric value expected.');
|
||||||
|
}
|
||||||
|
}
|
||||||
352
fpdi2/src/PdfParser/Type/PdfStream.php
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Type;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
|
||||||
|
use setasign\Fpdi\PdfParser\Filter\Ascii85;
|
||||||
|
use setasign\Fpdi\PdfParser\Filter\AsciiHex;
|
||||||
|
use setasign\Fpdi\PdfParser\Filter\FilterException;
|
||||||
|
use setasign\Fpdi\PdfParser\Filter\Flate;
|
||||||
|
use setasign\Fpdi\PdfParser\Filter\Lzw;
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParser;
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParserException;
|
||||||
|
use setasign\Fpdi\PdfParser\StreamReader;
|
||||||
|
use setasign\FpdiPdfParser\PdfParser\Filter\Predictor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a PDF stream object
|
||||||
|
*/
|
||||||
|
class PdfStream extends PdfType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Parses a stream from a stream reader.
|
||||||
|
*
|
||||||
|
* @param PdfDictionary $dictionary
|
||||||
|
* @param StreamReader $reader
|
||||||
|
* @param PdfParser|null $parser Optional to keep backwards compatibility
|
||||||
|
* @return self
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
public static function parse(PdfDictionary $dictionary, StreamReader $reader, ?PdfParser $parser = null)
|
||||||
|
{
|
||||||
|
$v = new self();
|
||||||
|
$v->value = $dictionary;
|
||||||
|
$v->reader = $reader;
|
||||||
|
$v->parser = $parser;
|
||||||
|
|
||||||
|
$offset = $reader->getOffset();
|
||||||
|
|
||||||
|
// Find the first "newline"
|
||||||
|
while (($firstByte = $reader->getByte($offset)) !== false) {
|
||||||
|
$offset++;
|
||||||
|
if ($firstByte === "\n" || $firstByte === "\r") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($firstByte === false) {
|
||||||
|
throw new PdfTypeException(
|
||||||
|
'Unable to parse stream data. No newline after the stream keyword found.',
|
||||||
|
PdfTypeException::NO_NEWLINE_AFTER_STREAM_KEYWORD
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sndByte = $reader->getByte($offset);
|
||||||
|
if ($sndByte === "\n" && $firstByte !== "\n") {
|
||||||
|
$offset++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reader->setOffset($offset);
|
||||||
|
// let's only save the byte-offset and read the stream only when needed
|
||||||
|
$v->stream = $reader->getPosition() + $reader->getOffset();
|
||||||
|
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to create an instance.
|
||||||
|
*
|
||||||
|
* @param PdfDictionary $dictionary
|
||||||
|
* @param string $stream
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function create(PdfDictionary $dictionary, $stream)
|
||||||
|
{
|
||||||
|
$v = new self();
|
||||||
|
$v->value = $dictionary;
|
||||||
|
$v->stream = (string) $stream;
|
||||||
|
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the passed value is a PdfStream instance.
|
||||||
|
*
|
||||||
|
* @param mixed $stream
|
||||||
|
* @return self
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
public static function ensure($stream)
|
||||||
|
{
|
||||||
|
return PdfType::ensureType(self::class, $stream, 'Stream value expected.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The stream or its byte-offset position.
|
||||||
|
*
|
||||||
|
* @var int|string
|
||||||
|
*/
|
||||||
|
protected $stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The stream reader instance.
|
||||||
|
*
|
||||||
|
* @var StreamReader|null
|
||||||
|
*/
|
||||||
|
protected $reader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The PDF parser instance.
|
||||||
|
*
|
||||||
|
* @var PdfParser
|
||||||
|
*/
|
||||||
|
protected $parser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the stream data.
|
||||||
|
*
|
||||||
|
* @param bool $cache Whether cache the stream data or not.
|
||||||
|
* @return bool|string
|
||||||
|
* @throws PdfTypeException
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
* @throws PdfParserException
|
||||||
|
*/
|
||||||
|
public function getStream($cache = false)
|
||||||
|
{
|
||||||
|
if (\is_int($this->stream)) {
|
||||||
|
$length = PdfDictionary::get($this->value, 'Length');
|
||||||
|
if ($this->parser !== null) {
|
||||||
|
$length = PdfType::resolve($length, $this->parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!($length instanceof PdfNumeric) || $length->value === 0) {
|
||||||
|
$this->reader->reset($this->stream, 100000);
|
||||||
|
$buffer = $this->extractStream();
|
||||||
|
} else {
|
||||||
|
$this->reader->reset($this->stream, $length->value);
|
||||||
|
$buffer = $this->reader->getBuffer(false);
|
||||||
|
if ($this->parser !== null) {
|
||||||
|
$this->reader->reset($this->stream + strlen($buffer));
|
||||||
|
$this->parser->getTokenizer()->clearStack();
|
||||||
|
$token = $this->parser->readValue();
|
||||||
|
if (!($token instanceof PdfToken) || $token->value !== 'endstream') {
|
||||||
|
$this->reader->reset($this->stream, 100000);
|
||||||
|
$buffer = $this->extractStream();
|
||||||
|
$this->reader->reset($this->stream + strlen($buffer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($cache === false) {
|
||||||
|
return $buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stream = $buffer;
|
||||||
|
$this->reader = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the stream "manually".
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
protected function extractStream()
|
||||||
|
{
|
||||||
|
while (true) {
|
||||||
|
$buffer = $this->reader->getBuffer(false);
|
||||||
|
$length = \strpos($buffer, 'endstream');
|
||||||
|
if ($length === false) {
|
||||||
|
if (!$this->reader->increaseLength(100000)) {
|
||||||
|
throw new PdfTypeException('Cannot extract stream.');
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$buffer = \substr($buffer, 0, $length);
|
||||||
|
$lastByte = \substr($buffer, -1);
|
||||||
|
|
||||||
|
/* Check for EOL marker =
|
||||||
|
* CARRIAGE RETURN (\r) and a LINE FEED (\n) or just a LINE FEED (\n},
|
||||||
|
* and not by a CARRIAGE RETURN (\r) alone
|
||||||
|
*/
|
||||||
|
if ($lastByte === "\n") {
|
||||||
|
$buffer = \substr($buffer, 0, -1);
|
||||||
|
|
||||||
|
$lastByte = \substr($buffer, -1);
|
||||||
|
if ($lastByte === "\r") {
|
||||||
|
$buffer = \substr($buffer, 0, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// There are streams in the wild, which have only white signs in them but need to be parsed manually due
|
||||||
|
// to a problem encountered before (e.g. Length === 0). We should set them to empty streams to avoid problems
|
||||||
|
// in further processing (e.g. applying of filters).
|
||||||
|
if (\trim($buffer) === '') {
|
||||||
|
$buffer = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all filters defined for this stream.
|
||||||
|
*
|
||||||
|
* @return PdfType[]
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
public function getFilters()
|
||||||
|
{
|
||||||
|
$filters = PdfDictionary::get($this->value, 'Filter');
|
||||||
|
if ($filters instanceof PdfNull) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($filters instanceof PdfArray) {
|
||||||
|
$filters = $filters->value;
|
||||||
|
} else {
|
||||||
|
$filters = [$filters];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the unfiltered stream data.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* @throws FilterException
|
||||||
|
* @throws PdfParserException
|
||||||
|
*/
|
||||||
|
public function getUnfilteredStream()
|
||||||
|
{
|
||||||
|
$stream = $this->getStream();
|
||||||
|
$filters = $this->getFilters();
|
||||||
|
if ($filters === []) {
|
||||||
|
return $stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decodeParams = PdfDictionary::get($this->value, 'DecodeParms');
|
||||||
|
if ($decodeParams instanceof PdfArray) {
|
||||||
|
$decodeParams = $decodeParams->value;
|
||||||
|
} else {
|
||||||
|
$decodeParams = [$decodeParams];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($filters as $key => $filter) {
|
||||||
|
if (!($filter instanceof PdfName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decodeParam = null;
|
||||||
|
if (isset($decodeParams[$key])) {
|
||||||
|
$decodeParam = ($decodeParams[$key] instanceof PdfDictionary ? $decodeParams[$key] : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($filter->value) {
|
||||||
|
case 'FlateDecode':
|
||||||
|
case 'Fl':
|
||||||
|
case 'LZWDecode':
|
||||||
|
case 'LZW':
|
||||||
|
if (\strpos($filter->value, 'LZW') === 0) {
|
||||||
|
$filterObject = new Lzw();
|
||||||
|
} else {
|
||||||
|
$filterObject = new Flate();
|
||||||
|
}
|
||||||
|
|
||||||
|
$stream = $filterObject->decode($stream);
|
||||||
|
|
||||||
|
if ($decodeParam instanceof PdfDictionary) {
|
||||||
|
$predictor = PdfDictionary::get($decodeParam, 'Predictor', PdfNumeric::create(1));
|
||||||
|
if ($predictor->value !== 1) {
|
||||||
|
if (!\class_exists(Predictor::class)) {
|
||||||
|
throw new PdfParserException(
|
||||||
|
'This PDF document makes use of features which are only implemented in the ' .
|
||||||
|
'commercial "FPDI PDF-Parser" add-on (see https://www.setasign.com/fpdi-pdf-' .
|
||||||
|
'parser).',
|
||||||
|
PdfParserException::IMPLEMENTED_IN_FPDI_PDF_PARSER
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$colors = PdfDictionary::get($decodeParam, 'Colors', PdfNumeric::create(1));
|
||||||
|
$bitsPerComponent = PdfDictionary::get(
|
||||||
|
$decodeParam,
|
||||||
|
'BitsPerComponent',
|
||||||
|
PdfNumeric::create(8)
|
||||||
|
);
|
||||||
|
|
||||||
|
$columns = PdfDictionary::get($decodeParam, 'Columns', PdfNumeric::create(1));
|
||||||
|
|
||||||
|
$filterObject = new Predictor(
|
||||||
|
$predictor->value,
|
||||||
|
$colors->value,
|
||||||
|
$bitsPerComponent->value,
|
||||||
|
$columns->value
|
||||||
|
);
|
||||||
|
|
||||||
|
$stream = $filterObject->decode($stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'ASCII85Decode':
|
||||||
|
case 'A85':
|
||||||
|
$filterObject = new Ascii85();
|
||||||
|
$stream = $filterObject->decode($stream);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ASCIIHexDecode':
|
||||||
|
case 'AHx':
|
||||||
|
$filterObject = new AsciiHex();
|
||||||
|
$stream = $filterObject->decode($stream);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Crypt':
|
||||||
|
if (!$decodeParam instanceof PdfDictionary) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Filter is "Identity"
|
||||||
|
$name = PdfDictionary::get($decodeParam, 'Name');
|
||||||
|
if (!$name instanceof PdfName || $name->value !== 'Identity') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new FilterException(
|
||||||
|
'Support for Crypt filters other than "Identity" is not implemented.',
|
||||||
|
FilterException::UNSUPPORTED_FILTER
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new FilterException(
|
||||||
|
\sprintf('Unsupported filter "%s".', $filter->value),
|
||||||
|
FilterException::UNSUPPORTED_FILTER
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $stream;
|
||||||
|
}
|
||||||
|
}
|
||||||
202
fpdi2/src/PdfParser/Type/PdfString.php
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Type;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\StreamReader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a PDF string object
|
||||||
|
*/
|
||||||
|
class PdfString extends PdfType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Parses a string object from the stream reader.
|
||||||
|
*
|
||||||
|
* @param StreamReader $streamReader
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function parse(StreamReader $streamReader)
|
||||||
|
{
|
||||||
|
$pos = $startPos = $streamReader->getOffset();
|
||||||
|
$openBrackets = 1;
|
||||||
|
do {
|
||||||
|
$buffer = $streamReader->getBuffer(false);
|
||||||
|
for ($length = \strlen($buffer); $openBrackets !== 0 && $pos < $length; $pos++) {
|
||||||
|
switch ($buffer[$pos]) {
|
||||||
|
case '(':
|
||||||
|
$openBrackets++;
|
||||||
|
break;
|
||||||
|
case ')':
|
||||||
|
$openBrackets--;
|
||||||
|
break;
|
||||||
|
case '\\':
|
||||||
|
$pos++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while ($openBrackets !== 0 && $streamReader->increaseLength());
|
||||||
|
|
||||||
|
$result = \substr($buffer, $startPos, $openBrackets + $pos - $startPos - 1);
|
||||||
|
$streamReader->setOffset($pos);
|
||||||
|
|
||||||
|
$v = new self();
|
||||||
|
$v->value = $result;
|
||||||
|
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to create an instance.
|
||||||
|
*
|
||||||
|
* @param string $value The string needs to be escaped accordingly.
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function create($value)
|
||||||
|
{
|
||||||
|
$v = new self();
|
||||||
|
$v->value = $value;
|
||||||
|
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the passed value is a PdfString instance.
|
||||||
|
*
|
||||||
|
* @param mixed $string
|
||||||
|
* @return self
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
public static function ensure($string)
|
||||||
|
{
|
||||||
|
return PdfType::ensureType(self::class, $string, 'String value expected.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes sequences in a string according to the PDF specification.
|
||||||
|
*
|
||||||
|
* @param string $s
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function escape($s)
|
||||||
|
{
|
||||||
|
// Still a bit faster, than direct replacing
|
||||||
|
if (
|
||||||
|
\strpos($s, '\\') !== false ||
|
||||||
|
\strpos($s, ')') !== false ||
|
||||||
|
\strpos($s, '(') !== false ||
|
||||||
|
\strpos($s, "\x0D") !== false ||
|
||||||
|
\strpos($s, "\x0A") !== false ||
|
||||||
|
\strpos($s, "\x09") !== false ||
|
||||||
|
\strpos($s, "\x08") !== false ||
|
||||||
|
\strpos($s, "\x0C") !== false
|
||||||
|
) {
|
||||||
|
// is faster than strtr(...)
|
||||||
|
return \str_replace(
|
||||||
|
['\\', ')', '(', "\x0D", "\x0A", "\x09", "\x08", "\x0C"],
|
||||||
|
['\\\\', '\\)', '\\(', '\r', '\n', '\t', '\b', '\f'],
|
||||||
|
$s
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unescapes escaped sequences in a PDF string according to the PDF specification.
|
||||||
|
*
|
||||||
|
* @param string $s
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function unescape($s)
|
||||||
|
{
|
||||||
|
$out = '';
|
||||||
|
/** @noinspection ForeachInvariantsInspection */
|
||||||
|
for ($count = 0, $n = \strlen($s); $count < $n; $count++) {
|
||||||
|
if ($s[$count] !== '\\') {
|
||||||
|
$out .= $s[$count];
|
||||||
|
} else {
|
||||||
|
// A backslash at the end of the string - ignore it
|
||||||
|
if ($count === ($n - 1)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($s[++$count]) {
|
||||||
|
case ')':
|
||||||
|
case '(':
|
||||||
|
case '\\':
|
||||||
|
$out .= $s[$count];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'f':
|
||||||
|
$out .= "\x0C";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'b':
|
||||||
|
$out .= "\x08";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 't':
|
||||||
|
$out .= "\x09";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'r':
|
||||||
|
$out .= "\x0D";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'n':
|
||||||
|
$out .= "\x0A";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "\r":
|
||||||
|
if ($count !== $n - 1 && $s[$count + 1] === "\n") {
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "\n":
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$actualChar = \ord($s[$count]);
|
||||||
|
// ascii 48 = number 0
|
||||||
|
// ascii 57 = number 9
|
||||||
|
if ($actualChar >= 48 && $actualChar <= 57) {
|
||||||
|
$oct = '' . $s[$count];
|
||||||
|
|
||||||
|
/** @noinspection NotOptimalIfConditionsInspection */
|
||||||
|
if (
|
||||||
|
$count + 1 < $n
|
||||||
|
&& \ord($s[$count + 1]) >= 48
|
||||||
|
&& \ord($s[$count + 1]) <= 57
|
||||||
|
) {
|
||||||
|
$count++;
|
||||||
|
$oct .= $s[$count];
|
||||||
|
|
||||||
|
/** @noinspection NotOptimalIfConditionsInspection */
|
||||||
|
if (
|
||||||
|
$count + 1 < $n
|
||||||
|
&& \ord($s[$count + 1]) >= 48
|
||||||
|
&& \ord($s[$count + 1]) <= 57
|
||||||
|
) {
|
||||||
|
$oct .= $s[++$count];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$out .= \chr(\octdec($oct));
|
||||||
|
} else {
|
||||||
|
// If the character is not one of those defined, the backslash is ignored
|
||||||
|
$out .= $s[$count];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
fpdi2/src/PdfParser/Type/PdfToken.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing PDF token object
|
||||||
|
*/
|
||||||
|
class PdfToken extends PdfType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Helper method to create an instance.
|
||||||
|
*
|
||||||
|
* @param string $token
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function create($token)
|
||||||
|
{
|
||||||
|
$v = new self();
|
||||||
|
$v->value = $token;
|
||||||
|
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the passed value is a PdfToken instance.
|
||||||
|
*
|
||||||
|
* @param mixed $token
|
||||||
|
* @return self
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
public static function ensure($token)
|
||||||
|
{
|
||||||
|
return PdfType::ensureType(self::class, $token, 'Token value expected.');
|
||||||
|
}
|
||||||
|
}
|
||||||
117
fpdi2/src/PdfParser/Type/PdfType.php
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Type;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParser;
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParserException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class defining a PDF data type
|
||||||
|
*/
|
||||||
|
class PdfType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Resolves a PdfType value to its value.
|
||||||
|
*
|
||||||
|
* This method is used to evaluate indirect and direct object references until a final value is reached.
|
||||||
|
*
|
||||||
|
* @param PdfType $value
|
||||||
|
* @param PdfParser $parser
|
||||||
|
* @param bool $stopAtIndirectObject
|
||||||
|
* @param array $ensuredObjectsList A list of all ensured indirect objects to prevent recursion
|
||||||
|
* @return PdfType
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
* @throws PdfParserException
|
||||||
|
*/
|
||||||
|
public static function resolve(
|
||||||
|
PdfType $value,
|
||||||
|
PdfParser $parser,
|
||||||
|
$stopAtIndirectObject = false,
|
||||||
|
array &$ensuredObjectsList = []
|
||||||
|
) {
|
||||||
|
if ($value instanceof PdfIndirectObjectReference) {
|
||||||
|
$value = $parser->getIndirectObject($value->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof PdfIndirectObject) {
|
||||||
|
if ($stopAtIndirectObject === true) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\in_array($value->objectNumber, $ensuredObjectsList, true)) {
|
||||||
|
throw new PdfParserException(
|
||||||
|
\sprintf('Indirect reference recursion detected (%s).', $value->objectNumber)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$ensuredObjectsList[] = $value->objectNumber;
|
||||||
|
return self::resolve($value->value, $parser, $stopAtIndirectObject, $ensuredObjectsList);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that a value is an instance of a specific PDF type.
|
||||||
|
*
|
||||||
|
* @param string $type
|
||||||
|
* @param PdfType $value
|
||||||
|
* @param string $errorMessage
|
||||||
|
* @return mixed
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
protected static function ensureType($type, $value, $errorMessage)
|
||||||
|
{
|
||||||
|
if (!($value instanceof $type)) {
|
||||||
|
throw new PdfTypeException(
|
||||||
|
$errorMessage,
|
||||||
|
PdfTypeException::INVALID_DATA_TYPE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flatten indirect object references to direct objects.
|
||||||
|
*
|
||||||
|
* @param PdfType $value
|
||||||
|
* @param PdfParser $parser
|
||||||
|
* @return PdfType
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
* @throws PdfParserException
|
||||||
|
*/
|
||||||
|
public static function flatten(PdfType $value, PdfParser $parser)
|
||||||
|
{
|
||||||
|
if ($value instanceof PdfIndirectObjectReference) {
|
||||||
|
return self::flatten(self::resolve($value, $parser), $parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof PdfDictionary || $value instanceof PdfArray) {
|
||||||
|
foreach ($value->value as $key => $_value) {
|
||||||
|
$value->value[$key] = self::flatten($_value, $parser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof PdfStream) {
|
||||||
|
throw new PdfTypeException('There is a stream object found which cannot be flattened to a direct object.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value of the PDF type.
|
||||||
|
*
|
||||||
|
* @var mixed
|
||||||
|
*/
|
||||||
|
public $value;
|
||||||
|
}
|
||||||
24
fpdi2/src/PdfParser/Type/PdfTypeException.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfParser\Type;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParserException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception class for pdf type classes
|
||||||
|
*/
|
||||||
|
class PdfTypeException extends PdfParserException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const NO_NEWLINE_AFTER_STREAM_KEYWORD = 0x0601;
|
||||||
|
}
|
||||||
179
fpdi2/src/PdfReader/DataStructure/Rectangle.php
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfReader\DataStructure;
|
||||||
|
|
||||||
|
use setasign\Fpdi\Math\Vector;
|
||||||
|
use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParser;
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParserException;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfArray;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfNumeric;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfType;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfTypeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a rectangle
|
||||||
|
*/
|
||||||
|
class Rectangle
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var int|float
|
||||||
|
*/
|
||||||
|
protected $llx;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int|float
|
||||||
|
*/
|
||||||
|
protected $lly;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int|float
|
||||||
|
*/
|
||||||
|
protected $urx;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int|float
|
||||||
|
*/
|
||||||
|
protected $ury;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a rectangle instance by a PdfArray.
|
||||||
|
*
|
||||||
|
* @param PdfArray|mixed $array
|
||||||
|
* @param PdfParser $parser
|
||||||
|
* @return Rectangle
|
||||||
|
* @throws PdfTypeException
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
* @throws PdfParserException
|
||||||
|
*/
|
||||||
|
public static function byPdfArray($array, PdfParser $parser)
|
||||||
|
{
|
||||||
|
$array = PdfArray::ensure(PdfType::resolve($array, $parser), 4)->value;
|
||||||
|
$ax = PdfNumeric::ensure(PdfType::resolve($array[0], $parser))->value;
|
||||||
|
$ay = PdfNumeric::ensure(PdfType::resolve($array[1], $parser))->value;
|
||||||
|
$bx = PdfNumeric::ensure(PdfType::resolve($array[2], $parser))->value;
|
||||||
|
$by = PdfNumeric::ensure(PdfType::resolve($array[3], $parser))->value;
|
||||||
|
|
||||||
|
return new self($ax, $ay, $bx, $by);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function byVectors(Vector $ll, Vector $ur)
|
||||||
|
{
|
||||||
|
return new self($ll->getX(), $ll->getY(), $ur->getX(), $ur->getY());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rectangle constructor.
|
||||||
|
*
|
||||||
|
* @param float|int $ax
|
||||||
|
* @param float|int $ay
|
||||||
|
* @param float|int $bx
|
||||||
|
* @param float|int $by
|
||||||
|
*/
|
||||||
|
public function __construct($ax, $ay, $bx, $by)
|
||||||
|
{
|
||||||
|
$this->llx = \min($ax, $bx);
|
||||||
|
$this->lly = \min($ay, $by);
|
||||||
|
$this->urx = \max($ax, $bx);
|
||||||
|
$this->ury = \max($ay, $by);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the width of the rectangle.
|
||||||
|
*
|
||||||
|
* @return float|int
|
||||||
|
*/
|
||||||
|
public function getWidth()
|
||||||
|
{
|
||||||
|
return $this->urx - $this->llx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the height of the rectangle.
|
||||||
|
*
|
||||||
|
* @return float|int
|
||||||
|
*/
|
||||||
|
public function getHeight()
|
||||||
|
{
|
||||||
|
return $this->ury - $this->lly;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the lower left abscissa.
|
||||||
|
*
|
||||||
|
* @return float|int
|
||||||
|
*/
|
||||||
|
public function getLlx()
|
||||||
|
{
|
||||||
|
return $this->llx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the lower left ordinate.
|
||||||
|
*
|
||||||
|
* @return float|int
|
||||||
|
*/
|
||||||
|
public function getLly()
|
||||||
|
{
|
||||||
|
return $this->lly;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the upper right abscissa.
|
||||||
|
*
|
||||||
|
* @return float|int
|
||||||
|
*/
|
||||||
|
public function getUrx()
|
||||||
|
{
|
||||||
|
return $this->urx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the upper right ordinate.
|
||||||
|
*
|
||||||
|
* @return float|int
|
||||||
|
*/
|
||||||
|
public function getUry()
|
||||||
|
{
|
||||||
|
return $this->ury;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the rectangle as an array.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function toArray()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
$this->llx,
|
||||||
|
$this->lly,
|
||||||
|
$this->urx,
|
||||||
|
$this->ury
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the rectangle as a PdfArray.
|
||||||
|
*
|
||||||
|
* @return PdfArray
|
||||||
|
*/
|
||||||
|
public function toPdfArray()
|
||||||
|
{
|
||||||
|
$array = new PdfArray();
|
||||||
|
$array->value[] = PdfNumeric::create($this->llx);
|
||||||
|
$array->value[] = PdfNumeric::create($this->lly);
|
||||||
|
$array->value[] = PdfNumeric::create($this->urx);
|
||||||
|
$array->value[] = PdfNumeric::create($this->ury);
|
||||||
|
|
||||||
|
return $array;
|
||||||
|
}
|
||||||
|
}
|
||||||
443
fpdi2/src/PdfReader/Page.php
Normal file
@ -0,0 +1,443 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfReader;
|
||||||
|
|
||||||
|
use setasign\Fpdi\FpdiException;
|
||||||
|
use setasign\Fpdi\GraphicsState;
|
||||||
|
use setasign\Fpdi\Math\Vector;
|
||||||
|
use setasign\Fpdi\PdfParser\Filter\FilterException;
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParser;
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParserException;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfArray;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfDictionary;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfHexString;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfName;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfNull;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfNumeric;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfStream;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfString;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfType;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfTypeException;
|
||||||
|
use setasign\Fpdi\PdfReader\DataStructure\Rectangle;
|
||||||
|
use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a page of a PDF document
|
||||||
|
*/
|
||||||
|
class Page
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var PdfIndirectObject
|
||||||
|
*/
|
||||||
|
protected $pageObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var PdfDictionary
|
||||||
|
*/
|
||||||
|
protected $pageDictionary;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var PdfParser
|
||||||
|
*/
|
||||||
|
protected $parser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inherited attributes
|
||||||
|
*
|
||||||
|
* @var null|array
|
||||||
|
*/
|
||||||
|
protected $inheritedAttributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page constructor.
|
||||||
|
*
|
||||||
|
* @param PdfIndirectObject $page
|
||||||
|
* @param PdfParser $parser
|
||||||
|
*/
|
||||||
|
public function __construct(PdfIndirectObject $page, PdfParser $parser)
|
||||||
|
{
|
||||||
|
$this->pageObject = $page;
|
||||||
|
$this->parser = $parser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the indirect object of this page.
|
||||||
|
*
|
||||||
|
* @return PdfIndirectObject
|
||||||
|
*/
|
||||||
|
public function getPageObject()
|
||||||
|
{
|
||||||
|
return $this->pageObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the dictionary of this page.
|
||||||
|
*
|
||||||
|
* @return PdfDictionary
|
||||||
|
* @throws PdfParserException
|
||||||
|
* @throws PdfTypeException
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
*/
|
||||||
|
public function getPageDictionary()
|
||||||
|
{
|
||||||
|
if ($this->pageDictionary === null) {
|
||||||
|
$this->pageDictionary = PdfDictionary::ensure(PdfType::resolve($this->getPageObject(), $this->parser));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->pageDictionary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a page attribute.
|
||||||
|
*
|
||||||
|
* @param string $name
|
||||||
|
* @param bool $inherited
|
||||||
|
* @return PdfType|null
|
||||||
|
* @throws PdfParserException
|
||||||
|
* @throws PdfTypeException
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
*/
|
||||||
|
public function getAttribute($name, $inherited = true)
|
||||||
|
{
|
||||||
|
$dict = $this->getPageDictionary();
|
||||||
|
|
||||||
|
if (isset($dict->value[$name])) {
|
||||||
|
return $dict->value[$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
$inheritedKeys = ['Resources', 'MediaBox', 'CropBox', 'Rotate'];
|
||||||
|
if ($inherited && \in_array($name, $inheritedKeys, true)) {
|
||||||
|
if ($this->inheritedAttributes === null) {
|
||||||
|
$this->inheritedAttributes = [];
|
||||||
|
$inheritedKeys = \array_filter($inheritedKeys, static function ($key) use ($dict) {
|
||||||
|
return !isset($dict->value[$key]);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (\count($inheritedKeys) > 0) {
|
||||||
|
$ensuredObjectList = [];
|
||||||
|
$parentDict = PdfType::resolve(
|
||||||
|
PdfDictionary::get($dict, 'Parent'),
|
||||||
|
$this->parser,
|
||||||
|
false,
|
||||||
|
$ensuredObjectList
|
||||||
|
);
|
||||||
|
while ($parentDict instanceof PdfDictionary) {
|
||||||
|
foreach ($inheritedKeys as $index => $key) {
|
||||||
|
if (isset($parentDict->value[$key])) {
|
||||||
|
$this->inheritedAttributes[$key] = $parentDict->value[$key];
|
||||||
|
unset($inheritedKeys[$index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @noinspection NotOptimalIfConditionsInspection */
|
||||||
|
if (isset($parentDict->value['Parent']) && \count($inheritedKeys) > 0) {
|
||||||
|
$parentDict = PdfType::resolve(
|
||||||
|
PdfDictionary::get($parentDict, 'Parent'),
|
||||||
|
$this->parser,
|
||||||
|
false,
|
||||||
|
$ensuredObjectList
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($this->inheritedAttributes[$name])) {
|
||||||
|
return $this->inheritedAttributes[$name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the rotation value.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
* @throws PdfParserException
|
||||||
|
* @throws PdfTypeException
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
*/
|
||||||
|
public function getRotation()
|
||||||
|
{
|
||||||
|
$rotation = $this->getAttribute('Rotate');
|
||||||
|
if ($rotation === null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rotation = PdfNumeric::ensure(PdfType::resolve($rotation, $this->parser))->value % 360;
|
||||||
|
|
||||||
|
if ($rotation < 0) {
|
||||||
|
$rotation += 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a boundary of this page.
|
||||||
|
*
|
||||||
|
* @param string $box
|
||||||
|
* @param bool $fallback
|
||||||
|
* @return bool|Rectangle
|
||||||
|
* @throws PdfParserException
|
||||||
|
* @throws PdfTypeException
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
* @see PageBoundaries
|
||||||
|
*/
|
||||||
|
public function getBoundary($box = PageBoundaries::CROP_BOX, $fallback = true)
|
||||||
|
{
|
||||||
|
$value = $this->getAttribute($box);
|
||||||
|
|
||||||
|
if ($value !== null) {
|
||||||
|
return Rectangle::byPdfArray($value, $this->parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fallback === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($box) {
|
||||||
|
case PageBoundaries::BLEED_BOX:
|
||||||
|
case PageBoundaries::TRIM_BOX:
|
||||||
|
case PageBoundaries::ART_BOX:
|
||||||
|
return $this->getBoundary(PageBoundaries::CROP_BOX, true);
|
||||||
|
case PageBoundaries::CROP_BOX:
|
||||||
|
return $this->getBoundary(PageBoundaries::MEDIA_BOX, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the width and height of this page.
|
||||||
|
*
|
||||||
|
* @param string $box
|
||||||
|
* @param bool $fallback
|
||||||
|
* @return array|bool
|
||||||
|
* @throws PdfParserException
|
||||||
|
* @throws PdfTypeException
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
*/
|
||||||
|
public function getWidthAndHeight($box = PageBoundaries::CROP_BOX, $fallback = true)
|
||||||
|
{
|
||||||
|
$boundary = $this->getBoundary($box, $fallback);
|
||||||
|
if ($boundary === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rotation = $this->getRotation();
|
||||||
|
$interchange = ($rotation / 90) % 2;
|
||||||
|
|
||||||
|
return [
|
||||||
|
$interchange ? $boundary->getHeight() : $boundary->getWidth(),
|
||||||
|
$interchange ? $boundary->getWidth() : $boundary->getHeight()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the raw content stream.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* @throws PdfReaderException
|
||||||
|
* @throws PdfTypeException
|
||||||
|
* @throws FilterException
|
||||||
|
* @throws PdfParserException
|
||||||
|
*/
|
||||||
|
public function getContentStream()
|
||||||
|
{
|
||||||
|
$dict = $this->getPageDictionary();
|
||||||
|
$contents = PdfType::resolve(PdfDictionary::get($dict, 'Contents'), $this->parser);
|
||||||
|
if ($contents instanceof PdfNull) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($contents instanceof PdfArray) {
|
||||||
|
$result = [];
|
||||||
|
foreach ($contents->value as $content) {
|
||||||
|
$content = PdfType::resolve($content, $this->parser);
|
||||||
|
if (!($content instanceof PdfStream)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result[] = $content->getUnfilteredStream();
|
||||||
|
} catch (FilterException $e) {
|
||||||
|
// ignore streams that cannot be unfiltered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return \implode("\n", $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($contents instanceof PdfStream) {
|
||||||
|
try {
|
||||||
|
return $contents->getUnfilteredStream();
|
||||||
|
} catch (FilterException $e) {
|
||||||
|
// ignore streams that cannot be unfiltered
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new PdfReaderException(
|
||||||
|
'Array or stream expected.',
|
||||||
|
PdfReaderException::UNEXPECTED_DATA_TYPE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get information of all external links on this page.
|
||||||
|
*
|
||||||
|
* All coordinates are normalized in view to rotation and translation of the boundary-box, so that their
|
||||||
|
* origin is lower-left.
|
||||||
|
*
|
||||||
|
* The URI is the binary value of the PDF string object. It can be in PdfDocEncoding or in UTF-16BE encoding.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getExternalLinks($box = PageBoundaries::CROP_BOX)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$dict = $this->getPageDictionary();
|
||||||
|
$annotations = PdfType::resolve(PdfDictionary::get($dict, 'Annots'), $this->parser);
|
||||||
|
} catch (FpdiException $e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$annotations instanceof PdfArray) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$links = [];
|
||||||
|
|
||||||
|
foreach ($annotations->value as $entry) {
|
||||||
|
try {
|
||||||
|
$annotation = PdfType::resolve($entry, $this->parser);
|
||||||
|
|
||||||
|
$value = PdfType::resolve(PdfDictionary::get($annotation, 'Subtype'), $this->parser);
|
||||||
|
if (!$value instanceof PdfName || $value->value !== 'Link') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dest = PdfType::resolve(PdfDictionary::get($annotation, 'Dest'), $this->parser);
|
||||||
|
if (!$dest instanceof PdfNull) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = PdfType::resolve(PdfDictionary::get($annotation, 'A'), $this->parser);
|
||||||
|
if (!$action instanceof PdfDictionary) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$actionType = PdfType::resolve(PdfDictionary::get($action, 'S'), $this->parser);
|
||||||
|
if (!$actionType instanceof PdfName || $actionType->value !== 'URI') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$uri = PdfType::resolve(PdfDictionary::get($action, 'URI'), $this->parser);
|
||||||
|
if ($uri instanceof PdfString) {
|
||||||
|
$uriValue = PdfString::unescape($uri->value);
|
||||||
|
} elseif ($uri instanceof PdfHexString) {
|
||||||
|
$uriValue = \hex2bin($uri->value);
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rect = PdfType::resolve(PdfDictionary::get($annotation, 'Rect'), $this->parser);
|
||||||
|
if (!$rect instanceof PdfArray || \count($rect->value) !== 4) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rect = Rectangle::byPdfArray($rect, $this->parser);
|
||||||
|
if ($rect->getWidth() === 0 || $rect->getHeight() === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bbox = $this->getBoundary($box);
|
||||||
|
$rotation = $this->getRotation();
|
||||||
|
|
||||||
|
$gs = new GraphicsState();
|
||||||
|
$gs->translate(-$bbox->getLlx(), -$bbox->getLly());
|
||||||
|
$gs->rotate($bbox->getLlx(), $bbox->getLly(), -$rotation);
|
||||||
|
|
||||||
|
switch ($rotation) {
|
||||||
|
case 90:
|
||||||
|
$gs->translate(-$bbox->getWidth(), 0);
|
||||||
|
break;
|
||||||
|
case 180:
|
||||||
|
$gs->translate(-$bbox->getWidth(), -$bbox->getHeight());
|
||||||
|
break;
|
||||||
|
case 270:
|
||||||
|
$gs->translate(0, -$bbox->getHeight());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedRect = Rectangle::byVectors(
|
||||||
|
$gs->toUserSpace(new Vector($rect->getLlx(), $rect->getLly())),
|
||||||
|
$gs->toUserSpace(new Vector($rect->getUrx(), $rect->getUry()))
|
||||||
|
);
|
||||||
|
|
||||||
|
$quadPoints = PdfType::resolve(PdfDictionary::get($annotation, 'QuadPoints'), $this->parser);
|
||||||
|
$normalizedQuadPoints = [];
|
||||||
|
if ($quadPoints instanceof PdfArray) {
|
||||||
|
$quadPointsCount = \count($quadPoints->value);
|
||||||
|
if ($quadPointsCount % 8 === 0) {
|
||||||
|
for ($i = 0; ($i + 1) < $quadPointsCount; $i += 2) {
|
||||||
|
$x = PdfNumeric::ensure(PdfType::resolve($quadPoints->value[$i], $this->parser));
|
||||||
|
$y = PdfNumeric::ensure(PdfType::resolve($quadPoints->value[$i + 1], $this->parser));
|
||||||
|
|
||||||
|
$v = $gs->toUserSpace(new Vector($x->value, $y->value));
|
||||||
|
$normalizedQuadPoints[] = $v->getX();
|
||||||
|
$normalizedQuadPoints[] = $v->getY();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we remove unsupported/unneeded values here
|
||||||
|
unset(
|
||||||
|
$annotation->value['P'],
|
||||||
|
$annotation->value['NM'],
|
||||||
|
$annotation->value['AP'],
|
||||||
|
$annotation->value['AS'],
|
||||||
|
$annotation->value['Type'],
|
||||||
|
$annotation->value['Subtype'],
|
||||||
|
$annotation->value['Rect'],
|
||||||
|
$annotation->value['A'],
|
||||||
|
$annotation->value['QuadPoints'],
|
||||||
|
$annotation->value['Rotate'],
|
||||||
|
$annotation->value['M'],
|
||||||
|
$annotation->value['StructParent'],
|
||||||
|
$annotation->value['OC']
|
||||||
|
);
|
||||||
|
|
||||||
|
// ...and flatten the PDF object to eliminate any indirect references.
|
||||||
|
// Indirect references are a problem when writing the output in FPDF
|
||||||
|
// because FPDF uses pre-calculated object numbers while FPDI creates
|
||||||
|
// them at runtime.
|
||||||
|
$annotation = PdfType::flatten($annotation, $this->parser);
|
||||||
|
|
||||||
|
$links[] = [
|
||||||
|
'rect' => $normalizedRect,
|
||||||
|
'quadPoints' => $normalizedQuadPoints,
|
||||||
|
'uri' => $uriValue,
|
||||||
|
'pdfObject' => $annotation
|
||||||
|
];
|
||||||
|
} catch (FpdiException $e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $links;
|
||||||
|
}
|
||||||
|
}
|
||||||
94
fpdi2/src/PdfReader/PageBoundaries.php
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfReader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstract class for page boundary constants and some helper methods
|
||||||
|
*/
|
||||||
|
abstract class PageBoundaries
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* MediaBox
|
||||||
|
*
|
||||||
|
* The media box defines the boundaries of the physical medium on which the page is to be printed.
|
||||||
|
*
|
||||||
|
* @see PDF 32000-1:2008 - 14.11.2 Page Boundaries
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const MEDIA_BOX = 'MediaBox';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CropBox
|
||||||
|
*
|
||||||
|
* The crop box defines the region to which the contents of the page shall be clipped (cropped) when displayed or
|
||||||
|
* printed.
|
||||||
|
*
|
||||||
|
* @see PDF 32000-1:2008 - 14.11.2 Page Boundaries
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const CROP_BOX = 'CropBox';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BleedBox
|
||||||
|
*
|
||||||
|
* The bleed box defines the region to which the contents of the page shall be clipped when output in a
|
||||||
|
* production environment.
|
||||||
|
*
|
||||||
|
* @see PDF 32000-1:2008 - 14.11.2 Page Boundaries
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const BLEED_BOX = 'BleedBox';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TrimBox
|
||||||
|
*
|
||||||
|
* The trim box defines the intended dimensions of the finished page after trimming.
|
||||||
|
*
|
||||||
|
* @see PDF 32000-1:2008 - 14.11.2 Page Boundaries
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const TRIM_BOX = 'TrimBox';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ArtBox
|
||||||
|
*
|
||||||
|
* The art box defines the extent of the page’s meaningful content (including potential white space) as intended
|
||||||
|
* by the page’s creator.
|
||||||
|
*
|
||||||
|
* @see PDF 32000-1:2008 - 14.11.2 Page Boundaries
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const ART_BOX = 'ArtBox';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All page boundaries
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public static $all = array(
|
||||||
|
self::MEDIA_BOX,
|
||||||
|
self::CROP_BOX,
|
||||||
|
self::BLEED_BOX,
|
||||||
|
self::TRIM_BOX,
|
||||||
|
self::ART_BOX
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a name is a valid page boundary name.
|
||||||
|
*
|
||||||
|
* @param string $name The boundary name
|
||||||
|
* @return boolean A boolean value whether the name is valid or not.
|
||||||
|
*/
|
||||||
|
public static function isValidName($name)
|
||||||
|
{
|
||||||
|
return \in_array($name, self::$all, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
259
fpdi2/src/PdfReader/PdfReader.php
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of FPDI
|
||||||
|
*
|
||||||
|
* @package setasign\Fpdi
|
||||||
|
* @copyright Copyright (c) 2026 Setasign GmbH & Co. KG (https://www.setasign.com)
|
||||||
|
* @license http://opensource.org/licenses/mit-license The MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace setasign\Fpdi\PdfReader;
|
||||||
|
|
||||||
|
use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParser;
|
||||||
|
use setasign\Fpdi\PdfParser\PdfParserException;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfArray;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfDictionary;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfIndirectObjectReference;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfNumeric;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfType;
|
||||||
|
use setasign\Fpdi\PdfParser\Type\PdfTypeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A PDF reader class
|
||||||
|
*/
|
||||||
|
class PdfReader
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var PdfParser
|
||||||
|
*/
|
||||||
|
protected $parser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $pageCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indirect objects of resolved pages.
|
||||||
|
*
|
||||||
|
* @var PdfIndirectObjectReference[]|PdfIndirectObject[]
|
||||||
|
*/
|
||||||
|
protected $pages = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PdfReader constructor.
|
||||||
|
*
|
||||||
|
* @param PdfParser $parser
|
||||||
|
*/
|
||||||
|
public function __construct(PdfParser $parser)
|
||||||
|
{
|
||||||
|
$this->parser = $parser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PdfReader destructor.
|
||||||
|
*/
|
||||||
|
public function __destruct()
|
||||||
|
{
|
||||||
|
if ($this->parser !== null) {
|
||||||
|
$this->parser->cleanUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the pdf parser instance.
|
||||||
|
*
|
||||||
|
* @return PdfParser
|
||||||
|
*/
|
||||||
|
public function getParser()
|
||||||
|
{
|
||||||
|
return $this->parser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the PDF version.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* @throws PdfParserException
|
||||||
|
*/
|
||||||
|
public function getPdfVersion()
|
||||||
|
{
|
||||||
|
return \implode('.', $this->parser->getPdfVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the page count.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
* @throws PdfTypeException
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
* @throws PdfParserException
|
||||||
|
*/
|
||||||
|
public function getPageCount()
|
||||||
|
{
|
||||||
|
if ($this->pageCount === null) {
|
||||||
|
$catalog = $this->parser->getCatalog();
|
||||||
|
|
||||||
|
$pages = PdfType::resolve(PdfDictionary::get($catalog, 'Pages'), $this->parser);
|
||||||
|
$count = PdfType::resolve(PdfDictionary::get($pages, 'Count'), $this->parser);
|
||||||
|
|
||||||
|
$this->pageCount = PdfNumeric::ensure($count)->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->pageCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a page instance.
|
||||||
|
*
|
||||||
|
* @param int|numeric-string $pageNumber
|
||||||
|
* @return Page
|
||||||
|
* @throws PdfTypeException
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
* @throws PdfParserException
|
||||||
|
* @throws \InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function getPage($pageNumber)
|
||||||
|
{
|
||||||
|
if (!\is_numeric($pageNumber)) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
'Page number needs to be a number.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageNumber = (int)$pageNumber;
|
||||||
|
|
||||||
|
if ($pageNumber < 1 || $pageNumber > $this->getPageCount()) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
\sprintf(
|
||||||
|
'Page number "%s" out of available page range (1 - %s)',
|
||||||
|
$pageNumber,
|
||||||
|
$this->getPageCount()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->readPages();
|
||||||
|
|
||||||
|
$page = $this->pages[$pageNumber - 1];
|
||||||
|
|
||||||
|
if ($page instanceof PdfIndirectObjectReference) {
|
||||||
|
$alreadyReadKids = [];
|
||||||
|
$readPages = function ($kids) use (&$readPages, &$alreadyReadKids) {
|
||||||
|
$kids = PdfArray::ensure($kids);
|
||||||
|
|
||||||
|
/** @noinspection LoopWhichDoesNotLoopInspection */
|
||||||
|
foreach ($kids->value as $reference) {
|
||||||
|
$reference = PdfIndirectObjectReference::ensure($reference);
|
||||||
|
|
||||||
|
if (\in_array($reference->value, $alreadyReadKids, true)) {
|
||||||
|
throw new PdfReaderException('Recursive pages dictionary detected.');
|
||||||
|
}
|
||||||
|
$alreadyReadKids[] = $reference->value;
|
||||||
|
|
||||||
|
$object = $this->parser->getIndirectObject($reference->value);
|
||||||
|
$type = PdfDictionary::get($object->value, 'Type');
|
||||||
|
|
||||||
|
if ($type->value === 'Pages') {
|
||||||
|
return $readPages(PdfDictionary::get($object->value, 'Kids'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new PdfReaderException(
|
||||||
|
'Kids array cannot be empty.',
|
||||||
|
PdfReaderException::KIDS_EMPTY
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
$page = $this->parser->getIndirectObject($page->value);
|
||||||
|
$dict = PdfType::resolve($page, $this->parser);
|
||||||
|
$type = PdfDictionary::get($dict, 'Type');
|
||||||
|
|
||||||
|
if ($type->value === 'Pages') {
|
||||||
|
$kids = PdfType::resolve(PdfDictionary::get($dict, 'Kids'), $this->parser);
|
||||||
|
try {
|
||||||
|
$alreadyReadKids[] = $page->objectNumber;
|
||||||
|
$page = $this->pages[$pageNumber - 1] = $readPages($kids);
|
||||||
|
} catch (PdfReaderException $e) {
|
||||||
|
if ($e->getCode() !== PdfReaderException::KIDS_EMPTY) {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// let's reset the pages array and read all page objects
|
||||||
|
$this->pages = [];
|
||||||
|
$this->readPages(true);
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
$page = $this->pages[$pageNumber - 1];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->pages[$pageNumber - 1] = $page;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Page($page, $this->parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walk the page tree and resolve all indirect objects of all pages.
|
||||||
|
*
|
||||||
|
* @param bool $readAll
|
||||||
|
* @throws CrossReferenceException
|
||||||
|
* @throws PdfParserException
|
||||||
|
* @throws PdfTypeException
|
||||||
|
*/
|
||||||
|
protected function readPages($readAll = false)
|
||||||
|
{
|
||||||
|
if (\count($this->pages) > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$expectedPageCount = $this->getPageCount();
|
||||||
|
$alreadyReadKids = [];
|
||||||
|
$readPages = function ($kids, $count) use (&$readPages, &$alreadyReadKids, $readAll, $expectedPageCount) {
|
||||||
|
$kids = PdfArray::ensure($kids);
|
||||||
|
$isLeaf = ($count->value === \count($kids->value));
|
||||||
|
|
||||||
|
foreach ($kids->value as $reference) {
|
||||||
|
$reference = PdfIndirectObjectReference::ensure($reference);
|
||||||
|
|
||||||
|
if (!$readAll && $isLeaf) {
|
||||||
|
$this->pages[] = $reference;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\in_array($reference->value, $alreadyReadKids, true)) {
|
||||||
|
throw new PdfReaderException('Recursive pages dictionary detected.');
|
||||||
|
}
|
||||||
|
$alreadyReadKids[] = $reference->value;
|
||||||
|
|
||||||
|
$object = $this->parser->getIndirectObject($reference->value);
|
||||||
|
$type = PdfDictionary::get($object->value, 'Type');
|
||||||
|
|
||||||
|
if ($type->value === 'Pages') {
|
||||||
|
$readPages(
|
||||||
|
PdfType::resolve(PdfDictionary::get($object->value, 'Kids'), $this->parser),
|
||||||
|
PdfType::resolve(PdfDictionary::get($object->value, 'Count'), $this->parser)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->pages[] = $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop if all pages are read - faulty documents exists with additional entries with invalid data.
|
||||||
|
if (count($this->pages) === $expectedPageCount) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$catalog = $this->parser->getCatalog();
|
||||||
|
$pages = PdfType::resolve(PdfDictionary::get($catalog, 'Pages'), $this->parser);
|
||||||
|
$count = PdfType::resolve(PdfDictionary::get($pages, 'Count'), $this->parser);
|
||||||
|
$kids = PdfType::resolve(PdfDictionary::get($pages, 'Kids'), $this->parser);
|
||||||
|
$readPages($kids, $count);
|
||||||
|
}
|
||||||
|
}
|
||||||