mirror of
https://github.com/espocrm/espocrm.git
synced 2026-03-05 11:07:01 +00:00
Compare commits
581 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
506e96e333 | ||
|
|
76a9177c46 | ||
|
|
a8baac3f92 | ||
|
|
8d9d073c16 | ||
|
|
177ecdb70b | ||
|
|
89ce80d5b2 | ||
|
|
7f07175bee | ||
|
|
8bca35934c | ||
|
|
65bef1df65 | ||
|
|
922a2e835c | ||
|
|
048c83def8 | ||
|
|
cf4a8c6c11 | ||
|
|
7259142117 | ||
|
|
746398c5ca | ||
|
|
6a61772e43 | ||
|
|
2412fb5151 | ||
|
|
5e5a229366 | ||
|
|
5d27a7a828 | ||
|
|
82cc4a7f03 | ||
|
|
d2033c53fc | ||
|
|
86043a5ce5 | ||
|
|
1fae4ac84b | ||
|
|
6301ba491c | ||
|
|
defe9965d4 | ||
|
|
937ad48841 | ||
|
|
781030df60 | ||
|
|
9b009962a5 | ||
|
|
255b6637ff | ||
|
|
28d1c052d2 | ||
|
|
828d8da741 | ||
|
|
54688795e2 | ||
|
|
6414a13c6f | ||
|
|
c48f17449b | ||
|
|
cbf65feb09 | ||
|
|
f1a3021c1c | ||
|
|
b70327874e | ||
|
|
9345ada934 | ||
|
|
92c66a3b9a | ||
|
|
c78f84650c | ||
|
|
cf1692a8a4 | ||
|
|
94321b3500 | ||
|
|
c1a1e1094a | ||
|
|
c2fefd6227 | ||
|
|
99946c8da5 | ||
|
|
86c2478721 | ||
|
|
5331332103 | ||
|
|
0e380e1c10 | ||
|
|
c54dbdc169 | ||
|
|
f88261a40a | ||
|
|
7ec8d1f69c | ||
|
|
77ce0a3b4e | ||
|
|
134bde3370 | ||
|
|
811c841420 | ||
|
|
48751813fb | ||
|
|
79c182d173 | ||
|
|
87a612a1bc | ||
|
|
388da3db70 | ||
|
|
60fdeafd77 | ||
|
|
2057534f19 | ||
|
|
b85f60b855 | ||
|
|
b4d02130fb | ||
|
|
9a9d924b19 | ||
|
|
1fb06f60e0 | ||
|
|
9d65855868 | ||
|
|
8cf2e270a5 | ||
|
|
07ebd43ac6 | ||
|
|
b1447a2922 | ||
|
|
66b85bf4e9 | ||
|
|
e9b58926b4 | ||
|
|
75724b28b2 | ||
|
|
79f0730257 | ||
|
|
13abed67f2 | ||
|
|
84b98c1cfe | ||
|
|
bc557706ca | ||
|
|
2ef0f5f80e | ||
|
|
33db2270d6 | ||
|
|
6810ee37ac | ||
|
|
9e25804f89 | ||
|
|
42d30a3f8e | ||
|
|
524f94cd54 | ||
|
|
739230c4b9 | ||
|
|
caeadc2f28 | ||
|
|
fd330f991c | ||
|
|
e09bf8fa31 | ||
|
|
2ea60e66ba | ||
|
|
f184e34838 | ||
|
|
d721e9b448 | ||
|
|
e1241eddb7 | ||
|
|
c693654e80 | ||
|
|
9d0f3dadad | ||
|
|
2c4033f363 | ||
|
|
3c62414c8d | ||
|
|
9dddd0b92f | ||
|
|
2284a3e2e5 | ||
|
|
60d83b138a | ||
|
|
c94d41a79c | ||
|
|
fc78cd28a6 | ||
|
|
b0e01a1fcb | ||
|
|
41222f8e9e | ||
|
|
64baaa5253 | ||
|
|
35a0f14d28 | ||
|
|
9bb2197717 | ||
|
|
1607240f5d | ||
|
|
8be1af0671 | ||
|
|
58ac0800f9 | ||
|
|
2acac3d0b0 | ||
|
|
5e44fc2d40 | ||
|
|
2b08f83ac2 | ||
|
|
cc1bfce3dd | ||
|
|
08647b3ed6 | ||
|
|
b97f4ee124 | ||
|
|
d4e73f500f | ||
|
|
5e13e6cf99 | ||
|
|
3fd06a89f1 | ||
|
|
6678500d1b | ||
|
|
1a6f236dc7 | ||
|
|
f57a95349a | ||
|
|
b3496268e0 | ||
|
|
fa38ece181 | ||
|
|
5866f02eca | ||
|
|
6edce56ca7 | ||
|
|
3dc239acc5 | ||
|
|
385c70845a | ||
|
|
4f65a46434 | ||
|
|
a9c4689500 | ||
|
|
75b544a995 | ||
|
|
451d5e5659 | ||
|
|
134f5862dd | ||
|
|
b59279ab16 | ||
|
|
0621c8aefc | ||
|
|
3229ba1043 | ||
|
|
06868b8b57 | ||
|
|
fce1d49407 | ||
|
|
8c2cf02891 | ||
|
|
ccdafc67b5 | ||
|
|
b6a470c52e | ||
|
|
76c63bede4 | ||
|
|
837e96c061 | ||
|
|
2bebe4b045 | ||
|
|
9ee1b41b52 | ||
|
|
f18a3043eb | ||
|
|
01d5f1a07f | ||
|
|
06b31d537f | ||
|
|
989a04dddf | ||
|
|
055e968660 | ||
|
|
585512773d | ||
|
|
71f4abeb97 | ||
|
|
4f973e057e | ||
|
|
416bf152f0 | ||
|
|
3f473617c0 | ||
|
|
56766e2246 | ||
|
|
a92e44bd6c | ||
|
|
369f3ba9a5 | ||
|
|
2d62c902cb | ||
|
|
5985b6d93f | ||
|
|
587bda5c73 | ||
|
|
230a9aced0 | ||
|
|
023b0ea892 | ||
|
|
2578b397e7 | ||
|
|
0659b30588 | ||
|
|
0afd5e1d73 | ||
|
|
f824d8eaf5 | ||
|
|
abab4e2061 | ||
|
|
36c6883743 | ||
|
|
9c44f79b4d | ||
|
|
cd3c7b1407 | ||
|
|
6afd616e42 | ||
|
|
7aabbc5d28 | ||
|
|
9589b0b70a | ||
|
|
8822104227 | ||
|
|
e34e6b5d9a | ||
|
|
6341492965 | ||
|
|
aeece9acda | ||
|
|
f5823d04dd | ||
|
|
28df6738e2 | ||
|
|
0e371ebe04 | ||
|
|
6fe4034237 | ||
|
|
48c3ea5f13 | ||
|
|
da5b1cf005 | ||
|
|
2655f0d9c5 | ||
|
|
e11e3d6168 | ||
|
|
90806c0e82 | ||
|
|
ca9f25636b | ||
|
|
abcc290a7c | ||
|
|
091b64dd3d | ||
|
|
8fd8434d68 | ||
|
|
9ef5c0bc1b | ||
|
|
8adfb80558 | ||
|
|
5ccbf49773 | ||
|
|
e2cde84447 | ||
|
|
26e7f3dfd2 | ||
|
|
6a75b28f8c | ||
|
|
e8b6db20a2 | ||
|
|
9e37739197 | ||
|
|
f0b49cd467 | ||
|
|
aae918886f | ||
|
|
cbf0a82c52 | ||
|
|
fb7683e35b | ||
|
|
105bb8a80f | ||
|
|
eab0596c33 | ||
|
|
7e84278cef | ||
|
|
f2a27c024f | ||
|
|
1b76097311 | ||
|
|
ae23f58bf5 | ||
|
|
f3bf7d93c1 | ||
|
|
656ff76a8d | ||
|
|
c72bcc365a | ||
|
|
f58687ad6b | ||
|
|
aefe547ef8 | ||
|
|
3619696b12 | ||
|
|
40d0ad5d08 | ||
|
|
c9d3a3f967 | ||
|
|
0b1f7d1548 | ||
|
|
59cee9a2ee | ||
|
|
e7373ab817 | ||
|
|
9570a28066 | ||
|
|
b3f7242bae | ||
|
|
823d371e9d | ||
|
|
2399d21829 | ||
|
|
01d5091aa3 | ||
|
|
ed867d1b95 | ||
|
|
3392c843e6 | ||
|
|
1e29280a1d | ||
|
|
aeecd1f3a6 | ||
|
|
5ecce30720 | ||
|
|
c02f87d1c0 | ||
|
|
929dce6b2e | ||
|
|
c82d35af27 | ||
|
|
a0a0b22d2f | ||
|
|
db3af5749c | ||
|
|
f1e13f4b95 | ||
|
|
9f43a0ff89 | ||
|
|
17bc8a6137 | ||
|
|
66b06b8baa | ||
|
|
1d6745396d | ||
|
|
4f53038578 | ||
|
|
79f5a7a94b | ||
|
|
77a433445b | ||
|
|
be54198265 | ||
|
|
7da892ba98 | ||
|
|
7b2526430a | ||
|
|
e416dac56f | ||
|
|
d11f0f4f1b | ||
|
|
f72fbed6e1 | ||
|
|
56fe4e2ef7 | ||
|
|
684a995e17 | ||
|
|
2f03572df8 | ||
|
|
5bb0222abd | ||
|
|
82cf211822 | ||
|
|
29fd164023 | ||
|
|
c52fedcf07 | ||
|
|
68b12ff848 | ||
|
|
a351af06a1 | ||
|
|
72c502b492 | ||
|
|
1047d243a2 | ||
|
|
5ab129bd9b | ||
|
|
6e5e940b30 | ||
|
|
fa577a4fa9 | ||
|
|
64f2c59134 | ||
|
|
a82408a06f | ||
|
|
b80d8830ba | ||
|
|
edcdf3c8be | ||
|
|
ae27f360ca | ||
|
|
474759ab6e | ||
|
|
02ea7cc041 | ||
|
|
0a67950913 | ||
|
|
4ef66b1601 | ||
|
|
33220d607a | ||
|
|
ac9f80312d | ||
|
|
0db2ee0a8d | ||
|
|
7d4ab54505 | ||
|
|
a06bfde766 | ||
|
|
46f333fced | ||
|
|
f9294c652d | ||
|
|
a22dc4b2fb | ||
|
|
b293086482 | ||
|
|
93139cb3ef | ||
|
|
1db70eeaa8 | ||
|
|
8ccda3fd2d | ||
|
|
60eb9008f7 | ||
|
|
635fa8b893 | ||
|
|
183603e09a | ||
|
|
3399b2cc01 | ||
|
|
9d4d441ec2 | ||
|
|
51bda2f5d2 | ||
|
|
92e62c7760 | ||
|
|
7bb204a432 | ||
|
|
39939191e5 | ||
|
|
aa43f29615 | ||
|
|
f109131d38 | ||
|
|
a6fb0bbded | ||
|
|
9b808908b6 | ||
|
|
0691a3cdda | ||
|
|
7208a4f88c | ||
|
|
7d37006450 | ||
|
|
73f1e425ca | ||
|
|
2336ae74da | ||
|
|
4419d0827a | ||
|
|
0d1bc90848 | ||
|
|
7665010bff | ||
|
|
5ba16b4c27 | ||
|
|
cbf6b0cc6b | ||
|
|
c51a51e110 | ||
|
|
b59ff42ac2 | ||
|
|
b635018949 | ||
|
|
fcb2a0b7a5 | ||
|
|
2048f3fa10 | ||
|
|
0fe7fb032f | ||
|
|
bb24b576e0 | ||
|
|
2a7a12f58c | ||
|
|
5ccb081af5 | ||
|
|
bf1ed3c287 | ||
|
|
ddcb5ccbf9 | ||
|
|
5b8dc4e629 | ||
|
|
1032b73b85 | ||
|
|
e7b914ff4b | ||
|
|
30e94f25e6 | ||
|
|
7fa7fe63f8 | ||
|
|
716a5b86ff | ||
|
|
a29ce1a873 | ||
|
|
ace2dc802a | ||
|
|
1018bfd4d4 | ||
|
|
cc828661da | ||
|
|
4c265dbae1 | ||
|
|
2615869691 | ||
|
|
fbbb7c99c0 | ||
|
|
da7fc9d6a0 | ||
|
|
9b63470e9a | ||
|
|
5879a57cd1 | ||
|
|
702b3f4e2b | ||
|
|
75749efacb | ||
|
|
854f6c6390 | ||
|
|
717c21e91b | ||
|
|
07f1100ccc | ||
|
|
3157bb4fcf | ||
|
|
a3a35be818 | ||
|
|
1c1042cd75 | ||
|
|
8f8eb4807d | ||
|
|
f97bb82d9e | ||
|
|
62b325c24b | ||
|
|
4051b83b30 | ||
|
|
75444b3e0b | ||
|
|
961a7bd0bf | ||
|
|
f4b09c0135 | ||
|
|
d09f83e267 | ||
|
|
b874cc283f | ||
|
|
b5873c9c1d | ||
|
|
89bde00e3b | ||
|
|
242de1824f | ||
|
|
02d277d045 | ||
|
|
76d31a3885 | ||
|
|
c6a8e36849 | ||
|
|
d88e47b508 | ||
|
|
46bd520b52 | ||
|
|
d9c0f7d055 | ||
|
|
4a9bd1b54c | ||
|
|
1626e4b6bf | ||
|
|
c1cfa0483a | ||
|
|
c7d5bc8169 | ||
|
|
37e091ddda | ||
|
|
a7c33afd93 | ||
|
|
f7678abad1 | ||
|
|
b181624064 | ||
|
|
8a6c63e12c | ||
|
|
aba1881a5c | ||
|
|
3350ffc0b7 | ||
|
|
c498d463e7 | ||
|
|
93c1f4ef8a | ||
|
|
54bf67e8bd | ||
|
|
59e852a285 | ||
|
|
dcbe24746e | ||
|
|
deb3ee0653 | ||
|
|
698db9ebd6 | ||
|
|
214cf538ad | ||
|
|
f578fd4af9 | ||
|
|
57c011ee94 | ||
|
|
e4285d1d2a | ||
|
|
5f4643d725 | ||
|
|
fcc13ef10c | ||
|
|
826c1734a2 | ||
|
|
849ddadb8c | ||
|
|
3e4bd12a76 | ||
|
|
e6aae5f4ed | ||
|
|
d8bd2f451f | ||
|
|
9c47341fc1 | ||
|
|
bbea0c0215 | ||
|
|
4e4e29e0f1 | ||
|
|
4658eb800e | ||
|
|
16a313f659 | ||
|
|
c2fcd8d86c | ||
|
|
8666a3977a | ||
|
|
fa06a437e5 | ||
|
|
0dcea2ad5b | ||
|
|
5a3142b252 | ||
|
|
4598b58fbb | ||
|
|
7ff9d85a11 | ||
|
|
89b912ee73 | ||
|
|
a4f75f1423 | ||
|
|
03a69cb364 | ||
|
|
e7fa98dc09 | ||
|
|
5eb49d6c3d | ||
|
|
1d84aad483 | ||
|
|
af847a8fe7 | ||
|
|
89d775a8a8 | ||
|
|
f09fe03f60 | ||
|
|
08a6a2c66b | ||
|
|
0a4a3938fb | ||
|
|
0bc05b4a6d | ||
|
|
b7ad2dd760 | ||
|
|
363842aff2 | ||
|
|
ac889dea91 | ||
|
|
89e31795cc | ||
|
|
9953ea8419 | ||
|
|
1ae1f07de2 | ||
|
|
b8386f3ea3 | ||
|
|
6fcd0e155d | ||
|
|
3e945b7fd8 | ||
|
|
99464c5210 | ||
|
|
64c9fbf4f8 | ||
|
|
029d09e689 | ||
|
|
be27fc45ee | ||
|
|
0eefc5d75d | ||
|
|
876817ffe9 | ||
|
|
f6b3e33e7e | ||
|
|
24301b22a9 | ||
|
|
31be2f81ff | ||
|
|
bc435b0729 | ||
|
|
4cd0961f80 | ||
|
|
70edfbb88b | ||
|
|
9066a2bf97 | ||
|
|
b39bffa1f2 | ||
|
|
c49db089f5 | ||
|
|
2a110851df | ||
|
|
3e60103516 | ||
|
|
481b870565 | ||
|
|
9564d0807a | ||
|
|
785746c801 | ||
|
|
a89ac23625 | ||
|
|
adc2cb5a66 | ||
|
|
4b5787c0d0 | ||
|
|
dcbd2bfa42 | ||
|
|
147fcb02b6 | ||
|
|
1762096532 | ||
|
|
d21857075e | ||
|
|
a81759b0f1 | ||
|
|
99fb897b63 | ||
|
|
f6e7da57f7 | ||
|
|
aa001b4a7f | ||
|
|
37c3cfc539 | ||
|
|
da45d958b7 | ||
|
|
aaf095e32b | ||
|
|
282f701b77 | ||
|
|
ea8ba18d2e | ||
|
|
d08e969915 | ||
|
|
5567cc6938 | ||
|
|
720188822d | ||
|
|
48e0e53ab1 | ||
|
|
03c123d63e | ||
|
|
7d4ded2480 | ||
|
|
1ce3825338 | ||
|
|
1fba05dd04 | ||
|
|
2c9b16a2d5 | ||
|
|
314b5bcd87 | ||
|
|
433af312cf | ||
|
|
30b18cf945 | ||
|
|
81b9991e6d | ||
|
|
84f3830eca | ||
|
|
cb878c70b5 | ||
|
|
8224eec990 | ||
|
|
d655ee92a2 | ||
|
|
a8d868e812 | ||
|
|
e5c400214a | ||
|
|
5be5275eda | ||
|
|
e61535ee92 | ||
|
|
e0abe23260 | ||
|
|
8d65a3256b | ||
|
|
d82c297b79 | ||
|
|
f7ed15c507 | ||
|
|
37b583d431 | ||
|
|
c3286c7c4a | ||
|
|
740c751d1b | ||
|
|
7e66f14e16 | ||
|
|
c3d0559260 | ||
|
|
27762e61a6 | ||
|
|
cdbeab8e47 | ||
|
|
15f337a319 | ||
|
|
dd0deeb967 | ||
|
|
7d36e685b8 | ||
|
|
48fa62105f | ||
|
|
7e354d560f | ||
|
|
bd1fdaf9d9 | ||
|
|
ab87cff5bc | ||
|
|
a5280ec0ac | ||
|
|
6339690e58 | ||
|
|
c5ae2800b5 | ||
|
|
62501dc20d | ||
|
|
c7b7848e69 | ||
|
|
a3be63c6f1 | ||
|
|
52fe2ca1c9 | ||
|
|
6513513108 | ||
|
|
442d2c030b | ||
|
|
391500a1c1 | ||
|
|
f239d2c478 | ||
|
|
101f8a29a3 | ||
|
|
dfb846fb7e | ||
|
|
ecd6d5a558 | ||
|
|
048d54a7af | ||
|
|
5645066d68 | ||
|
|
923ecc883a | ||
|
|
59a95fa3bc | ||
|
|
4789c3c2ad | ||
|
|
c149aa8560 | ||
|
|
e9f5d99e9b | ||
|
|
4309dfb57a | ||
|
|
4ad0a22a6a | ||
|
|
2daff0eabb | ||
|
|
cde59bce55 | ||
|
|
80b9d0dc7b | ||
|
|
bfdaa4721d | ||
|
|
60db5cdc6c | ||
|
|
1a5a9060a6 | ||
|
|
ddf9dfba47 | ||
|
|
f93060a5ca | ||
|
|
7e5b491313 | ||
|
|
b083a11099 | ||
|
|
63f67e0f66 | ||
|
|
141c6430c4 | ||
|
|
b872e6960a | ||
|
|
8ba47b11ec | ||
|
|
883351eaf3 | ||
|
|
7976e6d5b5 | ||
|
|
8fcda8c621 | ||
|
|
c1b2870c91 | ||
|
|
2b4f6239f7 | ||
|
|
bae0144393 | ||
|
|
af21cc4400 | ||
|
|
f50d7717a9 | ||
|
|
03a79ddefe | ||
|
|
353246281c | ||
|
|
747d8220e1 | ||
|
|
b6a1648486 | ||
|
|
e0e168945c | ||
|
|
385d7af1b6 | ||
|
|
14811a7c1a | ||
|
|
c828242359 | ||
|
|
c4610c4e24 | ||
|
|
583ba47e78 | ||
|
|
a9e8d5b2b2 | ||
|
|
3c4e33f0c5 | ||
|
|
a0d39be19c | ||
|
|
83ef7b32b5 | ||
|
|
f365a38858 | ||
|
|
e6f081c7d1 | ||
|
|
f463421a33 | ||
|
|
49bb6771f6 | ||
|
|
28d8fcd31e | ||
|
|
008935f054 | ||
|
|
7ed90d7bbf | ||
|
|
7fdcb41547 | ||
|
|
7eca082d9f | ||
|
|
ab81d0fae6 | ||
|
|
3f84551b50 | ||
|
|
0e56c727ff | ||
|
|
c911c0f6e5 | ||
|
|
9e424f16d7 | ||
|
|
dcf1698dad | ||
|
|
8b92df3f6a | ||
|
|
c343a4600f | ||
|
|
d196a574fd | ||
|
|
72b2a456b9 | ||
|
|
d9d42fd664 | ||
|
|
a131308e1a | ||
|
|
aff948f0a6 | ||
|
|
0657d78eb5 | ||
|
|
4807beca0b | ||
|
|
8a26478019 | ||
|
|
3a9851c89b | ||
|
|
2824d8cd84 | ||
|
|
e041238d03 | ||
|
|
df36d33b98 | ||
|
|
f34ca2fe6b |
22
.github/CONTRIBUTING.md
vendored
22
.github/CONTRIBUTING.md
vendored
@@ -1,17 +1,25 @@
|
||||
## Pull Requests
|
||||
|
||||
Before we can merge your pull request you need to accept our CLA [here](https://github.com/espocrm/cla). It's very simple to do.
|
||||
Before we can merge your pull request, you need to accept our CLA [here](https://github.com/espocrm/cla).
|
||||
|
||||
See [Code Style Guidelines](https://github.com/espocrm/espocrm/wiki/Code-Style-Guidelines).
|
||||
It's desirable that one PR solves one specific problem. Do not include code style changes to PRs
|
||||
(unless the main purpose of the PR is a code style fix).
|
||||
|
||||
If you would like to contribute something that is not a small fix, it's reasonable to create an issue first
|
||||
(a bug report or feature request).
|
||||
|
||||
Branches:
|
||||
|
||||
* *hotfix/** – upcoming maintenance release; fixes should be pushed to this branch;
|
||||
* *master* – develop branch; new features should be pushed to this branch;
|
||||
* *stable* – last stable release.
|
||||
* *master* – the develop branch; new features should be pushed to here;
|
||||
* *fix* – the upcoming maintenance release; small fixes should be pushed to here.
|
||||
|
||||
## Issues
|
||||
|
||||
When reporting a possible bug please provide detail steps so that we will be able to reproduce the issue. Please try not to use phrases like "very big bug", "huge issue", etc. No need to use exclamation marks as well.
|
||||
We'd appreciate if you prefer posting issues on weekdays rather than weekends.
|
||||
|
||||
Note that we don't provide developer help or any kind of support on Github. Please use our [forum](https://forum.espocrm.com) for this.
|
||||
When reporting a possible bug, please provide detail steps so that we will be able
|
||||
to reproduce the issue. Please try not to use phrases like "very big bug",
|
||||
"huge issue", etc. No need to use exclamation marks as well.
|
||||
|
||||
Note that we don't provide developer help or any kind of support on GitHub.
|
||||
For this, please use our [forum](https://forum.espocrm.com).
|
||||
|
||||
2
.github/SECURITY.md
vendored
2
.github/SECURITY.md
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
If you believe you have discovered a vulnerability in EspoCRM please contacts us via [this](https://www.espocrm.com/contacts/) or [this](https://www.espocrm.com/support/) forms.
|
||||
If you believe you have discovered a vulnerability in EspoCRM, please contacts us via [this](https://www.espocrm.com/contacts/) or [this](https://www.espocrm.com/support/) forms. Or create a private vulnerability report on GitHub.
|
||||
|
||||
## Supported versions
|
||||
|
||||
|
||||
63
.github/workflows/test-integration-pg.yml
vendored
Normal file
63
.github/workflows/test-integration-pg.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
name: Test Integration on PostgreSQL
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 11 * * *'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test on PHP ${{ matrix.php-versions }}
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
TEST_DATABASE_HOST: '127.0.0.1'
|
||||
TEST_DATABASE_PLATFORM: 'Postgresql'
|
||||
TEST_DATABASE_CHARSET: 'utf8'
|
||||
TEST_DATABASE_PORT: '8888'
|
||||
TEST_DATABASE_NAME: integration_test
|
||||
TEST_DATABASE_USER: postgres
|
||||
TEST_DATABASE_PASSWORD: password
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15.2
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: password
|
||||
POSTGRES_DB: integration_test
|
||||
ports:
|
||||
- '8888:5432'
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-versions: ['8.2']
|
||||
branches: ['master']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ matrix.branches }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- name: Setup PHP with Composer
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
tools: composer:v2
|
||||
ini-values: memory_limit=1024M
|
||||
|
||||
- name: NPM install
|
||||
run: npm install
|
||||
|
||||
- name: Build
|
||||
run: grunt test
|
||||
|
||||
- name: Integration testing
|
||||
run: vendor/bin/phpunit --testsuite integration-pg
|
||||
5
.idea/codeStyles/Project.xml
generated
5
.idea/codeStyles/Project.xml
generated
@@ -6,6 +6,11 @@
|
||||
<option name="KEEP_RPAREN_AND_LBRACE_ON_ONE_LINE" value="true" />
|
||||
<option name="FORCE_EMPTY_METHODS_IN_ONE_LINE" value="true" />
|
||||
</PHPCodeStyleSettings>
|
||||
<codeStyleSettings language="JSON">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="PHP">
|
||||
<option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
|
||||
<option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
|
||||
|
||||
2
.idea/inspectionProfiles/Project_Default.xml
generated
2
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -14,6 +14,8 @@
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PhpSwitchStatementWitSingleBranchInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PsalmAdvanceCallableParamsInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="SqlDialectInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="SqlNoDataSourceInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="TrivialIfJS" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
</profile>
|
||||
</component>
|
||||
@@ -21,10 +21,10 @@
|
||||
|
||||
/**
|
||||
* * `grunt` - full build;
|
||||
* * `grunt dev` - build only items needed for development (takes less time);
|
||||
* * `grunt dev` - build for development;
|
||||
* * `grunt offline` - build but skip *composer install*;
|
||||
* * `grunt internal` - build only libs and css;
|
||||
* * `grunt release` - full build plus upgrade packages`;
|
||||
* * `grunt release` - full build zipped with upgrade packages`;
|
||||
* * `grunt test` - build for tests running;
|
||||
* * `grunt run-tests` - build and run unit and integration tests.
|
||||
*/
|
||||
@@ -503,7 +503,8 @@ module.exports = grunt => {
|
||||
|
||||
grunt.registerTask('dev', [
|
||||
'composer-install-dev',
|
||||
'less',
|
||||
'npm-install',
|
||||
'internal',
|
||||
]);
|
||||
|
||||
grunt.registerTask('test', [
|
||||
|
||||
30
README.md
30
README.md
@@ -2,16 +2,26 @@
|
||||
|
||||
[](#espocrm)
|
||||
|
||||
[EspoCRM is an Open Source CRM](https://www.espocrm.com) (Customer Relationship Management) software that allows you to see, enter and evaluate all your company relationships regardless of the type. People, companies or opportunities – all in an easy and intuitive interface.
|
||||
[EspoCRM is an Open Source CRM](https://www.espocrm.com) (Customer Relationship Management)
|
||||
software that allows you to see, enter and evaluate all your company relationships regardless
|
||||
of the type. People, companies or opportunities – all in an easy and intuitive interface.
|
||||
|
||||
It's a web application with a frontend designed as a single page application and REST API backend written in PHP.
|
||||
It's a web application with a frontend designed as a single page application and REST API
|
||||
backend written in PHP.
|
||||
|
||||
[Download](https://www.espocrm.com/download/) the latest release from our website.
|
||||
[Download](https://www.espocrm.com/download/) the latest release from our website. Release notes
|
||||
and release packages are available at [Releases](https://github.com/espocrm/espocrm/releases) on GitHub.
|
||||
|
||||

|
||||
|
||||
### Demo
|
||||
|
||||
You can try the CRM on the online [demo](https://www.espocrm.com/demo/).
|
||||
|
||||
### Requirements
|
||||
|
||||
* PHP 8.0 and later;
|
||||
* MySQL 5.7 (and later), or MariaDB 10.1 (and later).
|
||||
* MySQL 5.7 (and later), or MariaDB 10.2 (and later).
|
||||
|
||||
For more information about server configuration see [this article](https://docs.espocrm.com/administration/server-configuration/).
|
||||
|
||||
@@ -21,11 +31,12 @@ The documentation for administrators, users and developers is available [here](h
|
||||
|
||||
### Bug reporting
|
||||
|
||||
Create an issue [here](https://github.com/espocrm/espocrm/issues) or post on our [forum](http://forum.espocrm.com/forum/bug-reports).
|
||||
Create an issue [here](https://github.com/espocrm/espocrm/issues) or post on our [forum](https://forum.espocrm.com/forum/bug-reports).
|
||||
We'd appreciate if you prefer posting issues on weekdays rather than weekends.
|
||||
|
||||
### Installing the stable version
|
||||
|
||||
See the [instructions](https://docs.espocrm.com/administration/installation/) about installation.
|
||||
See the [instructions](https://docs.espocrm.com/administration/installation/) on installation.
|
||||
|
||||
### Development
|
||||
|
||||
@@ -35,11 +46,14 @@ See the [instructions](https://docs.espocrm.com/administration/installation/) ab
|
||||
|
||||
### Contributing
|
||||
|
||||
Before we can merge your pull request you need to accept our CLA [here](https://github.com/espocrm/cla). It's very simple to do.
|
||||
Before we can merge your pull request, you need to accept our CLA [here](https://github.com/espocrm/cla). It's very simple to do.
|
||||
|
||||
Contribute translations to [POEditor](https://poeditor.com/join/project/gLDKZtUF4i). Changes
|
||||
are usually merged to the GitHub repository before minor releases.
|
||||
|
||||
Branches:
|
||||
|
||||
* *fix* – upcoming maintenance release; fixes should be pushed to this branch;
|
||||
* *fix* – upcoming maintenance release; minor fixes should be pushed to this branch;
|
||||
* *master* – develop branch; new features should be pushed to this branch;
|
||||
* *stable* – last stable release.
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ class Binding implements BindingProcessor
|
||||
public function process(Binder $binder): void
|
||||
{
|
||||
$this->bindServices($binder);
|
||||
$this->bindCore($binder);
|
||||
$this->bindMisc($binder);
|
||||
$this->bindAcl($binder);
|
||||
$this->bindWebSocket($binder);
|
||||
@@ -63,7 +64,12 @@ class Binding implements BindingProcessor
|
||||
);
|
||||
|
||||
$binder->bindService(
|
||||
'Espo\\Core\\Container\\Container',
|
||||
'Espo\\Core\\Container',
|
||||
'container'
|
||||
);
|
||||
|
||||
$binder->bindService(
|
||||
'Psr\\Container\\ContainerInterface',
|
||||
'container'
|
||||
);
|
||||
|
||||
@@ -72,6 +78,16 @@ class Binding implements BindingProcessor
|
||||
'module'
|
||||
);
|
||||
|
||||
$binder->bindService(
|
||||
'Espo\\Core\\Utils\\Config',
|
||||
'config'
|
||||
);
|
||||
|
||||
$binder->bindService(
|
||||
'Espo\\Core\\Utils\\File\\Manager',
|
||||
'fileManager'
|
||||
);
|
||||
|
||||
$binder->bindService(
|
||||
'Espo\\ORM\\EntityManager',
|
||||
'entityManager'
|
||||
@@ -208,8 +224,8 @@ class Binding implements BindingProcessor
|
||||
);
|
||||
|
||||
$binder->bindService(
|
||||
'Espo\\Core\\Acl',
|
||||
'acl'
|
||||
'Espo\\Core\\Utils\\ClientManager',
|
||||
'clientManager'
|
||||
);
|
||||
|
||||
$binder->bindService(
|
||||
@@ -218,6 +234,14 @@ class Binding implements BindingProcessor
|
||||
);
|
||||
}
|
||||
|
||||
private function bindCore(Binder $binder): void
|
||||
{
|
||||
$binder->bindImplementation(
|
||||
'Espo\\ORM\\PDO\\PDOProvider',
|
||||
'Espo\\ORM\\PDO\\DefaultPDOProvider'
|
||||
);
|
||||
}
|
||||
|
||||
private function bindMisc(Binder $binder): void
|
||||
{
|
||||
$binder->bindImplementation(
|
||||
@@ -246,7 +270,7 @@ class Binding implements BindingProcessor
|
||||
->for('Espo\\Core\\Authentication\\Oidc\\Login')
|
||||
->bindImplementation(
|
||||
'Espo\\Core\\Authentication\\Oidc\\UserProvider',
|
||||
'Espo\\Core\\Authentication\\Oidc\\DefaultUserProvider'
|
||||
'Espo\\Core\\Authentication\\Oidc\\UserProvider\\DefaultUserProvider'
|
||||
);
|
||||
|
||||
$binder->bindImplementation(
|
||||
|
||||
102
application/Espo/Classes/ConsoleCommands/CreateAdminUser.php
Normal file
102
application/Espo/Classes/ConsoleCommands/CreateAdminUser.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://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 General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\ConsoleCommands;
|
||||
|
||||
use Espo\Core\Console\Command;
|
||||
use Espo\Core\Console\Command\Params;
|
||||
use Espo\Core\Console\IO;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class CreateAdminUser implements Command
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Config $config
|
||||
) {}
|
||||
|
||||
public function run(Params $params, IO $io): void
|
||||
{
|
||||
$userName = $params->getArgument(0);
|
||||
|
||||
if (!$userName) {
|
||||
$io->writeLine("A username must be specified as the first argument.");
|
||||
$io->setExitStatus(1);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var ?string $regExp */
|
||||
$regExp = $this->config->get('userNameRegularExpression');
|
||||
|
||||
if (!$regExp) {
|
||||
throw new RuntimeException("No `userNameRegularExpression` in config.");
|
||||
}
|
||||
|
||||
if (
|
||||
str_contains($userName, ' ') ||
|
||||
preg_replace("/{$regExp}/", '_', $userName) !== $userName
|
||||
) {
|
||||
$io->writeLine("Not allowed username.");
|
||||
$io->setExitStatus(1);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$repository = $this->entityManager->getRDBRepositoryByClass(User::class);
|
||||
|
||||
$existingUser = $repository
|
||||
->where(['userName' => $userName])
|
||||
->findOne();
|
||||
|
||||
if ($existingUser) {
|
||||
$io->writeLine("A user with the same username already exists.");
|
||||
$io->setExitStatus(1);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $repository->getNew();
|
||||
|
||||
$user->set('userName', $userName);
|
||||
$user->set('type', User::TYPE_ADMIN);
|
||||
$user->set('name', $userName);
|
||||
|
||||
$repository->save($user);
|
||||
|
||||
$message = "The user '{$userName}' has been created. " .
|
||||
"Set password with the command: `bin/command set-password {$userName}`.";
|
||||
|
||||
$io->writeLine($message);
|
||||
}
|
||||
}
|
||||
@@ -32,26 +32,16 @@ namespace Espo\Classes\ConsoleCommands;
|
||||
use Espo\Tools\Import\Service;
|
||||
|
||||
use Espo\Core\Utils\File\Manager as FileManager;
|
||||
|
||||
use Espo\Core\{
|
||||
Console\Command,
|
||||
Console\Command\Params,
|
||||
Console\IO,
|
||||
};
|
||||
use Espo\Core\Console\Command;
|
||||
use Espo\Core\Console\Command\Params;
|
||||
use Espo\Core\Console\IO;
|
||||
|
||||
use Throwable;
|
||||
|
||||
class Import implements Command
|
||||
{
|
||||
private Service $service;
|
||||
|
||||
private FileManager $fileManager;
|
||||
|
||||
public function __construct(Service $service, FileManager $fileManager)
|
||||
{
|
||||
$this->service = $service;
|
||||
$this->fileManager = $fileManager;
|
||||
}
|
||||
public function __construct(private Service $service, private FileManager $fileManager)
|
||||
{}
|
||||
|
||||
public function run(Params $params, IO $io) : void
|
||||
{
|
||||
|
||||
@@ -63,7 +63,7 @@ class PopulateNumbers implements Command
|
||||
$field = $params->getArgument(1);
|
||||
|
||||
$orderBy = $params->getOption('orderBy') ?? 'createdAt';
|
||||
$order = $params->getOption('order') ?? Order::ASC;
|
||||
$order = strtoupper($params->getOption('order') ?? Order::ASC);
|
||||
|
||||
if (!$entityType) {
|
||||
throw new ArgumentNotSpecified("No entity type argument.");
|
||||
@@ -73,6 +73,10 @@ class PopulateNumbers implements Command
|
||||
throw new ArgumentNotSpecified("No field argument.");
|
||||
}
|
||||
|
||||
if ($order !== Order::ASC && $order !== Order::DESC) {
|
||||
throw new InvalidArgument("Bad order option.");
|
||||
}
|
||||
|
||||
$fieldType = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entityType)
|
||||
|
||||
@@ -33,22 +33,21 @@ use Espo\Core\Duplicate\WhereBuilder;
|
||||
use Espo\Core\Field\EmailAddressGroup;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
|
||||
use Espo\ORM\{
|
||||
Query\Part\Condition as Cond,
|
||||
Query\Part\WhereItem,
|
||||
Query\Part\Where\OrGroup,
|
||||
Entity,
|
||||
};
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Query\Part\Condition as Cond;
|
||||
use Espo\ORM\Query\Part\Where\OrGroup;
|
||||
use Espo\ORM\Query\Part\WhereItem;
|
||||
|
||||
/**
|
||||
* @implements WhereBuilder<CoreEntity>
|
||||
*/
|
||||
class Company implements WhereBuilder
|
||||
{
|
||||
/**
|
||||
* @param CoreEntity $entity
|
||||
*/
|
||||
public function build(Entity $entity): ?WhereItem
|
||||
{
|
||||
assert($entity instanceof CoreEntity);
|
||||
|
||||
$orBuilder = OrGroup::createBuilder();
|
||||
|
||||
$toCheck = false;
|
||||
|
||||
@@ -27,35 +27,28 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Export\Processor;
|
||||
namespace Espo\Classes\DuplicateWhereBuilders;
|
||||
|
||||
class Data
|
||||
use Espo\Core\Duplicate\WhereBuilder;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Query\Part\Condition as Cond;
|
||||
use Espo\ORM\Query\Part\WhereItem;
|
||||
|
||||
/**
|
||||
* @implements WhereBuilder<Entity>
|
||||
*/
|
||||
class Name implements WhereBuilder
|
||||
{
|
||||
/**
|
||||
* @var resource
|
||||
*/
|
||||
private $resource;
|
||||
|
||||
/**
|
||||
* @param resource $resource
|
||||
*/
|
||||
public function __construct($resource)
|
||||
public function build(Entity $entity): ?WhereItem
|
||||
{
|
||||
$this->resource = $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return ?array<string, mixed>
|
||||
*/
|
||||
public function readRow(): ?array
|
||||
{
|
||||
$line = fgets($this->resource);
|
||||
|
||||
if ($line === false) {
|
||||
return null;
|
||||
if ($entity->get('name')) {
|
||||
return Cond::equal(
|
||||
Cond::column('name'),
|
||||
$entity->get('name')
|
||||
);
|
||||
}
|
||||
|
||||
return unserialize(base64_decode($line));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -31,27 +31,24 @@ namespace Espo\Classes\DuplicateWhereBuilders;
|
||||
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
|
||||
use Espo\Core\{
|
||||
Duplicate\WhereBuilder,
|
||||
Field\EmailAddressGroup,
|
||||
};
|
||||
use Espo\Core\Duplicate\WhereBuilder;
|
||||
use Espo\Core\Field\EmailAddressGroup;
|
||||
|
||||
use Espo\ORM\{
|
||||
Query\Part\Condition as Cond,
|
||||
Query\Part\WhereItem,
|
||||
Query\Part\Where\OrGroup,
|
||||
Entity,
|
||||
};
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Query\Part\Condition as Cond;
|
||||
use Espo\ORM\Query\Part\Where\OrGroup;
|
||||
use Espo\ORM\Query\Part\WhereItem;
|
||||
|
||||
/**
|
||||
* @implements WhereBuilder<CoreEntity>
|
||||
*/
|
||||
class Person implements WhereBuilder
|
||||
{
|
||||
/**
|
||||
* @param CoreEntity $entity
|
||||
*/
|
||||
public function build(Entity $entity): ?WhereItem
|
||||
{
|
||||
assert($entity instanceof CoreEntity);
|
||||
|
||||
$orBuilder = OrGroup::createBuilder();
|
||||
|
||||
$toCheck = false;
|
||||
|
||||
@@ -38,17 +38,10 @@ use stdClass;
|
||||
|
||||
class ArrayType
|
||||
{
|
||||
private Metadata $metadata;
|
||||
private const DEFAULT_MAX_ITEM_LENGTH = 100;
|
||||
|
||||
private Defs $defs;
|
||||
|
||||
private const DEFAULT_MAX_LENGTH = 100;
|
||||
|
||||
public function __construct(Metadata $metadata, Defs $defs)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
$this->defs = $defs;
|
||||
}
|
||||
public function __construct(private Metadata $metadata, private Defs $defs)
|
||||
{}
|
||||
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
@@ -181,9 +174,9 @@ class ArrayType
|
||||
return false;
|
||||
}
|
||||
|
||||
public function checkMaxLength(Entity $entity, string $field, ?int $validationValue): bool
|
||||
public function checkMaxItemLength(Entity $entity, string $field, ?int $validationValue): bool
|
||||
{
|
||||
$maxLength = $validationValue ?? self::DEFAULT_MAX_LENGTH;
|
||||
$maxLength = $validationValue ?? self::DEFAULT_MAX_ITEM_LENGTH;
|
||||
|
||||
/** @var string[] $value */
|
||||
$value = $entity->get($field) ?? [];
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://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 General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\FieldValidators\AuthenticationProvider;
|
||||
|
||||
use Espo\Core\FieldValidation\Validator;
|
||||
use Espo\Core\FieldValidation\Validator\Data;
|
||||
use Espo\Core\FieldValidation\Validator\Failure;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\AuthenticationProvider;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* @implements Validator<AuthenticationProvider>
|
||||
*/
|
||||
class MethodValid implements Validator
|
||||
{
|
||||
public function __construct(private Metadata $metadata) {}
|
||||
|
||||
public function validate(Entity $entity, string $field, Data $data): ?Failure
|
||||
{
|
||||
$value = $entity->get($field);
|
||||
|
||||
if (!$value) {
|
||||
return Failure::create();
|
||||
}
|
||||
|
||||
$isAvailable = $this->metadata->get(['authenticationMethods', $value, 'provider', 'isAvailable']);
|
||||
|
||||
if (!$isAvailable) {
|
||||
return Failure::create();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -29,11 +29,15 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\Core\Field\Currency;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\ORM\BaseEntity;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class CurrencyType extends FloatType
|
||||
{
|
||||
private const DEFAULT_PRECISION = 13;
|
||||
|
||||
public function __construct(private Config $config) {}
|
||||
|
||||
protected function isNotEmpty(Entity $entity, string $field): bool
|
||||
@@ -44,6 +48,62 @@ class CurrencyType extends FloatType
|
||||
$entity->get($field . 'Currency') !== '';
|
||||
}
|
||||
|
||||
public function checkValid(Entity $entity, string $field): bool
|
||||
{
|
||||
if (!$this->isNotEmpty($entity, $field)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($entity->getAttributeType($field) !== Entity::VARCHAR) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var string $value */
|
||||
$value = $entity->get($field);
|
||||
|
||||
if (preg_match('/-?[0-9]+\\.?[0-9]*/', $value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function checkInPermittedRange(Entity $entity, string $field): bool
|
||||
{
|
||||
if (!$this->isNotEmpty($entity, $field)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($entity->getAttributeType($field) !== Entity::VARCHAR) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$entity instanceof BaseEntity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var int $precision */
|
||||
$precision = $entity->getAttributeParam($field, 'precision') ?? self::DEFAULT_PRECISION;
|
||||
|
||||
$value = $entity->get($field);
|
||||
|
||||
$currency = Currency::create($value, 'USD');
|
||||
|
||||
if ($currency->isNegative()) {
|
||||
$currency = $currency->multiply(-1);
|
||||
}
|
||||
|
||||
$pad = str_pad('', $precision, '9');
|
||||
|
||||
$limit = Currency::create($pad, 'USD');
|
||||
|
||||
if ($currency->compare($limit) === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkValidCurrency(Entity $entity, string $field): bool
|
||||
{
|
||||
$attribute = $field . 'Currency';
|
||||
|
||||
@@ -420,7 +420,7 @@ class Cleanup implements JobDataLess
|
||||
->from($scope)
|
||||
->withDeleted()
|
||||
->where([
|
||||
'deleted' => 1,
|
||||
'deleted' => true,
|
||||
'modifiedAt<' => $datetime->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT),
|
||||
'modifiedAt>' => $datetimeFrom->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT),
|
||||
])
|
||||
@@ -760,7 +760,7 @@ class Cleanup implements JobDataLess
|
||||
$service = $this->recordServiceContainer->get($scope);
|
||||
|
||||
$whereClause = [
|
||||
'deleted' => 1,
|
||||
'deleted' => true,
|
||||
];
|
||||
|
||||
if ($this->metadata->get(['entityDefs', $scope, 'fields', 'modifiedAt'])) {
|
||||
|
||||
@@ -41,29 +41,20 @@ use Espo\Entities\EmailFolder;
|
||||
use Espo\Entities\GroupEmailFolder;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Tools\Email\Folder;
|
||||
use Espo\Tools\Email\InboxService as EmailService;
|
||||
use Exception;
|
||||
|
||||
class MoveToFolder implements MassAction
|
||||
{
|
||||
private const FOLDER_INBOX = 'inbox';
|
||||
|
||||
private QueryBuilder $queryBuilder;
|
||||
private EntityManager $entityManager;
|
||||
private EmailService $service;
|
||||
private User $user;
|
||||
private const FOLDER_INBOX = Folder::INBOX;
|
||||
|
||||
public function __construct(
|
||||
QueryBuilder $queryBuilder,
|
||||
EntityManager $entityManager,
|
||||
EmailService $service,
|
||||
User $user
|
||||
) {
|
||||
$this->queryBuilder = $queryBuilder;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->service = $service;
|
||||
$this->user = $user;
|
||||
}
|
||||
private QueryBuilder $queryBuilder,
|
||||
private EntityManager $entityManager,
|
||||
private EmailService $service,
|
||||
private User $user
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
@@ -77,7 +68,7 @@ class MoveToFolder implements MassAction
|
||||
throw new BadRequest("No folder ID.");
|
||||
}
|
||||
|
||||
if ($folderId !== self::FOLDER_INBOX && strpos($folderId, 'group:') !== 0) {
|
||||
if ($folderId !== self::FOLDER_INBOX && !str_starts_with($folderId, 'group:')) {
|
||||
$folder = $this->entityManager
|
||||
->getRDBRepositoryByClass(EmailFolder::class)
|
||||
->where([
|
||||
@@ -91,7 +82,7 @@ class MoveToFolder implements MassAction
|
||||
}
|
||||
}
|
||||
|
||||
if ($folderId && strpos($folderId, 'group:') === 0) {
|
||||
if ($folderId && str_starts_with($folderId, 'group:')) {
|
||||
$folder = $this->entityManager
|
||||
->getRDBRepositoryByClass(GroupEmailFolder::class)
|
||||
->where(['id' => substr($folderId, 6)])
|
||||
@@ -117,7 +108,7 @@ class MoveToFolder implements MassAction
|
||||
try {
|
||||
$this->service->moveToFolder($email->getId(), $folderId, $this->user->getId());
|
||||
}
|
||||
catch (Exception $e) {
|
||||
catch (Exception) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,48 +29,36 @@
|
||||
|
||||
namespace Espo\Classes\MassAction\User;
|
||||
|
||||
use Espo\Core\{
|
||||
ApplicationUser,
|
||||
MassAction\Actions\MassDelete as MassDeleteOriginal,
|
||||
MassAction\QueryBuilder,
|
||||
MassAction\Params,
|
||||
MassAction\Result,
|
||||
MassAction\Data,
|
||||
MassAction\MassAction,
|
||||
Acl,
|
||||
ORM\EntityManager,
|
||||
Exceptions\Forbidden,
|
||||
};
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\MassAction\Actions\MassDelete as MassDeleteOriginal;
|
||||
use Espo\Core\MassAction\Data;
|
||||
use Espo\Core\MassAction\MassAction;
|
||||
use Espo\Core\MassAction\Params;
|
||||
use Espo\Core\MassAction\QueryBuilder;
|
||||
use Espo\Core\MassAction\Result;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
|
||||
use Espo\{
|
||||
Entities\User,
|
||||
ORM\Entity,
|
||||
};
|
||||
use Espo\Core\Utils\SystemUser;
|
||||
use Espo\Entities\User;
|
||||
|
||||
/**
|
||||
* Extended to forbid removal of own and system users.
|
||||
*/
|
||||
class MassDelete implements MassAction
|
||||
{
|
||||
private MassDeleteOriginal $massDeleteOriginal;
|
||||
private QueryBuilder $queryBuilder;
|
||||
private EntityManager $entityManager;
|
||||
private Acl $acl;
|
||||
private User $user;
|
||||
|
||||
public function __construct(
|
||||
MassDeleteOriginal $massDeleteOriginal,
|
||||
QueryBuilder $queryBuilder,
|
||||
EntityManager $entityManager,
|
||||
Acl $acl,
|
||||
User $user
|
||||
) {
|
||||
$this->massDeleteOriginal = $massDeleteOriginal;
|
||||
$this->queryBuilder = $queryBuilder;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->acl = $acl;
|
||||
$this->user = $user;
|
||||
}
|
||||
private MassDeleteOriginal $massDeleteOriginal,
|
||||
private QueryBuilder $queryBuilder,
|
||||
private EntityManager $entityManager,
|
||||
private Acl $acl,
|
||||
private User $user
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function process(Params $params, Data $data): Result
|
||||
{
|
||||
@@ -93,7 +81,7 @@ class MassDelete implements MassAction
|
||||
->getRDBRepository(User::ENTITY_TYPE)
|
||||
->clone($query)
|
||||
->sth()
|
||||
->select(['id'])
|
||||
->select(['id', 'userName'])
|
||||
->find();
|
||||
|
||||
foreach ($collection as $entity) {
|
||||
@@ -106,9 +94,9 @@ class MassDelete implements MassAction
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
protected function checkEntity(Entity $entity): void
|
||||
private function checkEntity(User $entity): void
|
||||
{
|
||||
if ($entity->getId() === ApplicationUser::SYSTEM_USER_ID) {
|
||||
if ($entity->getUserName() === SystemUser::NAME) {
|
||||
throw new Forbidden("Can't delete 'system' user.");
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Classes\MassAction\User;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\MassAction\Actions\MassUpdate as MassUpdateOriginal;
|
||||
use Espo\Core\MassAction\QueryBuilder;
|
||||
use Espo\Core\MassAction\Params;
|
||||
@@ -42,24 +43,15 @@ use Espo\Core\Acl\Table;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
|
||||
use Espo\Core\Utils\SystemUser;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use Espo\Tools\MassUpdate\Data as MassUpdateData;
|
||||
|
||||
class MassUpdate implements MassAction
|
||||
{
|
||||
private MassUpdateOriginal $massUpdateOriginal;
|
||||
private QueryBuilder $queryBuilder;
|
||||
private EntityManager $entityManager;
|
||||
private Acl $acl;
|
||||
private User $user;
|
||||
private FileManager $fileManager;
|
||||
private DataManager $dataManager;
|
||||
|
||||
private const PERMISSION = 'massUpdatePermission';
|
||||
private const SYSTEM_USER_ID = 'system';
|
||||
|
||||
/** @var string[] */
|
||||
private array $notAllowedAttributeList = [
|
||||
@@ -72,25 +64,18 @@ class MassUpdate implements MassAction
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
MassUpdateOriginal $massUpdateOriginal,
|
||||
QueryBuilder $queryBuilder,
|
||||
EntityManager $entityManager,
|
||||
Acl $acl,
|
||||
User $user,
|
||||
FileManager $fileManager,
|
||||
DataManager $dataManager
|
||||
) {
|
||||
$this->massUpdateOriginal = $massUpdateOriginal;
|
||||
$this->queryBuilder = $queryBuilder;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->acl = $acl;
|
||||
$this->user = $user;
|
||||
$this->fileManager = $fileManager;
|
||||
$this->dataManager = $dataManager;
|
||||
}
|
||||
private MassUpdateOriginal $massUpdateOriginal,
|
||||
private QueryBuilder $queryBuilder,
|
||||
private EntityManager $entityManager,
|
||||
private Acl $acl,
|
||||
private User $user,
|
||||
private FileManager $fileManager,
|
||||
private DataManager $dataManager
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function process(Params $params, Data $data): Result
|
||||
{
|
||||
@@ -118,7 +103,7 @@ class MassUpdate implements MassAction
|
||||
->getRDBRepository(User::ENTITY_TYPE)
|
||||
->clone($query)
|
||||
->sth()
|
||||
->select(['id'])
|
||||
->select(['id', 'userName'])
|
||||
->find();
|
||||
|
||||
foreach ($collection as $entity) {
|
||||
@@ -147,9 +132,9 @@ class MassUpdate implements MassAction
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function checkEntity(Entity $entity, MassUpdateData $data): void
|
||||
private function checkEntity(User $entity, MassUpdateData $data): void
|
||||
{
|
||||
if ($entity->getId() === self::SYSTEM_USER_ID) {
|
||||
if ($entity->getUserName() === SystemUser::NAME) {
|
||||
throw new Forbidden("Can't update 'system' user.");
|
||||
}
|
||||
|
||||
|
||||
@@ -29,34 +29,23 @@
|
||||
|
||||
namespace Espo\Classes\Select\Email\AccessControlFilters;
|
||||
|
||||
use Espo\Core\{
|
||||
Select\AccessControl\Filter,
|
||||
};
|
||||
use Espo\Core\Select\AccessControl\Filter;
|
||||
|
||||
use Espo\{
|
||||
ORM\Query\SelectBuilder as QueryBuilder,
|
||||
Classes\Select\Email\Helpers\JoinHelper,
|
||||
Entities\User,
|
||||
};
|
||||
use Espo\Classes\Select\Email\Helpers\JoinHelper;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
|
||||
|
||||
class OnlyOwn implements Filter
|
||||
{
|
||||
private $user;
|
||||
|
||||
private $joinHelper;
|
||||
|
||||
public function __construct(User $user, JoinHelper $joinHelper)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->joinHelper = $joinHelper;
|
||||
}
|
||||
public function __construct(
|
||||
private User $user,
|
||||
private JoinHelper $joinHelper
|
||||
) {}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder): void
|
||||
{
|
||||
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
|
||||
|
||||
$queryBuilder->where([
|
||||
'emailUser.userId' => $this->user->getId(),
|
||||
]);
|
||||
$queryBuilder->where(['emailUser.userId' => $this->user->getId()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,39 +29,47 @@
|
||||
|
||||
namespace Espo\Classes\Select\Email\AccessControlFilters;
|
||||
|
||||
use Espo\Core\{
|
||||
Select\AccessControl\Filter,
|
||||
};
|
||||
use Espo\Core\Select\AccessControl\Filter;
|
||||
|
||||
use Espo\{
|
||||
ORM\Query\SelectBuilder as QueryBuilder,
|
||||
Classes\Select\Email\Helpers\JoinHelper,
|
||||
Entities\User,
|
||||
};
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\Team;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Query\Part\Condition as Cond;
|
||||
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
|
||||
|
||||
class OnlyTeam implements Filter
|
||||
{
|
||||
private $user;
|
||||
|
||||
private $joinHelper;
|
||||
|
||||
public function __construct(User $user, JoinHelper $joinHelper)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->joinHelper = $joinHelper;
|
||||
}
|
||||
public function __construct(private User $user)
|
||||
{}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder): void
|
||||
{
|
||||
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
|
||||
|
||||
$queryBuilder->distinct();
|
||||
$queryBuilder->leftJoin('teams', 'teamsAccess');
|
||||
$queryBuilder->where([
|
||||
'OR' => [
|
||||
'teamsAccessMiddle.teamId' => $this->user->getLinkMultipleIdList('teams'),
|
||||
$subQuery = QueryBuilder::create()
|
||||
->select('id')
|
||||
->from(Email::ENTITY_TYPE)
|
||||
->leftJoin(Team::RELATIONSHIP_ENTITY_TEAM, 'entityTeam', [
|
||||
'entityTeam.entityId:' => 'id',
|
||||
'entityTeam.entityType' => Email::ENTITY_TYPE,
|
||||
'entityTeam.deleted' => false,
|
||||
])
|
||||
->leftJoin(Email::RELATIONSHIP_EMAIL_USER, 'emailUser', [
|
||||
'emailUser.emailId:' => 'id',
|
||||
'emailUser.deleted' => false,
|
||||
'emailUser.userId' => $this->user->getId(),
|
||||
]
|
||||
]);
|
||||
])
|
||||
->where([
|
||||
'OR' => [
|
||||
'entityTeam.teamId' => $this->user->getTeamIdList(),
|
||||
'emailUser.userId' => $this->user->getId(),
|
||||
]
|
||||
])
|
||||
->build();
|
||||
|
||||
$queryBuilder->where(
|
||||
Cond::in(
|
||||
Cond::column('id'),
|
||||
$subQuery
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,45 +30,53 @@
|
||||
namespace Espo\Classes\Select\Email\AdditionalAppliers;
|
||||
|
||||
use Espo\Core\Select\Applier\AdditionalApplier;
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
use Espo\Core\Select\SearchParams;
|
||||
|
||||
use Espo\Classes\Select\Email\Helpers\JoinHelper;
|
||||
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
use Espo\Tools\Email\Folder;
|
||||
|
||||
class Main implements AdditionalApplier
|
||||
{
|
||||
private $user;
|
||||
|
||||
private $joinHelper;
|
||||
|
||||
public function __construct(User $user, JoinHelper $joinHelper)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->joinHelper = $joinHelper;
|
||||
}
|
||||
public function __construct(
|
||||
private User $user,
|
||||
private JoinHelper $joinHelper
|
||||
) {}
|
||||
|
||||
public function apply(SelectBuilder $queryBuilder, SearchParams $searchParams): void
|
||||
{
|
||||
$folder = $this->retrieveFolder($searchParams);
|
||||
|
||||
if ($folder === 'drafts') {
|
||||
$queryBuilder->useIndex('createdById');
|
||||
}
|
||||
else if ($folder === 'important') {
|
||||
// skip
|
||||
}
|
||||
else if ($this->checkApplyDateSentIndex($queryBuilder, $searchParams)) {
|
||||
$queryBuilder->useIndex('dateSent');
|
||||
}
|
||||
$this->applyIndexes($folder, $queryBuilder, $searchParams);
|
||||
|
||||
if ($folder !== 'drafts') {
|
||||
if ($folder !== Folder::DRAFTS) {
|
||||
$this->joinEmailUser($queryBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
protected function joinEmailUser(SelectBuilder $queryBuilder): void
|
||||
private function applyIndexes(?string $folder, SelectBuilder $queryBuilder, SearchParams $searchParams): void
|
||||
{
|
||||
if ($searchParams->getTextFilter()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($folder === Folder::IMPORTANT) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($folder === Folder::DRAFTS) {
|
||||
$queryBuilder->useIndex('createdById');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->checkApplyDateSentIndex($queryBuilder, $searchParams)) {
|
||||
$queryBuilder->useIndex('dateSent');
|
||||
}
|
||||
}
|
||||
|
||||
private function joinEmailUser(SelectBuilder $queryBuilder): void
|
||||
{
|
||||
$this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId());
|
||||
|
||||
@@ -77,10 +85,10 @@ class Main implements AdditionalApplier
|
||||
}
|
||||
|
||||
$itemList = [
|
||||
'isRead',
|
||||
'isImportant',
|
||||
'inTrash',
|
||||
'folderId',
|
||||
Email::USERS_COLUMN_IS_READ,
|
||||
Email::USERS_COLUMN_IS_IMPORTANT,
|
||||
Email::USERS_COLUMN_IN_TRASH,
|
||||
Email::USERS_COLUMN_FOLDER_ID,
|
||||
];
|
||||
|
||||
foreach ($itemList as $item) {
|
||||
@@ -88,7 +96,7 @@ class Main implements AdditionalApplier
|
||||
}
|
||||
}
|
||||
|
||||
protected function retrieveFolder(SearchParams $searchParams): ?string
|
||||
private function retrieveFolder(SearchParams $searchParams): ?string
|
||||
{
|
||||
if (!$searchParams->getWhere()) {
|
||||
return null;
|
||||
@@ -103,7 +111,7 @@ class Main implements AdditionalApplier
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function checkApplyDateSentIndex(SelectBuilder $queryBuilder, SearchParams $searchParams): bool
|
||||
private function checkApplyDateSentIndex(SelectBuilder $queryBuilder, SearchParams $searchParams): bool
|
||||
{
|
||||
if ($searchParams->getTextFilter()) {
|
||||
return false;
|
||||
|
||||
@@ -29,9 +29,8 @@
|
||||
|
||||
namespace Espo\Classes\Select\Email\Helpers;
|
||||
|
||||
use Espo\{
|
||||
ORM\Query\SelectBuilder as QueryBuilder,
|
||||
};
|
||||
use Espo\Entities\Email;
|
||||
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
|
||||
|
||||
class JoinHelper
|
||||
{
|
||||
@@ -41,14 +40,10 @@ class JoinHelper
|
||||
return;
|
||||
}
|
||||
|
||||
$queryBuilder->leftJoin(
|
||||
'EmailUser',
|
||||
'emailUser',
|
||||
[
|
||||
'emailUser.emailId:' => 'id',
|
||||
'emailUser.deleted' => false,
|
||||
'emailUser.userId' => $userId,
|
||||
]
|
||||
);
|
||||
$queryBuilder->leftJoin(Email::RELATIONSHIP_EMAIL_USER, 'emailUser', [
|
||||
'emailUser.emailId:' => 'id',
|
||||
'emailUser.deleted' => false,
|
||||
'emailUser.userId' => $userId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Classes\Select\Email;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Select\Text\Filter;
|
||||
use Espo\Core\Select\Text\Filter\Data;
|
||||
use Espo\Core\Select\Text\DefaultFilter;
|
||||
@@ -44,22 +45,15 @@ use Espo\Entities\EmailAddress;
|
||||
|
||||
class TextFilter implements Filter
|
||||
{
|
||||
private $defaultFilter;
|
||||
|
||||
private $config;
|
||||
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(
|
||||
DefaultFilter $defaultFilter,
|
||||
ConfigProvider $config,
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->defaultFilter = $defaultFilter;
|
||||
$this->config = $config;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
private DefaultFilter $defaultFilter,
|
||||
private ConfigProvider $config,
|
||||
private EntityManager $entityManager
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function apply(QueryBuilder $queryBuilder, Data $data): void
|
||||
{
|
||||
$filter = $data->getFilter();
|
||||
@@ -67,7 +61,7 @@ class TextFilter implements Filter
|
||||
|
||||
if (
|
||||
mb_strlen($filter) < $this->config->getMinLengthForContentSearch() ||
|
||||
strpos($filter, '@') === false ||
|
||||
!str_contains($filter, '@') ||
|
||||
$data->forceFullTextSearch()
|
||||
) {
|
||||
$this->defaultFilter->apply($queryBuilder, $data);
|
||||
|
||||
@@ -39,46 +39,29 @@ use Espo\ORM\Query\Part\WhereClause;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Classes\Select\Email\Helpers\JoinHelper;
|
||||
use Espo\Tools\Email\Folder;
|
||||
|
||||
class InFolder implements ItemConverter
|
||||
{
|
||||
private User $user;
|
||||
private EntityManager $entityManager;
|
||||
private JoinHelper $joinHelper;
|
||||
|
||||
public function __construct(User $user, EntityManager $entityManager, JoinHelper $joinHelper)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->joinHelper = $joinHelper;
|
||||
}
|
||||
public function __construct(
|
||||
private User $user,
|
||||
private EntityManager $entityManager,
|
||||
private JoinHelper $joinHelper
|
||||
) {}
|
||||
|
||||
public function convert(QueryBuilder $queryBuilder, Item $item): WhereClauseItem
|
||||
{
|
||||
$folderId = $item->getValue();
|
||||
|
||||
switch ($folderId) {
|
||||
case 'all':
|
||||
return WhereClause::fromRaw([]);
|
||||
|
||||
case 'inbox':
|
||||
return $this->convertInbox($queryBuilder);
|
||||
|
||||
case 'important':
|
||||
return $this->convertImportant($queryBuilder);
|
||||
|
||||
case 'sent':
|
||||
return $this->convertSent($queryBuilder);
|
||||
|
||||
case 'trash':
|
||||
return $this->convertTrash($queryBuilder);
|
||||
|
||||
case 'drafts':
|
||||
return $this->convertDraft($queryBuilder);
|
||||
|
||||
default:
|
||||
return $this->convertFolderId($queryBuilder, $folderId);
|
||||
}
|
||||
return match ($folderId) {
|
||||
Folder::ALL => WhereClause::fromRaw([]),
|
||||
Folder::INBOX => $this->convertInbox($queryBuilder),
|
||||
Folder::IMPORTANT => $this->convertImportant($queryBuilder),
|
||||
Folder::SENT => $this->convertSent($queryBuilder),
|
||||
Folder::TRASH => $this->convertTrash($queryBuilder),
|
||||
Folder::DRAFTS => $this->convertDraft($queryBuilder),
|
||||
default => $this->convertFolderId($queryBuilder, $folderId),
|
||||
};
|
||||
}
|
||||
|
||||
protected function convertInbox(QueryBuilder $queryBuilder): WhereClauseItem
|
||||
@@ -171,7 +154,7 @@ class InFolder implements ItemConverter
|
||||
{
|
||||
$this->joinEmailUser($queryBuilder);
|
||||
|
||||
if (substr($folderId, 0, 6) === 'group:') {
|
||||
if (str_starts_with($folderId, 'group:')) {
|
||||
$groupFolderId = substr($folderId, 6);
|
||||
|
||||
if ($groupFolderId === '') {
|
||||
|
||||
@@ -30,22 +30,14 @@
|
||||
namespace Espo\Classes\Select\Team\AccessControlFilters;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\Core\Select\AccessControl\Filter;
|
||||
|
||||
use Espo\ORM\Query\{
|
||||
SelectBuilder,
|
||||
Part\Condition as Cond,
|
||||
};
|
||||
use Espo\ORM\Query\Part\Condition as Cond;
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
|
||||
class OnlyTeam implements Filter
|
||||
{
|
||||
private $user;
|
||||
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
public function __construct(private User $user)
|
||||
{}
|
||||
|
||||
public function apply(SelectBuilder $queryBuilder): void
|
||||
{
|
||||
|
||||
@@ -30,40 +30,31 @@
|
||||
namespace Espo\Classes\Select\User\AccessControlFilters;
|
||||
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
|
||||
use Espo\Core\{
|
||||
Select\AccessControl\Filter,
|
||||
AclManager,
|
||||
Acl\Table,
|
||||
};
|
||||
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\AclManager;
|
||||
use Espo\Core\Select\AccessControl\Filter;
|
||||
use Espo\Entities\User;
|
||||
|
||||
class Mandatory implements Filter
|
||||
{
|
||||
private $user;
|
||||
|
||||
private $aclManager;
|
||||
|
||||
public function __construct(User $user, AclManager $aclManager)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->aclManager = $aclManager;
|
||||
}
|
||||
public function __construct(
|
||||
private User $user,
|
||||
private AclManager $aclManager
|
||||
) {}
|
||||
|
||||
public function apply(SelectBuilder $queryBuilder): void
|
||||
{
|
||||
if (!$this->user->isAdmin()) {
|
||||
$queryBuilder->where([
|
||||
'isActive' => true,
|
||||
'type!=' => 'api',
|
||||
'type!=' => User::TYPE_API,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->aclManager->getPermissionLevel($this->user, 'portalPermission') !== Table::LEVEL_YES) {
|
||||
$queryBuilder->where([
|
||||
'OR' => [
|
||||
'type!=' => 'portal',
|
||||
'type!=' => User::TYPE_PORTAL,
|
||||
'id' => $this->user->getId(),
|
||||
]
|
||||
]);
|
||||
@@ -71,12 +62,12 @@ class Mandatory implements Filter
|
||||
|
||||
if (!$this->user->isSuperAdmin()) {
|
||||
$queryBuilder->where([
|
||||
'type!=' => 'super-admin'
|
||||
'type!=' => User::TYPE_SUPER_ADMIN,
|
||||
]);
|
||||
}
|
||||
|
||||
$queryBuilder->where([
|
||||
'type!=' => 'system'
|
||||
'type!=' => User::TYPE_SYSTEM,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,25 +31,18 @@ namespace Espo\Classes\Select\User\AccessControlFilters;
|
||||
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
|
||||
use Espo\Core\{
|
||||
Select\AccessControl\Filter,
|
||||
AclManager,
|
||||
Acl\Table,
|
||||
};
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\AclManager;
|
||||
use Espo\Core\Select\AccessControl\Filter;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
class OnlyTeam implements Filter
|
||||
{
|
||||
private $user;
|
||||
|
||||
private $aclManager;
|
||||
|
||||
public function __construct(User $user, AclManager $aclManager)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->aclManager = $aclManager;
|
||||
}
|
||||
public function __construct(
|
||||
private User $user,
|
||||
private AclManager $aclManager
|
||||
) {}
|
||||
|
||||
public function apply(SelectBuilder $queryBuilder): void
|
||||
{
|
||||
@@ -59,7 +52,7 @@ class OnlyTeam implements Filter
|
||||
];
|
||||
|
||||
if ($this->aclManager->getPermissionLevel($this->user, 'portalPermission') === Table::LEVEL_YES) {
|
||||
$orGroup['type'] = 'portal';
|
||||
$orGroup['type'] = User::TYPE_PORTAL;
|
||||
}
|
||||
|
||||
$queryBuilder
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
namespace Espo\Classes\Select\User\PrimaryFilters;
|
||||
|
||||
use Espo\Core\Select\Primary\Filter;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
|
||||
class Internal implements Filter
|
||||
@@ -37,7 +38,11 @@ class Internal implements Filter
|
||||
public function apply(SelectBuilder $queryBuilder): void
|
||||
{
|
||||
$queryBuilder->where([
|
||||
'type!=' => ['portal', 'api', 'system'],
|
||||
'type!=' => [
|
||||
User::TYPE_PORTAL,
|
||||
User::TYPE_API,
|
||||
User::TYPE_SYSTEM,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,16 +29,12 @@
|
||||
|
||||
namespace Espo\Classes\Select\User\Where\ItemConverters;
|
||||
|
||||
use Espo\Core\{
|
||||
Select\Where\ItemConverter,
|
||||
Select\Where\Item,
|
||||
};
|
||||
|
||||
use Espo\{
|
||||
ORM\Query\SelectBuilder as QueryBuilder,
|
||||
ORM\Query\Part\WhereItem as WhereClauseItem,
|
||||
ORM\Query\Part\WhereClause,
|
||||
};
|
||||
use Espo\Core\Select\Where\Item;
|
||||
use Espo\Core\Select\Where\ItemConverter;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Query\Part\WhereClause;
|
||||
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
|
||||
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
|
||||
|
||||
class IsOfType implements ItemConverter
|
||||
{
|
||||
@@ -46,23 +42,21 @@ class IsOfType implements ItemConverter
|
||||
{
|
||||
$type = $item->getValue();
|
||||
|
||||
switch ($type) {
|
||||
case 'internal':
|
||||
return WhereClause::fromRaw([
|
||||
'type!=' => ['portal', 'api', 'system'],
|
||||
]);
|
||||
|
||||
case 'api':
|
||||
return WhereClause::fromRaw([
|
||||
'type' => 'api',
|
||||
]);
|
||||
|
||||
case 'portal':
|
||||
return WhereClause::fromRaw([
|
||||
'type' => 'portal',
|
||||
]);
|
||||
}
|
||||
|
||||
return WhereClause::fromRaw(['id' => null]);
|
||||
return match ($type) {
|
||||
'internal' => WhereClause::fromRaw([
|
||||
'type!=' => [
|
||||
User::TYPE_PORTAL,
|
||||
User::TYPE_API,
|
||||
User::TYPE_SYSTEM,
|
||||
],
|
||||
]),
|
||||
User::TYPE_PORTAL => WhereClause::fromRaw([
|
||||
'type' => User::TYPE_PORTAL,
|
||||
]),
|
||||
User::TYPE_API => WhereClause::fromRaw([
|
||||
'type' => User::TYPE_API,
|
||||
]),
|
||||
default => WhereClause::fromRaw(['id' => null]),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +51,10 @@ class TableTag implements Helper
|
||||
|
||||
$content = $function !== null ? $function() : '';
|
||||
|
||||
$style = "border: {$border}; border-spacing: 0; border-collapse: collapse;";
|
||||
|
||||
return Result::createSafeString(
|
||||
"<table border=\"{$border}\" cellpadding=\"{$cellpadding}\" {$attributesPart}>" .
|
||||
"<table style=\"{$style}\" border=\"{$border}\" cellpadding=\"{$cellpadding}\" {$attributesPart}>" .
|
||||
$content .
|
||||
"</table>"
|
||||
);
|
||||
|
||||
@@ -56,7 +56,7 @@ class TdTag implements Helper
|
||||
$content = $function !== null ? $function() : '';
|
||||
|
||||
return Result::createSafeString(
|
||||
"<td>" . $content . "</td>"
|
||||
"<td {$attributesPart}>{$content}</td>"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,30 +44,15 @@ use Espo\Entities\User;
|
||||
|
||||
class Admin
|
||||
{
|
||||
private Container $container;
|
||||
private Config $config;
|
||||
private User $user;
|
||||
private AdminNotificationManager $adminNotificationManager;
|
||||
private SystemRequirements $systemRequirements;
|
||||
private ScheduledJob $scheduledJob;
|
||||
private DataManager $dataManager;
|
||||
|
||||
public function __construct(
|
||||
Container $container,
|
||||
Config $config,
|
||||
User $user,
|
||||
AdminNotificationManager $adminNotificationManager,
|
||||
SystemRequirements $systemRequirements,
|
||||
ScheduledJob $scheduledJob,
|
||||
DataManager $dataManager
|
||||
private Container $container,
|
||||
private Config $config,
|
||||
private User $user,
|
||||
private AdminNotificationManager $adminNotificationManager,
|
||||
private SystemRequirements $systemRequirements,
|
||||
private ScheduledJob $scheduledJob,
|
||||
private DataManager $dataManager
|
||||
) {
|
||||
$this->container = $container;
|
||||
$this->config = $config;
|
||||
$this->user = $user;
|
||||
$this->adminNotificationManager = $adminNotificationManager;
|
||||
$this->systemRequirements = $systemRequirements;
|
||||
$this->scheduledJob = $scheduledJob;
|
||||
$this->dataManager = $dataManager;
|
||||
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
|
||||
@@ -29,17 +29,11 @@
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Controllers\RecordBase;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Tools\Attachment\FieldData;
|
||||
use Espo\Tools\Attachment\Service;
|
||||
use Espo\Tools\Attachment\UploadUrlService;
|
||||
use Espo\Tools\Attachment\UploadService;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class Attachment extends RecordBase
|
||||
@@ -52,125 +46,4 @@ class Attachment extends RecordBase
|
||||
|
||||
return parent::getActionList($request, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
*/
|
||||
public function postActionGetAttachmentFromImageUrl(Request $request): stdClass
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$url = $data->url ?? null;
|
||||
$field = $data->field ?? null;
|
||||
$parentType = $data->parentType ?? null;
|
||||
$relatedType = $data->relatedType ?? null;
|
||||
|
||||
if (!$url || !$field) {
|
||||
throw new BadRequest("No `url` or `field`.");
|
||||
}
|
||||
|
||||
try {
|
||||
$fieldData = new FieldData(
|
||||
$field,
|
||||
$parentType,
|
||||
$relatedType
|
||||
);
|
||||
}
|
||||
catch (Error $e) {
|
||||
throw new BadRequest($e->getMessage());
|
||||
}
|
||||
|
||||
return $this->injectableFactory
|
||||
->create(UploadUrlService::class)
|
||||
->uploadImage($url, $fieldData)
|
||||
->getValueMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function postActionGetCopiedAttachment(Request $request): stdClass
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$id = $data->id ?? null;
|
||||
$field = $data->field ?? null;
|
||||
$parentType = $data->parentType ?? null;
|
||||
$relatedType = $data->relatedType ?? null;
|
||||
|
||||
if (!$id || !$field) {
|
||||
throw new BadRequest("No `id` or `field`.");
|
||||
}
|
||||
|
||||
try {
|
||||
$fieldData = new FieldData(
|
||||
$field,
|
||||
$parentType,
|
||||
$relatedType
|
||||
);
|
||||
}
|
||||
catch (Error $e) {
|
||||
throw new BadRequest($e->getMessage());
|
||||
}
|
||||
|
||||
return $this->getAttachmentService()
|
||||
->copy($id, $fieldData)
|
||||
->getValueMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function getActionFile(Request $request, Response $response): void
|
||||
{
|
||||
$id = $request->getRouteParam('id');
|
||||
|
||||
if (!$id) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$fileData = $this->getAttachmentService()->getFileData($id);
|
||||
|
||||
if ($fileData->getType()) {
|
||||
$response->setHeader('Content-Type', $fileData->getType());
|
||||
}
|
||||
|
||||
$response
|
||||
->setHeader('Content-Disposition', 'attachment; filename="' . $fileData->getName() . '"')
|
||||
->setHeader('Content-Length', (string) $fileData->getSize())
|
||||
->setBody($fileData->getStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function postActionChunk(Request $request, Response $response): void
|
||||
{
|
||||
$id = $request->getRouteParam('id');
|
||||
$body = $request->getBodyContents();
|
||||
|
||||
if (!$id || !$body) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$this->injectableFactory
|
||||
->create(UploadService::class)
|
||||
->uploadChunk($id, $body);
|
||||
|
||||
$response->writeBody('true');
|
||||
}
|
||||
|
||||
private function getAttachmentService(): Service
|
||||
{
|
||||
return $this->injectableFactory->create(Service::class);
|
||||
}
|
||||
}
|
||||
|
||||
44
application/Espo/Controllers/AuthenticationProvider.php
Normal file
44
application/Espo/Controllers/AuthenticationProvider.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://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 General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU 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;
|
||||
|
||||
class AuthenticationProvider extends RecordBase
|
||||
{
|
||||
protected function checkAccess(): bool
|
||||
{
|
||||
if (!$this->user->isAdmin()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -32,26 +32,17 @@ namespace Espo\Controllers;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
use Espo\Core\{
|
||||
Api\Request,
|
||||
Api\Response,
|
||||
Acl,
|
||||
};
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
|
||||
use Espo\Tools\DataPrivacy\Erasor;
|
||||
|
||||
class DataPrivacy
|
||||
{
|
||||
private $erasor;
|
||||
|
||||
private $acl;
|
||||
|
||||
public function __construct(Erasor $erasor, Acl $acl)
|
||||
public function __construct(private Erasor $erasor, private Acl $acl)
|
||||
{
|
||||
$this->erasor = $erasor;
|
||||
$this->acl = $acl;
|
||||
|
||||
if ($this->acl->get('dataPrivacyPermission') === 'no') {
|
||||
if ($this->acl->getPermissionLevel('dataPrivacyPermission') === Acl\Table::LEVEL_NO) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,356 +29,7 @@
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
|
||||
use Espo\Core\Controllers\Record;
|
||||
use Espo\Core\Api\Request;
|
||||
|
||||
use Espo\Core\Mail\SmtpParams;
|
||||
use Espo\Entities\Email as EmailEntity;
|
||||
use Espo\Tools\Attachment\FieldData;
|
||||
use Espo\Tools\Email\SendService;
|
||||
use Espo\Tools\Email\InboxService as InboxService;
|
||||
|
||||
use Espo\Tools\Email\Service;
|
||||
use Espo\Tools\Email\TestSendData;
|
||||
use Espo\Tools\EmailTemplate\InsertField\Service as InsertFieldService;
|
||||
use stdClass;
|
||||
|
||||
class Email extends Record
|
||||
{
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function postActionGetCopiedAttachments(Request $request): stdClass
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$id = $data->id ?? null;
|
||||
$field = $data->field ?? null;
|
||||
$parentType = $data->parentType ?? null;
|
||||
$relatedType = $data->relatedType ?? null;
|
||||
|
||||
if (!$id || !$field) {
|
||||
throw new BadRequest("No `id` or `field`.");
|
||||
}
|
||||
|
||||
try {
|
||||
$fieldData = new FieldData(
|
||||
$field,
|
||||
$parentType,
|
||||
$relatedType
|
||||
);
|
||||
}
|
||||
catch (Error $e) {
|
||||
throw new BadRequest($e->getMessage());
|
||||
}
|
||||
|
||||
$list = $this->injectableFactory
|
||||
->create(Service::class)
|
||||
->copyAttachments($id, $fieldData);
|
||||
|
||||
$ids = array_map(
|
||||
fn ($item) => $item->getId(),
|
||||
$list
|
||||
);
|
||||
|
||||
$names = (object) [];
|
||||
|
||||
foreach ($list as $item) {
|
||||
$names->{$item->getId()} = $item->getName();
|
||||
}
|
||||
|
||||
return (object) [
|
||||
'ids' => $ids,
|
||||
'names' => $names,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionSendTestEmail(Request $request): bool
|
||||
{
|
||||
if (!$this->acl->checkScope(EmailEntity::ENTITY_TYPE)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$type = $data->type ?? null;
|
||||
$id = $data->id ?? null;
|
||||
$server = $data->server ?? null;
|
||||
$port = $data->port ?? null;
|
||||
$username = $data->username ?? null;
|
||||
$password = $data->password ?? null;
|
||||
$auth = $data->auth ?? null;
|
||||
$authMechanism = $data->authMechanism ?? null;
|
||||
$security = $data->security ?? null;
|
||||
$userId = $data->userId ?? null;
|
||||
$fromAddress = $data->fromAddress ?? null;
|
||||
$fromName = $data->fromName ?? null;
|
||||
$emailAddress = $data->emailAddress ?? null;
|
||||
|
||||
if (!is_string($server)) {
|
||||
throw new BadRequest("`server`");
|
||||
}
|
||||
|
||||
|
||||
if (!is_int($port)) {
|
||||
throw new BadRequest("`port`.");
|
||||
}
|
||||
|
||||
if (!is_string($emailAddress)) {
|
||||
throw new BadRequest("`emailAddress`.");
|
||||
}
|
||||
|
||||
$smtpParams = SmtpParams
|
||||
::create($server, $port)
|
||||
->withSecurity($security)
|
||||
->withFromName($fromName)
|
||||
->withFromAddress($fromAddress)
|
||||
->withAuth($auth);
|
||||
|
||||
if ($auth) {
|
||||
$smtpParams = $smtpParams
|
||||
->withUsername($username)
|
||||
->withPassword($password)
|
||||
->withAuthMechanism($authMechanism);
|
||||
}
|
||||
|
||||
$data = new TestSendData($emailAddress, $type, $id, $userId);
|
||||
|
||||
$this->getSendService()->sendTestEmail($smtpParams, $data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionMarkAsRead(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (!empty($data->ids)) {
|
||||
$idList = $data->ids;
|
||||
}
|
||||
else {
|
||||
if (!empty($data->id)) {
|
||||
$idList = [$data->id];
|
||||
}
|
||||
else {
|
||||
throw new BadRequest();
|
||||
}
|
||||
}
|
||||
|
||||
$this->getInboxService()->markAsReadIdList($idList);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionMarkAsNotRead(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (!empty($data->ids)) {
|
||||
$idList = $data->ids;
|
||||
}
|
||||
else {
|
||||
if (!empty($data->id)) {
|
||||
$idList = [$data->id];
|
||||
}
|
||||
else {
|
||||
throw new BadRequest();
|
||||
}
|
||||
}
|
||||
|
||||
$this->getInboxService()->markAsNotReadIdList($idList);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function postActionMarkAllAsRead(): bool
|
||||
{
|
||||
$this->getInboxService()->markAllAsRead();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionMarkAsImportant(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (!empty($data->ids)) {
|
||||
$idList = $data->ids;
|
||||
}
|
||||
else {
|
||||
if (!empty($data->id)) {
|
||||
$idList = [$data->id];
|
||||
}
|
||||
else {
|
||||
throw new BadRequest();
|
||||
}
|
||||
}
|
||||
|
||||
$this->getInboxService()->markAsImportantIdList($idList);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionMarkAsNotImportant(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (!empty($data->ids)) {
|
||||
$idList = $data->ids;
|
||||
}
|
||||
else {
|
||||
if (!empty($data->id)) {
|
||||
$idList = [$data->id];
|
||||
}
|
||||
else {
|
||||
throw new BadRequest();
|
||||
}
|
||||
}
|
||||
|
||||
$this->getInboxService()->markAsNotImportantIdList($idList);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionMoveToTrash(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (!empty($data->ids)) {
|
||||
$idList = $data->ids;
|
||||
}
|
||||
else {
|
||||
if (!empty($data->id)) {
|
||||
$idList = [$data->id];
|
||||
}
|
||||
else {
|
||||
throw new BadRequest();
|
||||
}
|
||||
}
|
||||
|
||||
$this->getInboxService()->moveToTrashIdList($idList);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionRetrieveFromTrash(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (!empty($data->ids)) {
|
||||
$idList = $data->ids;
|
||||
}
|
||||
else {
|
||||
if (!empty($data->id)) {
|
||||
$idList = [$data->id];
|
||||
}
|
||||
else {
|
||||
throw new BadRequest();
|
||||
}
|
||||
}
|
||||
|
||||
$this->getInboxService()->retrieveFromTrashIdList($idList);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getActionGetFoldersNotReadCounts(): stdClass
|
||||
{
|
||||
return (object) $this->getInboxService()->getFoldersNotReadCounts();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function postActionMoveToFolder(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (!empty($data->ids)) {
|
||||
$idList = $data->ids;
|
||||
}
|
||||
else if (!empty($data->id)) {
|
||||
$idList = [$data->id];
|
||||
}
|
||||
else {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
if (empty($data->folderId)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
if (count($idList) === 1) {
|
||||
$this->getInboxService()->moveToFolder($idList[0], $data->folderId);
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
$this->getInboxService()->moveToFolderIdList($idList, $data->folderId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function getActionGetInsertFieldData(Request $request): stdClass
|
||||
{
|
||||
if (!$this->acl->checkScope(EmailEntity::ENTITY_TYPE, Table::ACTION_CREATE)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
return $this->injectableFactory
|
||||
->create(InsertFieldService::class)
|
||||
->getData(
|
||||
$request->getQueryParam('parentType'),
|
||||
$request->getQueryParam('parentId'),
|
||||
$request->getQueryParam('to')
|
||||
);
|
||||
}
|
||||
|
||||
private function getInboxService(): InboxService
|
||||
{
|
||||
return $this->injectableFactory->create(InboxService::class);
|
||||
}
|
||||
|
||||
private function getSendService(): SendService
|
||||
{
|
||||
return $this->injectableFactory->create(SendService::class);
|
||||
}
|
||||
}
|
||||
{}
|
||||
|
||||
@@ -29,52 +29,7 @@
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
use Espo\Tools\Email\AddressService as Service;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Controllers\RecordBase;
|
||||
|
||||
class EmailAddress extends RecordBase
|
||||
{
|
||||
private const ADDRESS_MAX_SIZE = 50;
|
||||
|
||||
/**
|
||||
* @return array<int,array<string,mixed>>
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function actionSearchInAddressBook(Request $request): array
|
||||
{
|
||||
if (!$this->acl->checkScope('Email')) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if (!$this->acl->checkScope('Email', 'create')) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$q = $request->getQueryParam('q');
|
||||
|
||||
if ($q === null) {
|
||||
throw new BadRequest("No `q` parameter.");
|
||||
}
|
||||
|
||||
$maxSize = intval($request->getQueryParam('maxSize'));
|
||||
|
||||
if (!$maxSize || $maxSize > self::ADDRESS_MAX_SIZE) {
|
||||
$maxSize = (int) $this->config->get('recordsPerPage');
|
||||
}
|
||||
|
||||
$onlyActual = $request->getQueryParam('onlyActual') === 'true';
|
||||
|
||||
return $this->getEmailAddressService()->searchInAddressBook($q, $maxSize, $onlyActual);
|
||||
}
|
||||
|
||||
private function getEmailAddressService(): Service
|
||||
{
|
||||
return $this->injectableFactory->create(Service::class);
|
||||
}
|
||||
}
|
||||
{}
|
||||
|
||||
@@ -31,5 +31,4 @@ namespace Espo\Controllers;
|
||||
|
||||
class EmailFilter extends \Espo\Core\Controllers\Record
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -29,48 +29,7 @@
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Tools\EmailTemplate\Data;
|
||||
use Espo\Tools\EmailTemplate\Service;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Controllers\Record;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class EmailTemplate extends Record
|
||||
{
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws NotFound
|
||||
* @throws ForbiddenSilent
|
||||
*/
|
||||
public function actionParse(Request $request): stdClass
|
||||
{
|
||||
$id = $request->getQueryParam('id');
|
||||
|
||||
if ($id === null) {
|
||||
throw new BadRequest("No `id`.");
|
||||
}
|
||||
|
||||
$data = Data::create()
|
||||
->withRelatedType($request->getQueryParam('relatedType'))
|
||||
->withRelatedId($request->getQueryParam('relatedId'))
|
||||
->withParentType($request->getQueryParam('parentType'))
|
||||
->withParentId($request->getQueryParam('parentId'))
|
||||
->withEmailAddress($request->getQueryParam('emailAddress'));
|
||||
|
||||
$result = $this->getEmailTemplateService()->process($id, $data);
|
||||
|
||||
return $result->getValueMap();
|
||||
|
||||
}
|
||||
|
||||
private function getEmailTemplateService(): Service
|
||||
{
|
||||
return $this->injectableFactory->create(Service::class);
|
||||
}
|
||||
}
|
||||
{}
|
||||
|
||||
@@ -31,5 +31,4 @@ namespace Espo\Controllers;
|
||||
|
||||
class EmailTemplateCategory extends \Espo\Core\Templates\Controllers\CategoryTree
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -29,27 +29,20 @@
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\{
|
||||
Entities\User,
|
||||
Tools\EntityManager\EntityManager as EntityManagerTool,
|
||||
};
|
||||
use Espo\Entities\User;
|
||||
use Espo\Tools\EntityManager\EntityManager as EntityManagerTool;
|
||||
|
||||
use Espo\Core\{
|
||||
Exceptions\Forbidden,
|
||||
Exceptions\BadRequest,
|
||||
Api\Request,
|
||||
};
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
|
||||
class EntityManager
|
||||
{
|
||||
private $user;
|
||||
|
||||
private $entityManagerTool;
|
||||
|
||||
public function __construct(User $user, EntityManagerTool $entityManagerTool)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->entityManagerTool = $entityManagerTool;
|
||||
public function __construct(
|
||||
private User $user,
|
||||
private EntityManagerTool $entityManagerTool
|
||||
) {
|
||||
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
|
||||
@@ -32,12 +32,10 @@ namespace Espo\Controllers;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
use Espo\Core\{
|
||||
Upgrades\ExtensionManager,
|
||||
Controllers\RecordBase,
|
||||
Api\Request,
|
||||
Api\Response,
|
||||
};
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Controllers\RecordBase;
|
||||
use Espo\Core\Upgrades\ExtensionManager;
|
||||
|
||||
use stdClass;
|
||||
|
||||
|
||||
@@ -29,36 +29,25 @@
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\{
|
||||
Entities\User,
|
||||
Tools\FieldManager\FieldManager as FieldManagerTool,
|
||||
};
|
||||
|
||||
use Espo\Core\{
|
||||
Exceptions\Conflict,
|
||||
Exceptions\Error,
|
||||
Exceptions\Forbidden,
|
||||
Exceptions\BadRequest,
|
||||
Api\Request,
|
||||
DataManager};
|
||||
use Espo\Entities\User;
|
||||
use Espo\Tools\FieldManager\FieldManager as FieldManagerTool;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\DataManager;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
|
||||
class FieldManager
|
||||
{
|
||||
private $user;
|
||||
|
||||
private $dataManager;
|
||||
|
||||
private $fieldManagerTool;
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function __construct(User $user, DataManager $dataManager, FieldManagerTool $fieldManagerTool)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->dataManager = $dataManager;
|
||||
$this->fieldManagerTool = $fieldManagerTool;
|
||||
|
||||
public function __construct(
|
||||
private User $user,
|
||||
private DataManager $dataManager,
|
||||
private FieldManagerTool $fieldManagerTool
|
||||
) {
|
||||
$this->checkControllerAccess();
|
||||
}
|
||||
|
||||
@@ -90,7 +79,7 @@ class FieldManager
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
* @return array<string, mixed>
|
||||
* @throws BadRequest
|
||||
* @throws Conflict
|
||||
* @throws Error
|
||||
@@ -123,7 +112,7 @@ class FieldManager
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
* @return array<string, mixed>
|
||||
* @throws BadRequest
|
||||
* @throws Error
|
||||
*/
|
||||
@@ -133,7 +122,7 @@ class FieldManager
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
* @return array<string, mixed>
|
||||
* @throws BadRequest
|
||||
* @throws Error
|
||||
*/
|
||||
@@ -176,6 +165,7 @@ class FieldManager
|
||||
|
||||
$result = $this->fieldManagerTool->delete($scope, $name);
|
||||
|
||||
$this->dataManager->clearCache();
|
||||
$this->dataManager->rebuildMetadata();
|
||||
|
||||
return $result;
|
||||
@@ -189,14 +179,20 @@ class FieldManager
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (empty($data->scope) || empty($data->name)) {
|
||||
$scope = $data->scope ?? null;
|
||||
$name = $data->name ?? null;
|
||||
|
||||
if (!$scope || !$name) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$this->fieldManagerTool->resetToDefault($data->scope, $data->name);
|
||||
if (!is_string($scope) || !is_string($name)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$this->dataManager->clearCache();
|
||||
$this->dataManager->rebuildMetadata();
|
||||
$this->fieldManagerTool->resetToDefault($scope, $name);
|
||||
|
||||
$this->dataManager->rebuild([$scope]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -30,16 +30,16 @@
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Tools\Import\Params as ImportParams;
|
||||
use Espo\Tools\Import\Service as Service;
|
||||
|
||||
use Espo\Core\{
|
||||
Controllers\Record,
|
||||
Api\Request,
|
||||
Api\Response,
|
||||
};
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Controllers\Record;
|
||||
|
||||
use Espo\Core\Di\InjectableFactoryAware;
|
||||
use Espo\Core\Di\InjectableFactorySetter;
|
||||
@@ -57,112 +57,6 @@ class Import extends Record
|
||||
return $this->acl->check('Import');
|
||||
}
|
||||
|
||||
private function getImportService(): Service
|
||||
{
|
||||
return $this->injectableFactory->create(Service::class);
|
||||
}
|
||||
|
||||
public function postActionUploadFile(Request $request): stdClass
|
||||
{
|
||||
$contents = $request->getBodyContents() ?? '';
|
||||
|
||||
$attachmentId = $this->getImportService()->uploadFile($contents);
|
||||
|
||||
return (object) [
|
||||
'attachmentId' => $attachmentId
|
||||
];
|
||||
}
|
||||
|
||||
public function postActionRevert(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$this->getImportService()->revert($data->id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function postActionRemoveDuplicates(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (empty($data->id)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$this->getImportService()->removeDuplicates($data->id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function postActionCreate(Request $request, Response $response): stdClass
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$entityType = $data->entityType ?? null;
|
||||
$attributeList = $data->attributeList ?? null;
|
||||
$attachmentId = $data->attachmentId ?? null;
|
||||
|
||||
if (!is_array($attributeList)) {
|
||||
throw new BadRequest("No attributeList.");
|
||||
}
|
||||
|
||||
if (!$attachmentId) {
|
||||
throw new BadRequest("No attachmentId.");
|
||||
}
|
||||
|
||||
if (!$entityType) {
|
||||
throw new BadRequest("No entityType.");
|
||||
}
|
||||
|
||||
$params = ImportParams::fromRaw($data);
|
||||
|
||||
$result = $this->getImportService()->import(
|
||||
$entityType,
|
||||
$attributeList,
|
||||
$attachmentId,
|
||||
$params
|
||||
);
|
||||
|
||||
return $result->getValueMap();
|
||||
}
|
||||
|
||||
public function postActionUnmarkAsDuplicate(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (
|
||||
empty($data->id) ||
|
||||
empty($data->entityType) ||
|
||||
empty($data->entityId)
|
||||
) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$this->getImportService()->unmarkAsDuplicate($data->id, $data->entityType, $data->entityId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws \Espo\Core\Exceptions\NotFound
|
||||
*/
|
||||
public function postActionExportErrors(Request $request): stdClass
|
||||
{
|
||||
$id = $request->getParsedBody()->id ?? null;
|
||||
|
||||
if (!$id) {
|
||||
throw new BadRequest("No `id`.");
|
||||
}
|
||||
|
||||
$attachmentId = $this->getImportService()->exportErrors($id);
|
||||
|
||||
return (object) [
|
||||
'attachmentId' => $attachmentId,
|
||||
];
|
||||
}
|
||||
|
||||
public function putActionUpdate(Request $request, Response $response): stdClass
|
||||
{
|
||||
throw new Forbidden();
|
||||
|
||||
@@ -58,9 +58,10 @@ class Oidc
|
||||
$response->writeBody(Json::encode($data));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws ForbiddenSilent
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function postActionBackchannelLogout(Request $request, Response $response): void
|
||||
{
|
||||
|
||||
@@ -55,7 +55,8 @@ class TwoFactorEmail
|
||||
|
||||
if (
|
||||
!$this->user->isAdmin() &&
|
||||
!$this->user->isRegular()
|
||||
!$this->user->isRegular() &&
|
||||
!$this->user->isPortal()
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
@@ -55,7 +55,8 @@ class TwoFactorSms
|
||||
|
||||
if (
|
||||
!$this->user->isAdmin() &&
|
||||
!$this->user->isRegular()
|
||||
!$this->user->isRegular() &&
|
||||
!$this->user->isPortal()
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
@@ -29,191 +29,14 @@
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Controllers\Record;
|
||||
use Espo\Core\Mail\Exceptions\SendingError;
|
||||
use Espo\Tools\UserSecurity\ApiService;
|
||||
use Espo\Tools\UserSecurity\Password\RecoveryService;
|
||||
use Espo\Core\Select\SearchParams;
|
||||
use Espo\Core\Select\Where\Item as WhereItem;
|
||||
|
||||
use Espo\Tools\UserSecurity\Password\Service as PasswordService;
|
||||
use stdClass;
|
||||
|
||||
class User extends Record
|
||||
{
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
*/
|
||||
public function getActionAcl(Request $request): stdClass
|
||||
{
|
||||
$userId = $request->getQueryParam('id');
|
||||
|
||||
if (empty($userId)) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if (
|
||||
!$this->user->isAdmin() &&
|
||||
$this->user->getId() !== $userId
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$user = $this->entityManager->getEntityById(\Espo\Entities\User::ENTITY_TYPE, $userId);
|
||||
|
||||
if (empty($user)) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
return $this->aclManager->getMapData($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function postActionChangeOwnPassword(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$password = $data->password ?? null;
|
||||
$currentPassword = $data->currentPassword ?? null;
|
||||
|
||||
if (
|
||||
!is_string($password) ||
|
||||
!is_string($currentPassword)
|
||||
) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$this->getPasswordService()->changePasswordWithCheck($this->user->getId(), $password, $currentPassword);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function postActionChangePasswordByRequest(Request $request): stdClass
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (empty($data->requestId) || empty($data->password)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$url = $this->getPasswordService()->changePasswordByRecovery($data->requestId, $data->password);
|
||||
|
||||
return (object) [
|
||||
'url' => $url,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function postActionPasswordChangeRequest(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$userName = $data->userName ?? null;
|
||||
$emailAddress = $data->emailAddress ?? null;
|
||||
$url = $data->url ?? null;
|
||||
|
||||
if (!$userName || !$emailAddress) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$this->injectableFactory
|
||||
->create(RecoveryService::class)
|
||||
->request($emailAddress, $userName, $url);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function postActionGenerateNewApiKey(Request $request): stdClass
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (empty($data->id)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
return $this->injectableFactory
|
||||
->create(ApiService::class)
|
||||
->generateNewApiKey($data->id)
|
||||
->getValueMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws SendingError
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function postActionGenerateNewPassword(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (empty($data->id)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$this->getPasswordService()->generateAndSendNewPasswordForUser($data->id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
*/
|
||||
public function postActionSendPasswordChangeLink(Request $request): bool
|
||||
{
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$id = $request->getParsedBody()->id ?? null;
|
||||
|
||||
if (!$id) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$this->getPasswordService()->createAndSendPasswordRecovery($id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function postActionCreateLink(Request $request): bool
|
||||
{
|
||||
if (!$this->user->isAdmin()) {
|
||||
@@ -250,9 +73,4 @@ class User extends Record
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
private function getPasswordService(): PasswordService
|
||||
{
|
||||
return $this->injectableFactory->create(PasswordService::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@ class UserSecurity
|
||||
|
||||
if (
|
||||
!$this->user->isAdmin() &&
|
||||
!$this->user->isRegular()
|
||||
!$this->user->isRegular() &&
|
||||
!$this->user->isPortal()
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
@@ -29,10 +29,9 @@
|
||||
|
||||
namespace Espo\Core;
|
||||
|
||||
use Espo\Core\{
|
||||
Acl\GlobalRestriction,
|
||||
Acl\Table,
|
||||
Acl\Exceptions\NotImplemented};
|
||||
use Espo\Core\Acl\Exceptions\NotImplemented;
|
||||
use Espo\Core\Acl\GlobalRestriction;
|
||||
use Espo\Core\Acl\Table;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\User;
|
||||
@@ -44,14 +43,10 @@ use stdClass;
|
||||
*/
|
||||
class Acl
|
||||
{
|
||||
protected AclManager $aclManager;
|
||||
protected User $user;
|
||||
|
||||
public function __construct(AclManager $aclManager, User $user)
|
||||
{
|
||||
$this->aclManager = $aclManager;
|
||||
$this->user = $user;
|
||||
}
|
||||
public function __construct(
|
||||
protected AclManager $aclManager,
|
||||
protected User $user
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get a full access data map.
|
||||
@@ -343,7 +338,7 @@ class Acl
|
||||
*/
|
||||
public function checkIsOwner(Entity $entity): bool
|
||||
{
|
||||
return $this->aclManager->checkIsOwner($this->user, $entity);
|
||||
return $this->aclManager->checkOwnershipOwn($this->user, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -351,11 +346,11 @@ class Acl
|
||||
*/
|
||||
public function checkInTeam(Entity $entity): bool
|
||||
{
|
||||
return $this->aclManager->checkInTeam($this->user, $entity);
|
||||
return $this->aclManager->checkOwnershipTeam($this->user, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @deprecated Use `checkUserPermission` instead.
|
||||
*/
|
||||
public function checkUser(string $permission, User $entity): bool
|
||||
{
|
||||
|
||||
@@ -30,9 +30,8 @@
|
||||
namespace Espo\Core\Acl;
|
||||
|
||||
/**
|
||||
* @deprecated Use AccessChecker interfaces instead.
|
||||
* @deprecated As of v7.0. Use AccessChecker interfaces instead.
|
||||
* @see https://docs.espocrm.com/development/metadata/acl-defs/
|
||||
*/
|
||||
class Acl extends Base
|
||||
{
|
||||
|
||||
}
|
||||
{}
|
||||
|
||||
@@ -44,7 +44,8 @@ use Espo\Core\{
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Use AccessChecker interfaces instead.
|
||||
* @deprecated As of v6.0. Use AccessChecker interfaces instead.
|
||||
* @see https://docs.espocrm.com/development/metadata/acl-defs/
|
||||
*/
|
||||
class Base implements AccessChecker, Injectable
|
||||
{
|
||||
|
||||
@@ -30,16 +30,12 @@
|
||||
namespace Espo\Core\Acl;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Espo\Core\{
|
||||
Acl\Table,
|
||||
Acl\AccessChecker\ScopeCheckerData,
|
||||
Acl\AccessChecker\ScopeChecker,
|
||||
AclManager,
|
||||
Utils\Config,
|
||||
};
|
||||
use Espo\Core\Acl\AccessChecker\ScopeChecker;
|
||||
use Espo\Core\Acl\AccessChecker\ScopeCheckerData;
|
||||
use Espo\Core\AclManager;
|
||||
use Espo\Core\Utils\Config;
|
||||
|
||||
use DateTime;
|
||||
use Exception;
|
||||
@@ -62,28 +58,15 @@ class DefaultAccessChecker implements
|
||||
AccessEntityStreamChecker
|
||||
{
|
||||
private const ATTR_CREATED_BY_ID = 'createdById';
|
||||
|
||||
private const ATTR_CREATED_AT = 'createdAt';
|
||||
|
||||
private const ATTR_ASSIGNED_USER_ID = 'assignedUserId';
|
||||
|
||||
private const ALLOW_DELETE_OWN_CREATED_PERIOD = '24 hours';
|
||||
|
||||
private AclManager $aclManager;
|
||||
|
||||
private Config $config;
|
||||
|
||||
private ScopeChecker $scopeChecker;
|
||||
|
||||
public function __construct(
|
||||
AclManager $aclManager,
|
||||
Config $config,
|
||||
ScopeChecker $scopeChecker
|
||||
) {
|
||||
$this->aclManager = $aclManager;
|
||||
$this->config = $config;
|
||||
$this->scopeChecker = $scopeChecker;
|
||||
}
|
||||
private AclManager $aclManager,
|
||||
private Config $config,
|
||||
private ScopeChecker $scopeChecker
|
||||
) {}
|
||||
|
||||
private function checkEntity(User $user, Entity $entity, ScopeData $data, string $action): bool
|
||||
{
|
||||
@@ -235,7 +218,7 @@ class DefaultAccessChecker implements
|
||||
try {
|
||||
$dt = new DateTime($value);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
catch (Exception) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,19 +33,12 @@ use Espo\Core\ORM\Entity as CoreEntity;
|
||||
|
||||
use Espo\Repositories\User as UserRepository;
|
||||
|
||||
use Espo\ORM\{
|
||||
Entity,
|
||||
EntityManager,
|
||||
Defs,
|
||||
};
|
||||
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\Core\{
|
||||
AclManager,
|
||||
Acl\AssignmentChecker,
|
||||
Acl\Table,
|
||||
};
|
||||
use Espo\Core\AclManager;
|
||||
|
||||
/**
|
||||
* @implements AssignmentChecker<CoreEntity>
|
||||
@@ -53,27 +46,16 @@ use Espo\Core\{
|
||||
class DefaultAssignmentChecker implements AssignmentChecker
|
||||
{
|
||||
protected const FIELD_ASSIGNED_USERS = 'assignedUsers';
|
||||
|
||||
protected const FIELD_TEAMS = 'teams';
|
||||
|
||||
protected const ATTR_ASSIGNED_USER_ID = 'assignedUserId';
|
||||
|
||||
protected const ATTR_TEAMS_IDS = 'teamsIds';
|
||||
|
||||
protected const ATTR_ASSIGNED_USERS_IDS = 'assignedUsersIds';
|
||||
|
||||
private AclManager $aclManager;
|
||||
|
||||
private EntityManager $entityManager;
|
||||
|
||||
private Defs $ormDefs;
|
||||
|
||||
public function __construct(AclManager $aclManager, EntityManager $entityManager, Defs $ormDefs)
|
||||
{
|
||||
$this->aclManager = $aclManager;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->ormDefs = $ormDefs;
|
||||
}
|
||||
public function __construct(
|
||||
private AclManager $aclManager,
|
||||
private EntityManager $entityManager,
|
||||
private Defs $ormDefs
|
||||
) {}
|
||||
|
||||
public function check(User $user, Entity $entity): bool
|
||||
{
|
||||
@@ -154,7 +136,7 @@ class DefaultAssignmentChecker implements AssignmentChecker
|
||||
}
|
||||
|
||||
if ($assignmentPermission === Table::LEVEL_NO) {
|
||||
if ($user->id !== $assignedUserId) {
|
||||
if ($user->getId() !== $assignedUserId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,13 +42,9 @@ use Espo\Entities\User;
|
||||
class DefaultOwnershipChecker implements OwnershipOwnChecker, OwnershipTeamChecker
|
||||
{
|
||||
private const ATTR_CREATED_BY_ID = 'createdById';
|
||||
|
||||
private const ATTR_ASSIGNED_USER_ID = 'assignedUserId';
|
||||
|
||||
private const ATTR_ASSIGNED_TEAMS_IDS = 'teamsIds';
|
||||
|
||||
private const FIELD_TEAMS = 'teams';
|
||||
|
||||
private const FIELD_ASSIGNED_USERS = 'assignedUsers';
|
||||
|
||||
public function checkOwn(User $user, Entity $entity): bool
|
||||
|
||||
@@ -38,7 +38,7 @@ use RuntimeException;
|
||||
class FieldData
|
||||
{
|
||||
/**
|
||||
* @var array<string,string>
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $actionData = [];
|
||||
|
||||
|
||||
@@ -29,12 +29,10 @@
|
||||
|
||||
namespace Espo\Core\Acl;
|
||||
|
||||
use Espo\Core\{
|
||||
Utils\Metadata,
|
||||
Utils\DataCache,
|
||||
Utils\FieldUtil,
|
||||
Utils\Config,
|
||||
};
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\DataCache;
|
||||
use Espo\Core\Utils\FieldUtil;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
use stdClass;
|
||||
|
||||
@@ -46,21 +44,17 @@ class GlobalRestriction
|
||||
{
|
||||
/** Totally forbidden. */
|
||||
public const TYPE_FORBIDDEN = 'forbidden';
|
||||
|
||||
/** Reading forbidden, writing allowed. */
|
||||
public const TYPE_INTERNAL = 'internal';
|
||||
|
||||
/** Forbidden for non-admin users. */
|
||||
public const TYPE_ONLY_ADMIN = 'onlyAdmin';
|
||||
|
||||
/** Read-only for all users. */
|
||||
public const TYPE_READ_ONLY = 'readOnly';
|
||||
|
||||
/** Read-only for non-admin users. */
|
||||
public const TYPE_NON_ADMIN_READ_ONLY = 'nonAdminReadOnly';
|
||||
|
||||
/**
|
||||
* @var array<int,self::TYPE_*>
|
||||
* @var array<int, self::TYPE_*>
|
||||
*/
|
||||
private $fieldTypeList = [
|
||||
self::TYPE_FORBIDDEN,
|
||||
@@ -71,7 +65,7 @@ class GlobalRestriction
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<int,self::TYPE_*>
|
||||
* @var array<int, self::TYPE_*>
|
||||
*/
|
||||
private $linkTypeList = [
|
||||
self::TYPE_FORBIDDEN,
|
||||
@@ -83,7 +77,7 @@ class GlobalRestriction
|
||||
|
||||
/**
|
||||
* Types that should also be taken from entityDefs.
|
||||
* @var array<int,self::TYPE_*>
|
||||
* @var array<int, self::TYPE_*>
|
||||
*/
|
||||
private array $entityDefsTypeList = [
|
||||
self::TYPE_READ_ONLY,
|
||||
@@ -93,19 +87,12 @@ class GlobalRestriction
|
||||
|
||||
private string $cacheKey = 'entityAcl';
|
||||
|
||||
private Metadata $metadata;
|
||||
private DataCache $dataCache;
|
||||
private FieldUtil $fieldUtil;
|
||||
|
||||
public function __construct(
|
||||
Metadata $metadata,
|
||||
DataCache $dataCache,
|
||||
FieldUtil $fieldUtil,
|
||||
private Metadata $metadata,
|
||||
private DataCache $dataCache,
|
||||
private FieldUtil $fieldUtil,
|
||||
Config $config
|
||||
) {
|
||||
$this->metadata = $metadata;
|
||||
$this->dataCache = $dataCache;
|
||||
$this->fieldUtil = $fieldUtil;
|
||||
|
||||
$useCache = $config->get('useCache');
|
||||
|
||||
|
||||
48
application/Espo/Core/Acl/LinkChecker.php
Normal file
48
application/Espo/Core/Acl/LinkChecker.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://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 General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Acl;
|
||||
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* Checks access for linking/unlinking two records.
|
||||
*
|
||||
* @template TEntity of Entity
|
||||
* @template TForeignEntity of Entity
|
||||
*/
|
||||
interface LinkChecker
|
||||
{
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
* @param TForeignEntity $foreignEntity
|
||||
*/
|
||||
public function check(User $user, Entity $entity, Entity $foreignEntity): bool;
|
||||
}
|
||||
74
application/Espo/Core/Acl/LinkChecker/LinkCheckerFactory.php
Normal file
74
application/Espo/Core/Acl/LinkChecker/LinkCheckerFactory.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://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 General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Acl\LinkChecker;
|
||||
|
||||
use Espo\Core\Acl\LinkChecker;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Entity;
|
||||
use RuntimeException;
|
||||
|
||||
class LinkCheckerFactory
|
||||
{
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a link checker.
|
||||
*
|
||||
* @return LinkChecker<Entity, Entity>
|
||||
*/
|
||||
public function create(string $scope, string $link): LinkChecker
|
||||
{
|
||||
$className = $this->getClassName($scope, $link);
|
||||
|
||||
if (!$className) {
|
||||
throw new RuntimeException("Link checker is not implemented for {$scope}.{$link}.");
|
||||
}
|
||||
|
||||
return $this->injectableFactory->create($className);
|
||||
}
|
||||
|
||||
public function isCreatable(string $scope, string $link): bool
|
||||
{
|
||||
return (bool) $this->getClassName($scope, $link);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?class-string<LinkChecker<Entity, Entity>>
|
||||
*/
|
||||
private function getClassName(string $scope, string $link): ?string
|
||||
{
|
||||
/** @var ?class-string<LinkChecker<Entity, Entity>> */
|
||||
return $this->metadata->get(['aclDefs', $scope, 'linkCheckerClassNameMap', $link]);
|
||||
}
|
||||
}
|
||||
@@ -31,16 +31,12 @@ namespace Espo\Core\Acl\Table;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\Core\{
|
||||
Acl\Table,
|
||||
Acl\Table\RoleListProvider,
|
||||
Acl\Table\CacheKeyProvider,
|
||||
Acl\ScopeData,
|
||||
Acl\FieldData,
|
||||
Utils\Config,
|
||||
Utils\Metadata,
|
||||
Utils\DataCache,
|
||||
};
|
||||
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;
|
||||
@@ -54,7 +50,6 @@ class DefaultTable implements Table
|
||||
private const LEVEL_NOT_SET = 'not-set';
|
||||
|
||||
protected string $type = 'acl';
|
||||
|
||||
protected string $defaultAclType = 'recordAllTeamOwnNo';
|
||||
|
||||
/**
|
||||
@@ -111,21 +106,14 @@ class DefaultTable implements Table
|
||||
*/
|
||||
private $valuePermissionList = [];
|
||||
|
||||
private RoleListProvider $roleListProvider;
|
||||
|
||||
protected User $user;
|
||||
|
||||
protected Metadata $metadata;
|
||||
|
||||
public function __construct(
|
||||
RoleListProvider $roleListProvider,
|
||||
private RoleListProvider $roleListProvider,
|
||||
CacheKeyProvider $cacheKeyProvider,
|
||||
User $user,
|
||||
protected User $user,
|
||||
Config $config,
|
||||
Metadata $metadata,
|
||||
protected Metadata $metadata,
|
||||
DataCache $dataCache
|
||||
) {
|
||||
$this->roleListProvider = $roleListProvider;
|
||||
|
||||
$this->data = (object) [
|
||||
'scopes' => (object) [],
|
||||
@@ -133,9 +121,6 @@ class DefaultTable implements Table
|
||||
'permissions' => (object) [],
|
||||
];
|
||||
|
||||
$this->user = $user;
|
||||
$this->metadata = $metadata;
|
||||
|
||||
if (!$this->user->isFetched()) {
|
||||
throw new RuntimeException('User must be fetched before ACL check.');
|
||||
}
|
||||
|
||||
@@ -30,61 +30,61 @@
|
||||
namespace Espo\Core;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use Espo\Core\{
|
||||
Acl,
|
||||
Acl\GlobalRestriction,
|
||||
Acl\OwnerUserFieldProvider,
|
||||
Acl\Table\TableFactory,
|
||||
Acl\Table,
|
||||
Acl\Map\Map,
|
||||
Acl\Map\MapFactory,
|
||||
Acl\OwnershipChecker\OwnershipCheckerFactory,
|
||||
Acl\OwnershipChecker,
|
||||
Acl\OwnershipOwnChecker,
|
||||
Acl\OwnershipTeamChecker,
|
||||
Acl\AccessChecker\AccessCheckerFactory,
|
||||
Acl\AccessChecker,
|
||||
Acl\AccessCreateChecker,
|
||||
Acl\AccessReadChecker,
|
||||
Acl\AccessEditChecker,
|
||||
Acl\AccessDeleteChecker,
|
||||
Acl\AccessStreamChecker,
|
||||
Acl\AccessEntityCreateChecker,
|
||||
Acl\AccessEntityReadChecker,
|
||||
Acl\AccessEntityEditChecker,
|
||||
Acl\AccessEntityDeleteChecker,
|
||||
Acl\AccessEntityStreamChecker,
|
||||
Acl\Exceptions\NotImplemented,
|
||||
};
|
||||
use Espo\Core\Acl\AccessChecker;
|
||||
use Espo\Core\Acl\AccessChecker\AccessCheckerFactory;
|
||||
use Espo\Core\Acl\AccessCreateChecker;
|
||||
use Espo\Core\Acl\AccessDeleteChecker;
|
||||
use Espo\Core\Acl\AccessEditChecker;
|
||||
use Espo\Core\Acl\AccessEntityCreateChecker;
|
||||
use Espo\Core\Acl\AccessEntityDeleteChecker;
|
||||
use Espo\Core\Acl\AccessEntityEditChecker;
|
||||
use Espo\Core\Acl\AccessEntityReadChecker;
|
||||
use Espo\Core\Acl\AccessEntityStreamChecker;
|
||||
use Espo\Core\Acl\AccessReadChecker;
|
||||
use Espo\Core\Acl\AccessStreamChecker;
|
||||
use Espo\Core\Acl\Exceptions\NotImplemented;
|
||||
use Espo\Core\Acl\GlobalRestriction;
|
||||
use Espo\Core\Acl\Map\Map;
|
||||
use Espo\Core\Acl\Map\MapFactory;
|
||||
use Espo\Core\Acl\OwnershipChecker;
|
||||
use Espo\Core\Acl\OwnershipChecker\OwnershipCheckerFactory;
|
||||
use Espo\Core\Acl\OwnershipOwnChecker;
|
||||
use Espo\Core\Acl\OwnershipTeamChecker;
|
||||
use Espo\Core\Acl\OwnerUserFieldProvider;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Acl\Table\TableFactory;
|
||||
|
||||
use stdClass;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* A central access point for access checking.
|
||||
*
|
||||
* @todo Refactor. Replace with an interface `Espo\Core\Acl\AclManager`.
|
||||
* Keep `Espo\Core\AclManager` as an extending interface for bc, bind it to the service.
|
||||
* Implementation in `Espo\Core\Acl\DefaultAclManager`.
|
||||
* The same for `Portal\AclManager`.
|
||||
*/
|
||||
class AclManager
|
||||
{
|
||||
protected const PERMISSION_ASSIGNMENT = 'assignment';
|
||||
|
||||
/** @var array<string,AccessChecker> */
|
||||
/** @var array<string, AccessChecker> */
|
||||
private $accessCheckerHashMap = [];
|
||||
/** @var array<string,OwnershipChecker> */
|
||||
/** @var array<string, OwnershipChecker> */
|
||||
private $ownershipCheckerHashMap = [];
|
||||
/** @var array<string,Table> */
|
||||
/** @var array<string, Table> */
|
||||
protected $tableHashMap = [];
|
||||
/** @var array<string,Map> */
|
||||
/** @var array<string, Map> */
|
||||
protected $mapHashMap = [];
|
||||
|
||||
/** @var class-string */
|
||||
protected $userAclClassName = Acl::class;
|
||||
|
||||
/** @var array<string,class-string<AccessChecker>> */
|
||||
/** @var array<string, class-string<AccessChecker>> */
|
||||
private $entityActionInterfaceMap = [
|
||||
Table::ACTION_CREATE => AccessEntityCreateChecker::class,
|
||||
Table::ACTION_READ => AccessEntityReadChecker::class,
|
||||
@@ -92,7 +92,7 @@ class AclManager
|
||||
Table::ACTION_DELETE => AccessEntityDeleteChecker::class,
|
||||
Table::ACTION_STREAM => AccessEntityStreamChecker::class,
|
||||
];
|
||||
/** @var array<string,class-string<AccessChecker>> */
|
||||
/** @var array<string, class-string<AccessChecker>> */
|
||||
private $actionInterfaceMap = [
|
||||
Table::ACTION_CREATE => AccessCreateChecker::class,
|
||||
Table::ACTION_READ => AccessReadChecker::class,
|
||||
@@ -101,37 +101,29 @@ class AclManager
|
||||
Table::ACTION_STREAM => AccessStreamChecker::class,
|
||||
];
|
||||
|
||||
/** @var AccessCheckerFactory|\Espo\Core\Portal\Acl\AccessChecker\AccessCheckerFactory */
|
||||
/** @var AccessCheckerFactory|Portal\Acl\AccessChecker\AccessCheckerFactory */
|
||||
protected $accessCheckerFactory;
|
||||
/** @var OwnershipCheckerFactory|\Espo\Core\Portal\Acl\OwnershipChecker\OwnershipCheckerFactory */
|
||||
/** @var OwnershipCheckerFactory|Portal\Acl\OwnershipChecker\OwnershipCheckerFactory */
|
||||
protected $ownershipCheckerFactory;
|
||||
/** @var TableFactory */
|
||||
|
||||
/** @var TableFactory */
|
||||
private $tableFactory;
|
||||
/** @var MapFactory */
|
||||
/** @var MapFactory */
|
||||
private $mapFactory;
|
||||
/** @var GlobalRestriction */
|
||||
protected $globalRestriction;
|
||||
/** @var OwnerUserFieldProvider */
|
||||
protected $ownerUserFieldProvider;
|
||||
/** @var EntityManager */
|
||||
protected $entityManager;
|
||||
|
||||
public function __construct(
|
||||
AccessCheckerFactory $accessCheckerFactory,
|
||||
OwnershipCheckerFactory $ownershipCheckerFactory,
|
||||
TableFactory $tableFactory,
|
||||
MapFactory $mapFactory,
|
||||
GlobalRestriction $globalRestriction,
|
||||
OwnerUserFieldProvider $ownerUserFieldProvider,
|
||||
EntityManager $entityManager
|
||||
protected GlobalRestriction $globalRestriction,
|
||||
protected OwnerUserFieldProvider $ownerUserFieldProvider,
|
||||
protected EntityManager $entityManager
|
||||
) {
|
||||
$this->accessCheckerFactory = $accessCheckerFactory;
|
||||
$this->ownershipCheckerFactory = $ownershipCheckerFactory;
|
||||
$this->tableFactory = $tableFactory;
|
||||
$this->mapFactory = $mapFactory;
|
||||
$this->globalRestriction = $globalRestriction;
|
||||
$this->ownerUserFieldProvider = $ownerUserFieldProvider;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -639,7 +631,7 @@ class AclManager
|
||||
/**
|
||||
* Get a restricted field list for a specific scope by a restriction type.
|
||||
*
|
||||
* @param GlobalRestriction::TYPE_*|array<int,GlobalRestriction::TYPE_*> $type
|
||||
* @param GlobalRestriction::TYPE_*|array<int, GlobalRestriction::TYPE_*> $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedFieldList(string $scope, $type): array
|
||||
@@ -691,7 +683,7 @@ class AclManager
|
||||
/**
|
||||
* Get a restricted link list for a specific scope by a restriction type.
|
||||
*
|
||||
* @param GlobalRestriction::TYPE_*|array<int,GlobalRestriction::TYPE_*> $type
|
||||
* @param GlobalRestriction::TYPE_*|array<int, GlobalRestriction::TYPE_*> $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedLinkList(string $scope, $type): array
|
||||
@@ -724,7 +716,7 @@ class AclManager
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated User `checkOwnershipOwn`.
|
||||
* @deprecated As of v7.0. Use `checkOwnershipOwn`.
|
||||
*/
|
||||
public function checkIsOwner(User $user, Entity $entity): bool
|
||||
{
|
||||
@@ -732,7 +724,7 @@ class AclManager
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated User `checkOwnershipTeam`.
|
||||
* @deprecated As of v7.0. Use `checkOwnershipTeam`.
|
||||
*/
|
||||
public function checkInTeam(User $user, Entity $entity): bool
|
||||
{
|
||||
@@ -740,7 +732,7 @@ class AclManager
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @deprecated As of v7.0. Access checkers not to be exposed.
|
||||
*/
|
||||
public function getImplementation(string $scope): object
|
||||
{
|
||||
|
||||
@@ -29,24 +29,20 @@
|
||||
|
||||
namespace Espo\Core\Action;
|
||||
|
||||
use Espo\Core\{
|
||||
Exceptions\NotFound,
|
||||
Exceptions\Forbidden,
|
||||
Utils\Metadata,
|
||||
InjectableFactory,
|
||||
};
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
class ActionFactory
|
||||
{
|
||||
private Metadata $metadata;
|
||||
private InjectableFactory $injectableFactory;
|
||||
|
||||
public function __construct(Metadata $metadata, InjectableFactory $injectableFactory)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
$this->injectableFactory = $injectableFactory;
|
||||
}
|
||||
public function __construct(private Metadata $metadata, private InjectableFactory $injectableFactory)
|
||||
{}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function create(string $action, ?string $entityType = null): Action
|
||||
{
|
||||
$className = $this->getClassName($action, $entityType);
|
||||
|
||||
@@ -155,7 +155,7 @@ class ConvertCurrency implements Action
|
||||
|
||||
$ratesArray[$baseCurrency] = 1.0;
|
||||
|
||||
return CurrencyRates::fromArray($ratesArray, $baseCurrency);
|
||||
return CurrencyRates::fromAssoc($ratesArray, $baseCurrency);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,7 +42,6 @@ use stdClass;
|
||||
|
||||
class Merge implements Action
|
||||
{
|
||||
|
||||
public function __construct(private Acl $acl, private Merger $merger)
|
||||
{}
|
||||
|
||||
|
||||
@@ -29,48 +29,31 @@
|
||||
|
||||
namespace Espo\Core\Action\Actions\Merge;
|
||||
|
||||
use Espo\Core\{
|
||||
Exceptions\Forbidden,
|
||||
Exceptions\NotFound,
|
||||
Action\Params,
|
||||
Acl,
|
||||
Acl\Table,
|
||||
ORM\EntityManager,
|
||||
Utils\Metadata,
|
||||
Utils\ObjectUtil,
|
||||
Record\ServiceContainer,
|
||||
};
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Action\Params;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\ObjectUtil;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Espo\Entities\{
|
||||
PhoneNumber,
|
||||
EmailAddress,
|
||||
};
|
||||
use Espo\Entities\EmailAddress;
|
||||
use Espo\Entities\PhoneNumber;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class Merger
|
||||
{
|
||||
private Acl $acl;
|
||||
|
||||
private Metadata $metadata;
|
||||
|
||||
private EntityManager $entityManager;
|
||||
|
||||
private ServiceContainer $serviceContainer;
|
||||
|
||||
public function __construct(
|
||||
Acl $acl,
|
||||
Metadata $metadata,
|
||||
EntityManager $entityManager,
|
||||
ServiceContainer $serviceContainer
|
||||
) {
|
||||
$this->acl = $acl;
|
||||
$this->metadata = $metadata;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->serviceContainer = $serviceContainer;
|
||||
}
|
||||
private Acl $acl,
|
||||
private Metadata $metadata,
|
||||
private EntityManager $entityManager,
|
||||
private ServiceContainer $serviceContainer
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param string[] $sourceIdList
|
||||
|
||||
@@ -27,29 +27,24 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Controllers;
|
||||
namespace Espo\Core\Action\Api;
|
||||
|
||||
use Espo\Core\{
|
||||
Exceptions\BadRequest,
|
||||
Action\Service,
|
||||
Api\Request,
|
||||
};
|
||||
|
||||
use stdClass;
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Action\Service;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
/**
|
||||
* Action framework.
|
||||
* Processes actions.
|
||||
*/
|
||||
class Action
|
||||
class PostProcess implements Action
|
||||
{
|
||||
private $service;
|
||||
public function __construct(private Service $service)
|
||||
{}
|
||||
|
||||
public function __construct(Service $service)
|
||||
{
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
public function postActionProcess(Request $request): stdClass
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$body = $request->getParsedBody();
|
||||
|
||||
@@ -64,6 +59,6 @@ class Action
|
||||
|
||||
$entity = $this->service->process($entityType, $action, $id, $data);
|
||||
|
||||
return $entity->getValueMap();
|
||||
return ResponseComposer::json($entity->getValueMap());
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ use stdClass;
|
||||
|
||||
class Data
|
||||
{
|
||||
private $data;
|
||||
private stdClass $data;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
|
||||
@@ -36,16 +36,8 @@ use RuntimeException;
|
||||
*/
|
||||
class Params
|
||||
{
|
||||
private string $entityType;
|
||||
private string $id;
|
||||
|
||||
/**
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function __construct(string $entityType, string $id)
|
||||
public function __construct(private string $entityType, private string $id)
|
||||
{
|
||||
$this->entityType = $entityType;
|
||||
$this->id = $id;
|
||||
|
||||
if (!$entityType || !$id) {
|
||||
throw new RuntimeException();
|
||||
|
||||
@@ -29,15 +29,13 @@
|
||||
|
||||
namespace Espo\Core\Action;
|
||||
|
||||
use Espo\Core\{
|
||||
Exceptions\Forbidden,
|
||||
Exceptions\ForbiddenSilent,
|
||||
Exceptions\BadRequest,
|
||||
Exceptions\NotFound,
|
||||
Record\ServiceContainer as RecordServiceContainer,
|
||||
Record\ReadParams,
|
||||
Acl,
|
||||
};
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Record\ReadParams;
|
||||
use Espo\Core\Record\ServiceContainer as RecordServiceContainer;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
@@ -45,21 +43,11 @@ use stdClass;
|
||||
|
||||
class Service
|
||||
{
|
||||
private $factory;
|
||||
|
||||
private $acl;
|
||||
|
||||
private $recordServiceContainer;
|
||||
|
||||
public function __construct(
|
||||
ActionFactory $factory,
|
||||
Acl $acl,
|
||||
RecordServiceContainer $recordServiceContainer
|
||||
) {
|
||||
$this->factory = $factory;
|
||||
$this->acl = $acl;
|
||||
$this->recordServiceContainer = $recordServiceContainer;
|
||||
}
|
||||
private ActionFactory $factory,
|
||||
private Acl $acl,
|
||||
private RecordServiceContainer $recordServiceContainer
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Perform an action.
|
||||
@@ -89,8 +77,6 @@ class Service
|
||||
|
||||
$service = $this->recordServiceContainer->get($entityType);
|
||||
|
||||
$entity = $service->read($id, ReadParams::create());
|
||||
|
||||
return $entity;
|
||||
return $service->read($id, ReadParams::create());
|
||||
}
|
||||
}
|
||||
|
||||
53
application/Espo/Core/Api/Action.php
Normal file
53
application/Espo/Core/Api/Action.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://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 General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Api;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
|
||||
/**
|
||||
* A route action.
|
||||
*/
|
||||
interface Action
|
||||
{
|
||||
/**
|
||||
* Process.
|
||||
*
|
||||
* @param Request $request A request.
|
||||
* @return Response A response. Use ResponseComposer for building.
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
*/
|
||||
public function process(Request $request): Response;
|
||||
}
|
||||
119
application/Espo/Core/Api/ActionHandler.php
Normal file
119
application/Espo/Core/Api/ActionHandler.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://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 General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Api;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
|
||||
use Espo\Core\Utils\Config;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Psr7Response;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Slim\Psr7\Factory\ResponseFactory;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ActionHandler implements RequestHandlerInterface
|
||||
{
|
||||
private const DEFAULT_CONTENT_TYPE = 'application/json';
|
||||
|
||||
public function __construct(
|
||||
private Action $action,
|
||||
private ProcessData $processData,
|
||||
private Config $config
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$requestWrapped = new RequestWrapper(
|
||||
$request,
|
||||
$this->processData->getBasePath(),
|
||||
$this->processData->getRouteParams()
|
||||
);
|
||||
|
||||
$response = $this->action->process($requestWrapped);
|
||||
|
||||
return $this->prepareResponse($response);
|
||||
}
|
||||
|
||||
private function prepareResponse(Response $response): Psr7Response
|
||||
{
|
||||
if (!$response->hasHeader('Content-Type')) {
|
||||
$response->setHeader('Content-Type', self::DEFAULT_CONTENT_TYPE);
|
||||
}
|
||||
|
||||
if (!$response->hasHeader('Cache-Control')) {
|
||||
$response->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
|
||||
}
|
||||
|
||||
if (!$response->hasHeader('Expires')) {
|
||||
$response->setHeader('Expires', '0');
|
||||
}
|
||||
|
||||
if (!$response->hasHeader('Last-Modified')) {
|
||||
$response->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT');
|
||||
}
|
||||
|
||||
$response->setHeader('X-App-Timestamp', (string) ($this->config->get('appTimestamp') ?? '0'));
|
||||
|
||||
return $response instanceof ResponseWrapper ?
|
||||
$response->toPsr7() :
|
||||
self::responseToPsr7($response);
|
||||
}
|
||||
|
||||
private static function responseToPsr7(Response $response): Psr7Response
|
||||
{
|
||||
$psr7Response = (new ResponseFactory())->createResponse();
|
||||
|
||||
$statusCode = $response->getStatusCode();
|
||||
$reason = $response->getReasonPhrase();
|
||||
$body = $response->getBody();
|
||||
|
||||
$psr7Response = $psr7Response
|
||||
->withStatus($statusCode, $reason)
|
||||
->withBody($body);
|
||||
|
||||
foreach ($response->getHeaderNames() as $name) {
|
||||
$psr7Response = $psr7Response->withHeader($name, $response->getHeaderAsArray($name));
|
||||
}
|
||||
|
||||
return $psr7Response;
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ class Auth
|
||||
$authenticationMethod = $this->obtainAuthenticationMethodFromRequest($request);
|
||||
|
||||
if (!$authenticationMethod) {
|
||||
list($username, $password) = $this->obtainUsernamePasswordFromRequest($request);
|
||||
[$username, $password] = $this->obtainUsernamePasswordFromRequest($request);
|
||||
}
|
||||
|
||||
$authenticationData = AuthenticationData::create()
|
||||
@@ -183,7 +183,7 @@ class Auth
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{string,string}
|
||||
* @return array{string, string}
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function decodeAuthorizationString(string $string): array
|
||||
@@ -195,11 +195,11 @@ class Auth
|
||||
throw new BadRequest("Auth: Bad authorization string provided.");
|
||||
}
|
||||
|
||||
/** @var array{string,string} */
|
||||
/** @var array{string, string} */
|
||||
return explode(':', $stringDecoded, 2);
|
||||
}
|
||||
|
||||
protected function handleSecondStepRequired(Response $response, Result $result): void
|
||||
private function handleSecondStepRequired(Response $response, Result $result): void
|
||||
{
|
||||
$response->setStatus(401);
|
||||
$response->setHeader('X-Status-Reason', 'second-step-required');
|
||||
@@ -218,7 +218,7 @@ class Auth
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function handleException(Response $response, Exception $e): void
|
||||
private function handleException(Response $response, Exception $e): void
|
||||
{
|
||||
if (
|
||||
$e instanceof BadRequest ||
|
||||
@@ -233,6 +233,10 @@ class Auth
|
||||
|
||||
$response->setStatus($e->getCode());
|
||||
|
||||
if ($e->getBody()) {
|
||||
$response->writeBody($e->getBody());
|
||||
}
|
||||
|
||||
$this->log->notice("Auth: " . $e->getMessage());
|
||||
|
||||
return;
|
||||
@@ -286,13 +290,13 @@ class Auth
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{?string,?string}
|
||||
* @return array{?string, ?string}
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function obtainUsernamePasswordFromRequest(Request $request): array
|
||||
{
|
||||
if ($request->hasHeader(self::HEADER_ESPO_AUTHORIZATION)) {
|
||||
list($username, $password) = $this->decodeAuthorizationString(
|
||||
[$username, $password] = $this->decodeAuthorizationString(
|
||||
$request->getHeader(self::HEADER_ESPO_AUTHORIZATION) ?? ''
|
||||
);
|
||||
|
||||
@@ -313,7 +317,7 @@ class Auth
|
||||
$request->getHeader('Redirect-Http-Espo-Cgi-Auth');
|
||||
|
||||
if ($cgiAuthString) {
|
||||
list($username, $password) = $this->decodeAuthorizationString(substr($cgiAuthString, 6));
|
||||
[$username, $password] = $this->decodeAuthorizationString(substr($cgiAuthString, 6));
|
||||
|
||||
return [$username, $password];
|
||||
}
|
||||
|
||||
@@ -33,12 +33,9 @@ use Espo\Core\InjectableFactory;
|
||||
|
||||
class AuthBuilderFactory
|
||||
{
|
||||
private InjectableFactory $injectableFactory;
|
||||
|
||||
public function __construct(InjectableFactory $injectableFactory)
|
||||
{
|
||||
$this->injectableFactory = $injectableFactory;
|
||||
}
|
||||
public function __construct(private InjectableFactory $injectableFactory)
|
||||
{}
|
||||
|
||||
public function create(): AuthBuilder
|
||||
{
|
||||
|
||||
91
application/Espo/Core/Api/ControllerActionHandler.php
Normal file
91
application/Espo/Core/Api/ControllerActionHandler.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://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 General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Api;
|
||||
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ControllerActionHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private string $controllerName,
|
||||
private string $actionName,
|
||||
private ProcessData $processData,
|
||||
private ResponseWrapper $responseWrapped,
|
||||
private ControllerActionProcessor $controllerActionProcessor,
|
||||
private Config $config
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$requestWrapped = new RequestWrapper(
|
||||
$request,
|
||||
$this->processData->getBasePath(),
|
||||
$this->processData->getRouteParams()
|
||||
);
|
||||
|
||||
$this->beforeProceed();
|
||||
|
||||
$responseWrapped = $this->controllerActionProcessor->process(
|
||||
$this->controllerName,
|
||||
$this->actionName,
|
||||
$requestWrapped,
|
||||
$this->responseWrapped
|
||||
);
|
||||
|
||||
$this->afterProceed($responseWrapped);
|
||||
|
||||
return $responseWrapped->toPsr7();
|
||||
}
|
||||
|
||||
private function beforeProceed(): void
|
||||
{
|
||||
$this->responseWrapped->setHeader('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
private function afterProceed(Response $responseWrapped): void
|
||||
{
|
||||
$responseWrapped
|
||||
->setHeader('X-App-Timestamp', (string) ($this->config->get('appTimestamp') ?? '0'))
|
||||
->setHeader('Expires', '0')
|
||||
->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT')
|
||||
->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0')
|
||||
->setHeader('Pragma', 'no-cache');
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@
|
||||
namespace Espo\Core\Api;
|
||||
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Exceptions\NotFoundSilent;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\ClassFinder;
|
||||
use Espo\Core\Utils\Json;
|
||||
@@ -41,7 +42,7 @@ use stdClass;
|
||||
/**
|
||||
* Creates controller instances and processes actions.
|
||||
*/
|
||||
class ActionProcessor
|
||||
class ControllerActionProcessor
|
||||
{
|
||||
public function __construct(
|
||||
private InjectableFactory $injectableFactory,
|
||||
@@ -55,8 +56,8 @@ class ActionProcessor
|
||||
string $controllerName,
|
||||
string $actionName,
|
||||
Request $request,
|
||||
Response $response
|
||||
): void {
|
||||
ResponseWrapper $response
|
||||
): ResponseWrapper {
|
||||
|
||||
$controller = $this->createController($controllerName);
|
||||
|
||||
@@ -69,9 +70,7 @@ class ActionProcessor
|
||||
$actionName = $controller::$defaultAction ?? 'index';
|
||||
}
|
||||
|
||||
$actionNameUcfirst = ucfirst($actionName);
|
||||
|
||||
$actionMethodName = 'action' . $actionNameUcfirst;
|
||||
$actionMethodName = 'action' . ucfirst($actionName);
|
||||
|
||||
$fullActionMethodName = strtolower($requestMethod) . ucfirst($actionMethodName);
|
||||
|
||||
@@ -80,7 +79,7 @@ class ActionProcessor
|
||||
$actionMethodName;
|
||||
|
||||
if (!method_exists($controller, $primaryActionMethodName)) {
|
||||
throw new NotFound(
|
||||
throw new NotFoundSilent(
|
||||
"Action {$requestMethod} '{$actionName}' does not exist in controller '{$controllerName}'.");
|
||||
}
|
||||
|
||||
@@ -89,7 +88,7 @@ class ActionProcessor
|
||||
|
||||
$this->handleResult($response, $result);
|
||||
|
||||
return;
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Below is a legacy way.
|
||||
@@ -102,7 +101,7 @@ class ActionProcessor
|
||||
|
||||
$params = $request->getRouteParams();
|
||||
|
||||
$beforeMethodName = 'before' . $actionNameUcfirst;
|
||||
$beforeMethodName = 'before' . ucfirst($actionName);
|
||||
|
||||
if (method_exists($controller, $beforeMethodName)) {
|
||||
$controller->$beforeMethodName($params, $data, $request, $response);
|
||||
@@ -110,13 +109,15 @@ class ActionProcessor
|
||||
|
||||
$result = $controller->$primaryActionMethodName($params, $data, $request, $response) ?? null;
|
||||
|
||||
$afterMethodName = 'after' . $actionNameUcfirst;
|
||||
$afterMethodName = 'after' . ucfirst($actionName);
|
||||
|
||||
if (method_exists($controller, $afterMethodName)) {
|
||||
$controller->$afterMethodName($params, $data, $request, $response);
|
||||
}
|
||||
|
||||
$this->handleResult($response, $result);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
128
application/Espo/Core/Api/MiddlewareProvider.php
Normal file
128
application/Espo/Core/Api/MiddlewareProvider.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://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 General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Api;
|
||||
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class MiddlewareProvider
|
||||
{
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return MiddlewareInterface[]
|
||||
*/
|
||||
public function getGlobalMiddlewareList(): array
|
||||
{
|
||||
return $this->createFromClassNameList($this->getGlobalMiddlewareClassNameList());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MiddlewareInterface[]
|
||||
*/
|
||||
public function getRouteMiddlewareList(Route $route): array
|
||||
{
|
||||
$key = strtolower($route->getMethod()) . '_' . $route->getRoute();
|
||||
|
||||
/** @var class-string<MiddlewareInterface>[] $classNameList */
|
||||
$classNameList = $this->metadata->get(['app', 'api', 'routeMiddlewareClassNameListMap', $key]) ?? [];
|
||||
|
||||
return $this->createFromClassNameList($classNameList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MiddlewareInterface[]
|
||||
*/
|
||||
public function getActionMiddlewareList(Route $route): array
|
||||
{
|
||||
$key = strtolower($route->getMethod()) . '_' . $route->getRoute();
|
||||
|
||||
/** @var class-string<MiddlewareInterface>[] $classNameList */
|
||||
$classNameList = $this->metadata->get(['app', 'api', 'actionMiddlewareClassNameListMap', $key]) ?? [];
|
||||
|
||||
return $this->createFromClassNameList($classNameList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MiddlewareInterface[]
|
||||
*/
|
||||
public function getControllerMiddlewareList(string $controller): array
|
||||
{
|
||||
/** @var class-string<MiddlewareInterface>[] $classNameList */
|
||||
$classNameList = $this->metadata
|
||||
->get(['app', 'api', 'controllerMiddlewareClassNameListMap', $controller]) ?? [];
|
||||
|
||||
return $this->createFromClassNameList($classNameList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MiddlewareInterface[]
|
||||
*/
|
||||
public function getControllerActionMiddlewareList(string $method, string $controller, string $action): array
|
||||
{
|
||||
$key = $controller . '_' . strtolower($method) . '_' . $action;
|
||||
|
||||
/** @var class-string<MiddlewareInterface>[] $classNameList */
|
||||
$classNameList = $this->metadata
|
||||
->get(['app', 'api', 'controllerActionMiddlewareClassNameListMap', $key]) ?? [];
|
||||
|
||||
return $this->createFromClassNameList($classNameList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<MiddlewareInterface>[]
|
||||
*/
|
||||
private function getGlobalMiddlewareClassNameList(): array
|
||||
{
|
||||
return $this->metadata->get(['app', 'api', 'globalMiddlewareClassNameList']) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<MiddlewareInterface>[] $classNameList
|
||||
* @return MiddlewareInterface[]
|
||||
*/
|
||||
private function createFromClassNameList(array $classNameList): array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
foreach ($classNameList as $className) {
|
||||
$list[] = $this->injectableFactory->create($className);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
66
application/Espo/Core/Api/ProcessData.php
Normal file
66
application/Espo/Core/Api/ProcessData.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://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 General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Api;
|
||||
|
||||
class ProcessData
|
||||
{
|
||||
/**
|
||||
* @param array<string, string> $routeParams
|
||||
*/
|
||||
public function __construct(
|
||||
private Route $route,
|
||||
private string $basePath,
|
||||
private array $routeParams
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return Route
|
||||
*/
|
||||
public function getRoute(): Route
|
||||
{
|
||||
return $this->route;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBasePath(): string
|
||||
{
|
||||
return $this->basePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getRouteParams(): array
|
||||
{
|
||||
return $this->routeParams;
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://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 General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Api;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Authentication\AuthenticationFactory;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\ApplicationUser;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* Processes requests. Handles authentication. Obtains a controller name, action, body from a request.
|
||||
* Then passes them to the action processor.
|
||||
*/
|
||||
class RequestProcessor
|
||||
{
|
||||
public function __construct(
|
||||
private AuthenticationFactory $authenticationFactory,
|
||||
private ActionProcessor $actionProcessor,
|
||||
private AuthBuilderFactory $authBuilderFactory,
|
||||
private ErrorOutput $errorOutput,
|
||||
private Config $config,
|
||||
private Log $log,
|
||||
private ApplicationUser $applicationUser
|
||||
) {}
|
||||
|
||||
public function process(Route $route, Request $request, Response $response): void
|
||||
{
|
||||
try {
|
||||
$this->processInternal($route, $request, $response);
|
||||
}
|
||||
catch (Exception $exception) {
|
||||
$this->handleException($exception, $request, $response, $route->getRoute());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws NotFound
|
||||
*/
|
||||
private function processInternal(Route $route, Request $request, Response $response): void
|
||||
{
|
||||
$authRequired = !$route->noAuth();
|
||||
|
||||
$apiAuth = $this->authBuilderFactory
|
||||
->create()
|
||||
->setAuthentication($this->authenticationFactory->create())
|
||||
->setAuthRequired($authRequired)
|
||||
->build();
|
||||
|
||||
$authResult = $apiAuth->process($request, $response);
|
||||
|
||||
if (!$authResult->isResolved()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($authResult->isResolvedUseNoAuth()) {
|
||||
$this->applicationUser->setupSystemUser();
|
||||
}
|
||||
|
||||
ob_start();
|
||||
|
||||
$this->proceed($request, $response);
|
||||
|
||||
ob_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotFound
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function proceed(Request $request, Response $response): void
|
||||
{
|
||||
$this->beforeProceed($response);
|
||||
|
||||
$controllerName = $this->getControllerName($request);
|
||||
$actionName = $request->getRouteParam('action');
|
||||
$requestMethod = $request->getMethod();
|
||||
|
||||
if (!$actionName) {
|
||||
$httpMethod = strtolower($requestMethod);
|
||||
|
||||
$crudList = $this->config->get('crud') ?? [];
|
||||
|
||||
$actionName = $crudList[$httpMethod] ?? null;
|
||||
|
||||
if (!$actionName) {
|
||||
throw new BadRequest("No action for method {$httpMethod}.");
|
||||
}
|
||||
}
|
||||
|
||||
$this->actionProcessor->process($controllerName, $actionName, $request, $response);
|
||||
|
||||
$this->afterProceed($response);
|
||||
}
|
||||
|
||||
private function getControllerName(Request $request): string
|
||||
{
|
||||
$controllerName = $request->getRouteParam('controller');
|
||||
|
||||
if (!$controllerName) {
|
||||
throw new LogicException("Route doesn't have specified controller.");
|
||||
}
|
||||
|
||||
return ucfirst($controllerName);
|
||||
}
|
||||
|
||||
private function handleException(
|
||||
Exception $exception,
|
||||
Request $request,
|
||||
Response $response,
|
||||
string $route
|
||||
): void {
|
||||
|
||||
try {
|
||||
$this->errorOutput->process($request, $response, $exception, $route);
|
||||
}
|
||||
catch (Throwable $exceptionAnother) {
|
||||
$this->log->error($exceptionAnother->getMessage());
|
||||
|
||||
$response->setStatus(500);
|
||||
}
|
||||
}
|
||||
|
||||
private function beforeProceed(Response $response): void
|
||||
{
|
||||
$response->setHeader('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
private function afterProceed(Response $response): void
|
||||
{
|
||||
$response
|
||||
->setHeader('X-App-Timestamp', (string) ($this->config->get('appTimestamp') ?? '0'))
|
||||
->setHeader('Expires', '0')
|
||||
->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT')
|
||||
->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0')
|
||||
->setHeader('Pragma', 'no-cache');
|
||||
}
|
||||
}
|
||||
@@ -31,14 +31,11 @@ namespace Espo\Core\Api;
|
||||
|
||||
use Espo\Core\Utils\Json;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
use Psr\Http\Message\{
|
||||
ServerRequestInterface as Psr7Request,
|
||||
UriInterface,
|
||||
};
|
||||
|
||||
use Espo\Core\Api\Request as ApiRequest;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface as Psr7Request;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
@@ -46,27 +43,22 @@ use stdClass;
|
||||
*/
|
||||
class RequestWrapper implements ApiRequest
|
||||
{
|
||||
private Psr7Request $request;
|
||||
private string $basePath;
|
||||
private ?stdClass $parsedBody = null;
|
||||
/** @var array<string,string> */
|
||||
private array $routeParams;
|
||||
|
||||
/**
|
||||
* @param array<string,string> $routeParams
|
||||
* @param array<string, string> $routeParams
|
||||
*/
|
||||
public function __construct(Psr7Request $request, string $basePath = '', array $routeParams = [])
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->basePath = $basePath;
|
||||
$this->routeParams = $routeParams;
|
||||
}
|
||||
public function __construct(
|
||||
private Psr7Request $psr7Request,
|
||||
private string $basePath = '',
|
||||
private array $routeParams = []
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get a route or query parameter. Route params have a higher priority.
|
||||
*
|
||||
* @todo Don't support NULL $name.
|
||||
* @deprecated For backward compatibility.
|
||||
* @deprecated As of v6.0. Use getQueryParam & getRouteParam. Left for backward compatibility.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
@@ -83,7 +75,7 @@ class RequestWrapper implements ApiRequest
|
||||
return $this->getRouteParam($name);
|
||||
}
|
||||
|
||||
return $this->request->getQueryParams()[$name] ?? null;
|
||||
return $this->psr7Request->getQueryParams()[$name] ?? null;
|
||||
}
|
||||
|
||||
public function hasRouteParam(string $name): bool
|
||||
@@ -97,7 +89,7 @@ class RequestWrapper implements ApiRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,string>
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getRouteParams(): array
|
||||
{
|
||||
@@ -106,12 +98,12 @@ class RequestWrapper implements ApiRequest
|
||||
|
||||
public function hasQueryParam(string $name): bool
|
||||
{
|
||||
return array_key_exists($name, $this->request->getQueryParams());
|
||||
return array_key_exists($name, $this->psr7Request->getQueryParams());
|
||||
}
|
||||
|
||||
public function getQueryParam(string $name): ?string
|
||||
{
|
||||
$value = $this->request->getQueryParams()[$name] ?? null;
|
||||
$value = $this->psr7Request->getQueryParams()[$name] ?? null;
|
||||
|
||||
if (!is_string($value)) {
|
||||
return null;
|
||||
@@ -122,21 +114,21 @@ class RequestWrapper implements ApiRequest
|
||||
|
||||
public function getQueryParams(): array
|
||||
{
|
||||
return $this->request->getQueryParams();
|
||||
return $this->psr7Request->getQueryParams();
|
||||
}
|
||||
|
||||
public function getHeader(string $name): ?string
|
||||
{
|
||||
if (!$this->request->hasHeader($name)) {
|
||||
if (!$this->psr7Request->hasHeader($name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->request->getHeaderLine($name);
|
||||
return $this->psr7Request->getHeaderLine($name);
|
||||
}
|
||||
|
||||
public function hasHeader(string $name): bool
|
||||
{
|
||||
return $this->request->hasHeader($name);
|
||||
return $this->psr7Request->hasHeader($name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,16 +136,16 @@ class RequestWrapper implements ApiRequest
|
||||
*/
|
||||
public function getHeaderAsArray(string $name): array
|
||||
{
|
||||
if (!$this->request->hasHeader($name)) {
|
||||
if (!$this->psr7Request->hasHeader($name)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->request->getHeader($name);
|
||||
return $this->psr7Request->getHeader($name);
|
||||
}
|
||||
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->request->getMethod();
|
||||
return $this->psr7Request->getMethod();
|
||||
}
|
||||
|
||||
public function getContentType(): ?string
|
||||
@@ -164,7 +156,7 @@ class RequestWrapper implements ApiRequest
|
||||
|
||||
$contentType = explode(
|
||||
';',
|
||||
$this->request->getHeader('Content-Type')[0]
|
||||
$this->psr7Request->getHeader('Content-Type')[0]
|
||||
)[0];
|
||||
|
||||
return strtolower($contentType);
|
||||
@@ -172,9 +164,9 @@ class RequestWrapper implements ApiRequest
|
||||
|
||||
public function getBodyContents(): ?string
|
||||
{
|
||||
$contents = $this->request->getBody()->getContents();
|
||||
$contents = $this->psr7Request->getBody()->getContents();
|
||||
|
||||
$this->request->getBody()->rewind();
|
||||
$this->psr7Request->getBody()->rewind();
|
||||
|
||||
return $contents;
|
||||
}
|
||||
@@ -226,7 +218,7 @@ class RequestWrapper implements ApiRequest
|
||||
in_array($contentType, ['application/x-www-form-urlencoded', 'multipart/form-data']) &&
|
||||
$contents
|
||||
) {
|
||||
$parsedBody = $this->request->getParsedBody();
|
||||
$parsedBody = $this->psr7Request->getParsedBody();
|
||||
|
||||
if (is_array($parsedBody)) {
|
||||
$this->parsedBody = (object) $parsedBody;
|
||||
@@ -246,7 +238,7 @@ class RequestWrapper implements ApiRequest
|
||||
|
||||
public function getCookieParam(string $name): ?string
|
||||
{
|
||||
$params = $this->request->getCookieParams();
|
||||
$params = $this->psr7Request->getCookieParams();
|
||||
|
||||
return $params[$name] ?? null;
|
||||
}
|
||||
@@ -256,19 +248,19 @@ class RequestWrapper implements ApiRequest
|
||||
*/
|
||||
public function getServerParam(string $name)
|
||||
{
|
||||
$params = $this->request->getServerParams();
|
||||
$params = $this->psr7Request->getServerParams();
|
||||
|
||||
return $params[$name] ?? null;
|
||||
}
|
||||
|
||||
public function getUri(): UriInterface
|
||||
{
|
||||
return $this->request->getUri();
|
||||
return $this->psr7Request->getUri();
|
||||
}
|
||||
|
||||
public function getResourcePath(): string
|
||||
{
|
||||
$path = $this->request->getUri()->getPath();
|
||||
$path = $this->psr7Request->getUri()->getPath();
|
||||
|
||||
return substr($path, strlen($this->basePath));
|
||||
}
|
||||
|
||||
@@ -36,6 +36,16 @@ use Psr\Http\Message\StreamInterface;
|
||||
*/
|
||||
interface Response
|
||||
{
|
||||
/**
|
||||
* Get a status code.
|
||||
*/
|
||||
public function getStatusCode(): int;
|
||||
|
||||
/**
|
||||
* Get a status reason phrase.
|
||||
*/
|
||||
public function getReasonPhrase(): string;
|
||||
|
||||
/**
|
||||
* Set a status code.
|
||||
*/
|
||||
@@ -61,6 +71,13 @@ interface Response
|
||||
*/
|
||||
public function hasHeader(string $name): bool;
|
||||
|
||||
/**
|
||||
* Get all set header names.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeaderNames(): array;
|
||||
|
||||
/**
|
||||
* Get a header values as an array.
|
||||
*
|
||||
@@ -77,4 +94,9 @@ interface Response
|
||||
* Set a body.
|
||||
*/
|
||||
public function setBody(StreamInterface $body): self;
|
||||
|
||||
/**
|
||||
* Get a body.
|
||||
*/
|
||||
public function getBody(): StreamInterface;
|
||||
}
|
||||
|
||||
59
application/Espo/Core/Api/ResponseComposer.php
Normal file
59
application/Espo/Core/Api/ResponseComposer.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://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 General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Api;
|
||||
|
||||
use Slim\Psr7\Factory\ResponseFactory;
|
||||
use Espo\Core\Utils\Json;
|
||||
use stdClass;
|
||||
|
||||
class ResponseComposer
|
||||
{
|
||||
/**
|
||||
* Compose a JSON response.
|
||||
*
|
||||
* @param array<string|int, mixed>|stdClass|scalar|null $data A data to encode.
|
||||
*/
|
||||
public static function json(mixed $data): Response
|
||||
{
|
||||
return self::empty()
|
||||
->writeBody(Json::encode($data))
|
||||
->setHeader('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose an empty response.
|
||||
*/
|
||||
public static function empty(): Response
|
||||
{
|
||||
$psr7Response = (new ResponseFactory())->createResponse();
|
||||
|
||||
return new ResponseWrapper($psr7Response);
|
||||
}
|
||||
}
|
||||
@@ -29,10 +29,8 @@
|
||||
|
||||
namespace Espo\Core\Api;
|
||||
|
||||
use Psr\Http\Message\{
|
||||
ResponseInterface as Psr7Response,
|
||||
StreamInterface,
|
||||
};
|
||||
use Psr\Http\Message\ResponseInterface as Psr7Response;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
use Espo\Core\Api\Response as ApiResponse;
|
||||
|
||||
@@ -41,49 +39,55 @@ use Espo\Core\Api\Response as ApiResponse;
|
||||
*/
|
||||
class ResponseWrapper implements ApiResponse
|
||||
{
|
||||
private Psr7Response $response;
|
||||
|
||||
public function __construct(Psr7Response $response)
|
||||
public function __construct(private Psr7Response $psr7Response)
|
||||
{
|
||||
$this->response = $response;
|
||||
|
||||
// Slim adds Authorization header. It's not needed.
|
||||
$this->response = $this->response->withoutHeader('Authorization');
|
||||
$this->psr7Response = $this->psr7Response->withoutHeader('Authorization');
|
||||
}
|
||||
|
||||
public function setStatus(int $code, ?string $reason = null): Response
|
||||
{
|
||||
$this->response = $this->response->withStatus($code, $reason ?? '');
|
||||
$this->psr7Response = $this->psr7Response->withStatus($code, $reason ?? '');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
return $this->psr7Response->getStatusCode();
|
||||
}
|
||||
|
||||
public function getReasonPhrase(): string
|
||||
{
|
||||
return $this->psr7Response->getReasonPhrase();
|
||||
}
|
||||
|
||||
public function setHeader(string $name, string $value): Response
|
||||
{
|
||||
$this->response = $this->response->withHeader($name, $value);
|
||||
$this->psr7Response = $this->psr7Response->withHeader($name, $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addHeader(string $name, string $value): Response
|
||||
{
|
||||
$this->response = $this->response->withAddedHeader($name, $value);
|
||||
$this->psr7Response = $this->psr7Response->withAddedHeader($name, $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHeader(string $name): ?string
|
||||
{
|
||||
if (!$this->response->hasHeader($name)) {
|
||||
if (!$this->psr7Response->hasHeader($name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->response->getHeaderLine($name);
|
||||
return $this->psr7Response->getHeaderLine($name);
|
||||
}
|
||||
|
||||
public function hasHeader(string $name): bool
|
||||
{
|
||||
return $this->response->hasHeader($name);
|
||||
return $this->psr7Response->hasHeader($name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,29 +95,42 @@ class ResponseWrapper implements ApiResponse
|
||||
*/
|
||||
public function getHeaderAsArray(string $name): array
|
||||
{
|
||||
if (!$this->response->hasHeader($name)) {
|
||||
if (!$this->psr7Response->hasHeader($name)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->response->getHeader($name);
|
||||
return $this->psr7Response->getHeader($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeaderNames(): array
|
||||
{
|
||||
return array_keys($this->psr7Response->getHeaders());
|
||||
}
|
||||
|
||||
public function writeBody(string $string): Response
|
||||
{
|
||||
$this->response->getBody()->write($string);
|
||||
$this->psr7Response->getBody()->write($string);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setBody(StreamInterface $body): Response
|
||||
{
|
||||
$this->response = $this->response->withBody($body);
|
||||
$this->psr7Response = $this->psr7Response->withBody($body);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getResponse(): Psr7Response
|
||||
public function getBody(): StreamInterface
|
||||
{
|
||||
return $this->response;
|
||||
return $this->psr7Response->getBody();
|
||||
}
|
||||
|
||||
public function toPsr7(): Psr7Response
|
||||
{
|
||||
return $this->psr7Response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,24 +32,28 @@ namespace Espo\Core\Api;
|
||||
class Route
|
||||
{
|
||||
private string $method;
|
||||
private string $route;
|
||||
/** @var array<string, string> */
|
||||
private array $params;
|
||||
private bool $noAuth;
|
||||
|
||||
/**
|
||||
* @param array<string, string> $params
|
||||
* @param ?class-string<Action> $actionClassName
|
||||
*/
|
||||
public function __construct(
|
||||
string $method,
|
||||
string $route,
|
||||
array $params,
|
||||
bool $noAuth
|
||||
private string $route,
|
||||
private string $adjustedRoute,
|
||||
private array $params,
|
||||
private bool $noAuth,
|
||||
private ?string $actionClassName
|
||||
) {
|
||||
$this->method = strtoupper($method);
|
||||
$this->route = $route;
|
||||
$this->params = $params;
|
||||
$this->noAuth = $noAuth;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?class-string<Action>
|
||||
*/
|
||||
public function getActionClassName(): ?string
|
||||
{
|
||||
return $this->actionClassName;
|
||||
}
|
||||
|
||||
public function getMethod(): string
|
||||
@@ -57,13 +61,24 @@ class Route
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a route.
|
||||
*/
|
||||
public function getRoute(): string
|
||||
{
|
||||
return $this->route;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,string>
|
||||
* Get an adjusted route for FastRoute.
|
||||
*/
|
||||
public function getAdjustedRoute(): string
|
||||
{
|
||||
return $this->adjustedRoute;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getParams(): array
|
||||
{
|
||||
|
||||
@@ -34,8 +34,8 @@ use Espo\Core\Api\Route;
|
||||
class RouteParamsFetcher
|
||||
{
|
||||
/**
|
||||
* @param array<string,mixed> $args
|
||||
* @return array<string,mixed>
|
||||
* @param array<string, mixed> $args
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function fetch(Route $item, array $args): array
|
||||
{
|
||||
|
||||
274
application/Espo/Core/Api/RouteProcessor.php
Normal file
274
application/Espo/Core/Api/RouteProcessor.php
Normal file
@@ -0,0 +1,274 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://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 General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Api;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Authentication\AuthenticationFactory;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\ApplicationUser;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Psr7Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Psr7Request;
|
||||
|
||||
use Slim\MiddlewareDispatcher;
|
||||
|
||||
use Throwable;
|
||||
use LogicException;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Processes routes. Handles authentication. Obtains a controller name, action, body from a request.
|
||||
* Then processes a controller action or an action.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class RouteProcessor
|
||||
{
|
||||
public function __construct(
|
||||
private AuthenticationFactory $authenticationFactory,
|
||||
private AuthBuilderFactory $authBuilderFactory,
|
||||
private ErrorOutput $errorOutput,
|
||||
private Config $config,
|
||||
private Log $log,
|
||||
private ApplicationUser $applicationUser,
|
||||
private ControllerActionProcessor $actionProcessor,
|
||||
private MiddlewareProvider $middlewareProvider,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {}
|
||||
|
||||
public function process(
|
||||
ProcessData $processData,
|
||||
Psr7Request $request,
|
||||
Psr7Response $response
|
||||
): Psr7Response {
|
||||
|
||||
$requestWrapped = new RequestWrapper($request, $processData->getBasePath(), $processData->getRouteParams());
|
||||
$responseWrapped = new ResponseWrapper($response);
|
||||
|
||||
try {
|
||||
return $this->processInternal(
|
||||
$processData,
|
||||
$request,
|
||||
$requestWrapped,
|
||||
$responseWrapped
|
||||
);
|
||||
}
|
||||
catch (Exception $exception) {
|
||||
$this->handleException(
|
||||
$exception,
|
||||
$requestWrapped,
|
||||
$responseWrapped,
|
||||
$processData->getRoute()->getAdjustedRoute()
|
||||
);
|
||||
|
||||
return $responseWrapped->toPsr7();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function processInternal(
|
||||
ProcessData $processData,
|
||||
Psr7Request $psrRequest,
|
||||
RequestWrapper $request,
|
||||
ResponseWrapper $response
|
||||
): Psr7Response {
|
||||
|
||||
$authRequired = !$processData->getRoute()->noAuth();
|
||||
|
||||
$apiAuth = $this->authBuilderFactory
|
||||
->create()
|
||||
->setAuthentication($this->authenticationFactory->create())
|
||||
->setAuthRequired($authRequired)
|
||||
->build();
|
||||
|
||||
$authResult = $apiAuth->process($request, $response);
|
||||
|
||||
if (!$authResult->isResolved()) {
|
||||
return $response->toPsr7();
|
||||
}
|
||||
|
||||
if ($authResult->isResolvedUseNoAuth()) {
|
||||
$this->applicationUser->setupSystemUser();
|
||||
}
|
||||
|
||||
ob_start();
|
||||
|
||||
$response = $this->processAfterAuth($processData, $psrRequest, $response);
|
||||
|
||||
ob_clean();
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function processAfterAuth(
|
||||
ProcessData $processData,
|
||||
Psr7Request $request,
|
||||
ResponseWrapper $responseWrapped
|
||||
): Psr7Response {
|
||||
|
||||
$actionClassName = $processData->getRoute()->getActionClassName();
|
||||
|
||||
if ($actionClassName) {
|
||||
return $this->processAction($actionClassName, $processData, $request, $responseWrapped);
|
||||
}
|
||||
|
||||
return $this->processControllerAction($processData, $request, $responseWrapped);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<Action> $actionClassName
|
||||
*/
|
||||
private function processAction(
|
||||
string $actionClassName,
|
||||
ProcessData $processData,
|
||||
Psr7Request $request,
|
||||
ResponseWrapper $responseWrapped
|
||||
): Psr7Response {
|
||||
|
||||
/** @var Action $action */
|
||||
$action = $this->injectableFactory->create($actionClassName);
|
||||
|
||||
$handler = new ActionHandler(
|
||||
action: $action,
|
||||
processData: $processData,
|
||||
config: $this->config,
|
||||
);
|
||||
|
||||
$dispatcher = new MiddlewareDispatcher($handler);
|
||||
|
||||
foreach ($this->middlewareProvider->getActionMiddlewareList($processData->getRoute()) as $middleware) {
|
||||
$dispatcher->addMiddleware($middleware);
|
||||
}
|
||||
|
||||
$response = $dispatcher->handle($request);
|
||||
|
||||
// Apply headers added by the authentication.
|
||||
foreach ($responseWrapped->getHeaderNames() as $name) {
|
||||
$response = $response->withHeader($name, $responseWrapped->getHeaderAsArray($name));
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function processControllerAction(
|
||||
ProcessData $processData,
|
||||
Psr7Request $request,
|
||||
ResponseWrapper $responseWrapped
|
||||
): Psr7Response {
|
||||
|
||||
$controller = $this->getControllerName($processData);
|
||||
$action = $processData->getRouteParams()['action'] ?? null;
|
||||
$method = $request->getMethod();
|
||||
|
||||
if (!$action) {
|
||||
$crudMethodActionMap = $this->config->get('crud') ?? [];
|
||||
$action = $crudMethodActionMap[strtolower($method)] ?? null;
|
||||
|
||||
if (!$action) {
|
||||
throw new BadRequest("No action for method `{$method}`.");
|
||||
}
|
||||
}
|
||||
|
||||
$handler = new ControllerActionHandler(
|
||||
controllerName: $controller,
|
||||
actionName: $action,
|
||||
processData: $processData,
|
||||
responseWrapped: $responseWrapped,
|
||||
controllerActionProcessor: $this->actionProcessor,
|
||||
config: $this->config,
|
||||
);
|
||||
|
||||
$dispatcher = new MiddlewareDispatcher($handler);
|
||||
|
||||
$this->addControllerMiddlewares($dispatcher, $method, $controller, $action);
|
||||
|
||||
return $dispatcher->handle($request);
|
||||
}
|
||||
|
||||
private function getControllerName(ProcessData $processData): string
|
||||
{
|
||||
$controllerName = $processData->getRouteParams()['controller'] ?? null;
|
||||
|
||||
if (!$controllerName) {
|
||||
throw new LogicException("Route doesn't have specified controller.");
|
||||
}
|
||||
|
||||
return ucfirst($controllerName);
|
||||
}
|
||||
|
||||
private function handleException(
|
||||
Exception $exception,
|
||||
Request $request,
|
||||
Response $response,
|
||||
string $route
|
||||
): void {
|
||||
|
||||
try {
|
||||
$this->errorOutput->process($request, $response, $exception, $route);
|
||||
}
|
||||
catch (Throwable $exceptionAnother) {
|
||||
$this->log->error($exceptionAnother->getMessage());
|
||||
|
||||
$response->setStatus(500);
|
||||
}
|
||||
}
|
||||
|
||||
private function addControllerMiddlewares(
|
||||
MiddlewareDispatcher $dispatcher,
|
||||
string $method,
|
||||
string $controller,
|
||||
string $action
|
||||
): void {
|
||||
|
||||
$controllerActionMiddlewareList = $this->middlewareProvider
|
||||
->getControllerActionMiddlewareList($method, $controller, $action);
|
||||
|
||||
foreach ($controllerActionMiddlewareList as $middleware) {
|
||||
$dispatcher->addMiddleware($middleware);
|
||||
}
|
||||
|
||||
$controllerMiddlewareList = $this->middlewareProvider
|
||||
->getControllerMiddlewareList($controller);
|
||||
|
||||
foreach ($controllerMiddlewareList as $middleware) {
|
||||
$dispatcher->addMiddleware($middleware);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@
|
||||
namespace Espo\Core\Api;
|
||||
|
||||
use Espo\Core\Api\Route\RouteParamsFetcher;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Route as RouteUtil;
|
||||
use Espo\Core\Utils\Log;
|
||||
|
||||
@@ -44,24 +45,44 @@ use Psr\Http\Message\ServerRequestInterface as Psr7Request;
|
||||
*/
|
||||
class Starter
|
||||
{
|
||||
private string $routeCacheFile = 'data/cache/application/slim-routes.php';
|
||||
|
||||
public function __construct(
|
||||
private RequestProcessor $requestProcessor,
|
||||
private RouteProcessor $routeProcessor,
|
||||
private RouteUtil $routeUtil,
|
||||
private RouteParamsFetcher $routeParamsFetcher,
|
||||
private Log $log
|
||||
) {}
|
||||
private MiddlewareProvider $middlewareProvider,
|
||||
private Log $log,
|
||||
private Config $config,
|
||||
?string $routeCacheFile = null
|
||||
) {
|
||||
$this->routeCacheFile = $routeCacheFile ?? $this->routeCacheFile;
|
||||
}
|
||||
|
||||
public function start(): void
|
||||
{
|
||||
$slim = SlimAppFactory::create();
|
||||
|
||||
if ($this->config->get('useCache')) {
|
||||
$slim->getRouteCollector()->setCacheFile($this->routeCacheFile);
|
||||
}
|
||||
|
||||
$slim->setBasePath(RouteUtil::detectBasePath());
|
||||
$this->addGlobalMiddlewares($slim);
|
||||
$slim->addRoutingMiddleware();
|
||||
$this->addRoutes($slim);
|
||||
$slim->addErrorMiddleware(false, true, true, $this->log);
|
||||
|
||||
$slim->run();
|
||||
}
|
||||
|
||||
private function addGlobalMiddlewares(SlimApp $slim): void
|
||||
{
|
||||
foreach ($this->middlewareProvider->getGlobalMiddlewareList() as $middleware) {
|
||||
$slim->add($middleware);
|
||||
}
|
||||
}
|
||||
|
||||
private function addRoutes(SlimApp $slim): void
|
||||
{
|
||||
$routeList = $this->routeUtil->getFullList();
|
||||
@@ -73,20 +94,26 @@ class Starter
|
||||
|
||||
private function addRoute(SlimApp $slim, Route $item): void
|
||||
{
|
||||
$slim->map(
|
||||
$slimRoute = $slim->map(
|
||||
[$item->getMethod()],
|
||||
$item->getRoute(),
|
||||
function (Psr7Request $request, Psr7Response $response, array $args) use ($slim, $item)
|
||||
{
|
||||
$item->getAdjustedRoute(),
|
||||
function (Psr7Request $request, Psr7Response $response, array $args) use ($slim, $item) {
|
||||
$routeParams = $this->routeParamsFetcher->fetch($item, $args);
|
||||
|
||||
$requestWrapped = new RequestWrapper($request, $slim->getBasePath(), $routeParams);
|
||||
$responseWrapped = new ResponseWrapper($response);
|
||||
$processData = new ProcessData(
|
||||
route: $item,
|
||||
basePath: $slim->getBasePath(),
|
||||
routeParams: $routeParams,
|
||||
);
|
||||
|
||||
$this->requestProcessor->process($item, $requestWrapped, $responseWrapped);
|
||||
|
||||
return $responseWrapped->getResponse();
|
||||
return $this->routeProcessor->process($processData, $request, $response);
|
||||
}
|
||||
);
|
||||
|
||||
$middlewareList = $this->middlewareProvider->getRouteMiddlewareList($item);
|
||||
|
||||
foreach ($middlewareList as $middleware) {
|
||||
$slimRoute->addMiddleware($middleware);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,32 +100,27 @@ class Application
|
||||
|
||||
protected function getInjectableFactory(): InjectableFactory
|
||||
{
|
||||
/** @var InjectableFactory */
|
||||
return $this->container->get('injectableFactory');
|
||||
return $this->container->getByClass(InjectableFactory::class);
|
||||
}
|
||||
|
||||
protected function getApplicationUser(): ApplicationUser
|
||||
{
|
||||
/** @var ApplicationUser */
|
||||
return $this->container->get('applicationUser');
|
||||
return $this->container->getByClass(ApplicationUser::class);
|
||||
}
|
||||
|
||||
protected function getClientManager(): ClientManager
|
||||
{
|
||||
/** @var ClientManager */
|
||||
return $this->container->get('clientManager');
|
||||
return $this->container->getByClass(ClientManager::class);
|
||||
}
|
||||
|
||||
protected function getMetadata(): Metadata
|
||||
{
|
||||
/** @var Metadata */
|
||||
return $this->container->get('metadata');
|
||||
return $this->container->getByClass(Metadata::class);
|
||||
}
|
||||
|
||||
protected function getConfig(): Config
|
||||
{
|
||||
/** @var Config */
|
||||
return $this->container->get('config');
|
||||
return $this->container->getByClass(Config::class);
|
||||
}
|
||||
|
||||
protected function initAutoloads(): void
|
||||
|
||||
@@ -100,6 +100,6 @@ class PortalClient implements RunnerParameterized
|
||||
{
|
||||
$this->errorOutput->processWithBodyPrinting($request, $response, $exception);
|
||||
|
||||
(new ResponseEmitter())->emit($response->getResponse());
|
||||
(new ResponseEmitter())->emit($response->toPsr7());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace Espo\Core\ApplicationRunners;
|
||||
|
||||
use Espo\Core\Application\Runner;
|
||||
use Espo\Core\DataManager;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
@@ -40,7 +41,7 @@ class Rebuild implements Runner
|
||||
{
|
||||
use Cli;
|
||||
|
||||
public function __construct(private DataManager $dataManager)
|
||||
public function __construct(private DataManager $dataManager, private Log $log)
|
||||
{}
|
||||
|
||||
public function run(): void
|
||||
@@ -51,6 +52,11 @@ class Rebuild implements Runner
|
||||
catch (Exception $e) {
|
||||
echo "Error: " . $e->getMessage() . "\n";
|
||||
|
||||
$this->log->error('Rebuild: ' . $e->getMessage(), [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
]);
|
||||
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
|
||||
namespace Espo\Core;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Entities\Portal as PortalEntity;
|
||||
use Espo\Entities\User as UserEntity;
|
||||
|
||||
@@ -40,19 +39,18 @@ use LogicException;
|
||||
*/
|
||||
class ApplicationState
|
||||
{
|
||||
private Container $container;
|
||||
private const KEY_USER = 'user';
|
||||
private const KEY_PORTAL = 'portal';
|
||||
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
public function __construct(private Container $container)
|
||||
{}
|
||||
|
||||
/**
|
||||
* Whether an application is initialized as a portal.
|
||||
*/
|
||||
public function isPortal(): bool
|
||||
{
|
||||
return $this->container->has('portal');
|
||||
return $this->container->has(self::KEY_PORTAL);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,7 +75,7 @@ class ApplicationState
|
||||
}
|
||||
|
||||
/** @var PortalEntity */
|
||||
return $this->container->get('portal');
|
||||
return $this->container->get(self::KEY_PORTAL);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,7 +83,7 @@ class ApplicationState
|
||||
*/
|
||||
public function hasUser(): bool
|
||||
{
|
||||
return $this->container->has('user');
|
||||
return $this->container->has(self::KEY_USER);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,7 +96,7 @@ class ApplicationState
|
||||
}
|
||||
|
||||
/** @var UserEntity */
|
||||
return $this->container->get('user');
|
||||
return $this->container->get(self::KEY_USER);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,7 +112,7 @@ class ApplicationState
|
||||
*/
|
||||
public function isLogged(): bool
|
||||
{
|
||||
if (!$this->container->has('user')) {
|
||||
if (!$this->container->has(self::KEY_USER)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Core;
|
||||
|
||||
use Espo\Core\Utils\SystemUser;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Core\ORM\EntityManagerProxy;
|
||||
|
||||
@@ -39,16 +40,13 @@ use RuntimeException;
|
||||
*/
|
||||
class ApplicationUser
|
||||
{
|
||||
/** @deprecated As of v7.4. Different IDs may be used. Use Espo\Core\Utils\SystemUser. */
|
||||
public const SYSTEM_USER_ID = 'system';
|
||||
|
||||
private Container $container;
|
||||
private EntityManagerProxy $entityManagerProxy;
|
||||
|
||||
public function __construct(Container $container, EntityManagerProxy $entityManagerProxy)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->entityManagerProxy = $entityManagerProxy;
|
||||
}
|
||||
public function __construct(
|
||||
private Container $container,
|
||||
private EntityManagerProxy $entityManagerProxy
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Set up the system user as a current user. The system user is used when no user is logged in.
|
||||
@@ -67,7 +65,7 @@ class ApplicationUser
|
||||
'lastName',
|
||||
'deleted',
|
||||
])
|
||||
->where(['id' => self::SYSTEM_USER_ID])
|
||||
->where(['userName' => SystemUser::NAME])
|
||||
->findOne();
|
||||
|
||||
if (!$user) {
|
||||
|
||||
@@ -45,8 +45,7 @@ class Data
|
||||
private bool $createSecret = false;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
{}
|
||||
|
||||
public function getUserId(): string
|
||||
{
|
||||
@@ -74,7 +73,7 @@ class Data
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $data
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function create(array $data): self
|
||||
{
|
||||
|
||||
@@ -35,9 +35,15 @@ use Espo\Entities\AuthToken as AuthTokenEntity;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* A default auth token manager. Auth tokens are stored in database.
|
||||
* Consider creating a custom implementation if you need to store auth tokens
|
||||
* in another storage. E.g. a single Redis data store can be utilized with
|
||||
* multiple Espo replicas (for scalability purposes).
|
||||
* Defined at metadata > app > containerServices > authTokenManager.
|
||||
*/
|
||||
class EspoManager implements Manager
|
||||
{
|
||||
private EntityManager $entityManager;
|
||||
/** @var RDBRepository<AuthTokenEntity> */
|
||||
private RDBRepository $repository;
|
||||
|
||||
@@ -45,19 +51,13 @@ class EspoManager implements Manager
|
||||
|
||||
public function __construct(EntityManager $entityManager)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
|
||||
/** @var RDBRepository<AuthTokenEntity> $repository */
|
||||
$repository = $entityManager->getRDBRepository(AuthTokenEntity::ENTITY_TYPE);
|
||||
|
||||
$this->repository = $repository;
|
||||
$this->repository = $entityManager->getRDBRepositoryByClass(AuthTokenEntity::class);
|
||||
}
|
||||
|
||||
public function get(string $token): ?AuthToken
|
||||
{
|
||||
/** @var ?AuthTokenEntity $authToken */
|
||||
$authToken = $this->entityManager
|
||||
->getRDBRepository(AuthTokenEntity::ENTITY_TYPE)
|
||||
$authToken = $this->repository
|
||||
->select([
|
||||
'id',
|
||||
'isActive',
|
||||
@@ -70,9 +70,7 @@ class EspoManager implements Manager
|
||||
'lastAccess',
|
||||
'modifiedAt',
|
||||
])
|
||||
->where([
|
||||
'token' => $token,
|
||||
])
|
||||
->where(['token' => $token])
|
||||
->findOne();
|
||||
|
||||
return $authToken;
|
||||
@@ -83,17 +81,16 @@ class EspoManager implements Manager
|
||||
/** @var AuthTokenEntity $authToken */
|
||||
$authToken = $this->repository->getNew();
|
||||
|
||||
$authToken->set([
|
||||
'userId' => $data->getUserId(),
|
||||
'portalId' => $data->getPortalId(),
|
||||
'hash' => $data->getHash(),
|
||||
'ipAddress' => $data->getIpAddress(),
|
||||
'lastAccess' => date('Y-m-d H:i:s'),
|
||||
'token' => $this->generateToken(),
|
||||
]);
|
||||
$authToken
|
||||
->setUserId($data->getUserId())
|
||||
->setPortalId($data->getPortalId())
|
||||
->setHash($data->getHash())
|
||||
->setIpAddress($data->getIpAddress())
|
||||
->setToken($this->generateToken())
|
||||
->setLastAccessNow();
|
||||
|
||||
if ($data->toCreateSecret()) {
|
||||
$authToken->set('secret', $this->generateToken());
|
||||
$authToken->setSecret($this->generateToken());
|
||||
}
|
||||
|
||||
$this->validate($authToken);
|
||||
@@ -111,7 +108,7 @@ class EspoManager implements Manager
|
||||
|
||||
$this->validateNotChanged($authToken);
|
||||
|
||||
$authToken->set('isActive', false);
|
||||
$authToken->setIsActive(false);
|
||||
|
||||
$this->repository->save($authToken);
|
||||
}
|
||||
@@ -128,12 +125,12 @@ class EspoManager implements Manager
|
||||
throw new RuntimeException("Can renew only not new auth token.");
|
||||
}
|
||||
|
||||
$authToken->set('lastAccess', date('Y-m-d H:i:s'));
|
||||
$authToken->setLastAccessNow();
|
||||
|
||||
$this->repository->save($authToken);
|
||||
}
|
||||
|
||||
protected function validate(AuthToken $authToken): void
|
||||
private function validate(AuthToken $authToken): void
|
||||
{
|
||||
if (!$authToken->getToken()) {
|
||||
throw new RuntimeException("Empty token.");
|
||||
@@ -144,7 +141,7 @@ class EspoManager implements Manager
|
||||
}
|
||||
}
|
||||
|
||||
protected function validateNotChanged(AuthTokenEntity $authToken): void
|
||||
private function validateNotChanged(AuthTokenEntity $authToken): void
|
||||
{
|
||||
if (
|
||||
$authToken->isAttributeChanged('token') ||
|
||||
@@ -157,7 +154,7 @@ class EspoManager implements Manager
|
||||
}
|
||||
}
|
||||
|
||||
protected function generateToken(): string
|
||||
private function generateToken(): string
|
||||
{
|
||||
$length = self::TOKEN_RANDOM_LENGTH;
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
|
||||
namespace Espo\Core\Authentication;
|
||||
|
||||
use Espo\Core\Authentication\Logout\Params as LogoutParams;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Repositories\UserData as UserDataRepository;
|
||||
use Espo\Entities\Portal;
|
||||
use Espo\Entities\User;
|
||||
@@ -37,6 +38,9 @@ use Espo\Entities\AuthLogRecord;
|
||||
use Espo\Entities\AuthToken as AuthTokenEntity;
|
||||
use Espo\Entities\UserData;
|
||||
|
||||
use Espo\Core\Exceptions\Error\Body;
|
||||
use Espo\Core\Authentication\Logout\Params as LogoutParams;
|
||||
use Espo\Core\Authentication\Util\MethodProvider;
|
||||
use Espo\Core\Authentication\Result\FailReason;
|
||||
use Espo\Core\Authentication\TwoFactor\LoginFactory as TwoFactorLoginFactory;
|
||||
use Espo\Core\Authentication\AuthToken\Manager as AuthTokenManager;
|
||||
@@ -44,7 +48,6 @@ use Espo\Core\Authentication\AuthToken\Data as AuthTokenData;
|
||||
use Espo\Core\Authentication\AuthToken\AuthToken;
|
||||
use Espo\Core\Authentication\Hook\Manager as HookManager;
|
||||
use Espo\Core\Authentication\Login\Data as LoginData;
|
||||
|
||||
use Espo\Core\ApplicationUser;
|
||||
use Espo\Core\ApplicationState;
|
||||
use Espo\Core\Api\Request;
|
||||
@@ -53,6 +56,7 @@ use Espo\Core\Utils\Log;
|
||||
use Espo\Core\ORM\EntityManagerProxy;
|
||||
use Espo\Core\Exceptions\ServiceUnavailable;
|
||||
|
||||
use LogicException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
@@ -70,50 +74,22 @@ class Authentication
|
||||
|
||||
private const COOKIE_AUTH_TOKEN_SECRET = 'auth-token-secret';
|
||||
|
||||
private bool $allowAnyAccess;
|
||||
private ?Portal $portal = null;
|
||||
|
||||
private ApplicationUser $applicationUser;
|
||||
private ApplicationState $applicationState;
|
||||
private ConfigDataProvider $configDataProvider;
|
||||
private EntityManagerProxy $entityManager;
|
||||
private LoginFactory $loginFactory;
|
||||
private TwoFactorLoginFactory $twoFactorLoginFactory;
|
||||
private AuthTokenManager $authTokenManager;
|
||||
private HookManager $hookManager;
|
||||
private LogoutFactory $logoutFactory;
|
||||
private Log $log;
|
||||
|
||||
public function __construct(
|
||||
ApplicationUser $applicationUser,
|
||||
ApplicationState $applicationState,
|
||||
ConfigDataProvider $configDataProvider,
|
||||
EntityManagerProxy $entityManagerProxy,
|
||||
LoginFactory $loginFactory,
|
||||
TwoFactorLoginFactory $twoFactorLoginFactory,
|
||||
AuthTokenManager $authTokenManager,
|
||||
HookManager $hookManager,
|
||||
Log $log,
|
||||
LogoutFactory $logoutFactory,
|
||||
bool $allowAnyAccess = false
|
||||
) {
|
||||
$this->allowAnyAccess = $allowAnyAccess;
|
||||
|
||||
$this->applicationUser = $applicationUser;
|
||||
$this->applicationState = $applicationState;
|
||||
$this->configDataProvider = $configDataProvider;
|
||||
$this->entityManager = $entityManagerProxy;
|
||||
$this->loginFactory = $loginFactory;
|
||||
$this->twoFactorLoginFactory = $twoFactorLoginFactory;
|
||||
$this->authTokenManager = $authTokenManager;
|
||||
$this->hookManager = $hookManager;
|
||||
$this->logoutFactory = $logoutFactory;
|
||||
$this->log = $log;
|
||||
}
|
||||
private ApplicationUser $applicationUser,
|
||||
private ApplicationState $applicationState,
|
||||
private ConfigDataProvider $configDataProvider,
|
||||
private EntityManagerProxy $entityManager,
|
||||
private LoginFactory $loginFactory,
|
||||
private TwoFactorLoginFactory $twoFactorLoginFactory,
|
||||
private AuthTokenManager $authTokenManager,
|
||||
private HookManager $hookManager,
|
||||
private Log $log,
|
||||
private LogoutFactory $logoutFactory,
|
||||
private MethodProvider $methodProvider
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Process logging in.
|
||||
* Note: This method can change the state of the object (by setting the `portal` property.).
|
||||
*
|
||||
* @throws ServiceUnavailable
|
||||
*/
|
||||
@@ -121,22 +97,22 @@ class Authentication
|
||||
{
|
||||
$username = $data->getUsername();
|
||||
$password = $data->getPassword();
|
||||
$authenticationMethod = $data->getMethod();
|
||||
$method = $data->getMethod();
|
||||
$byTokenOnly = $data->byTokenOnly();
|
||||
|
||||
if (
|
||||
$authenticationMethod &&
|
||||
!$this->configDataProvider->authenticationMethodIsApi($authenticationMethod)
|
||||
$method &&
|
||||
!$this->configDataProvider->authenticationMethodIsApi($method)
|
||||
) {
|
||||
$this->log
|
||||
->warning("AUTH: Trying to use not allowed authentication method '{$authenticationMethod}'.");
|
||||
->warning("AUTH: Trying to use not allowed authentication method '{$method}'.");
|
||||
|
||||
return $this->processFail(Result::fail(FailReason::METHOD_NOT_ALLOWED), $data, $request);
|
||||
}
|
||||
|
||||
$this->hookManager->processBeforeLogin($data, $request);
|
||||
|
||||
if (!$authenticationMethod && $password === null) {
|
||||
if (!$method && $password === null) {
|
||||
$this->log->error("AUTH: Trying to login w/o password.");
|
||||
|
||||
return Result::fail(FailReason::NO_PASSWORD);
|
||||
@@ -144,7 +120,7 @@ class Authentication
|
||||
|
||||
$authToken = null;
|
||||
|
||||
if (!$authenticationMethod) {
|
||||
if (!$method) {
|
||||
$authToken = $this->authTokenManager->get($password);
|
||||
}
|
||||
|
||||
@@ -172,7 +148,7 @@ class Authentication
|
||||
|
||||
$byTokenAndUsername = $request->getHeader(self::HEADER_BY_TOKEN) === 'true';
|
||||
|
||||
if ($authenticationMethod && $byTokenAndUsername) {
|
||||
if ($method && $byTokenAndUsername) {
|
||||
return Result::fail(FailReason::DISCREPANT_DATA);
|
||||
}
|
||||
|
||||
@@ -194,9 +170,9 @@ class Authentication
|
||||
}
|
||||
}
|
||||
|
||||
$authenticationMethod ??= $this->configDataProvider->getDefaultAuthenticationMethod();
|
||||
$method ??= $this->methodProvider->get();
|
||||
|
||||
$login = $this->loginFactory->create($authenticationMethod, $this->isPortal());
|
||||
$login = $this->loginFactory->create($method, $this->isPortal());
|
||||
|
||||
$loginData = LoginData
|
||||
::createBuilder()
|
||||
@@ -210,7 +186,7 @@ class Authentication
|
||||
$user = $result->getUser();
|
||||
|
||||
$authLogRecord = !$authTokenIsFound ?
|
||||
$this->createAuthLogRecord($username, $user, $request, $authenticationMethod) :
|
||||
$this->createAuthLogRecord($username, $user, $request, $method) :
|
||||
null;
|
||||
|
||||
if ($result->isFail()) {
|
||||
@@ -223,7 +199,12 @@ class Authentication
|
||||
}
|
||||
|
||||
if (!$user->isAdmin() && $this->configDataProvider->isMaintenanceMode()) {
|
||||
throw new ServiceUnavailable("Application is in maintenance mode.");
|
||||
throw ServiceUnavailable::createWithBody(
|
||||
"Application is in maintenance mod1e.",
|
||||
Body::create()
|
||||
->withMessageTranslation('maintenanceModeError', 'messages')
|
||||
->encode()
|
||||
);
|
||||
}
|
||||
|
||||
if (!$this->processUserCheck($user, $authLogRecord)) {
|
||||
@@ -315,9 +296,7 @@ class Authentication
|
||||
$loggedUser->set('token', $authToken->getToken());
|
||||
$loggedUser->set('authTokenId', $authTokenId);
|
||||
|
||||
if ($authLogRecord) {
|
||||
$authLogRecord->set('authTokenId', $authTokenId);
|
||||
}
|
||||
$authLogRecord?->setAuthTokenId($authTokenId);
|
||||
|
||||
return $authToken;
|
||||
}
|
||||
@@ -334,7 +313,7 @@ class Authentication
|
||||
|
||||
if (
|
||||
!$authLogRecord &&
|
||||
$authToken instanceof AuthLogRecord &&
|
||||
$authToken instanceof AuthTokenEntity &&
|
||||
$authToken->hasId()
|
||||
) {
|
||||
$authLogRecord = $this->entityManager
|
||||
@@ -350,40 +329,18 @@ class Authentication
|
||||
}
|
||||
}
|
||||
|
||||
private function setPortal(Portal $portal): void
|
||||
{
|
||||
$this->portal = $portal;
|
||||
}
|
||||
|
||||
private function isPortal(): bool
|
||||
{
|
||||
return $this->portal || $this->applicationState->isPortal();
|
||||
return $this->applicationState->isPortal();
|
||||
}
|
||||
|
||||
private function getPortal(): Portal
|
||||
{
|
||||
if ($this->portal) {
|
||||
return $this->portal;
|
||||
}
|
||||
|
||||
return $this->applicationState->getPortal();
|
||||
}
|
||||
|
||||
private function processAuthTokenCheck(AuthToken $authToken): bool
|
||||
{
|
||||
if ($this->allowAnyAccess && $authToken->getPortalId() && !$this->isPortal()) {
|
||||
/** @var ?Portal $portal */
|
||||
$portal = $this->entityManager->getEntity('Portal', $authToken->getPortalId());
|
||||
|
||||
if ($portal) {
|
||||
$this->setPortal($portal);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->allowAnyAccess) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->isPortal() && $authToken->getPortalId() !== $this->getPortal()->getId()) {
|
||||
$this->log->info("AUTH: Trying to login to portal with a token not related to portal.");
|
||||
|
||||
@@ -446,8 +403,7 @@ class Authentication
|
||||
if (!$isPortalRelatedToUser) {
|
||||
$this->log->info(
|
||||
"AUTH: Trying to login to portal as user '" . $user->getUserName() . "' ".
|
||||
"which is portal user but does not belongs to portal."
|
||||
);
|
||||
"which is portal user but does not belongs to portal.");
|
||||
|
||||
$this->logDenied($authLogRecord, AuthLogRecord::DENIAL_REASON_USER_IS_NOT_IN_PORTAL);
|
||||
|
||||
@@ -546,52 +502,80 @@ class Authentication
|
||||
return $authToken;
|
||||
}
|
||||
|
||||
public function destroyAuthToken(string $token, Request $request, Response $response): bool
|
||||
/**
|
||||
* Destroy an auth token.
|
||||
*
|
||||
* @param string $token A token to destroy.
|
||||
* @param Request $request A request.
|
||||
* @param Response $response A response.
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function destroyAuthToken(string $token, Request $request, Response $response): void
|
||||
{
|
||||
$authToken = $this->authTokenManager->get($token);
|
||||
|
||||
if (!$authToken) {
|
||||
return false;
|
||||
throw new NotFound("Auth token not found.");
|
||||
}
|
||||
|
||||
if (!$this->applicationState->hasUser()) {
|
||||
throw new LogicException("No logged user.");
|
||||
}
|
||||
|
||||
$user = $this->applicationState->getUser();
|
||||
|
||||
$this->authTokenManager->inactivate($authToken);
|
||||
|
||||
if ($authToken->getSecret()) {
|
||||
$sentSecret = $request->getCookieParam(self::COOKIE_AUTH_TOKEN_SECRET);
|
||||
|
||||
if (
|
||||
// Still need the ability to destroy auth tokens of another users
|
||||
// for login-as-another-user feature.
|
||||
$authToken->getUserId() !== $user->getId() &&
|
||||
$sentSecret !== $authToken->getSecret()
|
||||
) {
|
||||
throw new Forbidden("Can't destroy auth token.");
|
||||
}
|
||||
|
||||
if ($sentSecret === $authToken->getSecret()) {
|
||||
$this->setSecretInCookie(null, $response);
|
||||
}
|
||||
}
|
||||
|
||||
$method = $this->configDataProvider->getDefaultAuthenticationMethod();
|
||||
$method = $this->methodProvider->get();
|
||||
|
||||
if ($this->logoutFactory->isCreatable($method)) {
|
||||
$logout = $this->logoutFactory->create($method);
|
||||
|
||||
$result = $logout->logout($authToken, LogoutParams::create());
|
||||
|
||||
$redirectUrl = $result->getRedirectUrl();
|
||||
|
||||
if ($redirectUrl) {
|
||||
$response->setHeader(self::HEADER_LOGOUT_REDIRECT_URL, $redirectUrl);
|
||||
}
|
||||
if (!$this->logoutFactory->isCreatable($method)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return true;
|
||||
$result = $this->logoutFactory
|
||||
->create($method)
|
||||
->logout($authToken, LogoutParams::create());
|
||||
|
||||
$redirectUrl = $result->getRedirectUrl();
|
||||
|
||||
if ($redirectUrl) {
|
||||
$response->setHeader(self::HEADER_LOGOUT_REDIRECT_URL, $redirectUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private function createAuthLogRecord(
|
||||
?string $username,
|
||||
?User $user,
|
||||
Request $request,
|
||||
?string $authenticationMethod = null
|
||||
?string $method = null
|
||||
): ?AuthLogRecord {
|
||||
|
||||
if ($username === self::LOGOUT_USERNAME) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->configDataProvider->isAuthLogDisabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var AuthLogRecord $authLogRecord */
|
||||
$authLogRecord = $this->entityManager->getNewEntity(AuthLogRecord::ENTITY_TYPE);
|
||||
|
||||
@@ -604,27 +588,28 @@ class Authentication
|
||||
$username = $user->getUserName();
|
||||
}
|
||||
|
||||
$authLogRecord->set([
|
||||
'username' => $username,
|
||||
'ipAddress' => $request->getServerParam('REMOTE_ADDR'),
|
||||
'requestTime' => $request->getServerParam('REQUEST_TIME_FLOAT'),
|
||||
'requestMethod' => $request->getMethod(),
|
||||
'requestUrl' => $requestUrl,
|
||||
'authenticationMethod' => $authenticationMethod,
|
||||
]);
|
||||
$authLogRecord
|
||||
->setUsername($username)
|
||||
->setIpAddress($request->getServerParam('REMOTE_ADDR'))
|
||||
->setRequestTime($request->getServerParam('REQUEST_TIME_FLOAT'))
|
||||
->setRequestMethod($request->getMethod())
|
||||
->setRequestUrl($requestUrl)
|
||||
->setAuthenticationMethod($method)
|
||||
->setPortalId($this->isPortal() ? $this->getPortal()->getId() : null);
|
||||
|
||||
if ($this->isPortal()) {
|
||||
$authLogRecord->set('portalId', $this->getPortal()->getId());
|
||||
if ($user && $user->isApi() && $this->configDataProvider->isApiUserAuthLogDisabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($user) {
|
||||
$authLogRecord->set('userId', $user->hasId() ? $user->getId() : null);
|
||||
$authLogRecord->setUserId($user->hasId() ? $user->getId() : null);
|
||||
|
||||
return $authLogRecord;
|
||||
}
|
||||
|
||||
$authLogRecord->set('isDenied', true);
|
||||
$authLogRecord->set('denialReason', AuthLogRecord::DENIAL_REASON_CREDENTIALS);
|
||||
$authLogRecord
|
||||
->setIsDenied()
|
||||
->setDenialReason(AuthLogRecord::DENIAL_REASON_CREDENTIALS);
|
||||
|
||||
$this->entityManager->saveEntity($authLogRecord);
|
||||
|
||||
@@ -637,7 +622,9 @@ class Authentication
|
||||
return;
|
||||
}
|
||||
|
||||
$authLogRecord->set('denialReason', $denialReason);
|
||||
$authLogRecord
|
||||
->setIsDenied()
|
||||
->setDenialReason($denialReason);
|
||||
|
||||
$this->entityManager->saveEntity($authLogRecord);
|
||||
}
|
||||
|
||||
@@ -33,22 +33,11 @@ use Espo\Core\InjectableFactory;
|
||||
|
||||
class AuthenticationFactory
|
||||
{
|
||||
private InjectableFactory $injectableFactory;
|
||||
|
||||
public function __construct(InjectableFactory $injectableFactory)
|
||||
{
|
||||
$this->injectableFactory = $injectableFactory;
|
||||
}
|
||||
public function __construct(private InjectableFactory $injectableFactory)
|
||||
{}
|
||||
|
||||
public function create(): Authentication
|
||||
{
|
||||
return $this->injectableFactory->create(Authentication::class);
|
||||
}
|
||||
|
||||
public function createWithAnyAccessAllowed(): Authentication
|
||||
{
|
||||
return $this->injectableFactory->createWith(Authentication::class, [
|
||||
'allowAnyAccess' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,16 @@ class ConfigDataProvider
|
||||
return (bool) $this->config->get('authAnotherUserDisabled');
|
||||
}
|
||||
|
||||
public function isAuthLogDisabled(): bool
|
||||
{
|
||||
return (bool) $this->config->get('authLogDisabled');
|
||||
}
|
||||
|
||||
public function isApiUserAuthLogDisabled(): bool
|
||||
{
|
||||
return (bool) $this->config->get('authApiUserLogDisabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MetadataParams[]
|
||||
*/
|
||||
|
||||
@@ -32,31 +32,25 @@ namespace Espo\Core\Authentication\Hook\Hooks;
|
||||
use Espo\Core\Authentication\Hook\BeforeLogin;
|
||||
use Espo\Core\Authentication\AuthenticationData;
|
||||
use Espo\Core\Api\Request;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Authentication\ConfigDataProvider;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Core\Utils\Log;
|
||||
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Entities\AuthLogRecord;
|
||||
|
||||
use DateTime;
|
||||
|
||||
class FailedAttemptsLimit implements BeforeLogin
|
||||
{
|
||||
private $configDataProvider;
|
||||
|
||||
private $entityManager;
|
||||
|
||||
private $log;
|
||||
|
||||
public function __construct(ConfigDataProvider $configDataProvider, EntityManager $entityManager, Log $log)
|
||||
{
|
||||
$this->configDataProvider = $configDataProvider;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->log = $log;
|
||||
}
|
||||
public function __construct(
|
||||
private ConfigDataProvider $configDataProvider,
|
||||
private EntityManager $entityManager,
|
||||
private Log $log
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function process(AuthenticationData $data, Request $request): void
|
||||
{
|
||||
$isByTokenOnly = !$data->getMethod() && $request->getHeader('Espo-Authorization-By-Token') === 'true';
|
||||
@@ -65,6 +59,10 @@ class FailedAttemptsLimit implements BeforeLogin
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->configDataProvider->isAuthLogDisabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$failedAttemptsPeriod = $this->configDataProvider->getFailedAttemptsPeriod();
|
||||
$maxFailedAttempts = $this->configDataProvider->getMaxFailedAttemptNumber();
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user