Compare commits
720 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4baab4dff4 | |||
| 9c9f35b33a | |||
| b31b71756b | |||
| 285f0bc011 | |||
| 4e08872f68 | |||
| 929611f317 | |||
| a2fce2d425 | |||
| ca98fb39af | |||
| 97e5aa8f90 | |||
| 8649f1ac0c | |||
| c2f4afd520 | |||
| 4a38109d10 | |||
| cb6d806d89 | |||
| 709e5b7e62 | |||
| d00af77206 | |||
| d60a49a1bf | |||
| bd33d70365 | |||
| 26c2234ead | |||
| 8cfc2321d1 | |||
| 133d6144ad | |||
| fe8bbb0d2e | |||
| d406a7d78c | |||
| fabbacdcc1 | |||
| 809defb24a | |||
| 72f9ff1a4a | |||
| 923bb27a93 | |||
| 38f8fedec8 | |||
| a94fb01ceb | |||
| a90cd8a718 | |||
| e091db9063 | |||
| 4f9cfb487f | |||
| 5f1a0f4f75 | |||
| dc18d5166f | |||
| 859f0c970b | |||
| 4f574abfe7 | |||
| 042b15ae45 | |||
| ccccc13662 | |||
| 3008f47620 | |||
| 34df8c5f63 | |||
| 8feaadc162 | |||
| bf95f79775 | |||
| f2c4e0931d | |||
| 9982ed0bef | |||
| cd860cb7d0 | |||
| 22f7d2a8b0 | |||
| e37d35bc9b | |||
| 4ef5f24212 | |||
| 6aebcaed97 | |||
| d35f00b2cb | |||
| 999f6f5593 | |||
| f92e261ea8 | |||
| ba03bfa6da | |||
| bd3e06d403 | |||
| bf43ab05ff | |||
| 8d81ac3acb | |||
| 2d0255909e | |||
| 2634bb7208 | |||
| a4c80bf498 | |||
| eb19cd14b5 | |||
| c227fe7c34 | |||
| 1fc92333dd | |||
| 47c1a6fd05 | |||
| acf5c2984d | |||
| 98eb304903 | |||
| 1b385f987b | |||
| 34c1cdbe86 | |||
| 8563945199 | |||
| 162e05449c | |||
| 0b6fd83b14 | |||
| 3631f17a77 | |||
| 4100af5ad3 | |||
| b16024f205 | |||
| a0dbef3b78 | |||
| 12c24098c3 | |||
| 8d03944e68 | |||
| 3b5207871b | |||
| 0f2689deee | |||
| 37d437dbae | |||
| bfe109e9e4 | |||
| d8f7127528 | |||
| 986af05002 | |||
| bdbfbdabf1 | |||
| 04f74150ac | |||
| 74ce903cb7 | |||
| 666dd116bd | |||
| e0d262e733 | |||
| 128d7ec94d | |||
| e0277038cd | |||
| 845a50a99f | |||
| a30a353156 | |||
| 27a7e2f6f5 | |||
| febf78dab6 | |||
| fab0efa5e5 | |||
| 72794768af | |||
| 75c71d4b28 | |||
| 81364eefb3 | |||
| 07a09dd08e | |||
| 673370b9a5 | |||
| c398ab0a40 | |||
| 86f3801513 | |||
| 678be304c5 | |||
| 0243a2a5a0 | |||
| 15d95ddac9 | |||
| 33a15bdbd6 | |||
| b71a237cad | |||
| 11d9d2aee4 | |||
| 6a1a3b67c8 | |||
| d1bb6be354 | |||
| 55a387c785 | |||
| 6efa058293 | |||
| 8f054a475c | |||
| 5b1919b760 | |||
| 50ded4b933 | |||
| c4b0629e4f | |||
| 3d5991495b | |||
| 1ee853890c | |||
| 400347740d | |||
| eae84b9d8f | |||
| 1ba4937f32 | |||
| 205091dc6f | |||
| d704d089a3 | |||
| 4cf6e4f9d3 | |||
| 88565e2e19 | |||
| 3a295bcbad | |||
| 3cf87f8bd9 | |||
| 469f0e1913 | |||
| 6e8e2abd5f | |||
| a7016ca153 | |||
| 7359367569 | |||
| e394779737 | |||
| 9bb1b1a36c | |||
| 7ac977ef6c | |||
| aabe966bcf | |||
| ffbe6f5efa | |||
| 68f568949e | |||
| 3fbaeab264 | |||
| bc0bcf3594 | |||
| 0258f5e8b1 | |||
| 5e2ba0d28e | |||
| ed03e6637f | |||
| ccb17b1170 | |||
| 8ce3f4dcc1 | |||
| dbf8f15333 | |||
| 51d838e1b2 | |||
| c5381488ce | |||
| 662b63e47b | |||
| 3e8459d5c2 | |||
| dcf0d4265b | |||
| ef3330d0fb | |||
| def0bbd55e | |||
| 44c3aa0f19 | |||
| 96c8382143 | |||
| 6e980eaea9 | |||
| 42b703204a | |||
| 5ebad1c816 | |||
| 4e230845f9 | |||
| 8855161518 | |||
| 4418133ec0 | |||
| 5b9ac837dd | |||
| 2a570dd573 | |||
| a2e34ff6ce | |||
| d75ff87824 | |||
| 9fd4d78485 | |||
| b9a5f1cbff | |||
| 123605c442 | |||
| 0c539faaee | |||
| a571e283d3 | |||
| 0a17b974c9 | |||
| cf3497c817 | |||
| 22665ee951 | |||
| 416adafaa5 | |||
| 96bf57fea7 | |||
| a41374ef74 | |||
| e2800c7dbf | |||
| 8a8b800302 | |||
| 48ed42c29c | |||
| d8722a4087 | |||
| ac7eb466fc | |||
| 839c31dbb8 | |||
| dbaa7adf20 | |||
| 98628a273b | |||
| 90bcd57d2c | |||
| e1acf4a2b8 | |||
| 449a8fcbb4 | |||
| 268615d8fe | |||
| 2b98eef915 | |||
| 39e7b397e9 | |||
| 16576623f6 | |||
| cc5c979af6 | |||
| 3fd33d3269 | |||
| 39cefe99e6 | |||
| d8d9c3eb66 | |||
| 1502361fa3 | |||
| 61b50b5288 | |||
| 2303a81751 | |||
| 0e7c405862 | |||
| 23eb78d5c3 | |||
| 5e2c134194 | |||
| 3dcbff1d08 | |||
| 4559f2746e | |||
| 963fb40b2b | |||
| 1682075267 | |||
| 3032b2be87 | |||
| 2624c15763 | |||
| 981be27151 | |||
| 79b06df170 | |||
| 0eafe2ba35 | |||
| 11c6b79a4d | |||
| b2ce89754f | |||
| 1c1aac38c2 | |||
| 587ab9a492 | |||
| 3a38c8245b | |||
| 99cb0ed8ce | |||
| 1d685dc772 | |||
| cea34c95b5 | |||
| 17bfe2d969 | |||
| 7e4254ab42 | |||
| f7d0e54f7a | |||
| 41ea5e22ca | |||
| 85907320b4 | |||
| a52a5e31e5 | |||
| 21bf43e33d | |||
| 61c5ad9802 | |||
| 6b58d30eec | |||
| 84f7fc562c | |||
| 56f66976c7 | |||
| 16cc10bca0 | |||
| b18ccfcb7b | |||
| 3c3ea2135b | |||
| f860a8ecba | |||
| 8fa5240dcd | |||
| 9935c86538 | |||
| 8ec1d5353f | |||
| 85ccb0da15 | |||
| b431f40f9f | |||
| 85bb013a0e | |||
| aeeb779ab0 | |||
| ed1f07d872 | |||
| 97938b8fcd | |||
| 54d73fa073 | |||
| 2404dfff75 | |||
| e401f3aef2 | |||
| 33914a9b61 | |||
| 46c23cec26 | |||
| 71cb29c507 | |||
| 034a0b8ff1 | |||
| f4c6c16df7 | |||
| 1bfc8a2b38 | |||
| 601cf8fdfd | |||
| 4be5829805 | |||
| 66ae50b129 | |||
| daa94f53d0 | |||
| 42c54f4c84 | |||
| 633ac6e63f | |||
| 8cfa9bd8af | |||
| 7c63eabd76 | |||
| 1dafe99fa9 | |||
| 35431ede0d | |||
| 869d73511c | |||
| 3fab26158f | |||
| b1b0df004e | |||
| 53bbfde2b2 | |||
| 6c32b94376 | |||
| 8dd35ec9d3 | |||
| 55c7cff79d | |||
| 06bd8ef175 | |||
| 1894c5cc9a | |||
| 927fd7a589 | |||
| fe70eba7d6 | |||
| 4a4618ef56 | |||
| 3af94e3545 | |||
| 80ad254cf6 | |||
| e4154a70ff | |||
| 0fe44b0885 | |||
| ad00a8cff8 | |||
| 8a71c651ae | |||
| c09c1a8298 | |||
| c950559038 | |||
| 251700eb50 | |||
| 7bf3406735 | |||
| 30e123ccbb | |||
| d993f99ad1 | |||
| e9fbb1d4d4 | |||
| 6f9ba6c7f6 | |||
| 08f5bbeaba | |||
| 520d55b98d | |||
| d57cea662d | |||
| 2f5b78f887 | |||
| ef9795f6e4 | |||
| f8166a7109 | |||
| bcfd735844 | |||
| 54f91df984 | |||
| 19843c19d1 | |||
| e620317966 | |||
| 224187ba11 | |||
| 9e9af5769a | |||
| 1224e778d4 | |||
| 0ee1cbeaaf | |||
| d0c6400644 | |||
| 19ae8f2499 | |||
| 0aae87a248 | |||
| fa1bd30ac3 | |||
| 73d0203b8f | |||
| caf3a729d1 | |||
| 7e49e6491d | |||
| ff045692d0 | |||
| 2e0f0fb7db | |||
| 7763ddb802 | |||
| eafd465ba2 | |||
| 512ddc0d76 | |||
| b2156f36b9 | |||
| a56ccbd1f8 | |||
| e91a98d0d5 | |||
| 3d914d0591 | |||
| b95c1c8878 | |||
| f18ed3a531 | |||
| d2feca38d8 | |||
| 41b0dd4e78 | |||
| e92b200eb3 | |||
| c1ae7dff95 | |||
| 6cbf6cea73 | |||
| b8e1515015 | |||
| 399bc60525 | |||
| 913dc48bfe | |||
| 2af34da28f | |||
| 9043d37a08 | |||
| 72cd6583d5 | |||
| 03969a20fe | |||
| f2a5e103dd | |||
| 45d66461e8 | |||
| 79fb3eaa29 | |||
| 946ad47765 | |||
| 488d5ab6ae | |||
| 9c8d7d2062 | |||
| ce6abcd3f0 | |||
| 8f2ccb45a2 | |||
| 415e9503f7 | |||
| aec3e459b5 | |||
| 48ce79811f | |||
| 0d2c30bcd9 | |||
| c939deb589 | |||
| 5cdcb8bd74 | |||
| e57018732f | |||
| 4869d5f5b6 | |||
| 7ffcb9031b | |||
| 474fd25d3c | |||
| 22d50aee21 | |||
| 9fec3a57b2 | |||
| d3c480d481 | |||
| 58e091b45e | |||
| c4303483b7 | |||
| c742c0859b | |||
| 45ab1653a4 | |||
| bc10efa640 | |||
| 757e2fbda1 | |||
| a09959a969 | |||
| c39052a0d3 | |||
| e321d61237 | |||
| 678b3bb47d | |||
| 489a5befde | |||
| c0a16d8b7c | |||
| 2ce99b3702 | |||
| 8008741e35 | |||
| 47edd2215f | |||
| 91740192d2 | |||
| bd900d0b48 | |||
| ca5aa8edf0 | |||
| 81937ca606 | |||
| ba15bf8d7e | |||
| e529a72a00 | |||
| 080a7864b5 | |||
| 3db7261c0e | |||
| bb47ab3261 | |||
| 0ae0365ee5 | |||
| cbcc560bd3 | |||
| ee819fb58a | |||
| 69eb1a1948 | |||
| a71bf16c51 | |||
| 2a2e0cc15d | |||
| fe99cf8db1 | |||
| 5e7a089cb0 | |||
| a198cf8c65 | |||
| 57e5202a28 | |||
| cf3cc2c4a5 | |||
| 9eda4c48e9 | |||
| 06153536c1 | |||
| 77fdd9e194 | |||
| 0e383acfe3 | |||
| 9e65549eb3 | |||
| a6c0e2aa0e | |||
| 67702f5a55 | |||
| 4270a452a7 | |||
| 5178d72850 | |||
| 832cfb8915 | |||
| 79cd6ad7b9 | |||
| a445c4d2a9 | |||
| 4a1cb6a9d5 | |||
| 5069547464 | |||
| 335a62cb10 | |||
| e74e9e519b | |||
| 3f8fcbcb74 | |||
| 90db4b29c4 | |||
| 8fce28df3b | |||
| 6ac4e7ef18 | |||
| 9945080bec | |||
| 7dac68b6c0 | |||
| 209e828a49 | |||
| 398743fe63 | |||
| 92f6759591 | |||
| 368c2fb866 | |||
| 985c6fb64b | |||
| 9b9ec31f0f | |||
| 946d0137af | |||
| 37d2d8cf4f | |||
| 3072c2a236 | |||
| a38803389c | |||
| 4708cf00c0 | |||
| 4d525ae8ef | |||
| 57b69fe5a9 | |||
| 6ee1387fcf | |||
| 5cfbdb21e9 | |||
| 2abf1ccb73 | |||
| 09b977a798 | |||
| 80fac162af | |||
| c5e43e6e49 | |||
| 08571e805a | |||
| dd5691ef7f | |||
| 6e43dc87f2 | |||
| 06dc8ef281 | |||
| b05b4c78e8 | |||
| 9726d63d0b | |||
| 4ca7d27d39 | |||
| 29b2304b25 | |||
| 8bca3b2113 | |||
| 7181d7196f | |||
| 28ebc6ea81 | |||
| f3c4e631cd | |||
| da14a41387 | |||
| cc14c69a5e | |||
| 5ae385527b | |||
| b6aec6360b | |||
| 5a5528f78f | |||
| a3f11919c9 | |||
| 3847afc50f | |||
| a3e7f5c5fa | |||
| 1fe2c74691 | |||
| 7b5789e44c | |||
| 61e03fd12a | |||
| 5f6bd7b748 | |||
| 1e9a59f084 | |||
| 9e11cecfa5 | |||
| 8a76e70fcb | |||
| 92e4d03495 | |||
| a9d059c0df | |||
| 2b14e11259 | |||
| 50cde83341 | |||
| 0283f781d7 | |||
| ddd54c321e | |||
| 01a40e311d | |||
| 6209733497 | |||
| 5e37bc2d62 | |||
| 8477416063 | |||
| 5020c132b5 | |||
| b70a354556 | |||
| 92f5afef3c | |||
| 9a407eef6d | |||
| e635a28f16 | |||
| adbed86617 | |||
| eb556b978b | |||
| 5b1647a42f | |||
| 153c09f282 | |||
| 8350bebb50 | |||
| 75e5701a3b | |||
| eb7ca6188c | |||
| e80a885208 | |||
| b1076ba1bd | |||
| ed281abd92 | |||
| e63111c93f | |||
| e87ba0c4e1 | |||
| 79f7243e70 | |||
| b19f148ee8 | |||
| 4ee79811bc | |||
| e79e9d6ce1 | |||
| ddae4383dd | |||
| 683751ac0c | |||
| 8407d27ea0 | |||
| 4126b99a7b | |||
| 69003eae7f | |||
| ec5e173e40 | |||
| 06603bd2b3 | |||
| 11263671b2 | |||
| e9f0880f01 | |||
| 2fac2328d0 | |||
| 3ef7382ab0 | |||
| fb4379ea34 | |||
| e7bea7e7fe | |||
| 20ea95238b | |||
| 1098540ae4 | |||
| 3ae31f6341 | |||
| 014c6a3b73 | |||
| 65f9cd35c9 | |||
| 324bc26e1e | |||
| 234ff55376 | |||
| e13f79134e | |||
| f78ca003d2 | |||
| d7460a9e0a | |||
| 85a98326fe | |||
| 5d1584ce83 | |||
| 5f02605aca | |||
| b3b1441510 | |||
| 84ba790ef9 | |||
| de93e83abe | |||
| 1bcb8349de | |||
| dc527c2cf6 | |||
| 71daa4ce2f | |||
| a13710e9d2 | |||
| bf2e7ac236 | |||
| 72120f262d | |||
| 6cde07a75b | |||
| 42213789ba | |||
| 598655ef33 | |||
| b247b00bd7 | |||
| d39a732d17 | |||
| 86e2e39772 | |||
| 6405ab2b17 | |||
| cbbde8af9b | |||
| 2c4779a98c | |||
| b465faf936 | |||
| a0a801a7c7 | |||
| 2863654f8f | |||
| 717869d0da | |||
| 115465f017 | |||
| 738f483aef | |||
| f7e782f081 | |||
| 2214c70b2b | |||
| 7f4bc6f952 | |||
| bf28abc4ca | |||
| 963b9e9e59 | |||
| 8a8de30949 | |||
| cee67cbd97 | |||
| d60d834a65 | |||
| c1a10bf926 | |||
| a1dd707315 | |||
| 60d778bcb1 | |||
| d6291b03e3 | |||
| 24b5b94d63 | |||
| 85d4f6f304 | |||
| da2c80b4fc | |||
| 18a91d633d | |||
| 65ec35f4dd | |||
| 515e388521 | |||
| b9037b9a52 | |||
| 0bf7bcb204 | |||
| bec4a188a3 | |||
| df6b5ae043 | |||
| e29d0959c4 | |||
| cce73a8c32 | |||
| eee9d37c97 | |||
| 4b983afc5c | |||
| 3a1dd252e1 | |||
| c2d7bc818e | |||
| 732a4a24e6 | |||
| 64e3cd23ad | |||
| aca48f024d | |||
| 9b0549a762 | |||
| b0681b89ea | |||
| 2182cf6a89 | |||
| 2e13d48518 | |||
| 841e570e57 | |||
| 1a08df7824 | |||
| 3f0140c716 | |||
| 38a9c69bde | |||
| 3366a27575 | |||
| a93ba33e92 | |||
| 80c5decee8 | |||
| 21b84c9f36 | |||
| 970966e4c5 | |||
| 4336344d40 | |||
| fe9740e3eb | |||
| 7581942b0c | |||
| 5dfab7e325 | |||
| e93f338a9e | |||
| d1b3afdd01 | |||
| 054fde7288 | |||
| f887d05d5e | |||
| 09dea0be01 | |||
| 40157bcb8c | |||
| e0e80c5a56 | |||
| 926a052e99 | |||
| a7d10ae9d6 | |||
| 0757ebf520 | |||
| 20479cdcab | |||
| 1954efd7e0 | |||
| cb34377363 | |||
| 2e9437572d | |||
| 0e639ef6a8 | |||
| 5c7467e4bf | |||
| b9b71b2015 | |||
| 50b7f2cf30 | |||
| cf20fe65cc | |||
| a676f26c36 | |||
| 8ba7fdfa85 | |||
| f81861df19 | |||
| 1d6f654c2f | |||
| 67df53dda8 | |||
| 11bc32ab8a | |||
| f5610bbdb4 | |||
| 24cd1b3c5d | |||
| 48c2bda539 | |||
| 7888f5ca74 | |||
| 1dfdb261e1 | |||
| 7b0217be6a | |||
| bab9140ef2 | |||
| ddee6815af | |||
| 4e9dbc98e3 | |||
| c313e72f75 | |||
| e0c84e007c | |||
| fe2f15c576 | |||
| 87ef57eb29 | |||
| 6f75dd3dbe | |||
| 1d45e6d839 | |||
| 87875c1a7f | |||
| 8d6e8f2610 | |||
| fe81fd3549 | |||
| fbfa219615 | |||
| 1e1ee6ba8c | |||
| c51bb22822 | |||
| 7c32b525a3 | |||
| b242119e00 | |||
| ecf90a3941 | |||
| c97ba58dcb | |||
| e802ceea86 | |||
| 280accd9ad | |||
| f59122254d | |||
| 137b6fcff7 | |||
| 44b9f60478 | |||
| a34c0ff776 | |||
| 4dfce57bd1 | |||
| 9c45642d79 | |||
| bfd1ff5fa7 | |||
| 2322b2d1da | |||
| b8dc4e2dcd | |||
| e4959e0c38 | |||
| dbafd6ba4d | |||
| 0f1057a6cf | |||
| 88b0479366 | |||
| 4ce40dd85c | |||
| bf2813c241 | |||
| cf4045faec | |||
| 6349db3122 | |||
| 62c7af7b38 | |||
| 6ab55fcd22 | |||
| 947f049893 | |||
| 71cbbbe556 | |||
| bfc5e8054d | |||
| 62a290c197 | |||
| b2ee92d606 | |||
| b68100df38 | |||
| 41b8bb0acc | |||
| 1b52f3ca3e | |||
| 8ff6c8bd03 | |||
| ed249366f9 | |||
| ed9cb77bba | |||
| 6ad1d917bd | |||
| e5c24a5fdc | |||
| 15d771e2a8 | |||
| b38bd4bc1c | |||
| b35efdaec5 | |||
| bdf81780f1 | |||
| db030173e2 | |||
| aced5f2d5d | |||
| 59c06302a1 | |||
| ba64637e6f | |||
| 574a1f8d6a | |||
| d93bc73f13 | |||
| 293cc6d775 | |||
| e7539d4930 | |||
| 53ade3bd23 | |||
| 1991adc48e | |||
| a6dd1d9db0 | |||
| 28694059c2 | |||
| bd13d45207 | |||
| abd9fb9985 | |||
| b334da55ed | |||
| c6c2d5a0d9 | |||
| fc89a1bdf6 | |||
| 4ab929dc93 | |||
| c7a56b5c38 | |||
| 80c7c12361 | |||
| 489f71ea47 | |||
| 029331b3b7 | |||
| 70aba2ec52 | |||
| ed0b8b4758 | |||
| 251fb2cd49 | |||
| 0dc5b8282a | |||
| d5eb165ea4 | |||
| 9d4b39c1f7 | |||
| c3dda027bf | |||
| 5e2b841ae2 | |||
| 324dab780d | |||
| ba166c86c4 | |||
| 3e08e8f166 | |||
| 685069a761 | |||
| c152c399bd | |||
| 0307e9752d | |||
| c5ff0197bb | |||
| 2417dbf7de | |||
| 4f97358b2f | |||
| 3244347b71 | |||
| 54be896942 | |||
| 8f690966fc | |||
| bccf348b7e | |||
| b02c3d8d9b | |||
| 39e29a9d02 | |||
| 26b1be00e8 | |||
| aff531e62b | |||
| e8de15e548 | |||
| 03cd76fff5 | |||
| 4750313d37 | |||
| 0c1a310329 |
@@ -7,7 +7,7 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
name: Test on PHP ${{ matrix.php-versions }}
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
TEST_DATABASE_HOST: '127.0.0.1'
|
||||
TEST_DATABASE_PLATFORM: 'Postgresql'
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
name: Test on PHP ${{ matrix.php-versions }}
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
TEST_DATABASE_HOST: '127.0.0.1'
|
||||
TEST_DATABASE_PORT: '8888'
|
||||
|
||||
@@ -23,7 +23,7 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
name: Test on PHP ${{ matrix.php-versions }}
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-versions: ['8.2', '8.3', '8.4']
|
||||
|
||||
Generated
+38
@@ -341,6 +341,25 @@
|
||||
</SchemaInfo>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="metadata/app/clientIcons">
|
||||
<value>
|
||||
<SchemaInfo>
|
||||
<option name="generatedName" value="New Schema" />
|
||||
<option name="name" value="metadata/app/clientIcons" />
|
||||
<option name="relativePathToSchema" value="schema/metadata/app/clientIcons.json" />
|
||||
<option name="schemaVersion" value="JSON Schema version 7" />
|
||||
<option name="patterns">
|
||||
<list>
|
||||
<Item>
|
||||
<option name="pattern" value="true" />
|
||||
<option name="path" value="*/Resources/metadata/app/clientIcons.json" />
|
||||
<option name="mappingKind" value="Pattern" />
|
||||
</Item>
|
||||
</list>
|
||||
</option>
|
||||
</SchemaInfo>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="metadata/app/clientRecord">
|
||||
<value>
|
||||
<SchemaInfo>
|
||||
@@ -1386,6 +1405,25 @@
|
||||
</SchemaInfo>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="metadata/logicDefs">
|
||||
<value>
|
||||
<SchemaInfo>
|
||||
<option name="generatedName" value="New Schema" />
|
||||
<option name="name" value="metadata/logicDefs" />
|
||||
<option name="relativePathToSchema" value="schema/metadata/logicDefs.json" />
|
||||
<option name="schemaVersion" value="JSON Schema version 7" />
|
||||
<option name="patterns">
|
||||
<list>
|
||||
<Item>
|
||||
<option name="pattern" value="true" />
|
||||
<option name="path" value="*/metadata/logicDefs/*.json" />
|
||||
<option name="mappingKind" value="Pattern" />
|
||||
</Item>
|
||||
</list>
|
||||
</option>
|
||||
</SchemaInfo>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="metadata/notificationDefs">
|
||||
<value>
|
||||
<SchemaInfo>
|
||||
|
||||
Vendored
+12
@@ -82,6 +82,12 @@
|
||||
],
|
||||
"url": "./schema/metadata/integrations.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"*/metadata/logicDefs/*.json"
|
||||
],
|
||||
"url": "./schema/metadata/logicDefs.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"*/metadata/notificationDefs/*.json"
|
||||
@@ -184,6 +190,12 @@
|
||||
],
|
||||
"url": "./schema/metadata/app/client.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"*/Resources/metadata/app/clientIcons.json"
|
||||
],
|
||||
"url": "./schema/metadata/app/clientIcons.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"*/Resources/metadata/app/clientRecord.json"
|
||||
|
||||
+6
-2
@@ -184,8 +184,12 @@ module.exports = grunt => {
|
||||
comments: /^!/,
|
||||
},
|
||||
beautify: false,
|
||||
mangle: true,
|
||||
compress: true
|
||||
compress: {
|
||||
sequences: false,
|
||||
collapse_vars: false,
|
||||
keep_fargs: true,
|
||||
webkit: true,
|
||||
},
|
||||
},
|
||||
bundle: {
|
||||
options: {
|
||||
|
||||
@@ -232,6 +232,16 @@ class Binding implements BindingProcessor
|
||||
'Espo\\Tools\\Stream\\Service',
|
||||
'streamService'
|
||||
);
|
||||
|
||||
$binder->bindService(
|
||||
'Espo\\Core\\Utils\\Config\\SystemConfig',
|
||||
'systemConfig'
|
||||
);
|
||||
|
||||
$binder->bindService(
|
||||
'Espo\\Core\\Utils\\Config\\ApplicationConfig',
|
||||
'applicationConfig'
|
||||
);
|
||||
}
|
||||
|
||||
private function bindCore(Binder $binder): void
|
||||
|
||||
@@ -46,14 +46,10 @@ class AccessChecker implements AccessEntityCREDSChecker
|
||||
{
|
||||
use DefaultAccessCheckerDependency;
|
||||
|
||||
private DefaultAccessChecker $defaultAccessChecker;
|
||||
private AclManager $aclManager;
|
||||
|
||||
public function __construct(DefaultAccessChecker $defaultAccessChecker, AclManager $aclManager)
|
||||
{
|
||||
$this->defaultAccessChecker = $defaultAccessChecker;
|
||||
$this->aclManager = $aclManager;
|
||||
}
|
||||
public function __construct(
|
||||
private DefaultAccessChecker $defaultAccessChecker,
|
||||
private AclManager $aclManager,
|
||||
) {}
|
||||
|
||||
public function checkEntityCreate(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
@@ -70,6 +66,10 @@ class AccessChecker implements AccessEntityCREDSChecker
|
||||
|
||||
public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
if (!$user->isAdmin() && !$entity->isActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($entity->isPortal()) {
|
||||
if ($this->aclManager->getPermissionLevel($user, Permission::PORTAL) === Table::LEVEL_YES) {
|
||||
return true;
|
||||
|
||||
@@ -68,9 +68,9 @@ class AccessChecker implements AccessEntityCREDSChecker
|
||||
|
||||
assert($entity instanceof CoreEntity);
|
||||
|
||||
$userIdList = $entity->getLinkMultipleIdLIst('users');
|
||||
$userIdList = $entity->getLinkMultipleIdList('users');
|
||||
|
||||
if (is_array($userIdList) && in_array($user->getId(), $userIdList)) {
|
||||
if (in_array($user->getId(), $userIdList)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,10 @@ class AppLog implements Cleanup
|
||||
|
||||
public function process(): void
|
||||
{
|
||||
if (!$this->config->get('cleanupAppLog')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$query = DeleteBuilder::create()
|
||||
->from(AppLogRecord::ENTITY_TYPE)
|
||||
->where(['createdAt<' => $this->getBefore()->toString()])
|
||||
|
||||
@@ -31,42 +31,40 @@ namespace Espo\Classes\Cleanup;
|
||||
|
||||
use Espo\Core\Cleanup\Cleanup;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\DateTime as DateTimeUtil;
|
||||
use Espo\Entities\WebhookEventQueueItem;
|
||||
use Espo\Entities\WebhookQueueItem;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use DateTime;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class WebhookQueue implements Cleanup
|
||||
{
|
||||
private string $cleanupWebhookQueuePeriod = '10 days';
|
||||
|
||||
private $config;
|
||||
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(Config $config, EntityManager $entityManager)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
public function __construct(private Config $config, private EntityManager $entityManager)
|
||||
{}
|
||||
|
||||
public function process(): void
|
||||
{
|
||||
$period = '-' . $this->config->get('cleanupWebhookQueuePeriod', $this->cleanupWebhookQueuePeriod);
|
||||
|
||||
$datetime = new DateTime();
|
||||
|
||||
$datetime->modify($period);
|
||||
$from = $datetime->format('Y-m-d H:i:s');
|
||||
$from = $datetime->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT);
|
||||
|
||||
$query1 = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->delete()
|
||||
->from('WebhookQueueItem')
|
||||
->from(WebhookQueueItem::ENTITY_TYPE)
|
||||
->where([
|
||||
'DATE:(createdAt)<' => $from,
|
||||
'OR' => [
|
||||
'status!=' => 'Pending',
|
||||
'deleted' => true,
|
||||
'status!=' => WebhookQueueItem::STATUS_PENDING,
|
||||
Attribute::DELETED => true,
|
||||
],
|
||||
])
|
||||
->build();
|
||||
@@ -76,12 +74,12 @@ class WebhookQueue implements Cleanup
|
||||
$query2 = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->delete()
|
||||
->from('WebhookEventQueueItem')
|
||||
->from(WebhookEventQueueItem::ENTITY_TYPE)
|
||||
->where([
|
||||
'DATE:(createdAt)<' => $from,
|
||||
'OR' => [
|
||||
'isProcessed' => true,
|
||||
'deleted' => true,
|
||||
Attribute::DELETED => true,
|
||||
],
|
||||
])
|
||||
->build();
|
||||
|
||||
@@ -85,7 +85,7 @@ class RelationshipRole implements FieldConverter
|
||||
'from' => $midTable,
|
||||
'select' => [$nearKey],
|
||||
'whereClause' => [
|
||||
'deleted' => false,
|
||||
Attribute::DELETED => false,
|
||||
$column => '{value}',
|
||||
],
|
||||
],
|
||||
@@ -97,7 +97,7 @@ class RelationshipRole implements FieldConverter
|
||||
'from' => $midTable,
|
||||
'select' => [$nearKey],
|
||||
'whereClause' => [
|
||||
'deleted' => false,
|
||||
Attribute::DELETED => false,
|
||||
$column => '{value}',
|
||||
],
|
||||
],
|
||||
@@ -109,7 +109,7 @@ class RelationshipRole implements FieldConverter
|
||||
'from' => $midTable,
|
||||
'select' => [$nearKey],
|
||||
'whereClause' => [
|
||||
'deleted' => false,
|
||||
Attribute::DELETED => false,
|
||||
$column => '{value}',
|
||||
],
|
||||
],
|
||||
@@ -121,7 +121,7 @@ class RelationshipRole implements FieldConverter
|
||||
'from' => $midTable,
|
||||
'select' => [$nearKey],
|
||||
'whereClause' => [
|
||||
'deleted' => false,
|
||||
Attribute::DELETED => false,
|
||||
$column => '{value}',
|
||||
],
|
||||
],
|
||||
@@ -133,7 +133,7 @@ class RelationshipRole implements FieldConverter
|
||||
'from' => $midTable,
|
||||
'select' => [$nearKey],
|
||||
'whereClause' => [
|
||||
'deleted' => false,
|
||||
Attribute::DELETED => false,
|
||||
"$column*" => '{value}',
|
||||
],
|
||||
],
|
||||
@@ -145,7 +145,7 @@ class RelationshipRole implements FieldConverter
|
||||
'from' => $midTable,
|
||||
'select' => [$nearKey],
|
||||
'whereClause' => [
|
||||
'deleted' => false,
|
||||
Attribute::DELETED => false,
|
||||
"$column*" => '{value}',
|
||||
],
|
||||
],
|
||||
|
||||
@@ -52,11 +52,6 @@ class AddressDataLoader implements Loader
|
||||
/** @var EmailRepository $repository */
|
||||
$repository = $this->entityManager->getRepository(Email::ENTITY_TYPE);
|
||||
|
||||
$repository->loadFromField($entity);
|
||||
$repository->loadToField($entity);
|
||||
$repository->loadCcField($entity);
|
||||
$repository->loadBccField($entity);
|
||||
$repository->loadReplyToField($entity);
|
||||
$repository->loadNameHash($entity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\FieldProcessing\Email;
|
||||
|
||||
use Espo\Core\FieldProcessing\Loader;
|
||||
use Espo\Core\FieldProcessing\Loader\Params;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Repositories\Email as EmailRepository;
|
||||
|
||||
/**
|
||||
* @implements Loader<Email>
|
||||
*/
|
||||
class AddressLoader implements Loader
|
||||
{
|
||||
public function __construct(private EntityManager $entityManager)
|
||||
{}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function process(Entity $entity, Params $params): void
|
||||
{
|
||||
/** @var EmailRepository $repository */
|
||||
$repository = $this->entityManager->getRepository(Email::ENTITY_TYPE);
|
||||
|
||||
$repository->loadFromField($entity);
|
||||
$repository->loadToField($entity);
|
||||
$repository->loadCcField($entity);
|
||||
$repository->loadBccField($entity);
|
||||
$repository->loadReplyToField($entity);
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ use Espo\Core\FieldProcessing\Loader;
|
||||
use Espo\Core\FieldProcessing\Loader\Params;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
|
||||
/**
|
||||
* @implements Loader<Email>
|
||||
@@ -57,7 +58,7 @@ class UserColumnsLoader implements Loader
|
||||
Email::USERS_COLUMN_IN_ARCHIVE,
|
||||
])
|
||||
->where([
|
||||
'deleted' => false,
|
||||
Attribute::DELETED => false,
|
||||
'userId' => $this->user->getId(),
|
||||
'emailId' => $entity->getId(),
|
||||
])
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Espo\Classes\FieldProcessing\InboundEmail;
|
||||
|
||||
use Espo\Core\FieldProcessing\Loader;
|
||||
use Espo\Core\FieldProcessing\Loader\Params;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Mail\ConfigDataProvider;
|
||||
use Espo\Entities\InboundEmail;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
@@ -41,12 +41,12 @@ use Espo\ORM\Entity;
|
||||
class IsSystemLoader implements Loader
|
||||
{
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private ConfigDataProvider $configDataProvider,
|
||||
) {}
|
||||
|
||||
public function process(Entity $entity, Params $params): void
|
||||
{
|
||||
$isSystem = $entity->getEmailAddress() === $this->config->get('outboundEmailFromAddress');
|
||||
$isSystem = $entity->getEmailAddress() === $this->configDataProvider->getSystemOutboundAddress();
|
||||
|
||||
$entity->set('isSystem', $isSystem);
|
||||
}
|
||||
|
||||
+38
-25
@@ -1,3 +1,4 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
@@ -26,34 +27,46 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
define('crm:views/calendar/record/edit-view', ['views/record/base'], function (Dep) {
|
||||
namespace Espo\Classes\FieldProcessing\OAuthAccount;
|
||||
|
||||
return Dep.extend({
|
||||
use Espo\Core\FieldProcessing\Loader;
|
||||
use Espo\Core\FieldProcessing\Loader\Params;
|
||||
use Espo\Entities\OAuthAccount;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Tools\OAuth\ConfigDataProvider;
|
||||
|
||||
template: 'crm:calendar/record/edit-view',
|
||||
/**
|
||||
* @implements Loader<OAuthAccount>
|
||||
*/
|
||||
class DataLoader implements Loader
|
||||
{
|
||||
public function __construct(
|
||||
private ConfigDataProvider $configDataProvider,
|
||||
) {}
|
||||
|
||||
setup: function () {
|
||||
Dep.prototype.setup.call(this);
|
||||
public function process(Entity $entity, Params $params): void
|
||||
{
|
||||
if (!$entity->get('providerId')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.createField('mode', 'views/fields/enum', {
|
||||
options: this.getMetadata().get(['clientDefs', 'Calendar', 'sharedViewModeList']) || [],
|
||||
translation: 'DashletOptions.options.mode'
|
||||
}, null, null, {
|
||||
labelText: this.translate('mode', 'fields', 'DashletOptions')
|
||||
});
|
||||
$provider = $entity->getProvider();
|
||||
|
||||
this.createField('name', 'views/fields/varchar', {
|
||||
required: true
|
||||
}, null, null, {
|
||||
labelText: this.translate('name', 'fields')
|
||||
});
|
||||
$scope = null;
|
||||
|
||||
this.createField('teams', 'crm:views/calendar/fields/teams', {
|
||||
required: true
|
||||
}, null, null, {
|
||||
labelText: this.translate('teams', 'fields'),
|
||||
foreignScope: 'Team'
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
if ($provider->getScopes()) {
|
||||
$scope = implode($provider->getScopeSeparator() ?? ' ', $provider->getScopes());
|
||||
}
|
||||
|
||||
$data = [
|
||||
'endpoint' => $provider->getAuthorizationEndpoint(),
|
||||
'clientId' => $provider->getClientId(),
|
||||
'redirectUri' => $this->configDataProvider->getRedirectUri(),
|
||||
'scope' => $scope,
|
||||
'prompt' => $provider->getAuthorizationPrompt(),
|
||||
'params' => $provider->getAuthorizationParams(),
|
||||
];
|
||||
|
||||
$entity->set('data', $data);
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\FieldProcessing\OAuthProvider;
|
||||
|
||||
use Espo\Core\FieldProcessing\Loader;
|
||||
use Espo\Core\FieldProcessing\Loader\Params;
|
||||
use Espo\Entities\OAuthProvider;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Tools\OAuth\ConfigDataProvider;
|
||||
|
||||
/**
|
||||
* @implements Loader<OAuthProvider>
|
||||
*/
|
||||
class AuthorizationRedirectUriLoader implements Loader
|
||||
{
|
||||
public function __construct(
|
||||
private ConfigDataProvider $configDataProvider,
|
||||
) {}
|
||||
|
||||
public function process(Entity $entity, Params $params): void
|
||||
{
|
||||
$entity->set('authorizationRedirectUri', $this->configDataProvider->getRedirectUri());
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ namespace Espo\Classes\FieldValidators;
|
||||
use Espo\Core\Field\Currency;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\ORM\BaseEntity;
|
||||
use Espo\ORM\Defs\Params\AttributeParam;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class CurrencyType extends FloatType
|
||||
@@ -83,7 +84,7 @@ class CurrencyType extends FloatType
|
||||
}
|
||||
|
||||
/** @var int $precision */
|
||||
$precision = $entity->getAttributeParam($field, 'precision') ?? self::DEFAULT_PRECISION;
|
||||
$precision = $entity->getAttributeParam($field, AttributeParam::PRECISION) ?? self::DEFAULT_PRECISION;
|
||||
|
||||
$value = $entity->get($field);
|
||||
|
||||
@@ -95,6 +96,8 @@ class CurrencyType extends FloatType
|
||||
|
||||
$pad = str_pad('', $precision, '9');
|
||||
|
||||
assert(is_numeric($pad));
|
||||
|
||||
$limit = Currency::create($pad, 'USD');
|
||||
|
||||
if ($currency->compare($limit) === 1) {
|
||||
|
||||
@@ -41,7 +41,6 @@ use Espo\Entities\ArrayValue;
|
||||
use Espo\Entities\Attachment;
|
||||
use Espo\Entities\AuthLogRecord;
|
||||
use Espo\Entities\AuthToken;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\Job;
|
||||
use Espo\Entities\Note;
|
||||
use Espo\Entities\Notification;
|
||||
@@ -98,7 +97,6 @@ class Cleanup implements JobDataLess
|
||||
$this->cleanupJobs();
|
||||
$this->cleanupScheduledJobLog();
|
||||
$this->cleanupAttachments();
|
||||
$this->cleanupEmails();
|
||||
$this->cleanupNotifications();
|
||||
$this->cleanupActionHistory();
|
||||
$this->cleanupAuthToken();
|
||||
@@ -149,7 +147,7 @@ class Cleanup implements JobDataLess
|
||||
->where([
|
||||
'modifiedAt<' => $this->getCleanupJobFromDate(),
|
||||
'status=' => JobStatus::PENDING,
|
||||
'deleted' => true,
|
||||
Attribute::DELETED => true,
|
||||
])
|
||||
->build();
|
||||
|
||||
@@ -412,7 +410,7 @@ class Cleanup implements JobDataLess
|
||||
->from($scope)
|
||||
->withDeleted()
|
||||
->where([
|
||||
'deleted' => true,
|
||||
Attribute::DELETED => true,
|
||||
'modifiedAt<' => $datetime->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT),
|
||||
'modifiedAt>' => $datetimeFrom->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT),
|
||||
])
|
||||
@@ -465,7 +463,7 @@ class Cleanup implements JobDataLess
|
||||
->delete()
|
||||
->from(Attachment::ENTITY_TYPE)
|
||||
->where([
|
||||
'deleted' => true,
|
||||
Attribute::DELETED => true,
|
||||
'createdAt<' => $datetime->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT),
|
||||
])
|
||||
->build();
|
||||
@@ -473,68 +471,6 @@ class Cleanup implements JobDataLess
|
||||
$this->entityManager->getQueryExecutor()->execute($delete);
|
||||
}
|
||||
|
||||
private function cleanupEmails(): void
|
||||
{
|
||||
$dateBefore = date(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT, time() - 3600 * 24 * 20);
|
||||
|
||||
$query = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->select()
|
||||
->from(Email::ENTITY_TYPE)
|
||||
->withDeleted()
|
||||
->build();
|
||||
|
||||
/** @var iterable<Email> $emails */
|
||||
$emails = $this->entityManager
|
||||
->getRDBRepository(Email::ENTITY_TYPE)
|
||||
->clone($query)
|
||||
->sth()
|
||||
->select([Attribute::ID])
|
||||
->where([
|
||||
'createdAt<' => $dateBefore,
|
||||
'deleted' => true,
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($emails as $email) {
|
||||
$id = $email->getId();
|
||||
|
||||
$attachments = $this->entityManager
|
||||
->getRDBRepository(Attachment::ENTITY_TYPE)
|
||||
->where([
|
||||
'parentId' => $id,
|
||||
'parentType' => Email::ENTITY_TYPE,
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
$this->entityManager->removeEntity($attachment);
|
||||
}
|
||||
|
||||
$delete = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->delete()
|
||||
->from(Email::ENTITY_TYPE)
|
||||
->where([
|
||||
'deleted' => true,
|
||||
'id' => $id,
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($delete);
|
||||
|
||||
$delete = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->delete()
|
||||
->from(Email::RELATIONSHIP_EMAIL_USER)
|
||||
->where([
|
||||
'emailId' => $id,
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($delete);
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanupNotifications(): void
|
||||
{
|
||||
@@ -584,17 +520,16 @@ class Cleanup implements JobDataLess
|
||||
{
|
||||
$scope = $entity->getEntityType();
|
||||
|
||||
if (!$entity->get('deleted')) {
|
||||
if (!$entity->get(Attribute::DELETED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$repository = $this->entityManager->getRepository($scope);
|
||||
|
||||
if (!$repository instanceof RDBRepository) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
if (
|
||||
!$repository instanceof RDBRepository ||
|
||||
!$entity instanceof CoreEntity
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -647,82 +582,14 @@ class Cleanup implements JobDataLess
|
||||
}
|
||||
}
|
||||
|
||||
$query = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->select()
|
||||
->from(Note::ENTITY_TYPE)
|
||||
->withDeleted()
|
||||
->build();
|
||||
|
||||
$noteList = $this->entityManager
|
||||
->getRDBRepository(Note::ENTITY_TYPE)
|
||||
->clone($query)
|
||||
->sth()
|
||||
->where([
|
||||
'OR' => [
|
||||
[
|
||||
'relatedType' => $scope,
|
||||
'relatedId' => $entity->getId(),
|
||||
],
|
||||
[
|
||||
'parentType' => $scope,
|
||||
'parentId' => $entity->getId(),
|
||||
]
|
||||
]
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($noteList as $note) {
|
||||
$this->entityManager->removeEntity($note);
|
||||
|
||||
$note->set('deleted', true);
|
||||
|
||||
$this->cleanupDeletedEntity($note);
|
||||
}
|
||||
$this->cleanupEntityNotes($entity);
|
||||
$this->cleanupEntityAttachments($entity);
|
||||
|
||||
if ($scope === Note::ENTITY_TYPE) {
|
||||
$attachmentList = $this->entityManager
|
||||
->getRDBRepository(Attachment::ENTITY_TYPE)
|
||||
->where([
|
||||
'parentId' => $entity->getId(),
|
||||
'parentType' => Note::ENTITY_TYPE,
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($attachmentList as $attachment) {
|
||||
$this->entityManager->removeEntity($attachment);
|
||||
$this->entityManager
|
||||
->getRDBRepository(Attachment::ENTITY_TYPE)
|
||||
->deleteFromDb($attachment->getId());
|
||||
}
|
||||
|
||||
// @todo If ever reactions are supported not only for notes, then move out of the if-block.
|
||||
|
||||
$deleteReactionsQuery = DeleteBuilder::create()
|
||||
->from(UserReaction::ENTITY_TYPE)
|
||||
->where([
|
||||
'parentId' => $entity->getId(),
|
||||
'parentType' => Note::ENTITY_TYPE,
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($deleteReactionsQuery);
|
||||
$this->cleanupNoteReactions($entity);
|
||||
}
|
||||
|
||||
$arrayValueList = $this->entityManager
|
||||
->getRDBRepository(ArrayValue::ENTITY_TYPE)
|
||||
->sth()
|
||||
->where([
|
||||
'entityType' => $entity->getEntityType(),
|
||||
'entityId' => $entity->getId(),
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($arrayValueList as $arrayValue) {
|
||||
$this->entityManager
|
||||
->getRDBRepository(ArrayValue::ENTITY_TYPE)
|
||||
->deleteFromDb($arrayValue->getId());
|
||||
}
|
||||
$this->cleanupEntityArrayValues($entity);
|
||||
}
|
||||
|
||||
private function cleanupDeletedRecords(): void
|
||||
@@ -759,13 +626,13 @@ class Cleanup implements JobDataLess
|
||||
|
||||
$service = $this->recordServiceContainer->get($scope);
|
||||
|
||||
$whereClause = ['deleted' => true];
|
||||
$whereClause = [Attribute::DELETED => true];
|
||||
|
||||
if (
|
||||
!$this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($scope)
|
||||
->hasAttribute('deleted')
|
||||
->hasAttribute(Attribute::DELETED)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -816,4 +683,96 @@ class Cleanup implements JobDataLess
|
||||
|
||||
return $datetime;
|
||||
}
|
||||
|
||||
private function cleanupEntityAttachments(CoreEntity $entity): void
|
||||
{
|
||||
// @todo Add file, image types support.
|
||||
|
||||
$attachments = $this->entityManager
|
||||
->getRDBRepository(Attachment::ENTITY_TYPE)
|
||||
->where([
|
||||
'parentId' => $entity->getId(),
|
||||
'parentType' => $entity->getEntityType(),
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
$this->entityManager->removeEntity($attachment);
|
||||
|
||||
$this->entityManager
|
||||
->getRDBRepository(Attachment::ENTITY_TYPE)
|
||||
->deleteFromDb($attachment->getId());
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanupEntityNotes(CoreEntity $entity): void
|
||||
{
|
||||
$scope = $entity->getEntityType();
|
||||
|
||||
$query = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->select()
|
||||
->from(Note::ENTITY_TYPE)
|
||||
->withDeleted()
|
||||
->build();
|
||||
|
||||
$noteList = $this->entityManager
|
||||
->getRDBRepository(Note::ENTITY_TYPE)
|
||||
->clone($query)
|
||||
->sth()
|
||||
->where([
|
||||
'OR' => [
|
||||
[
|
||||
'relatedType' => $scope,
|
||||
'relatedId' => $entity->getId(),
|
||||
],
|
||||
[
|
||||
'parentType' => $scope,
|
||||
'parentId' => $entity->getId(),
|
||||
]
|
||||
]
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($noteList as $note) {
|
||||
$this->entityManager->removeEntity($note);
|
||||
|
||||
$note->set(Attribute::DELETED, true);
|
||||
|
||||
$this->cleanupDeletedEntity($note);
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanupNoteReactions(CoreEntity $entity): void
|
||||
{
|
||||
// @todo If ever reactions are supported not only for notes, then move out of the if-block.
|
||||
|
||||
$deleteReactionsQuery = DeleteBuilder::create()
|
||||
->from(UserReaction::ENTITY_TYPE)
|
||||
->where([
|
||||
'parentId' => $entity->getId(),
|
||||
'parentType' => Note::ENTITY_TYPE,
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($deleteReactionsQuery);
|
||||
}
|
||||
|
||||
private function cleanupEntityArrayValues(CoreEntity $entity): void
|
||||
{
|
||||
$arrayValues = $this->entityManager
|
||||
->getRDBRepository(ArrayValue::ENTITY_TYPE)
|
||||
->sth()
|
||||
->where([
|
||||
'entityType' => $entity->getEntityType(),
|
||||
'entityId' => $entity->getId(),
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($arrayValues as $arrayValue) {
|
||||
$this->entityManager
|
||||
->getRDBRepository(ArrayValue::ENTITY_TYPE)
|
||||
->deleteFromDb($arrayValue->getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\Record\OAuthProvider;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Record\Input\Data;
|
||||
use Espo\Core\Record\Input\Filter;
|
||||
use Espo\Core\Utils\Crypt;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class GeneralFilter implements Filter
|
||||
{
|
||||
private const ATTR_CLIENT_SECRET = 'clientSecret';
|
||||
|
||||
public function __construct(private Crypt $crypt) {}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function filter(Data $data): void
|
||||
{
|
||||
$this->processClientSecret($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function processClientSecret(Data $data): void
|
||||
{
|
||||
$value = $data->get(self::ATTR_CLIENT_SECRET);
|
||||
|
||||
if ($value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_string($value)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$data->set(self::ATTR_CLIENT_SECRET, $this->crypt->encrypt($value));
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
namespace Espo\Classes\RecordHooks\Email;
|
||||
|
||||
use Espo\Core\Mail\Sender;
|
||||
use Espo\Core\Mail\EmailSender;
|
||||
use Espo\Core\Record\Hook\SaveHook;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\ORM\Entity;
|
||||
@@ -42,7 +42,7 @@ class BeforeCreate implements SaveHook
|
||||
public function process(Entity $entity): void
|
||||
{
|
||||
if ($entity->getStatus() === Email::STATUS_SENDING) {
|
||||
$messageId = Sender::generateMessageId($entity);
|
||||
$messageId = EmailSender::generateMessageId($entity);
|
||||
|
||||
$entity->setMessageId('<' . $messageId . '>');
|
||||
}
|
||||
|
||||
@@ -33,13 +33,13 @@ use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Record\Hook\SaveHook;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Tools\Email\Util;
|
||||
|
||||
/**
|
||||
* @implements SaveHook<Email>
|
||||
*/
|
||||
class BeforeSave implements SaveHook
|
||||
{
|
||||
|
||||
public function process(Entity $entity): void
|
||||
{
|
||||
if (
|
||||
@@ -49,5 +49,22 @@ class BeforeSave implements SaveHook
|
||||
) {
|
||||
throw new BadRequest("Cannot set send-at if status is not Draft.");
|
||||
}
|
||||
|
||||
$this->processBodyPlain($entity);
|
||||
}
|
||||
|
||||
private function processBodyPlain(Email $entity): void
|
||||
{
|
||||
if (!$entity->isHtml() || !$entity->isAttributeChanged('body')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$body = $entity->getBody();
|
||||
|
||||
if ($body) {
|
||||
$body = Util::stripHtml($body) ?: null;
|
||||
}
|
||||
|
||||
$entity->setBodyPlain($body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
namespace Espo\Classes\RecordHooks\Email;
|
||||
|
||||
use Espo\Core\Mail\Sender;
|
||||
use Espo\Core\Mail\EmailSender;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\Record\Hook\SaveHook;
|
||||
use Espo\Core\Utils\FieldUtil;
|
||||
@@ -94,7 +94,7 @@ class BeforeUpdate implements SaveHook
|
||||
}
|
||||
|
||||
if ($entity->getStatus() == Email::STATUS_SENDING) {
|
||||
$messageId = Sender::generateMessageId($entity);
|
||||
$messageId = EmailSender::generateMessageId($entity);
|
||||
|
||||
$entity->setMessageId('<' . $messageId . '>');
|
||||
}
|
||||
|
||||
@@ -201,11 +201,16 @@ class InFolder implements ItemConverter
|
||||
return WhereClause::fromRaw([
|
||||
'groupFolderId' => $groupFolderId,
|
||||
'groupStatusFolder' => null,
|
||||
'createdById!=' => $this->user->getId(),
|
||||
'fromEmailAddressId!=' => $this->getEmailAddressIdList(),
|
||||
'status' => [
|
||||
Email::STATUS_ARCHIVED,
|
||||
Email::STATUS_SENT,
|
||||
],
|
||||
'OR' => [
|
||||
'status' => Email::STATUS_ARCHIVED,
|
||||
'createdById!=' => $this->user->getId(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
namespace Espo\Classes\Select\EmailAddress\PrimaryFilters;
|
||||
|
||||
use Espo\Core\Select\Primary\Filter;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
|
||||
class Orphan implements Filter
|
||||
@@ -42,24 +43,24 @@ class Orphan implements Filter
|
||||
'EntityEmailAddress',
|
||||
'entityEmailAddress',
|
||||
[
|
||||
'emailAddressId:' => 'id',
|
||||
'deleted' => false,
|
||||
'emailAddressId:' => Attribute::ID,
|
||||
Attribute::DELETED => false,
|
||||
]
|
||||
)
|
||||
->leftJoin(
|
||||
'EmailEmailAddress',
|
||||
'emailEmailAddress',
|
||||
[
|
||||
'emailAddressId:' => 'id',
|
||||
'deleted' => false,
|
||||
'emailAddressId:' => Attribute::ID,
|
||||
Attribute::DELETED => false,
|
||||
]
|
||||
)
|
||||
->leftJoin(
|
||||
'Email',
|
||||
'email',
|
||||
[
|
||||
'fromEmailAddressId:' => 'id',
|
||||
'deleted' => false,
|
||||
'fromEmailAddressId:' => Attribute::ID,
|
||||
Attribute::DELETED => false,
|
||||
]
|
||||
)
|
||||
->where([
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
namespace Espo\Classes\Select\PhoneNumber\PrimaryFilters;
|
||||
|
||||
use Espo\Core\Select\Primary\Filter;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
|
||||
class Orphan implements Filter
|
||||
@@ -44,8 +45,8 @@ class Orphan implements Filter
|
||||
'EntityPhoneNumber',
|
||||
'entityPhoneNumber',
|
||||
[
|
||||
'phoneNumberId:' => 'id',
|
||||
'deleted' => false,
|
||||
'phoneNumberId:' => Attribute::ID,
|
||||
Attribute::DELETED => false,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -32,30 +32,26 @@ namespace Espo\Classes\TemplateHelpers;
|
||||
use Espo\Core\Htmlizer\Helper;
|
||||
use Espo\Core\Htmlizer\Helper\Data;
|
||||
use Espo\Core\Htmlizer\Helper\Result;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Log;
|
||||
use const CURLOPT_FOLLOWLOCATION;
|
||||
use const CURLOPT_HEADER;
|
||||
use const CURLOPT_HTTPHEADER;
|
||||
use const CURLOPT_RETURNTRANSFER;
|
||||
use const CURLOPT_TIMEOUT;
|
||||
use const CURLOPT_URL;
|
||||
use const CURLOPT_USERAGENT;
|
||||
|
||||
class GoogleMaps implements Helper
|
||||
{
|
||||
private const DEFAULT_SIZE = '400x400';
|
||||
|
||||
private $metadata;
|
||||
|
||||
private $config;
|
||||
|
||||
private $log;
|
||||
|
||||
public function __construct(
|
||||
Metadata $metadata,
|
||||
Config $config,
|
||||
Log $log
|
||||
) {
|
||||
$this->metadata = $metadata;
|
||||
$this->config = $config;
|
||||
$this->log = $log;
|
||||
}
|
||||
private Metadata $metadata,
|
||||
private Config $config,
|
||||
private Log $log,
|
||||
) {}
|
||||
|
||||
public function render(Data $data): Result
|
||||
{
|
||||
@@ -194,6 +190,7 @@ class GoogleMaps implements Helper
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $url
|
||||
* @return string|bool
|
||||
*/
|
||||
private function getImage(string $url)
|
||||
@@ -207,13 +204,13 @@ class GoogleMaps implements Helper
|
||||
|
||||
$c = curl_init();
|
||||
|
||||
curl_setopt($c, \CURLOPT_URL, $url);
|
||||
curl_setopt($c, \CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($c, \CURLOPT_HEADER, 0);
|
||||
curl_setopt($c, \CURLOPT_USERAGENT, $agent);
|
||||
curl_setopt($c, \CURLOPT_TIMEOUT, 10);
|
||||
curl_setopt($c, \CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($c, \CURLOPT_FOLLOWLOCATION, 1);
|
||||
curl_setopt($c, CURLOPT_URL, $url);
|
||||
curl_setopt($c, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($c, CURLOPT_HEADER, false);
|
||||
curl_setopt($c, CURLOPT_USERAGENT, $agent);
|
||||
curl_setopt($c, CURLOPT_TIMEOUT, 10);
|
||||
curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($c, CURLOPT_FOLLOWLOCATION, true);
|
||||
|
||||
$raw = curl_exec($c);
|
||||
|
||||
|
||||
@@ -30,31 +30,33 @@
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
|
||||
use Espo\Core\Exceptions\NotFoundSilent;
|
||||
use Espo\Tools\Formula\Service;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\Core\Field\LinkParent;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class Formula
|
||||
{
|
||||
private Service $service;
|
||||
|
||||
public function __construct(Service $service, User $user)
|
||||
{
|
||||
$this->service = $service;
|
||||
|
||||
/**
|
||||
* @throws ForbiddenSilent
|
||||
*/
|
||||
public function __construct(
|
||||
private Service $service,
|
||||
User $user,
|
||||
) {
|
||||
if (!$user->isAdmin()) {
|
||||
throw new ForbiddenSilent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionCheckSyntax(Request $request): stdClass
|
||||
{
|
||||
$expression = $request->getParsedBody()->expression ?? null;
|
||||
@@ -66,6 +68,10 @@ class Formula
|
||||
return $this->service->checkSyntax($expression)->toStdClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws NotFoundSilent
|
||||
*/
|
||||
public function postActionRun(Request $request): stdClass
|
||||
{
|
||||
$expression = $request->getParsedBody()->expression ?? null;
|
||||
|
||||
@@ -29,31 +29,34 @@
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Services\Integration as Service;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Api\Request;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class Integration
|
||||
{
|
||||
private $service;
|
||||
|
||||
private $user;
|
||||
|
||||
public function __construct(Service $service, User $user)
|
||||
{
|
||||
$this->service = $service;
|
||||
$this->user = $user;
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function __construct(
|
||||
private Service $service,
|
||||
private User $user,
|
||||
) {
|
||||
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function getActionRead(Request $request): stdClass
|
||||
{
|
||||
/** @var string $id */
|
||||
@@ -64,6 +67,10 @@ class Integration
|
||||
return $entity->getValueMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function putActionUpdate(Request $request): stdClass
|
||||
{
|
||||
/** @var string $id */
|
||||
|
||||
@@ -35,6 +35,9 @@ use Espo\Tools\ActionHistory\Service as Service;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class LastViewed
|
||||
{
|
||||
public function __construct(private SearchParamsFetcher $searchParamsFetcher, private Service $service)
|
||||
@@ -49,9 +52,6 @@ class LastViewed
|
||||
|
||||
$result = $this->service->getLastViewed($maxSize, $offset);
|
||||
|
||||
return (object) [
|
||||
'total' => $result->getTotal(),
|
||||
'list' => $result->getValueMapList(),
|
||||
];
|
||||
return $result->toApiOutput();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,10 +83,7 @@ class Notification extends RecordBase
|
||||
|
||||
$recordCollection = $this->getNotificationService()->get($userId, $searchParams);
|
||||
|
||||
return (object) [
|
||||
'total' => $recordCollection->getTotal(),
|
||||
'list' => $recordCollection->getValueMapList(),
|
||||
];
|
||||
return $recordCollection->toApiOutput();
|
||||
}
|
||||
|
||||
public function getActionNotReadCount(): int
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Controllers\RecordBase;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class OAuthAccount extends RecordBase
|
||||
{
|
||||
protected function checkAccess(): bool
|
||||
{
|
||||
if (!$this->user->isAdmin()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Controllers\Record;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class OAuthProvider extends Record
|
||||
{
|
||||
protected function checkAccess(): bool
|
||||
{
|
||||
if (!$this->user->isAdmin()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -79,12 +79,12 @@ class Stream
|
||||
|
||||
$reactionsCheckDate = DateTime::createNow();
|
||||
|
||||
return (object) [
|
||||
'total' => $collection->getTotal(),
|
||||
'list' => $collection->getValueMapList(),
|
||||
'reactionsCheckDate' => $reactionsCheckDate->toString(),
|
||||
'updatedReactions' => $this->getReactionUpdates($request, $id),
|
||||
];
|
||||
$output = $collection->toApiOutput();
|
||||
|
||||
$output->reactionsCheckDate = $reactionsCheckDate->toString();
|
||||
$output->updatedReactions = $this->getReactionUpdates($request, $id);
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
if ($id === null) {
|
||||
@@ -94,11 +94,11 @@ class Stream
|
||||
$collection = $this->service->find($scope, $id, $searchParams);
|
||||
$pinnedCollection = $this->service->getPinned($scope, $id);
|
||||
|
||||
return (object) [
|
||||
'total' => $collection->getTotal(),
|
||||
'list' => $collection->getValueMapList(),
|
||||
'pinnedList' => $pinnedCollection->getValueMapList(),
|
||||
];
|
||||
$output = $collection->toApiOutput();
|
||||
|
||||
$output->pinnedList = $pinnedCollection->getValueMapList();
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,10 +126,7 @@ class Stream
|
||||
$this->userRecordService->find($id, $searchParams) :
|
||||
$this->service->find($scope, $id ?? '', $searchParams);
|
||||
|
||||
return (object) [
|
||||
'total' => $result->getTotal(),
|
||||
'list' => $result->getValueMapList(),
|
||||
];
|
||||
return $result->toApiOutput();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,10 +147,7 @@ class Stream
|
||||
|
||||
$result = $this->service->findUpdates($scope, $id, $searchParams);
|
||||
|
||||
return (object) [
|
||||
'total' => $result->getTotal(),
|
||||
'list' => $result->getValueMapList(),
|
||||
];
|
||||
return $result->toApiOutput();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,11 +31,10 @@ namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Controllers\RecordBase;
|
||||
|
||||
use Espo\Core\Controllers\Record;
|
||||
use stdClass;
|
||||
|
||||
class Webhook extends RecordBase
|
||||
class Webhook extends Record
|
||||
{
|
||||
protected function checkAccess(): bool
|
||||
{
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Controllers\RecordBase;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class WebhookEventQueueItem extends RecordBase
|
||||
{
|
||||
protected function checkAccess(): bool
|
||||
{
|
||||
if (!$this->user->isAdmin()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function postActionCreate(Request $request, Response $response): stdClass
|
||||
{
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
public function putActionUpdate(Request $request, Response $response): stdClass
|
||||
{
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,11 @@ namespace Espo\Core\Acl;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
/**
|
||||
* Bindings:
|
||||
* - `$entityType` – as of v9.1.0.
|
||||
* - `Espo\Core\AclManager`
|
||||
*/
|
||||
interface AccessChecker
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -58,7 +58,7 @@ class AccessCheckerFactory
|
||||
{
|
||||
$className = $this->getClassName($scope);
|
||||
|
||||
$bindingContainer = $this->createBindingContainer($aclManager);
|
||||
$bindingContainer = $this->createBindingContainer($className, $aclManager, $scope);
|
||||
|
||||
return $this->injectableFactory->createWithBinding($className, $bindingContainer);
|
||||
}
|
||||
@@ -77,19 +77,27 @@ class AccessCheckerFactory
|
||||
}
|
||||
|
||||
if (!$this->metadata->get(['scopes', $scope])) {
|
||||
throw new NotImplemented("Access checker is not implemented for '{$scope}'.");
|
||||
throw new NotImplemented("Access checker is not implemented for '$scope'.");
|
||||
}
|
||||
|
||||
return $this->defaultClassName;
|
||||
}
|
||||
|
||||
private function createBindingContainer(AclManager $aclManager): BindingContainer
|
||||
/**
|
||||
* @param class-string<AccessChecker> $className
|
||||
*/
|
||||
private function createBindingContainer(string $className, AclManager $aclManager, string $scope): BindingContainer
|
||||
{
|
||||
$bindingData = new BindingData();
|
||||
|
||||
$binder = new Binder($bindingData);
|
||||
|
||||
$binder->bindInstance(AclManager::class, $aclManager);
|
||||
|
||||
$binder
|
||||
->for($className)
|
||||
->bindValue('$entityType', $scope);
|
||||
|
||||
return new BindingContainer($bindingData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
namespace Espo\Core\Acl;
|
||||
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Config\SystemConfig;
|
||||
use Espo\Core\Utils\DataCache;
|
||||
use Espo\Core\Utils\FieldUtil;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
@@ -91,10 +91,10 @@ class GlobalRestriction
|
||||
private Metadata $metadata,
|
||||
private DataCache $dataCache,
|
||||
private FieldUtil $fieldUtil,
|
||||
Config $config
|
||||
SystemConfig $systemConfig,
|
||||
) {
|
||||
|
||||
$useCache = $config->get('useCache');
|
||||
$useCache = $systemConfig->useCache();
|
||||
|
||||
if ($useCache && $this->dataCache->has($this->cacheKey)) {
|
||||
/** @var stdClass $cachedData */
|
||||
|
||||
@@ -58,6 +58,9 @@ class DataBuilder
|
||||
public function __construct(private MetadataProvider $metadataProvider, private FieldUtil $fieldUtil)
|
||||
{}
|
||||
|
||||
/**
|
||||
* @return stdClass&object{table: stdClass, fieldTable: stdClass}
|
||||
*/
|
||||
public function build(Table $table): stdClass
|
||||
{
|
||||
$data = (object) [
|
||||
|
||||
@@ -57,14 +57,14 @@ class Map
|
||||
public function __construct(
|
||||
Table $table,
|
||||
private DataBuilder $dataBuilder,
|
||||
private Config $config,
|
||||
private DataCache $dataCache,
|
||||
CacheKeyProvider $cacheKeyProvider
|
||||
CacheKeyProvider $cacheKeyProvider,
|
||||
Config\SystemConfig $systemConfig,
|
||||
) {
|
||||
|
||||
$this->cacheKey = $cacheKeyProvider->get();
|
||||
|
||||
if ($this->config->get('useCache') && $this->dataCache->has($this->cacheKey)) {
|
||||
if ($systemConfig->useCache() && $this->dataCache->has($this->cacheKey)) {
|
||||
/** @var stdClass $cachedData */
|
||||
$cachedData = $this->dataCache->get($this->cacheKey);
|
||||
|
||||
@@ -72,7 +72,7 @@ class Map
|
||||
} else {
|
||||
$this->data = $this->dataBuilder->build($table);
|
||||
|
||||
if ($this->config->get('useCache')) {
|
||||
if ($systemConfig->useCache()) {
|
||||
$this->dataCache->store($this->cacheKey, $this->data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,4 +29,9 @@
|
||||
|
||||
namespace Espo\Core\Acl;
|
||||
|
||||
/**
|
||||
* Bindings:
|
||||
* - `$entityType` – as of v9.1.0.
|
||||
* - `Espo\Core\AclManager`
|
||||
*/
|
||||
interface OwnershipChecker {}
|
||||
|
||||
@@ -58,7 +58,7 @@ class OwnershipCheckerFactory
|
||||
{
|
||||
$className = $this->getClassName($scope);
|
||||
|
||||
$bindingContainer = $this->createBindingContainer($aclManager);
|
||||
$bindingContainer = $this->createBindingContainer($className, $aclManager, $scope);
|
||||
|
||||
return $this->injectableFactory->createWithBinding($className, $bindingContainer);
|
||||
}
|
||||
@@ -83,7 +83,10 @@ class OwnershipCheckerFactory
|
||||
return $this->defaultClassName;
|
||||
}
|
||||
|
||||
private function createBindingContainer(AclManager $aclManager): BindingContainer
|
||||
/**
|
||||
* @param class-string<OwnershipChecker> $className
|
||||
*/
|
||||
private function createBindingContainer(string $className, AclManager $aclManager, string $scope): BindingContainer
|
||||
{
|
||||
$bindingData = new BindingData();
|
||||
|
||||
@@ -91,6 +94,10 @@ class OwnershipCheckerFactory
|
||||
|
||||
$binder->bindInstance(AclManager::class, $aclManager);
|
||||
|
||||
$binder
|
||||
->for($className)
|
||||
->bindValue('$entityType', $scope);
|
||||
|
||||
return new BindingContainer($bindingData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,15 +29,14 @@
|
||||
|
||||
namespace Espo\Core\Acl\Table;
|
||||
|
||||
use Espo\Core\Utils\Config\SystemConfig;
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\Core\Acl\FieldData;
|
||||
use Espo\Core\Acl\ScopeData;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\DataCache;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
use stdClass;
|
||||
use RuntimeException;
|
||||
|
||||
@@ -96,7 +95,7 @@ class DefaultTable implements Table
|
||||
private RoleListProvider $roleListProvider,
|
||||
CacheKeyProvider $cacheKeyProvider,
|
||||
protected User $user,
|
||||
Config $config,
|
||||
SystemConfig $systemConfig,
|
||||
protected Metadata $metadata,
|
||||
DataCache $dataCache,
|
||||
) {
|
||||
@@ -116,7 +115,7 @@ class DefaultTable implements Table
|
||||
|
||||
$this->cacheKey = $cacheKeyProvider->get();
|
||||
|
||||
if ($config->get('useCache') && $dataCache->has($this->cacheKey)) {
|
||||
if ($systemConfig->useCache() && $dataCache->has($this->cacheKey)) {
|
||||
/** @var stdClass $cachedData */
|
||||
$cachedData = $dataCache->get($this->cacheKey);
|
||||
|
||||
@@ -124,7 +123,7 @@ class DefaultTable implements Table
|
||||
} else {
|
||||
$this->load();
|
||||
|
||||
if ($config->get('useCache')) {
|
||||
if ($systemConfig->useCache()) {
|
||||
$dataCache->store($this->cacheKey, $this->data);
|
||||
}
|
||||
}
|
||||
@@ -219,7 +218,7 @@ class DefaultTable implements Table
|
||||
$this->applyAdminMandatory($aclTable, $fieldTable);
|
||||
}
|
||||
|
||||
foreach ($aclTable as $scope => $data) {
|
||||
foreach (get_object_vars($aclTable) as $scope => $data) {
|
||||
if (is_string($data) && isset($aclTable->$data)) {
|
||||
$aclTable->$scope = $aclTable->$data;
|
||||
}
|
||||
|
||||
@@ -308,11 +308,12 @@ class AclManager
|
||||
|
||||
$checker = $this->getAccessChecker($scope);
|
||||
|
||||
/** @var non-falsy-string $methodName */
|
||||
$methodName = 'checkEntity' . ucfirst($action);
|
||||
|
||||
$interface = $this->entityActionInterfaceMap[$action] ?? null;
|
||||
|
||||
if ($interface && $checker instanceof $interface) {
|
||||
if ($interface && $checker instanceof $interface && method_exists($checker, $methodName)) {
|
||||
return $checker->$methodName($user, $entity, $data);
|
||||
}
|
||||
|
||||
@@ -322,7 +323,7 @@ class AclManager
|
||||
/**
|
||||
* Check 'read' access to a specific entity.
|
||||
*
|
||||
* @throws NotImplemented.
|
||||
* @throws NotImplemented
|
||||
*/
|
||||
public function checkEntityRead(User $user, Entity $entity): bool
|
||||
{
|
||||
|
||||
@@ -47,6 +47,9 @@ use Espo\Entities\User;
|
||||
use Espo\Tools\Currency\Conversion\EntityConverterFactory;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class ConvertCurrency implements Action
|
||||
{
|
||||
public function __construct(
|
||||
|
||||
@@ -34,16 +34,19 @@ use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Action\Params;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Core\Record\ActionHistory\Action;
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\ObjectUtil;
|
||||
use Espo\Entities\Note;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\EmailAddress;
|
||||
use Espo\Entities\PhoneNumber;
|
||||
|
||||
use Espo\ORM\Type\RelationType;
|
||||
use stdClass;
|
||||
|
||||
class Merger
|
||||
@@ -94,12 +97,12 @@ class Merger
|
||||
$entityDefs = $this->entityManager->getDefs()->getEntity($entityType);
|
||||
|
||||
$hasPhoneNumber =
|
||||
$entityDefs->hasField('phoneNumber') &&
|
||||
$entityDefs->getField('phoneNumber')->getType() === FieldType::PHONE;
|
||||
$entityDefs->hasField(Field::PHONE_NUMBER) &&
|
||||
$entityDefs->getField(Field::PHONE_NUMBER)->getType() === FieldType::PHONE;
|
||||
|
||||
$hasEmailAddress =
|
||||
$entityDefs->hasField('emailAddress') &&
|
||||
$entityDefs->getField('emailAddress')->getType() === FieldType::EMAIL;
|
||||
$entityDefs->hasField(Field::EMAIL_ADDRESS) &&
|
||||
$entityDefs->getField(Field::EMAIL_ADDRESS)->getType() === FieldType::EMAIL;
|
||||
|
||||
if ($hasPhoneNumber) {
|
||||
$phoneNumberToRelateList = $this->fetchEntityPhoneNumberList($entity);
|
||||
@@ -196,7 +199,6 @@ class Merger
|
||||
|
||||
/** @var iterable<PhoneNumber> $collection */
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository($entity->getEntityType())
|
||||
->getRelation($entity, 'phoneNumbers')
|
||||
->find();
|
||||
|
||||
@@ -216,7 +218,6 @@ class Merger
|
||||
|
||||
/** @var iterable<EmailAddress> $collection */
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository($entity->getEntityType())
|
||||
->getRelation($entity, 'emailAddresses')
|
||||
->find();
|
||||
|
||||
@@ -232,13 +233,17 @@ class Merger
|
||||
$updateQuery = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->update()
|
||||
->in('Note')
|
||||
->in(Note::ENTITY_TYPE)
|
||||
->set([
|
||||
'parentId' => $targetEntity->getId(),
|
||||
'parentType' => $targetEntity->getEntityType(),
|
||||
])
|
||||
->where([
|
||||
'type' => ['Post', 'EmailSent', 'EmailReceived'],
|
||||
'type' => [
|
||||
Note::TYPE_POST,
|
||||
Note::TYPE_EMAIL_SENT,
|
||||
Note::TYPE_EMAIL_RECEIVED,
|
||||
],
|
||||
'parentId' => $sourceEntity->getId(),
|
||||
'parentType' => $sourceEntity->getEntityType(),
|
||||
])
|
||||
@@ -251,16 +256,24 @@ class Merger
|
||||
|
||||
private function updateRelations(Entity $sourceEntity, Entity $targetEntity, string $link): void
|
||||
{
|
||||
$repository = $this->entityManager->getRDBRepository($targetEntity->getEntityType());
|
||||
$entityType = $sourceEntity->getEntityType();
|
||||
|
||||
$collection = $repository
|
||||
$columnAttributeMap = $this->getLinkColumnAttributeMap($entityType, $link);
|
||||
|
||||
$collection = $this->entityManager
|
||||
->getRelation($sourceEntity, $link)
|
||||
->find();
|
||||
|
||||
foreach ($collection as $relatedEntity) {
|
||||
$repository
|
||||
$map = null;
|
||||
|
||||
if ($columnAttributeMap) {
|
||||
$map = array_map(fn ($attribute) => $relatedEntity->get($attribute), $columnAttributeMap);
|
||||
}
|
||||
|
||||
$this->entityManager
|
||||
->getRelation($targetEntity, $link)
|
||||
->relate($relatedEntity);
|
||||
->relate($relatedEntity, $map);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,4 +396,35 @@ class Merger
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?array<string, string>
|
||||
*/
|
||||
private function getLinkColumnAttributeMap(string $entityType, string $link): ?array
|
||||
{
|
||||
$entityDefs = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entityType);
|
||||
|
||||
$columnAttributeMap = null;
|
||||
|
||||
$relationDefs = $entityDefs->tryGetRelation($link);
|
||||
|
||||
if (
|
||||
$relationDefs &&
|
||||
$relationDefs->getType() === RelationType::MANY_MANY &&
|
||||
$relationDefs->hasForeignEntityType() &&
|
||||
$relationDefs->hasForeignRelationName()
|
||||
) {
|
||||
$foreignRelationDefs = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($relationDefs->getForeignEntityType())
|
||||
->getRelation($relationDefs->getForeignRelationName());
|
||||
|
||||
/** ?@var array<string, string> $columnAttributeMap */
|
||||
$columnAttributeMap = $foreignRelationDefs->getParam('columnAttributeMap');
|
||||
}
|
||||
|
||||
return $columnAttributeMap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Espo\Core\Action;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @immutable
|
||||
* Immutable.
|
||||
*/
|
||||
class Params
|
||||
{
|
||||
|
||||
@@ -30,14 +30,15 @@
|
||||
namespace Espo\Core\Api;
|
||||
|
||||
use Espo\Core\Api\Route\RouteParamsFetcher;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Config\SystemConfig;
|
||||
use Espo\Core\Utils\Route as RouteUtil;
|
||||
use Espo\Core\Utils\Log;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Slim\App as SlimApp;
|
||||
use Slim\Exception\HttpBadRequestException;
|
||||
use Slim\Factory\AppFactory as SlimAppFactory;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Psr7Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Psr7Request;
|
||||
|
||||
@@ -54,7 +55,7 @@ class Starter
|
||||
private RouteParamsFetcher $routeParamsFetcher,
|
||||
private MiddlewareProvider $middlewareProvider,
|
||||
private Log $log,
|
||||
private Config $config,
|
||||
private SystemConfig $systemConfig,
|
||||
?string $routeCacheFile = null
|
||||
) {
|
||||
$this->routeCacheFile = $routeCacheFile ?? $this->routeCacheFile;
|
||||
@@ -64,7 +65,13 @@ class Starter
|
||||
{
|
||||
$slim = SlimAppFactory::create();
|
||||
|
||||
if ($this->config->get('useCache')) {
|
||||
if (RouteUtil::isBadUri()) {
|
||||
$this->processError($slim);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->systemConfig->useCache()) {
|
||||
$slim->getRouteCollector()->setCacheFile($this->routeCacheFile);
|
||||
}
|
||||
|
||||
@@ -126,4 +133,22 @@ class Starter
|
||||
$slimRoute->addMiddleware($middleware);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SlimApp<ContainerInterface|null> $slim
|
||||
*/
|
||||
private function processError(SlimApp $slim): void
|
||||
{
|
||||
$slim->add(function (Psr7Request $request): Psr7Response {
|
||||
throw new HttpBadRequestException($request, 'Malformed request path.');
|
||||
});
|
||||
|
||||
$slim->addErrorMiddleware(
|
||||
displayErrorDetails: false,
|
||||
logErrors: false,
|
||||
logErrorDetails: false,
|
||||
);
|
||||
|
||||
$slim->run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,8 @@ class Util
|
||||
|
||||
public function obtainIpFromRequest(Request $request): ?string
|
||||
{
|
||||
// Do not add support of any more parameters here.
|
||||
|
||||
$param = $this->config->get('ipAddressServerParam') ?? 'REMOTE_ADDR';
|
||||
|
||||
return $request->getServerParam($param);
|
||||
|
||||
@@ -29,10 +29,15 @@
|
||||
|
||||
namespace Espo\Core\Application;
|
||||
|
||||
use Espo\Core\Application\Exceptions\RunnerException;
|
||||
|
||||
/**
|
||||
* Runs an application.
|
||||
*/
|
||||
interface Runner
|
||||
{
|
||||
/**
|
||||
* @throws RunnerException
|
||||
*/
|
||||
public function run(): void;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ use SensitiveParameter;
|
||||
/**
|
||||
* An auth token data. Used for auth token creation.
|
||||
*
|
||||
* @immutable
|
||||
* Immutable.
|
||||
*/
|
||||
class Data
|
||||
{
|
||||
|
||||
@@ -159,17 +159,6 @@ class EspoManager implements Manager
|
||||
{
|
||||
$length = self::TOKEN_RANDOM_LENGTH;
|
||||
|
||||
if (function_exists('random_bytes')) {
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
return bin2hex(random_bytes($length));
|
||||
}
|
||||
|
||||
if (function_exists('openssl_random_pseudo_bytes')) {
|
||||
$randomValue = openssl_random_pseudo_bytes($length);
|
||||
|
||||
return bin2hex($randomValue);
|
||||
}
|
||||
|
||||
throw new RuntimeException("Could not generate token.");
|
||||
return bin2hex(random_bytes($length));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ class Authentication
|
||||
|
||||
if (!$user->isAdmin() && $this->configDataProvider->isMaintenanceMode()) {
|
||||
throw ServiceUnavailable::createWithBody(
|
||||
"Application is in maintenance mod1e.",
|
||||
"Application is in maintenance mode.",
|
||||
Body::create()
|
||||
->withMessage($this->language->translateLabel('maintenanceModeError', 'messages'))
|
||||
);
|
||||
|
||||
@@ -29,8 +29,10 @@
|
||||
|
||||
namespace Espo\Core\Authentication;
|
||||
|
||||
use SensitiveParameter;
|
||||
|
||||
/**
|
||||
* @immutable
|
||||
* Immutable.
|
||||
*/
|
||||
class AuthenticationData
|
||||
{
|
||||
@@ -87,7 +89,7 @@ class AuthenticationData
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withPassword(?string $password): self
|
||||
public function withPassword(#[SensitiveParameter] ?string $password): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->password = $password;
|
||||
|
||||
@@ -34,7 +34,7 @@ use UnexpectedValueException;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @immutable
|
||||
* Immutable.
|
||||
*/
|
||||
class Rsa implements Key
|
||||
{
|
||||
|
||||
@@ -36,7 +36,7 @@ use RuntimeException;
|
||||
/**
|
||||
* JWT token.
|
||||
*
|
||||
* @immutable
|
||||
* Immutable.
|
||||
*/
|
||||
class Token
|
||||
{
|
||||
|
||||
@@ -35,7 +35,7 @@ use JsonException;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @immutable
|
||||
* Immutable.
|
||||
*/
|
||||
class Header
|
||||
{
|
||||
|
||||
@@ -35,7 +35,7 @@ use JsonException;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @immutable
|
||||
* Immutable.
|
||||
*/
|
||||
class Payload
|
||||
{
|
||||
|
||||
@@ -345,11 +345,10 @@ class LdapLogin implements Login
|
||||
private function createUser(array $userData, bool $isPortal = false): ?User
|
||||
{
|
||||
$this->log->info("LDAP: Creating new user.");
|
||||
$this->log->debug("LDAP: user data: {userData}", ['userData' => print_r($userData, true)]);
|
||||
|
||||
$data = [];
|
||||
|
||||
$this->log->debug("LDAP: user data: {userData}", ['userData' => print_r($userData, true)]);
|
||||
|
||||
$ldapFields = $this->loadFields('ldap');
|
||||
|
||||
foreach ($ldapFields as $espo => $ldap) {
|
||||
@@ -366,21 +365,20 @@ class LdapLogin implements Login
|
||||
}
|
||||
|
||||
if ($isPortal) {
|
||||
$userFields = $this->loadFields('portalUser');
|
||||
$userAttributes = $this->loadFields('portalUser');
|
||||
|
||||
$userFields['type'] = 'portal';
|
||||
$userAttributes['type'] = User::TYPE_PORTAL;
|
||||
} else {
|
||||
$userFields = $this->loadFields('user');
|
||||
$userAttributes = $this->loadFields('user');
|
||||
}
|
||||
|
||||
foreach ($userFields as $fieldName => $fieldValue) {
|
||||
$data[$fieldName] = $fieldValue;
|
||||
foreach ($userAttributes as $key => $value) {
|
||||
$data[$key] = $value;
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->entityManager->getNewEntity(User::ENTITY_TYPE);
|
||||
$user = $this->entityManager->getRDBRepositoryByClass(User::class)->getNew();
|
||||
|
||||
$user->set($data);
|
||||
$user->setMultiple($data);
|
||||
|
||||
$this->entityManager->saveEntity($user, [
|
||||
// Prevent `user` service being loaded by hooks.
|
||||
@@ -388,10 +386,7 @@ class LdapLogin implements Login
|
||||
SaveOption::KEEP_NEW => true,
|
||||
]);
|
||||
|
||||
$saverParams = SaverParams::create()
|
||||
->withRawOptions([
|
||||
'skipLinkMultipleHooks' => true,
|
||||
]);
|
||||
$saverParams = SaverParams::create()->withRawOptions(['skipLinkMultipleHooks' => true]);
|
||||
|
||||
$this->linkMultipleSaver->process($user, Field::TEAMS, $saverParams);
|
||||
$this->linkMultipleSaver->process($user, 'portals', $saverParams);
|
||||
@@ -402,31 +397,27 @@ class LdapLogin implements Login
|
||||
$user->setAsNotNew();
|
||||
$user->updateFetchedValues();
|
||||
|
||||
/** @var ?User */
|
||||
return $this->entityManager->getEntityById(User::ENTITY_TYPE, $user->getId());
|
||||
return $this->entityManager->getRDBRepositoryByClass(User::class)->getById($user->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Find LDAP user DN by his username.
|
||||
* Find LDAP user DN by their username.
|
||||
*
|
||||
* @throws LdapException
|
||||
*/
|
||||
private function findLdapUserDnByUsername(string $username): ?string
|
||||
{
|
||||
$ldapClient = $this->getLdapClient();
|
||||
|
||||
$options = $this->utils->getOptions();
|
||||
|
||||
$loginFilterString = '';
|
||||
$filterString = !empty($options['userLoginFilter']) ?
|
||||
$this->convertToFilterFormat($options['userLoginFilter']) : '';
|
||||
|
||||
if (!empty($options['userLoginFilter'])) {
|
||||
$loginFilterString = $this->convertToFilterFormat($options['userLoginFilter']);
|
||||
}
|
||||
$objectClass = $options['userObjectClass'];
|
||||
$attribute = $options['userNameAttribute'];
|
||||
$usernameEscaped = $this->escapeUsernameFilter($username);
|
||||
|
||||
$searchString =
|
||||
'(&(objectClass=' . $options['userObjectClass'] . ')' .
|
||||
'(' . $options['userNameAttribute'] . '=' . $username . ')' .
|
||||
$loginFilterString . ')';
|
||||
$searchString = "(&(objectClass=$objectClass)($attribute=$usernameEscaped)$filterString)";
|
||||
|
||||
/** @var array<int, array{dn: string}> $result */
|
||||
/** @noinspection PhpRedundantOptionalArgumentInspection */
|
||||
@@ -435,12 +426,25 @@ class LdapLogin implements Login
|
||||
$this->log->debug("LDAP: user search string: {string}.", ['string' => $searchString]);
|
||||
|
||||
foreach ($result as $item) {
|
||||
return $item["dn"];
|
||||
return $item['dn'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function escapeUsernameFilter(string $username): string
|
||||
{
|
||||
$map = [
|
||||
'\\' => '\\5c',
|
||||
'*' => '\\2a',
|
||||
'(' => '\\28',
|
||||
')' => '\\29',
|
||||
"\x00" => '\\00',
|
||||
];
|
||||
|
||||
return strtr($username, $map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and convert filter item into LDAP format.
|
||||
*/
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
namespace Espo\Core\Authentication\Login;
|
||||
|
||||
use Espo\Core\Authentication\AuthToken\AuthToken;
|
||||
use SensitiveParameter;
|
||||
|
||||
class DataBuilder
|
||||
{
|
||||
@@ -44,7 +45,7 @@ class DataBuilder
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setPassword(?string $password): self
|
||||
public function setPassword(#[SensitiveParameter] ?string $password): self
|
||||
{
|
||||
$this->password = $password;
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
namespace Espo\Core\Authentication\Login;
|
||||
|
||||
/**
|
||||
* @immutable
|
||||
* Immutable.
|
||||
*/
|
||||
class MetadataParams
|
||||
{
|
||||
|
||||
@@ -50,7 +50,7 @@ class Hmac implements Login
|
||||
{
|
||||
$authString = base64_decode($request->getHeader('X-Hmac-Authorization') ?? '');
|
||||
|
||||
list($apiKey, $hash) = explode(':', $authString, 2);
|
||||
[$apiKey, $hash] = explode(':', $authString, 2);
|
||||
|
||||
if (!$apiKey) {
|
||||
return Result::fail(FailReason::WRONG_CREDENTIALS);
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
namespace Espo\Core\Authentication\Logout;
|
||||
|
||||
/**
|
||||
* @immutable
|
||||
* Immutable.
|
||||
*/
|
||||
class Params
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
namespace Espo\Core\Authentication\Logout;
|
||||
|
||||
/**
|
||||
* @immutable
|
||||
* Immutable.
|
||||
*/
|
||||
class Result
|
||||
{
|
||||
|
||||
@@ -104,6 +104,11 @@ class ConfigDataProvider
|
||||
return $this->object->get('oidcTokenEndpoint');
|
||||
}
|
||||
|
||||
public function getUserInfoEndpoint(): ?string
|
||||
{
|
||||
return $this->object->get('oidcUserInfoEndpoint');
|
||||
}
|
||||
|
||||
public function getJwksEndpoint(): ?string
|
||||
{
|
||||
return $this->object->get('oidcJwksEndpoint');
|
||||
|
||||
@@ -33,7 +33,7 @@ use Espo\Core\Authentication\Jwt\Exceptions\UnsupportedKey;
|
||||
use Espo\Core\Authentication\Jwt\Key;
|
||||
use Espo\Core\Authentication\Jwt\KeyFactory;
|
||||
use Espo\Core\Field\DateTime;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Config\SystemConfig;
|
||||
use Espo\Core\Utils\DataCache;
|
||||
use Espo\Core\Utils\Json;
|
||||
use Espo\Core\Utils\Log;
|
||||
@@ -48,10 +48,10 @@ class KeysProvider
|
||||
|
||||
public function __construct(
|
||||
private DataCache $dataCache,
|
||||
private Config $config,
|
||||
private ConfigDataProvider $configDataProvider,
|
||||
private KeyFactory $factory,
|
||||
private Log $log
|
||||
private Log $log,
|
||||
private SystemConfig $systemConfig,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -147,7 +147,7 @@ class KeysProvider
|
||||
*/
|
||||
private function getRawFromCache(): ?array
|
||||
{
|
||||
if (!$this->config->get('useCache')) {
|
||||
if (!$this->systemConfig->useCache()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ class KeysProvider
|
||||
*/
|
||||
private function storeRawToCache(array $raw): void
|
||||
{
|
||||
if (!$this->config->get('useCache')) {
|
||||
if (!$this->systemConfig->useCache()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ use Espo\Core\Authentication\Logins\Espo;
|
||||
use Espo\Core\Authentication\Jwt\Exceptions\Invalid;
|
||||
use Espo\Core\Authentication\Jwt\Exceptions\SignatureNotVerified;
|
||||
use Espo\Core\Authentication\Jwt\Validator;
|
||||
use Espo\Core\Authentication\Oidc\UserProvider\UserInfo;
|
||||
use Espo\Core\Authentication\Result;
|
||||
use Espo\Core\Authentication\Result\FailReason;
|
||||
use Espo\Core\Utils\Json;
|
||||
@@ -45,6 +46,7 @@ use Espo\Core\Utils\Log;
|
||||
use JsonException;
|
||||
use LogicException;
|
||||
use RuntimeException;
|
||||
use SensitiveParameter;
|
||||
use stdClass;
|
||||
|
||||
class Login implements LoginInterface
|
||||
@@ -62,7 +64,8 @@ class Login implements LoginInterface
|
||||
private Validator $validator,
|
||||
private TokenValidator $tokenValidator,
|
||||
private UserProvider $userProvider,
|
||||
private ApplicationState $applicationState
|
||||
private ApplicationState $applicationState,
|
||||
private UserInfoDataProvider $userInfoDataProvider,
|
||||
) {}
|
||||
|
||||
public function login(Data $data, Request $request): Result
|
||||
@@ -99,7 +102,8 @@ class Login implements LoginInterface
|
||||
throw new RuntimeException("No client secret.");
|
||||
}
|
||||
|
||||
[$rawToken, $failResult] = $this->requestToken($endpoint, $clientId, $code, $redirectUri, $clientSecret);
|
||||
[$rawToken, $failResult, $accessToken] =
|
||||
$this->requestToken($endpoint, $clientId, $code, $redirectUri, $clientSecret);
|
||||
|
||||
if ($failResult) {
|
||||
return $failResult;
|
||||
@@ -144,7 +148,9 @@ class Login implements LoginInterface
|
||||
return Result::fail(FailReason::DENIED);
|
||||
}
|
||||
|
||||
$user = $this->userProvider->get($tokenPayload);
|
||||
$userInfo = $this->getUserInfo($tokenPayload, $accessToken);
|
||||
|
||||
$user = $this->userProvider->get($userInfo);
|
||||
|
||||
if (!$user) {
|
||||
return Result::fail(FailReason::USER_NOT_FOUND);
|
||||
@@ -198,7 +204,7 @@ class Login implements LoginInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{?string, ?Result}
|
||||
* @return array{?string, ?Result, ?string}
|
||||
*/
|
||||
private function requestToken(
|
||||
string $endpoint,
|
||||
@@ -250,7 +256,7 @@ class Login implements LoginInterface
|
||||
|
||||
$this->log->warning(self::composeLogMessage('Token request error.', $status, $response));
|
||||
|
||||
return [null, Result::fail(FailReason::DENIED)];
|
||||
return [null, Result::fail(FailReason::DENIED), null];
|
||||
}
|
||||
|
||||
$parsedResponse = null;
|
||||
@@ -266,6 +272,7 @@ class Login implements LoginInterface
|
||||
}
|
||||
|
||||
$token = $parsedResponse->id_token ?? null;
|
||||
$accessToken = $parsedResponse->access_token ?? null;
|
||||
|
||||
if (!$token || !is_string($token)) {
|
||||
$this->log->error(self::composeLogMessage('Bad token response.', $status, $response));
|
||||
@@ -273,7 +280,7 @@ class Login implements LoginInterface
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
return [$token, null];
|
||||
return [$token, null, $accessToken];
|
||||
}
|
||||
|
||||
private static function composeLogMessage(string $text, ?int $status = null, ?string $response = null): string
|
||||
@@ -295,4 +302,21 @@ class Login implements LoginInterface
|
||||
$this->tokenValidator->validateFields($token);
|
||||
$this->tokenValidator->validateSignature($token);
|
||||
}
|
||||
|
||||
private function getUserInfo(Token\Payload $payload, #[SensitiveParameter] ?string $accessToken): UserInfo
|
||||
{
|
||||
$endpoint = $this->configDataProvider->getUserInfoEndpoint();
|
||||
|
||||
if (!$endpoint) {
|
||||
return new UserInfo($payload, []);
|
||||
}
|
||||
|
||||
if (!$accessToken) {
|
||||
throw new RuntimeException("OIDC: No access token received.");
|
||||
}
|
||||
|
||||
$data = $this->userInfoDataProvider->get($accessToken);
|
||||
|
||||
return new UserInfo($payload, $data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Authentication\Oidc;
|
||||
|
||||
use Espo\Core\Utils\Json;
|
||||
use Espo\Core\Utils\Log;
|
||||
use JsonException;
|
||||
use RuntimeException;
|
||||
use SensitiveParameter;
|
||||
|
||||
class UserInfoDataProvider
|
||||
{
|
||||
private const REQUEST_TIMEOUT = 10;
|
||||
|
||||
public function __construct(
|
||||
private ConfigDataProvider $configDataProvider,
|
||||
private Log $log,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function get(#[SensitiveParameter] string $accessToken): array
|
||||
{
|
||||
return $this->load($accessToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function load(#[SensitiveParameter] string $accessToken): array
|
||||
{
|
||||
$endpoint = $this->configDataProvider->getUserInfoEndpoint();
|
||||
|
||||
if (!$endpoint) {
|
||||
throw new RuntimeException("No userinfo endpoint.");
|
||||
}
|
||||
|
||||
$curl = curl_init();
|
||||
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_URL => $endpoint,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_ENCODING => '',
|
||||
CURLOPT_MAXREDIRS => 10,
|
||||
CURLOPT_TIMEOUT => self::REQUEST_TIMEOUT,
|
||||
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
|
||||
CURLOPT_CUSTOMREQUEST => 'GET',
|
||||
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: Bearer ' . $accessToken,
|
||||
'Accept: application/json',
|
||||
],
|
||||
]);
|
||||
|
||||
/** @var string|false $response */
|
||||
$response = curl_exec($curl);
|
||||
$error = curl_error($curl);
|
||||
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
|
||||
curl_close($curl);
|
||||
|
||||
if ($response === false) {
|
||||
$response = '';
|
||||
}
|
||||
|
||||
if ($error || is_int($status) && ($status >= 400 && $status < 500)) {
|
||||
$this->log->error(self::composeLogMessage('UserInfo response error.', $status, $response));
|
||||
|
||||
throw new RuntimeException("OIDC: Userinfo request error.");
|
||||
}
|
||||
|
||||
$parsedResponse = null;
|
||||
|
||||
try {
|
||||
$parsedResponse = Json::decode($response, true);
|
||||
} catch (JsonException) {}
|
||||
|
||||
if (!is_array($parsedResponse)) {
|
||||
throw new RuntimeException("OIDC: Bad userinfo response.");
|
||||
}
|
||||
|
||||
return $parsedResponse;
|
||||
}
|
||||
|
||||
private static function composeLogMessage(string $text, ?int $status = null, ?string $response = null): string
|
||||
{
|
||||
if ($status === null) {
|
||||
return "OIDC: $text";
|
||||
}
|
||||
|
||||
return "OIDC: $text; Status: $status; Response: $response";
|
||||
}
|
||||
}
|
||||
@@ -29,10 +29,10 @@
|
||||
|
||||
namespace Espo\Core\Authentication\Oidc;
|
||||
|
||||
use Espo\Core\Authentication\Jwt\Token\Payload;
|
||||
use Espo\Core\Authentication\Oidc\UserProvider\UserInfo;
|
||||
use Espo\Entities\User;
|
||||
|
||||
interface UserProvider
|
||||
{
|
||||
public function get(Payload $payload): ?User;
|
||||
public function get(UserInfo $userInfo): ?User;
|
||||
}
|
||||
|
||||
@@ -30,11 +30,11 @@
|
||||
namespace Espo\Core\Authentication\Oidc\UserProvider;
|
||||
|
||||
use Espo\Core\ApplicationState;
|
||||
use Espo\Core\Authentication\Jwt\Token\Payload;
|
||||
use Espo\Core\Authentication\Oidc\ConfigDataProvider;
|
||||
use Espo\Core\Authentication\Oidc\UserProvider;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Entities\User;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class DefaultUserProvider implements UserProvider
|
||||
@@ -44,30 +44,30 @@ class DefaultUserProvider implements UserProvider
|
||||
private Sync $sync,
|
||||
private UserRepository $userRepository,
|
||||
private ApplicationState $applicationState,
|
||||
private Log $log
|
||||
private Log $log,
|
||||
) {}
|
||||
|
||||
public function get(Payload $payload): ?User
|
||||
public function get(UserInfo $userInfo): ?User
|
||||
{
|
||||
$user = $this->findUser($payload);
|
||||
$user = $this->findUser($userInfo);
|
||||
|
||||
if ($user === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($user) {
|
||||
$this->syncUser($user, $payload);
|
||||
$this->syncUser($user, $userInfo);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
return $this->tryToCreateUser($payload);
|
||||
return $this->tryToCreateUser($userInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User|false|null
|
||||
*/
|
||||
private function findUser(Payload $payload): User|bool|null
|
||||
private function findUser(UserInfo $userInfo): User|bool|null
|
||||
{
|
||||
$usernameClaim = $this->configDataProvider->getUsernameClaim();
|
||||
|
||||
@@ -75,10 +75,10 @@ class DefaultUserProvider implements UserProvider
|
||||
throw new RuntimeException("No username claim in config.");
|
||||
}
|
||||
|
||||
$username = $payload->get($usernameClaim);
|
||||
$username = $userInfo->get($usernameClaim);
|
||||
|
||||
if (!$username) {
|
||||
throw new RuntimeException("No username claim `$usernameClaim` in token.");
|
||||
throw new RuntimeException("No username claim `$usernameClaim` in token and userinfo.");
|
||||
}
|
||||
|
||||
$username = $this->sync->normalizeUsername($username);
|
||||
@@ -136,7 +136,7 @@ class DefaultUserProvider implements UserProvider
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function tryToCreateUser(Payload $payload): ?User
|
||||
private function tryToCreateUser(UserInfo $userInfo): ?User
|
||||
{
|
||||
if (!$this->configDataProvider->createUser()) {
|
||||
return null;
|
||||
@@ -148,16 +148,16 @@ class DefaultUserProvider implements UserProvider
|
||||
throw new RuntimeException("Could not create a user. No OIDC username claim in config.");
|
||||
}
|
||||
|
||||
$username = $payload->get($usernameClaim);
|
||||
$username = $userInfo->get($usernameClaim);
|
||||
|
||||
if (!$username) {
|
||||
throw new RuntimeException("Could not create a user. No username claim returned in token.");
|
||||
throw new RuntimeException("Could not create a user. No username claim in token and userinfo.");
|
||||
}
|
||||
|
||||
return $this->sync->createUser($payload);
|
||||
return $this->sync->createUser($userInfo);
|
||||
}
|
||||
|
||||
private function syncUser(User $user, Payload $payload): void
|
||||
private function syncUser(User $user, UserInfo $userInfo): void
|
||||
{
|
||||
if (
|
||||
!$this->configDataProvider->sync() &&
|
||||
@@ -166,6 +166,6 @@ class DefaultUserProvider implements UserProvider
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sync->syncUser($user, $payload);
|
||||
$this->sync->syncUser($user, $userInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ namespace Espo\Core\Authentication\Oidc\UserProvider;
|
||||
|
||||
use Espo\Core\Acl\Cache\Clearer as AclCacheClearer;
|
||||
use Espo\Core\ApplicationState;
|
||||
use Espo\Core\Authentication\Jwt\Token\Payload;
|
||||
use Espo\Core\Authentication\Oidc\ConfigDataProvider;
|
||||
use Espo\Core\Field\LinkMultiple;
|
||||
use Espo\Core\Name\Field;
|
||||
@@ -50,30 +49,31 @@ class Sync
|
||||
private UserRepository $userRepository,
|
||||
private PasswordHash $passwordHash,
|
||||
private AclCacheClearer $aclCacheClearer,
|
||||
private ApplicationState $applicationState
|
||||
private ApplicationState $applicationState,
|
||||
) {}
|
||||
|
||||
public function createUser(Payload $payload): User
|
||||
public function createUser(UserInfo $userInfo): User
|
||||
{
|
||||
$username = $this->getUsernameFromToken($payload);
|
||||
$username = $this->getUsernameFromToken($userInfo);
|
||||
|
||||
$this->usernameValidator->validate($username);
|
||||
|
||||
$user = $this->userRepository->getNew();
|
||||
|
||||
$user->set([
|
||||
'type' => User::TYPE_REGULAR,
|
||||
'userName' => $username,
|
||||
$user->setType(User::TYPE_REGULAR);
|
||||
$user->setUserName($username);
|
||||
|
||||
$user->setMultiple([
|
||||
'password' => $this->passwordHash->hash(Util::generatePassword(10, 4, 2, true)),
|
||||
]);
|
||||
|
||||
$user->set($this->getUserDataFromToken($payload));
|
||||
$user->set($this->getUserTeamsDataFromToken($payload));
|
||||
$user->set($this->getUserDataFromToken($userInfo));
|
||||
$user->set($this->getUserTeamsDataFromToken($userInfo));
|
||||
|
||||
if ($this->applicationState->isPortal()) {
|
||||
$portalId = $this->applicationState->getPortalId();
|
||||
|
||||
$user->set('type', User::TYPE_PORTAL);
|
||||
$user->setType(User::TYPE_PORTAL);
|
||||
$user->setPortals(LinkMultiple::create()->withAddedId($portalId));
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ class Sync
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function syncUser(User $user, Payload $payload): void
|
||||
public function syncUser(User $user, UserInfo $payload): void
|
||||
{
|
||||
$username = $this->getUsernameFromToken($payload);
|
||||
|
||||
@@ -116,19 +116,19 @@ class Sync
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function getUserDataFromToken(Payload $payload): array
|
||||
private function getUserDataFromToken(UserInfo $userInfo): array
|
||||
{
|
||||
return [
|
||||
'emailAddress' => $payload->get('email'),
|
||||
'phoneNumber' => $payload->get('phone_number'),
|
||||
'emailAddress' => $userInfo->get('email'),
|
||||
'phoneNumber' => $userInfo->get('phone_number'),
|
||||
'emailAddressData' => null,
|
||||
'phoneNumberData' => null,
|
||||
'firstName' => $payload->get('given_name'),
|
||||
'lastName' => $payload->get('family_name'),
|
||||
'middle_name' => $payload->get('middle_name'),
|
||||
'firstName' => $userInfo->get('given_name'),
|
||||
'lastName' => $userInfo->get('family_name'),
|
||||
'middle_name' => $userInfo->get('middle_name'),
|
||||
'gender' =>
|
||||
in_array($payload->get('gender'), ['male', 'female']) ?
|
||||
ucfirst($payload->get('gender') ?? '') :
|
||||
in_array($userInfo->get('gender'), ['male', 'female']) ?
|
||||
ucfirst($userInfo->get('gender') ?? '') :
|
||||
null,
|
||||
];
|
||||
}
|
||||
@@ -136,14 +136,14 @@ class Sync
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function getUserTeamsDataFromToken(Payload $payload): array
|
||||
private function getUserTeamsDataFromToken(UserInfo $userInfo): array
|
||||
{
|
||||
return [
|
||||
'teamsIds' => $this->getTeamIdList($payload),
|
||||
'teamsIds' => $this->getTeamIdList($userInfo),
|
||||
];
|
||||
}
|
||||
|
||||
private function getUsernameFromToken(Payload $payload): string
|
||||
private function getUsernameFromToken(UserInfo $userInfo): string
|
||||
{
|
||||
$usernameClaim = $this->configDataProvider->getUsernameClaim();
|
||||
|
||||
@@ -151,7 +151,7 @@ class Sync
|
||||
throw new RuntimeException("No OIDC username claim in config.");
|
||||
}
|
||||
|
||||
$username = $payload->get($usernameClaim);
|
||||
$username = $userInfo->get($usernameClaim);
|
||||
|
||||
if (!$username) {
|
||||
throw new RuntimeException("No username claim returned in token.");
|
||||
@@ -167,7 +167,7 @@ class Sync
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getTeamIdList(Payload $payload): array
|
||||
private function getTeamIdList(UserInfo $userInfo): array
|
||||
{
|
||||
$idList = $this->configDataProvider->getTeamIds() ?? [];
|
||||
$columns = $this->configDataProvider->getTeamColumns() ?? (object) [];
|
||||
@@ -176,7 +176,7 @@ class Sync
|
||||
return [];
|
||||
}
|
||||
|
||||
$groupList = $this->getGroups($payload);
|
||||
$groupList = $this->getGroups($userInfo);
|
||||
|
||||
$resultIdList = [];
|
||||
|
||||
@@ -194,7 +194,7 @@ class Sync
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getGroups(Payload $payload): array
|
||||
private function getGroups(UserInfo $userInfo): array
|
||||
{
|
||||
$groupClaim = $this->configDataProvider->getGroupClaim();
|
||||
|
||||
@@ -202,7 +202,7 @@ class Sync
|
||||
return [];
|
||||
}
|
||||
|
||||
$value = $payload->get($groupClaim);
|
||||
$value = $userInfo->get($groupClaim);
|
||||
|
||||
if (!$value) {
|
||||
return [];
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Authentication\Oidc\UserProvider;
|
||||
|
||||
use Espo\Core\Authentication\Jwt\Token\Payload;
|
||||
|
||||
class UserInfo
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function __construct(
|
||||
private Payload $payload,
|
||||
private array $data,
|
||||
) {}
|
||||
|
||||
public function get(string $name): mixed
|
||||
{
|
||||
return $this->payload->get($name) ?? $this->data[$name] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ use stdClass;
|
||||
/**
|
||||
* An authentication result.
|
||||
*
|
||||
* @immutable
|
||||
* Immutable.
|
||||
*/
|
||||
class Result
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Espo\Core\Authentication\Result;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @immutable
|
||||
* Immutable.
|
||||
*/
|
||||
class Data
|
||||
{
|
||||
|
||||
@@ -75,7 +75,7 @@ class ContextualBinder
|
||||
*
|
||||
* @template T of object
|
||||
* @param class-string<T>|NamedClassKey<T> $key An interface or interface with a parameter name.
|
||||
* @param class-string<T> $serviceName A service name.
|
||||
* @param string $serviceName A service name.
|
||||
*/
|
||||
public function bindService(string|NamedClassKey $key, string $serviceName): self
|
||||
{
|
||||
|
||||
@@ -73,6 +73,7 @@ class EspoBindingLoader implements BindingLoader
|
||||
|
||||
private function loadCustom(Binder $binder): void
|
||||
{
|
||||
/** @var class-string<BindingProcessor>|string $className */
|
||||
$className = 'Espo\\Custom\\Binding';
|
||||
|
||||
if (!class_exists($className)) {
|
||||
|
||||
@@ -34,7 +34,7 @@ use Espo\Core\Utils\Util;
|
||||
/**
|
||||
* Command parameters.
|
||||
*
|
||||
* @immutable
|
||||
* Immutable.
|
||||
*/
|
||||
class Params
|
||||
{
|
||||
|
||||
@@ -248,11 +248,10 @@ class Extension implements Command
|
||||
private function printList(IO $io): void
|
||||
{
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository(ExtensionEntity::ENTITY_TYPE)
|
||||
->getRDBRepositoryByClass(ExtensionEntity::class)
|
||||
->find();
|
||||
|
||||
/** @noinspection PhpParamsInspection */
|
||||
$count = is_countable($collection) ? count($collection) : iterator_count($collection);
|
||||
$count = count($collection);
|
||||
|
||||
/** @noinspection PhpIfWithCommonPartsInspection */
|
||||
if ($count === 0) {
|
||||
@@ -268,9 +267,9 @@ class Extension implements Command
|
||||
$io->writeLine("");
|
||||
|
||||
foreach ($collection as $extension) {
|
||||
$isInstalled = $extension->get('isInstalled');
|
||||
$isInstalled = $extension->isInstalled();
|
||||
|
||||
$io->writeLine(' Name: ' . $extension->get(Field::NAME));
|
||||
$io->writeLine(' Name: ' . $extension->getName());
|
||||
$io->writeLine(' ID: ' . $extension->getId());
|
||||
$io->writeLine(' Version: ' . $extension->getVersion());
|
||||
$io->writeLine(' Installed: ' . ($isInstalled ? 'yes' : 'no'));
|
||||
|
||||
@@ -151,6 +151,8 @@ class Upgrade implements Command
|
||||
if (!$packageFile) {
|
||||
fwrite(STDOUT, "Error: Unable to download upgrade package.\n");
|
||||
|
||||
$io->setExitStatus(1);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -177,6 +179,8 @@ class Upgrade implements Command
|
||||
|
||||
fwrite(STDOUT, $errorMessage . "\n");
|
||||
|
||||
$io->setExitStatus(1);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -491,13 +495,19 @@ class Upgrade implements Command
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var string */
|
||||
return realpath($localFilePath);
|
||||
$path = realpath($localFilePath);
|
||||
|
||||
assert($path !== false);
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
private function isShellEnabled(): bool
|
||||
{
|
||||
if (!function_exists('exec') || !is_callable('shell_exec')) {
|
||||
if (
|
||||
!function_exists('exec') ||
|
||||
!is_callable('shell_exec') /** @phpstan-ignore-line */
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -512,7 +522,7 @@ class Upgrade implements Command
|
||||
|
||||
private function getCurrentVersion(): ?string
|
||||
{
|
||||
$configData = include "data/config.php";
|
||||
$configData = include "data/config.php"; /** @phpstan-ignore-line */
|
||||
|
||||
if (!$configData) {
|
||||
return null;
|
||||
|
||||
@@ -36,16 +36,12 @@ use Espo\Core\Utils\Config;
|
||||
|
||||
class Version implements Command
|
||||
{
|
||||
public function __construct(private Config $config)
|
||||
public function __construct(private Config\SystemConfig $config)
|
||||
{}
|
||||
|
||||
public function run(Params $params, IO $io): void
|
||||
{
|
||||
$version = $this->config->get('version');
|
||||
|
||||
if (is_null($version)) {
|
||||
return;
|
||||
}
|
||||
$version = $this->config->getVersion();
|
||||
|
||||
$io->writeLine($version);
|
||||
}
|
||||
|
||||
@@ -187,13 +187,19 @@ class Container implements ContainerInterface
|
||||
}
|
||||
|
||||
if ($id === self::ID_CONTAINER) {
|
||||
$this->classCache[$id] = new ReflectionClass(Container::class);
|
||||
/** @var ReflectionClass<object> $object */
|
||||
$object = new ReflectionClass(Container::class);
|
||||
|
||||
$this->classCache[$id] = $object;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($id === self::ID_INJECTABLE_FACTORY) {
|
||||
$this->classCache[$id] = new ReflectionClass(InjectableFactory::class);
|
||||
/** @var ReflectionClass<object> $object */
|
||||
$object = new ReflectionClass(InjectableFactory::class);
|
||||
|
||||
$this->classCache[$id] = $object;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -168,12 +168,14 @@ class ContainerBuilder
|
||||
)
|
||||
);
|
||||
|
||||
/** @var FileManager $fileManager */
|
||||
$fileManager = $this->services['fileManager'] ?? (
|
||||
new $this->fileManagerClassName(
|
||||
$config->get('defaultPermissions')
|
||||
)
|
||||
);
|
||||
|
||||
/** @var DataCache $dataCache */
|
||||
$dataCache = $this->services['dataCache'] ?? (
|
||||
new $this->dataCacheClassName($fileManager)
|
||||
);
|
||||
@@ -185,10 +187,13 @@ class ContainerBuilder
|
||||
new $this->moduleClassName($fileManager, $dataCache, $useCache)
|
||||
);
|
||||
|
||||
$systemConfig = new Config\SystemConfig($config);
|
||||
|
||||
$this->services['config'] = $config;
|
||||
$this->services['fileManager'] = $fileManager;
|
||||
$this->services['dataCache'] = $dataCache;
|
||||
$this->services['module'] = $module;
|
||||
$this->services['systemConfig'] = $systemConfig;
|
||||
|
||||
$bindingLoader = $this->bindingLoader ?? (
|
||||
new EspoBindingLoader($module)
|
||||
|
||||
@@ -39,8 +39,8 @@ class ContainerConfiguration implements Configuration
|
||||
{
|
||||
/**
|
||||
* Log must be loaded before anything.
|
||||
* @phpstan-ignore-next-line
|
||||
* @noinspection PhpPropertyOnlyWrittenInspection
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
private Log $log;
|
||||
|
||||
|
||||
@@ -63,12 +63,12 @@ abstract class Base
|
||||
private $container;
|
||||
|
||||
/**
|
||||
* @var User;
|
||||
* @var User
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* @var Acl;
|
||||
* @var Acl
|
||||
*/
|
||||
protected $acl;
|
||||
|
||||
@@ -161,12 +161,11 @@ abstract class Base
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void;
|
||||
* @return void
|
||||
* @deprecated
|
||||
*/
|
||||
protected function checkControllerAccess()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,6 @@ namespace Espo\Core\Controllers;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Exceptions\NotFoundSilent;
|
||||
@@ -65,12 +64,9 @@ class Record extends RecordBase
|
||||
|
||||
$searchParams = $this->fetchSearchParamsFromRequest($request);
|
||||
|
||||
$recordCollection = $this->getRecordService()->findLinked($id, $link, $searchParams);
|
||||
$result = $this->getRecordService()->findLinked($id, $link, $searchParams);
|
||||
|
||||
return (object) [
|
||||
'total' => $recordCollection->getTotal(),
|
||||
'list' => $recordCollection->getValueMapList(),
|
||||
];
|
||||
return $result->toApiOutput();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,6 +40,7 @@ use Espo\Core\Record\ServiceContainer as RecordServiceContainer;
|
||||
use Espo\Core\Record\SearchParamsFetcher;
|
||||
use Espo\Core\Record\CreateParamsFetcher;
|
||||
use Espo\Core\Record\ReadParamsFetcher;
|
||||
use Espo\Core\Record\UpdateContext;
|
||||
use Espo\Core\Record\UpdateParamsFetcher;
|
||||
use Espo\Core\Record\DeleteParamsFetcher;
|
||||
use Espo\Core\Record\FindParamsFetcher;
|
||||
@@ -222,8 +223,16 @@ class RecordBase extends Base implements
|
||||
|
||||
$params = $this->updateParamsFetcher->fetch($request);
|
||||
|
||||
$context = new UpdateContext();
|
||||
|
||||
$params = $params->withContext($context);
|
||||
|
||||
$entity = $this->getRecordService()->update($id, $data, $params);
|
||||
|
||||
if ($context->linkUpdated) {
|
||||
$response->setHeader('X-Record-Link-Updated', 'true');
|
||||
}
|
||||
|
||||
return $entity->getValueMap();
|
||||
}
|
||||
|
||||
@@ -246,10 +255,7 @@ class RecordBase extends Base implements
|
||||
|
||||
$recordCollection = $this->getRecordService()->find($searchParams, $findParams);
|
||||
|
||||
return (object) [
|
||||
'total' => $recordCollection->getTotal(),
|
||||
'list' => $recordCollection->getValueMapList(),
|
||||
];
|
||||
return $recordCollection->toApiOutput();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -63,15 +63,12 @@ class RecordTree extends Record
|
||||
return (object) $this->actionListTree($request->getRouteParams(), $request->getParsedBody(), $request);
|
||||
}
|
||||
|
||||
$where = $request->getQueryParams()['where'] ?? null;
|
||||
$selectParams = $this->fetchSearchParamsFromRequest($request);
|
||||
|
||||
$parentId = $request->getQueryParam('parentId');
|
||||
$maxDepth = $request->getQueryParam('maxDepth');
|
||||
$onlyNotEmpty = (bool) $request->getQueryParam('onlyNotEmpty');
|
||||
|
||||
if ($where !== null && !is_array($where)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
if ($maxDepth !== null) {
|
||||
$maxDepth = (int) $maxDepth;
|
||||
}
|
||||
@@ -79,7 +76,7 @@ class RecordTree extends Record
|
||||
$collection = $this->getRecordTreeService()->getTree(
|
||||
$parentId,
|
||||
[
|
||||
'where' => $where,
|
||||
'where' => $selectParams->getWhere(),
|
||||
'onlyNotEmpty' => $onlyNotEmpty,
|
||||
],
|
||||
$maxDepth
|
||||
|
||||
@@ -35,6 +35,11 @@ class CalculatorUtil
|
||||
{
|
||||
private const SCALE = 14;
|
||||
|
||||
/**
|
||||
* @param numeric-string $arg1
|
||||
* @param numeric-string $arg2
|
||||
* @return numeric-string
|
||||
*/
|
||||
public static function add(string $arg1, string $arg2): string
|
||||
{
|
||||
if (!function_exists('bcadd')) {
|
||||
@@ -50,6 +55,11 @@ class CalculatorUtil
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric-string $arg1
|
||||
* @param numeric-string $arg2
|
||||
* @return numeric-string
|
||||
*/
|
||||
public static function subtract(string $arg1, string $arg2): string
|
||||
{
|
||||
if (!function_exists('bcsub')) {
|
||||
@@ -65,6 +75,11 @@ class CalculatorUtil
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric-string $arg1
|
||||
* @param numeric-string $arg2
|
||||
* @return numeric-string
|
||||
*/
|
||||
public static function multiply(string $arg1, string $arg2): string
|
||||
{
|
||||
if (!function_exists('bcmul')) {
|
||||
@@ -80,6 +95,11 @@ class CalculatorUtil
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric-string $arg1
|
||||
* @param numeric-string $arg2
|
||||
* @return numeric-string
|
||||
*/
|
||||
public static function divide(string $arg1, string $arg2): string
|
||||
{
|
||||
if (!function_exists('bcdiv')) {
|
||||
@@ -88,20 +108,23 @@ class CalculatorUtil
|
||||
);
|
||||
}
|
||||
|
||||
/** @var ?string $result */
|
||||
$result = bcdiv(
|
||||
$arg1,
|
||||
$arg2,
|
||||
self::SCALE
|
||||
);
|
||||
|
||||
if ($result === null) {
|
||||
if ($result === null) { /** @phpstan-ignore-line */
|
||||
throw new DivisionByZeroError();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric-string $arg
|
||||
* @return numeric-string
|
||||
*/
|
||||
public static function round(string $arg, int $precision = 0): string
|
||||
{
|
||||
if (!function_exists('bcadd')) {
|
||||
@@ -114,6 +137,8 @@ class CalculatorUtil
|
||||
$addition = '-' . $addition;
|
||||
}
|
||||
|
||||
assert(is_numeric($addition));
|
||||
|
||||
return bcadd(
|
||||
$arg,
|
||||
$addition,
|
||||
@@ -121,6 +146,10 @@ class CalculatorUtil
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric-string $arg1
|
||||
* @param numeric-string $arg2
|
||||
*/
|
||||
public static function compare(string $arg1, string $arg2): int
|
||||
{
|
||||
if (!function_exists('bccomp')) {
|
||||
|
||||
@@ -100,6 +100,10 @@ class Converter
|
||||
return $this->convert($value, $targetCurrencyCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric-string $amount
|
||||
* @return numeric-string
|
||||
*/
|
||||
private function convertAmount(string $amount, float $rate, float $targetRate): string
|
||||
{
|
||||
return CalculatorUtil::divide(
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace Espo\Core\Di;
|
||||
|
||||
use Espo\Core\Utils\Crypt;
|
||||
|
||||
/**
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
trait CryptSetter
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace Espo\Core\Di;
|
||||
|
||||
use Espo\Core\DataManager;
|
||||
|
||||
/**
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
trait DataManagerSetter
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace Espo\Core\Di;
|
||||
|
||||
use Espo\Core\Mail\EmailSender as EmailSender;
|
||||
|
||||
/**
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
trait EmailSenderSetter
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace Espo\Core\Di;
|
||||
|
||||
use Espo\Core\Htmlizer\HtmlizerFactory as HtmlizerFactory;
|
||||
|
||||
/**
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
trait HtmlizerFactorySetter
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace Espo\Core\Di;
|
||||
|
||||
use Espo\Core\Utils\Language;
|
||||
|
||||
/**
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
trait LanguageSetter
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace Espo\Core\Di;
|
||||
|
||||
use Espo\Entities\Preferences;
|
||||
|
||||
/**
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
trait PreferencesSetter
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace Espo\Core\Di;
|
||||
|
||||
use Espo\Core\Select\SelectManagerFactory;
|
||||
|
||||
/**
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
trait SelectManagerFactorySetter
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace Espo\Core\Di;
|
||||
|
||||
use Espo\Core\Utils\TemplateFileManager;
|
||||
|
||||
/**
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
trait TemplateFileManagerSetter
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace Espo\Core\Di;
|
||||
|
||||
use Espo\Core\WebSocket\Submission;
|
||||
|
||||
/**
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
trait WebSocketSubmissionSetter
|
||||
{
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user