mirror of
https://github.com/espocrm/espocrm.git
synced 2026-03-11 19:57:01 +00:00
Compare commits
660 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a20c3f14ac | ||
|
|
62a0ca4fb5 | ||
|
|
2171f9c848 | ||
|
|
384aed2a91 | ||
|
|
2d9f20f2cc | ||
|
|
8430e24eae | ||
|
|
d13755ba5f | ||
|
|
d496892878 | ||
|
|
6d32879f21 | ||
|
|
40e7eeb118 | ||
|
|
02ef6b73af | ||
|
|
e831768d92 | ||
|
|
a0695b492e | ||
|
|
cddcebfe27 | ||
|
|
71ca25854f | ||
|
|
ad962d5bcd | ||
|
|
0083d99d37 | ||
|
|
5cd710e420 | ||
|
|
6da1bc083e | ||
|
|
47aaecf3ac | ||
|
|
a3f2fadd9e | ||
|
|
0cf851755b | ||
|
|
1361168d00 | ||
|
|
b888d3bcbc | ||
|
|
203dce371c | ||
|
|
1a81f4a8af | ||
|
|
9b788c3c2d | ||
|
|
17e487d011 | ||
|
|
b2b11fba32 | ||
|
|
4d4af995c8 | ||
|
|
0d04aedd00 | ||
|
|
53930866df | ||
|
|
9ce5c7c2fa | ||
|
|
dc30bd3991 | ||
|
|
70c90972dc | ||
|
|
9daa5b8583 | ||
|
|
5916cfd345 | ||
|
|
dec78c447d | ||
|
|
100359ad6f | ||
|
|
26e6f658fd | ||
|
|
15bf2bf772 | ||
|
|
4e72413829 | ||
|
|
0fb214434e | ||
|
|
2b4c62eab8 | ||
|
|
0ed5f41fa8 | ||
|
|
7e973667c8 | ||
|
|
8639fc5b39 | ||
|
|
f503a08813 | ||
|
|
634403cd6e | ||
|
|
6a539bcdcc | ||
|
|
c43830cd6a | ||
|
|
ca6c9dc312 | ||
|
|
c5194edcdd | ||
|
|
7905c1e254 | ||
|
|
fa0d46dba9 | ||
|
|
2bf8a96ec8 | ||
|
|
41a15cc254 | ||
|
|
2364ae67dd | ||
|
|
3c64db25b9 | ||
|
|
87b2703b1e | ||
|
|
d86a8e554b | ||
|
|
af809c66b1 | ||
|
|
702087f0b6 | ||
|
|
d91db9ef62 | ||
|
|
7af819a656 | ||
|
|
633e678590 | ||
|
|
a5f6d4e56a | ||
|
|
c71cf46cbe | ||
|
|
3dc0dbbbac | ||
|
|
eeba4fd8bf | ||
|
|
a797764534 | ||
|
|
9f4aca43ee | ||
|
|
849b992732 | ||
|
|
cdf0e079b2 | ||
|
|
be647848ae | ||
|
|
48369b203c | ||
|
|
dd60c26591 | ||
|
|
5e834689c4 | ||
|
|
141c848260 | ||
|
|
796a2fffda | ||
|
|
6ea6102ae6 | ||
|
|
bfeb504243 | ||
|
|
174a86b497 | ||
|
|
951902319e | ||
|
|
dcb08a1d47 | ||
|
|
6afe2beb5b | ||
|
|
906169fedc | ||
|
|
68f5e58b57 | ||
|
|
9755e8d08c | ||
|
|
b1872bb08a | ||
|
|
dd356cdfdc | ||
|
|
8231f1f839 | ||
|
|
d248515d76 | ||
|
|
113e075fb6 | ||
|
|
c6a2622744 | ||
|
|
cefec5e565 | ||
|
|
54ee4490c7 | ||
|
|
2ab21839cb | ||
|
|
cbdf954eac | ||
|
|
6504505297 | ||
|
|
fa5307dc87 | ||
|
|
80561c65fc | ||
|
|
3d3128b831 | ||
|
|
b228d5188d | ||
|
|
fdc8eeb650 | ||
|
|
8162f1f935 | ||
|
|
b456037a08 | ||
|
|
90f8eb20fc | ||
|
|
19cb848877 | ||
|
|
7ef4535892 | ||
|
|
5e5d38aef2 | ||
|
|
f2b8dd8109 | ||
|
|
8c706895b2 | ||
|
|
8c479396f1 | ||
|
|
fe83a35c0c | ||
|
|
902b5887fc | ||
|
|
9f9e2d7507 | ||
|
|
ed50b357ad | ||
|
|
65c45fa47d | ||
|
|
3d4ceb9efb | ||
|
|
a6940d38a7 | ||
|
|
204410d599 | ||
|
|
20f53eb50f | ||
|
|
887d0b208a | ||
|
|
69dc2cf5a5 | ||
|
|
a7353ee38a | ||
|
|
541b0579e4 | ||
|
|
ebb9ca1298 | ||
|
|
90aa40bfb5 | ||
|
|
6261d316f5 | ||
|
|
3bb439e4b1 | ||
|
|
d52d16ee19 | ||
|
|
9d3dc38a13 | ||
|
|
2888e1fadd | ||
|
|
fcc990ac24 | ||
|
|
5f9916202d | ||
|
|
53a95ccf51 | ||
|
|
5c4f7b62a6 | ||
|
|
8186e6ebf0 | ||
|
|
df2bd841ed | ||
|
|
048fde70e2 | ||
|
|
5d87c27f23 | ||
|
|
9d44f250ab | ||
|
|
31ace7d3f4 | ||
|
|
d893328343 | ||
|
|
1ae22f929d | ||
|
|
73121fadf7 | ||
|
|
4bba92cdac | ||
|
|
c3d6fa229c | ||
|
|
4b09a6e29a | ||
|
|
876b8cc984 | ||
|
|
cb188cc54d | ||
|
|
f3d4e85699 | ||
|
|
74992821f6 | ||
|
|
0299884306 | ||
|
|
b459ef5550 | ||
|
|
d526ce904a | ||
|
|
8b456270be | ||
|
|
21569e4025 | ||
|
|
621ada0c10 | ||
|
|
50055b6500 | ||
|
|
c81851d48d | ||
|
|
8904d577e5 | ||
|
|
34a7406167 | ||
|
|
ab1b769240 | ||
|
|
652ad7b344 | ||
|
|
48fdfd88fa | ||
|
|
1318f8d19d | ||
|
|
c5fe52ede2 | ||
|
|
ae51d341e6 | ||
|
|
96b08180d4 | ||
|
|
f0618e8465 | ||
|
|
c9dd7722c3 | ||
|
|
025cdf246a | ||
|
|
7fdd7cd280 | ||
|
|
4bb9c11e55 | ||
|
|
1b0dccab42 | ||
|
|
31c9a36bf4 | ||
|
|
534b7e7226 | ||
|
|
6b9d14a68b | ||
|
|
d41dfc86c7 | ||
|
|
b86e9c51f5 | ||
|
|
0088a53ab8 | ||
|
|
efdff66593 | ||
|
|
837f8b2796 | ||
|
|
82ff1772b2 | ||
|
|
36aa6739fc | ||
|
|
0c26d35287 | ||
|
|
753daebadf | ||
|
|
d21293ca13 | ||
|
|
1e8fd10a0a | ||
|
|
b9df4b36ab | ||
|
|
3832dec3e3 | ||
|
|
45ff49da16 | ||
|
|
dfc000c1e1 | ||
|
|
604b7abc89 | ||
|
|
a4676be9d7 | ||
|
|
19af4d8c96 | ||
|
|
272cff1cd9 | ||
|
|
d2f006892c | ||
|
|
05637fd7bf | ||
|
|
a9581907c7 | ||
|
|
4d9fff91d4 | ||
|
|
28b9cf683a | ||
|
|
20e1179085 | ||
|
|
c0ed4b7ed3 | ||
|
|
96fd21b381 | ||
|
|
49d4595049 | ||
|
|
eb98a1979d | ||
|
|
9416e3bc77 | ||
|
|
350b121328 | ||
|
|
47dfbce810 | ||
|
|
e2c51f9d00 | ||
|
|
d738188c8b | ||
|
|
0c8b3bc79e | ||
|
|
7f8a152909 | ||
|
|
50c208e4db | ||
|
|
92e9ef31a3 | ||
|
|
d6cce6a887 | ||
|
|
8f7a76f2fe | ||
|
|
6957de71ee | ||
|
|
ee9436d45f | ||
|
|
45c972f813 | ||
|
|
aa547c3934 | ||
|
|
894cd14802 | ||
|
|
950815890f | ||
|
|
3d28723a92 | ||
|
|
4b1519367f | ||
|
|
b515b58ed1 | ||
|
|
09cc9910a2 | ||
|
|
463cedc12c | ||
|
|
9d3abb7a0c | ||
|
|
fef056fbe9 | ||
|
|
acc8eaa4e2 | ||
|
|
d3f768975d | ||
|
|
385dfa697a | ||
|
|
560addae85 | ||
|
|
4924b5f883 | ||
|
|
49ed6eee31 | ||
|
|
f6a5f3cf06 | ||
|
|
704e274e6a | ||
|
|
6f71ad125d | ||
|
|
c891b3a02e | ||
|
|
417ce64c39 | ||
|
|
d3d0c33fe2 | ||
|
|
ae8f56381b | ||
|
|
5752ccdf98 | ||
|
|
7818f76e71 | ||
|
|
0a7d80cbad | ||
|
|
54a753ec7e | ||
|
|
d46f9ebd1f | ||
|
|
41453b0fb3 | ||
|
|
2cebfc8046 | ||
|
|
a20e7566c9 | ||
|
|
29b7bffadf | ||
|
|
2142598058 | ||
|
|
0ed319927a | ||
|
|
033f7a6a5f | ||
|
|
ac551b4448 | ||
|
|
2299906c23 | ||
|
|
607b7c6985 | ||
|
|
79c2378826 | ||
|
|
241b668073 | ||
|
|
e5ca8acdea | ||
|
|
9e8800a8ba | ||
|
|
b0d137b6bb | ||
|
|
5174b409fb | ||
|
|
0c0a4da7d3 | ||
|
|
aead8f6edd | ||
|
|
83f2899892 | ||
|
|
3f25833d3d | ||
|
|
26d2092ace | ||
|
|
c82c34f6a4 | ||
|
|
00dca8b099 | ||
|
|
5c9c1ee4ed | ||
|
|
4e9c4e62a2 | ||
|
|
3e5377c830 | ||
|
|
e3875812e7 | ||
|
|
ef16795ea7 | ||
|
|
9490623d54 | ||
|
|
f4f9086f79 | ||
|
|
22d057328a | ||
|
|
ef8c58e3b7 | ||
|
|
60c8502d27 | ||
|
|
e65583dba7 | ||
|
|
bebf02dccb | ||
|
|
219d8b2e41 | ||
|
|
d20c0d452b | ||
|
|
347c8bcef7 | ||
|
|
93176838e2 | ||
|
|
7dc6c4e5d0 | ||
|
|
430709b308 | ||
|
|
417cdf5901 | ||
|
|
ec0303ab09 | ||
|
|
b80fb7c817 | ||
|
|
962e48c77a | ||
|
|
4b1578237e | ||
|
|
247d8763f7 | ||
|
|
a3e2a32a7d | ||
|
|
237e39f495 | ||
|
|
a23b28bee9 | ||
|
|
d8d13d5ae2 | ||
|
|
0f6b35cbf6 | ||
|
|
969d06f934 | ||
|
|
ebcddd9477 | ||
|
|
0cbe196bd3 | ||
|
|
6087cad5ff | ||
|
|
511d3f8eae | ||
|
|
bbc8408c5a | ||
|
|
f0723940e4 | ||
|
|
34516008f5 | ||
|
|
000e3c0601 | ||
|
|
ec6416bfcb | ||
|
|
baa72e6f52 | ||
|
|
81f0b5ebe2 | ||
|
|
e833af4e9d | ||
|
|
1255dd0728 | ||
|
|
61ab5478bc | ||
|
|
c99eb08741 | ||
|
|
52f1329ca4 | ||
|
|
8f574b1275 | ||
|
|
5e171464c3 | ||
|
|
92bcb03cef | ||
|
|
ead45003e7 | ||
|
|
37a37e95c6 | ||
|
|
f68345d014 | ||
|
|
9605afbb05 | ||
|
|
7aa57ad36c | ||
|
|
82efcefac7 | ||
|
|
9b176299ab | ||
|
|
f70902255c | ||
|
|
cbe79473ef | ||
|
|
158df5f652 | ||
|
|
253f54cb20 | ||
|
|
90fa429fb9 | ||
|
|
d7063a914f | ||
|
|
0a6ba698ba | ||
|
|
b11bfb0e92 | ||
|
|
ece7d1f3b4 | ||
|
|
6d9a813d67 | ||
|
|
f6382c7774 | ||
|
|
2525c53029 | ||
|
|
ee9520811f | ||
|
|
212e8e34ab | ||
|
|
1fe53ac5c2 | ||
|
|
e1f7b64fe1 | ||
|
|
074d949d0b | ||
|
|
0c07c7454d | ||
|
|
82eb456565 | ||
|
|
285fc8ba8c | ||
|
|
91e971a9ac | ||
|
|
a12fcfd6b9 | ||
|
|
1d2ae3d282 | ||
|
|
2a42cc8542 | ||
|
|
e512f272a5 | ||
|
|
8a795dea4d | ||
|
|
bc1333c5c5 | ||
|
|
ab378f4a59 | ||
|
|
a0bb78bc58 | ||
|
|
8d7a64587c | ||
|
|
860dd74748 | ||
|
|
a8c4bb1a25 | ||
|
|
b4f73192ae | ||
|
|
bd48715737 | ||
|
|
9a31099821 | ||
|
|
1dba594fa4 | ||
|
|
e1f5f7b713 | ||
|
|
a5ed3f1d0c | ||
|
|
86ddf9d7cf | ||
|
|
6bd0a0ee1f | ||
|
|
05084c67db | ||
|
|
cbb51c92e9 | ||
|
|
ae7b42c3b0 | ||
|
|
3da75e9dd0 | ||
|
|
e7561911cc | ||
|
|
0d727e74c0 | ||
|
|
36dbe5556f | ||
|
|
8b8d08afaa | ||
|
|
dda95dd741 | ||
|
|
50cc43a742 | ||
|
|
d4ab9850f2 | ||
|
|
f561bb57f4 | ||
|
|
99d8681e46 | ||
|
|
ecd28f6c2d | ||
|
|
5a5f8845ca | ||
|
|
8ac34018ab | ||
|
|
95b1560c8a | ||
|
|
a7efea44d1 | ||
|
|
42d8d2256d | ||
|
|
6063a295a0 | ||
|
|
3c8bdeb539 | ||
|
|
08ecfe58d5 | ||
|
|
dd1bde9830 | ||
|
|
5861923f3b | ||
|
|
5aef00dfaa | ||
|
|
ea4790eb6f | ||
|
|
6de98fd652 | ||
|
|
2d180f5a07 | ||
|
|
0ec3991b15 | ||
|
|
d1a6a17c88 | ||
|
|
f1f1d1506d | ||
|
|
d8af0ee835 | ||
|
|
164f96e30a | ||
|
|
b444f8b0fb | ||
|
|
3bac29828c | ||
|
|
1c60d0b314 | ||
|
|
e7f4b555e9 | ||
|
|
5347d994ef | ||
|
|
bb20fe1929 | ||
|
|
b282110d40 | ||
|
|
87fd4e7f57 | ||
|
|
025b312a2e | ||
|
|
b08813f9b6 | ||
|
|
415199d814 | ||
|
|
6f7869784a | ||
|
|
ebe9784b23 | ||
|
|
645b891a1d | ||
|
|
34fbf2df6d | ||
|
|
9ad2cfc855 | ||
|
|
e4c7bd1baa | ||
|
|
1ee8037d2b | ||
|
|
275ee96750 | ||
|
|
8efff2f795 | ||
|
|
b66cb676a1 | ||
|
|
975b7b72c3 | ||
|
|
a520b9e57f | ||
|
|
65b9fabfd7 | ||
|
|
4be63cb75e | ||
|
|
910ed80ae2 | ||
|
|
8677a8354d | ||
|
|
d902417b21 | ||
|
|
338e2e9089 | ||
|
|
fa88ba1583 | ||
|
|
619c14ef65 | ||
|
|
3012967384 | ||
|
|
295aa8861b | ||
|
|
447d949537 | ||
|
|
c87fa9463e | ||
|
|
f7f424bad7 | ||
|
|
e0606e20d8 | ||
|
|
91a2216d27 | ||
|
|
d2643a3372 | ||
|
|
fa01e80386 | ||
|
|
31caa27d39 | ||
|
|
c78962f5f6 | ||
|
|
4a1bdae913 | ||
|
|
2dfd14f3c5 | ||
|
|
023400e84e | ||
|
|
05fd772d46 | ||
|
|
d409075b6c | ||
|
|
8267c48aad | ||
|
|
afbaf931eb | ||
|
|
12105fb25f | ||
|
|
0bd9dbdba2 | ||
|
|
f48fbc58b5 | ||
|
|
c287d0ff5a | ||
|
|
bac2240c0b | ||
|
|
300ed327df | ||
|
|
e14ca3ab77 | ||
|
|
2b5ba6049f | ||
|
|
caf6217b9a | ||
|
|
716976c078 | ||
|
|
00cf85abd4 | ||
|
|
8ef2ed4144 | ||
|
|
befec82120 | ||
|
|
eb7923f4bc | ||
|
|
d70687ff1e | ||
|
|
e7f0c461c6 | ||
|
|
0bbf3f5f0d | ||
|
|
321fd40355 | ||
|
|
add5bcbe6a | ||
|
|
f39e59ba5b | ||
|
|
a65bdbece5 | ||
|
|
184fa6fd9b | ||
|
|
897a14d07f | ||
|
|
36d09b923e | ||
|
|
0928c52100 | ||
|
|
daa1fcdcba | ||
|
|
6bea09f246 | ||
|
|
e88bd5898c | ||
|
|
a5bb5005f8 | ||
|
|
ec664163e5 | ||
|
|
b130076313 | ||
|
|
372a9c9640 | ||
|
|
958a1e6634 | ||
|
|
306a8728b4 | ||
|
|
e9527a6bbf | ||
|
|
d261a019bd | ||
|
|
6465277fb0 | ||
|
|
fc8be6d56f | ||
|
|
cc32089a51 | ||
|
|
56dd0aa594 | ||
|
|
71d8327b32 | ||
|
|
e322e036a4 | ||
|
|
4f2651dd2b | ||
|
|
f53553e301 | ||
|
|
324f776ed5 | ||
|
|
4c346bebff | ||
|
|
083a2c5235 | ||
|
|
1f04ba5083 | ||
|
|
1ad265611f | ||
|
|
7adfb6c1c0 | ||
|
|
190330c204 | ||
|
|
56f975c65e | ||
|
|
a79892028a | ||
|
|
3086b75616 | ||
|
|
9955da3521 | ||
|
|
0ba9130e96 | ||
|
|
0af14b93c5 | ||
|
|
191884d5af | ||
|
|
dd1aac89ce | ||
|
|
24203e8d07 | ||
|
|
e53f79be03 | ||
|
|
435dc2d818 | ||
|
|
39e446ecf2 | ||
|
|
5a013ddc88 | ||
|
|
14b595940f | ||
|
|
b6d4b96aa8 | ||
|
|
de4c5d641d | ||
|
|
f731419f86 | ||
|
|
0e2f00665c | ||
|
|
cae0f541b5 | ||
|
|
88dec452f6 | ||
|
|
28072ad24f | ||
|
|
857c252b14 | ||
|
|
04906c5307 | ||
|
|
19e9abb7c4 | ||
|
|
89fa2f0523 | ||
|
|
6686e78069 | ||
|
|
928ee586a0 | ||
|
|
08d661d275 | ||
|
|
f7709207d2 | ||
|
|
6174171cab | ||
|
|
6242e7c8b8 | ||
|
|
8a4fd72261 | ||
|
|
80588d0f5f | ||
|
|
377f977f79 | ||
|
|
68393c778b | ||
|
|
8b8458cf78 | ||
|
|
44e8dcf680 | ||
|
|
b83e6170a4 | ||
|
|
dde0e24e0e | ||
|
|
2d31c9bfb6 | ||
|
|
c0bdd74837 | ||
|
|
3ccf2f4a00 | ||
|
|
b9d81b7994 | ||
|
|
fe8af42814 | ||
|
|
03f2c33601 | ||
|
|
1613398492 | ||
|
|
87ad554531 | ||
|
|
d74363baea | ||
|
|
535baff2aa | ||
|
|
dcad84aeff | ||
|
|
df3e0bf956 | ||
|
|
4215ad6e88 | ||
|
|
5c6a4ae7b2 | ||
|
|
33cece0cd1 | ||
|
|
4b3b48c981 | ||
|
|
99eec81983 | ||
|
|
e1b5e2db33 | ||
|
|
cd76087c2f | ||
|
|
ff34df3e42 | ||
|
|
db43fd3a12 | ||
|
|
de42e7e4c1 | ||
|
|
5baeea2b5d | ||
|
|
6d7be12c60 | ||
|
|
98fb897785 | ||
|
|
7bdd9b25ce | ||
|
|
70b995e6f7 | ||
|
|
ecb6ec3adf | ||
|
|
431f7e6fd5 | ||
|
|
e0f8688cb9 | ||
|
|
6fcd6bc6e4 | ||
|
|
56d72946bc | ||
|
|
9099f84441 | ||
|
|
fefadc58bd | ||
|
|
1628d3b566 | ||
|
|
b456d86b22 | ||
|
|
c3a1236ca9 | ||
|
|
544e8e9d3f | ||
|
|
a032a41935 | ||
|
|
e43215cdf1 | ||
|
|
781aab0c1d | ||
|
|
197aade0d1 | ||
|
|
5de9f545b0 | ||
|
|
43cf24d8d6 | ||
|
|
b3858b977a | ||
|
|
1a38c79d94 | ||
|
|
7ea46bb370 | ||
|
|
2b8120811c | ||
|
|
3498ebb978 | ||
|
|
5f71c2c1b6 | ||
|
|
c68141d4bb | ||
|
|
70b6f456af | ||
|
|
0b86592d5c | ||
|
|
91ee5af643 | ||
|
|
9df03c1119 | ||
|
|
80fb01fd7d | ||
|
|
febbfa2126 | ||
|
|
c9a959d548 | ||
|
|
24473783e9 | ||
|
|
cd429f9556 | ||
|
|
c9b9023f38 | ||
|
|
c8adc8c654 | ||
|
|
08501ce207 | ||
|
|
1e3a9cc07d | ||
|
|
d9dc87f9ab | ||
|
|
31f8cf94da | ||
|
|
c9c5913845 | ||
|
|
95e3276394 | ||
|
|
8d06c82338 | ||
|
|
5da31f76f4 | ||
|
|
8c3933e60e | ||
|
|
c67ea8309a | ||
|
|
b7c3b32b49 | ||
|
|
f6f32fbaa0 | ||
|
|
06f807bb4d | ||
|
|
20975628b2 | ||
|
|
c6b7337317 | ||
|
|
4e040700ab | ||
|
|
673fcfc9fe | ||
|
|
ed1ac27f75 | ||
|
|
d202d53ce2 | ||
|
|
8485e7b436 | ||
|
|
e85c110697 | ||
|
|
a9c0aefa58 | ||
|
|
3a5c840ce8 | ||
|
|
bbf7f8a98b | ||
|
|
24dee6aa62 | ||
|
|
c33452fd0b | ||
|
|
ad099dff27 | ||
|
|
d31e90fb42 | ||
|
|
c150ee66af | ||
|
|
dac6d74fd1 | ||
|
|
598a87c349 | ||
|
|
34ec6cfc8d | ||
|
|
39865b4c09 | ||
|
|
116a605053 | ||
|
|
a1da459343 | ||
|
|
12440e7ab6 | ||
|
|
39af37f20c | ||
|
|
0b9abdd70d | ||
|
|
29c9b0fc78 | ||
|
|
aee977ac36 | ||
|
|
8b03a82480 | ||
|
|
95f9946476 | ||
|
|
a18866a900 | ||
|
|
ab65f99a44 | ||
|
|
2a4158a130 | ||
|
|
0befa49f1a | ||
|
|
d4aa9745ca | ||
|
|
260a5c89ef | ||
|
|
b19aec81b4 | ||
|
|
9eea792fb1 | ||
|
|
d05a084509 | ||
|
|
1722d3b69f | ||
|
|
53a6384515 | ||
|
|
5e492d3e3f | ||
|
|
1daac4cfad | ||
|
|
646939bcf8 |
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report. Not to be used for help requests or server configuration issues. We appreciate if you prefer posting bug reports on weekdays rather than weekends.
|
||||
about: Create a bug report. Not to be used for help requests or server configuration issues. Only for issues related to open source EspoCRM. Issues related to extensions should not to be posted here.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
@@ -11,7 +11,7 @@ assignees: ''
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
Explicit steps to reproduce the behavior:
|
||||
1. ...
|
||||
2. ...
|
||||
3. ...
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Freature requests are frozen til mid-June 2023. Please post on the forum instead. (Suggest an idea for EspoCRM).
|
||||
about: Suggest an idea for EspoCRM.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
3
.idea/codeStyles/Project.xml
generated
3
.idea/codeStyles/Project.xml
generated
@@ -1,5 +1,8 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||
</JSCodeStyleSettings>
|
||||
<PHPCodeStyleSettings>
|
||||
<option name="GROUP_USE_WRAP" value="2" />
|
||||
<option name="VARIABLE_NAMING_STYLE" value="CAMEL_CASE" />
|
||||
|
||||
60
.idea/jsonSchemas.xml
generated
60
.idea/jsonSchemas.xml
generated
@@ -22,6 +22,9 @@
|
||||
<option name="path" value="schema/metadata" />
|
||||
<option name="mappingKind" value="Directory" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="path" value="schema/autoload.json" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="path" value="schema/module.json" />
|
||||
</Item>
|
||||
@@ -33,6 +36,25 @@
|
||||
</SchemaInfo>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="autoload">
|
||||
<value>
|
||||
<SchemaInfo>
|
||||
<option name="generatedName" value="New Schema" />
|
||||
<option name="name" value="autoload" />
|
||||
<option name="relativePathToSchema" value="schema/autoload.json" />
|
||||
<option name="schemaVersion" value="JSON Schema version 7" />
|
||||
<option name="patterns">
|
||||
<list>
|
||||
<Item>
|
||||
<option name="pattern" value="true" />
|
||||
<option name="path" value="*/Resources/autoload.json" />
|
||||
<option name="mappingKind" value="Pattern" />
|
||||
</Item>
|
||||
</list>
|
||||
</option>
|
||||
</SchemaInfo>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="layouts/detail">
|
||||
<value>
|
||||
<SchemaInfo>
|
||||
@@ -566,6 +588,25 @@
|
||||
</SchemaInfo>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="metadata/app/entityTemplates">
|
||||
<value>
|
||||
<SchemaInfo>
|
||||
<option name="generatedName" value="New Schema" />
|
||||
<option name="name" value="metadata/app/entityTemplates" />
|
||||
<option name="relativePathToSchema" value="schema/metadata/app/entityTemplates.json" />
|
||||
<option name="schemaVersion" value="JSON Schema version 7" />
|
||||
<option name="patterns">
|
||||
<list>
|
||||
<Item>
|
||||
<option name="pattern" value="true" />
|
||||
<option name="path" value="*/Resources/metadata/app/entityTemplates.json" />
|
||||
<option name="mappingKind" value="Pattern" />
|
||||
</Item>
|
||||
</list>
|
||||
</option>
|
||||
</SchemaInfo>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="metadata/app/export">
|
||||
<value>
|
||||
<SchemaInfo>
|
||||
@@ -756,6 +797,25 @@
|
||||
</SchemaInfo>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="metadata/app/entityManager">
|
||||
<value>
|
||||
<SchemaInfo>
|
||||
<option name="generatedName" value="New Schema" />
|
||||
<option name="name" value="metadata/app/entityManager" />
|
||||
<option name="relativePathToSchema" value="schema/metadata/app/entityManager.json" />
|
||||
<option name="schemaVersion" value="JSON Schema version 7" />
|
||||
<option name="patterns">
|
||||
<list>
|
||||
<Item>
|
||||
<option name="pattern" value="true" />
|
||||
<option name="path" value="*/Resources/metadata/app/entityManager.json" />
|
||||
<option name="mappingKind" value="Pattern" />
|
||||
</Item>
|
||||
</list>
|
||||
</option>
|
||||
</SchemaInfo>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="metadata/app/massActions">
|
||||
<value>
|
||||
<SchemaInfo>
|
||||
|
||||
18
.vscode/settings.json
vendored
18
.vscode/settings.json
vendored
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": [
|
||||
"*/Resources/autoload.json"
|
||||
],
|
||||
"url": "./schema/autoload.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"*/Resources/routes.json"
|
||||
@@ -262,6 +268,12 @@
|
||||
],
|
||||
"url": "./schema/metadata/app/entityTemplateList.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"*/Resources/metadata/app/entityTemplates.json"
|
||||
],
|
||||
"url": "./schema/metadata/app/entityTemplates.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"*/Resources/metadata/app/export.json"
|
||||
@@ -322,6 +334,12 @@
|
||||
],
|
||||
"url": "./schema/metadata/app/linkManager.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"*/Resources/metadata/app/entityManager.json"
|
||||
],
|
||||
"url": "./schema/metadata/app/entityManager.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"*/Resources/metadata/app/massActions.json"
|
||||
|
||||
82
Gruntfile.js
82
Gruntfile.js
@@ -33,16 +33,32 @@ const fs = require('fs');
|
||||
const cp = require('child_process');
|
||||
const path = require('path');
|
||||
const buildUtils = require('./js/build-utils');
|
||||
const {TemplateBundler, Bundler} = require('espo-frontend-build-tools');
|
||||
const LayoutTypeBundler = require('./js/layout-template-bundler');
|
||||
|
||||
const bundleConfig = require('./frontend/bundle-config.json');
|
||||
const libs = require('./frontend/libs.json');
|
||||
|
||||
module.exports = grunt => {
|
||||
|
||||
const pkg = grunt.file.readJSON('package.json');
|
||||
const bundleConfig = require('./frontend/bundle-config.json');
|
||||
const libs = require('./frontend/libs.json');
|
||||
|
||||
const originalLibDir = 'client/lib/original';
|
||||
|
||||
let bundleJsFileList = buildUtils.getPreparedBundleLibList(libs).concat(originalLibDir + '/espo.js');
|
||||
let libsBundleFileList = [
|
||||
'client/src/namespace.js',
|
||||
'client/src/loader.js',
|
||||
...buildUtils.getPreparedBundleLibList(libs),
|
||||
];
|
||||
|
||||
let bundleFileMap = {'client/lib/espo.js': libsBundleFileList};
|
||||
|
||||
for (let name in bundleConfig.chunks) {
|
||||
let namePart = 'espo-' + name;
|
||||
|
||||
bundleFileMap[`client/lib/${namePart}.js`] = originalLibDir + `/${namePart}.js`
|
||||
}
|
||||
|
||||
let copyJsFileList = buildUtils.getCopyLibDataList(libs);
|
||||
|
||||
let minifyLibFileList = copyJsFileList
|
||||
@@ -118,6 +134,10 @@ module.exports = grunt => {
|
||||
'!build/tmp/client/custom/modules',
|
||||
'build/tmp/client/custom/modules/*',
|
||||
'!build/tmp/client/custom/modules/dummy.txt',
|
||||
'build/tmp/client/lib/original/espo.js',
|
||||
'build/tmp/client/lib/original/espo-*.js',
|
||||
'!build/tmp/client/lib/original/espo-funnel-chart.js',
|
||||
'build/tmp/client/lib/transpiled',
|
||||
]
|
||||
},
|
||||
},
|
||||
@@ -132,19 +152,19 @@ module.exports = grunt => {
|
||||
|
||||
uglify: {
|
||||
options: {
|
||||
mangle: true,
|
||||
sourceMap: true,
|
||||
output: {
|
||||
comments: /^!/,
|
||||
},
|
||||
beautify: false,
|
||||
mangle: true,
|
||||
compress: true
|
||||
},
|
||||
bundle: {
|
||||
options: {
|
||||
banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n',
|
||||
},
|
||||
files: {
|
||||
'client/lib/espo.min.js': bundleJsFileList,
|
||||
},
|
||||
files: bundleFileMap,
|
||||
},
|
||||
lib: {
|
||||
files: minifyLibFileList,
|
||||
@@ -162,7 +182,6 @@ module.exports = grunt => {
|
||||
'src/**',
|
||||
'res/**',
|
||||
'fonts/**',
|
||||
'cfg/**',
|
||||
'modules/**',
|
||||
'img/**',
|
||||
'css/**',
|
||||
@@ -249,16 +268,43 @@ module.exports = grunt => {
|
||||
},
|
||||
});
|
||||
|
||||
grunt.registerTask('espo-bundle', () => {
|
||||
const Bundler = require('./js/bundler');
|
||||
|
||||
let contents = (new Bundler()).bundle(bundleConfig.jsFiles);
|
||||
|
||||
const writeOriginalLib = (name, contents) => {
|
||||
if (!fs.existsSync(originalLibDir)) {
|
||||
fs.mkdirSync(originalLibDir);
|
||||
}
|
||||
|
||||
fs.writeFileSync(originalLibDir + '/espo.js', contents, 'utf8');
|
||||
let file = originalLibDir + `/${name}.js`;
|
||||
|
||||
fs.writeFileSync(file, contents, 'utf8');
|
||||
};
|
||||
|
||||
grunt.registerTask('bundle', () => {
|
||||
let bundler = new Bundler(bundleConfig, libs);
|
||||
|
||||
let result = bundler.bundle();
|
||||
|
||||
for (let name in result) {
|
||||
let contents = result[name];
|
||||
|
||||
let key = 'espo-' + name;
|
||||
|
||||
if (name === 'main') {
|
||||
contents += '\n' + (new LayoutTypeBundler()).bundle();
|
||||
}
|
||||
|
||||
writeOriginalLib(key, contents);
|
||||
}
|
||||
});
|
||||
|
||||
grunt.registerTask('bundle-templates', () => {
|
||||
let templateBundler = new TemplateBundler({
|
||||
dirs: [
|
||||
'client/res/templates',
|
||||
'client/modules/crm/res/templates',
|
||||
],
|
||||
});
|
||||
|
||||
templateBundler.process();
|
||||
});
|
||||
|
||||
grunt.registerTask('prepare-lib-original', () => {
|
||||
@@ -270,6 +316,10 @@ module.exports = grunt => {
|
||||
cp.execSync("node js/scripts/prepare-lib");
|
||||
});
|
||||
|
||||
grunt.registerTask('transpile', () => {
|
||||
cp.execSync("node js/transpile");
|
||||
});
|
||||
|
||||
grunt.registerTask('chmod-folders', () => {
|
||||
cp.execSync(
|
||||
"find . -type d -exec chmod 755 {} +",
|
||||
@@ -443,8 +493,10 @@ module.exports = grunt => {
|
||||
grunt.registerTask('internal', [
|
||||
'less',
|
||||
'cssmin',
|
||||
'espo-bundle',
|
||||
'prepare-lib-original',
|
||||
'transpile',
|
||||
'bundle',
|
||||
'bundle-templates',
|
||||
'uglify:bundle',
|
||||
'copy:frontendLib',
|
||||
'prepare-lib',
|
||||
|
||||
@@ -22,6 +22,7 @@ You can try the CRM on the online [demo](https://www.espocrm.com/demo/).
|
||||
|
||||
* PHP 8.0 and later;
|
||||
* MySQL 5.7 (and later), or MariaDB 10.2 (and later).
|
||||
* PostgreSQL 15 (and later) (yet experimental, officially supported soon)
|
||||
|
||||
For more information about server configuration see [this article](https://docs.espocrm.com/administration/server-configuration/).
|
||||
|
||||
@@ -60,7 +61,7 @@ Branches:
|
||||
|
||||
### Language
|
||||
|
||||
If you want to improve existing translation or add a language that is not available yet, you can contribute on our [POEditor](https://poeditor.com/join/project/gLDKZtUF4i) project. See instructions [here](https://www.espocrm.com/blog/how-to-use-poeditor-to-translate-espocrm/).
|
||||
If you want to improve existing translation or add a language that is not available yet, you can contribute on our [POEditor](https://poeditor.com/join/project/gLDKZtUF4i) project. See instructions [here](https://www.espocrm.com/blog/how-to-use-poeditor-to-translate-espocrm/). It may be reasonable to let us know about your intention to join the POEditor project by posting on our forum or via the contact form on our website.
|
||||
|
||||
Changes on POEditor are usually merged to the GitHub repository before minor releases.
|
||||
|
||||
|
||||
@@ -240,6 +240,11 @@ class Binding implements BindingProcessor
|
||||
'Espo\\ORM\\PDO\\PDOProvider',
|
||||
'Espo\\ORM\\PDO\\DefaultPDOProvider'
|
||||
);
|
||||
|
||||
$binder->bindImplementation(
|
||||
'Espo\\Core\\Utils\\Database\\ConfigDataProvider',
|
||||
'Espo\\Core\\Utils\\Database\\DefaultConfigDataProvider'
|
||||
);
|
||||
}
|
||||
|
||||
private function bindMisc(Binder $binder): void
|
||||
@@ -282,6 +287,16 @@ class Binding implements BindingProcessor
|
||||
'Espo\\Core\\Mail\\Importer\\DuplicateFinder',
|
||||
'Espo\\Core\\Mail\\Importer\\DefaultDuplicateFinder'
|
||||
);
|
||||
|
||||
$binder->bindImplementation(
|
||||
'Espo\\Tools\\Api\\Cors\\Helper',
|
||||
'Espo\\Tools\\Api\\Cors\\DefaultHelper'
|
||||
);
|
||||
|
||||
$binder->bindImplementation(
|
||||
'Espo\\Core\\Record\\ActionHistory\\ActionLogger',
|
||||
'Espo\\Core\\Record\\ActionHistory\\DefaultActionLogger'
|
||||
);
|
||||
}
|
||||
|
||||
private function bindAcl(Binder $binder): void
|
||||
|
||||
@@ -48,17 +48,12 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
{
|
||||
use DefaultAccessCheckerDependency;
|
||||
|
||||
private AclManager $aclManager;
|
||||
private EntityManager $entityManager;
|
||||
|
||||
public function __construct(
|
||||
DefaultAccessChecker $defaultAccessChecker,
|
||||
AclManager $aclManager,
|
||||
EntityManager $entityManager
|
||||
private AclManager $aclManager,
|
||||
private EntityManager $entityManager
|
||||
) {
|
||||
$this->defaultAccessChecker = $defaultAccessChecker;
|
||||
$this->aclManager = $aclManager;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool
|
||||
@@ -150,6 +145,10 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($note->getTargetType() === Note::TARGET_ALL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$note->getParentId() || !$note->getParentType()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -29,85 +29,5 @@
|
||||
|
||||
namespace Espo\Classes\DuplicateWhereBuilders;
|
||||
|
||||
use Espo\Core\Duplicate\WhereBuilder;
|
||||
use Espo\Core\Field\EmailAddressGroup;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
|
||||
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
|
||||
{
|
||||
$orBuilder = OrGroup::createBuilder();
|
||||
|
||||
$toCheck = false;
|
||||
|
||||
if ($entity->get('name')) {
|
||||
$orBuilder->add(
|
||||
Cond::equal(
|
||||
Cond::column('name'),
|
||||
$entity->get('name')
|
||||
),
|
||||
);
|
||||
|
||||
$toCheck = true;
|
||||
}
|
||||
|
||||
if (
|
||||
($entity->get('emailAddress') || $entity->get('emailAddressData')) &&
|
||||
(
|
||||
$entity->isNew() ||
|
||||
$entity->isAttributeChanged('emailAddress') ||
|
||||
$entity->isAttributeChanged('emailAddressData')
|
||||
)
|
||||
) {
|
||||
foreach ($this->getEmailAddressList($entity) as $emailAddress) {
|
||||
$orBuilder->add(
|
||||
Cond::equal(
|
||||
Cond::column('emailAddress'),
|
||||
$emailAddress
|
||||
)
|
||||
);
|
||||
|
||||
$toCheck = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$toCheck) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $orBuilder->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getEmailAddressList(CoreEntity $entity): array
|
||||
{
|
||||
if ($entity->get('emailAddressData')) {
|
||||
/** @var EmailAddressGroup $eaGroup */
|
||||
$eaGroup = $entity->getValueObject('emailAddress');
|
||||
|
||||
return $eaGroup->getAddressList();
|
||||
}
|
||||
|
||||
if ($entity->get('emailAddress')) {
|
||||
return [
|
||||
$entity->get('emailAddress')
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
class Company extends General
|
||||
{}
|
||||
|
||||
262
application/Espo/Classes/DuplicateWhereBuilders/General.php
Normal file
262
application/Espo/Classes/DuplicateWhereBuilders/General.php
Normal file
@@ -0,0 +1,262 @@
|
||||
<?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\DuplicateWhereBuilders;
|
||||
|
||||
use Espo\Core\Duplicate\WhereBuilder;
|
||||
use Espo\Core\Field\EmailAddressGroup;
|
||||
use Espo\Core\Field\PhoneNumberGroup;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Query\Part\Condition as Cond;
|
||||
use Espo\ORM\Query\Part\Where\OrGroup;
|
||||
use Espo\ORM\Query\Part\Where\OrGroupBuilder;
|
||||
use Espo\ORM\Query\Part\WhereItem;
|
||||
use Espo\ORM\Type\AttributeType;
|
||||
|
||||
/**
|
||||
* @implements WhereBuilder<CoreEntity>
|
||||
*/
|
||||
class General implements WhereBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private Defs $ormDefs
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param CoreEntity $entity
|
||||
*/
|
||||
public function build(Entity $entity): ?WhereItem
|
||||
{
|
||||
/** @var string[] $fieldList */
|
||||
$fieldList = $this->metadata->get(['scopes', $entity->getEntityType(), 'duplicateCheckFieldList']) ?? [];
|
||||
|
||||
$orBuilder = OrGroup::createBuilder();
|
||||
|
||||
$toCheck = false;
|
||||
|
||||
foreach ($fieldList as $field) {
|
||||
$toCheckItem = $this->applyField($field, $entity, $orBuilder);
|
||||
|
||||
if ($toCheckItem) {
|
||||
$toCheck = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$toCheck) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $orBuilder->build();
|
||||
}
|
||||
|
||||
private function applyField(
|
||||
string $field,
|
||||
CoreEntity $entity,
|
||||
OrGroupBuilder $orBuilder
|
||||
): bool {
|
||||
|
||||
$type = $this->ormDefs
|
||||
->getEntity($entity->getEntityType())
|
||||
->tryGetField($field)
|
||||
?->getType();
|
||||
|
||||
if ($type === 'personName') {
|
||||
return $this->applyFieldPersonName($field, $entity, $orBuilder);
|
||||
}
|
||||
|
||||
if ($type === 'email') {
|
||||
return $this->applyFieldEmail($field, $entity, $orBuilder);
|
||||
}
|
||||
|
||||
if ($type === 'phone') {
|
||||
return $this->applyFieldPhone($field, $entity, $orBuilder);
|
||||
}
|
||||
|
||||
if ($entity->getAttributeType($field) === AttributeType::VARCHAR) {
|
||||
return $this->applyFieldVarchar($field, $entity, $orBuilder);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function applyFieldPersonName(
|
||||
string $field,
|
||||
CoreEntity $entity,
|
||||
OrGroupBuilder $orBuilder
|
||||
): bool {
|
||||
|
||||
$first = 'first' . ucfirst($field);
|
||||
$last = 'last' . ucfirst($field);
|
||||
|
||||
if (!$entity->get($first) && !$entity->get($last)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$orBuilder->add(
|
||||
Cond::and(
|
||||
Cond::equal(
|
||||
Cond::column($first),
|
||||
$entity->get($first)
|
||||
),
|
||||
Cond::equal(
|
||||
Cond::column($last),
|
||||
$entity->get($last)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function applyFieldEmail(
|
||||
string $field,
|
||||
CoreEntity $entity,
|
||||
OrGroupBuilder $orBuilder
|
||||
): bool {
|
||||
|
||||
$toCheck = false;
|
||||
|
||||
if (
|
||||
($entity->get($field) || $entity->get($field . 'Data')) &&
|
||||
(
|
||||
$entity->isNew() ||
|
||||
$entity->isAttributeChanged($field) ||
|
||||
$entity->isAttributeChanged($field . 'Data')
|
||||
)
|
||||
) {
|
||||
foreach ($this->getEmailAddressList($entity) as $emailAddress) {
|
||||
$orBuilder->add(
|
||||
Cond::equal(
|
||||
Cond::column($field),
|
||||
$emailAddress
|
||||
)
|
||||
);
|
||||
|
||||
$toCheck = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $toCheck;
|
||||
}
|
||||
|
||||
private function applyFieldPhone(
|
||||
string $field,
|
||||
CoreEntity $entity,
|
||||
OrGroupBuilder $orBuilder
|
||||
): bool {
|
||||
|
||||
$toCheck = false;
|
||||
|
||||
if (
|
||||
($entity->get($field) || $entity->get($field . 'Data')) &&
|
||||
(
|
||||
$entity->isNew() ||
|
||||
$entity->isAttributeChanged($field) ||
|
||||
$entity->isAttributeChanged($field . 'Data')
|
||||
)
|
||||
) {
|
||||
foreach ($this->getPhoneNumberList($entity) as $phoneNumber) {
|
||||
$orBuilder->add(
|
||||
Cond::equal(
|
||||
Cond::column($field),
|
||||
$phoneNumber
|
||||
)
|
||||
);
|
||||
|
||||
$toCheck = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $toCheck;
|
||||
}
|
||||
|
||||
private function applyFieldVarchar(
|
||||
string $field,
|
||||
CoreEntity $entity,
|
||||
OrGroupBuilder $orBuilder
|
||||
): bool {
|
||||
|
||||
if (!$entity->get($field)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$orBuilder->add(
|
||||
Cond::equal(
|
||||
Cond::column($field),
|
||||
$entity->get($field)
|
||||
),
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getEmailAddressList(CoreEntity $entity): array
|
||||
{
|
||||
if ($entity->get('emailAddressData')) {
|
||||
/** @var EmailAddressGroup $eaGroup */
|
||||
$eaGroup = $entity->getValueObject('emailAddress');
|
||||
|
||||
return $eaGroup->getAddressList();
|
||||
}
|
||||
|
||||
if ($entity->get('emailAddress')) {
|
||||
return [
|
||||
$entity->get('emailAddress')
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getPhoneNumberList(CoreEntity $entity): array
|
||||
{
|
||||
if ($entity->get('phoneNumberData')) {
|
||||
/** @var PhoneNumberGroup $eaGroup */
|
||||
$eaGroup = $entity->getValueObject('phoneNumber');
|
||||
|
||||
return $eaGroup->getNumberList();
|
||||
}
|
||||
|
||||
if ($entity->get('phoneNumber')) {
|
||||
return [$entity->get('phoneNumber')];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -29,92 +29,5 @@
|
||||
|
||||
namespace Espo\Classes\DuplicateWhereBuilders;
|
||||
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
|
||||
use Espo\Core\Duplicate\WhereBuilder;
|
||||
use Espo\Core\Field\EmailAddressGroup;
|
||||
|
||||
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
|
||||
{
|
||||
$orBuilder = OrGroup::createBuilder();
|
||||
|
||||
$toCheck = false;
|
||||
|
||||
if ($entity->get('firstName') || $entity->get('lastName')) {
|
||||
$orBuilder->add(
|
||||
Cond::and(
|
||||
Cond::equal(
|
||||
Cond::column('firstName'),
|
||||
$entity->get('firstName')
|
||||
),
|
||||
Cond::equal(
|
||||
Cond::column('lastName'),
|
||||
$entity->get('lastName')
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
$toCheck = true;
|
||||
}
|
||||
|
||||
if (
|
||||
($entity->get('emailAddress') || $entity->get('emailAddressData')) &&
|
||||
(
|
||||
$entity->isNew() ||
|
||||
$entity->isAttributeChanged('emailAddress') ||
|
||||
$entity->isAttributeChanged('emailAddressData')
|
||||
)
|
||||
) {
|
||||
foreach ($this->getEmailAddressList($entity) as $emailAddress) {
|
||||
$orBuilder->add(
|
||||
Cond::equal(
|
||||
Cond::column('emailAddress'),
|
||||
$emailAddress
|
||||
)
|
||||
);
|
||||
|
||||
$toCheck = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$toCheck) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $orBuilder->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getEmailAddressList(CoreEntity $entity): array
|
||||
{
|
||||
if ($entity->get('emailAddressData')) {
|
||||
/** @var EmailAddressGroup $eaGroup */
|
||||
$eaGroup = $entity->getValueObject('emailAddress');
|
||||
|
||||
return $eaGroup->getAddressList();
|
||||
}
|
||||
|
||||
if ($entity->get('emailAddress')) {
|
||||
return [
|
||||
$entity->get('emailAddress')
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
class Person extends General
|
||||
{}
|
||||
|
||||
199
application/Espo/Classes/FieldConverters/RelationshipRole.php
Normal file
199
application/Espo/Classes/FieldConverters/RelationshipRole.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?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\FieldConverters;
|
||||
|
||||
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
|
||||
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
|
||||
use Espo\Core\Utils\Database\Orm\FieldConverter;
|
||||
use Espo\ORM\Defs\FieldDefs;
|
||||
use Espo\ORM\Type\AttributeType;
|
||||
use RuntimeException;
|
||||
|
||||
class RelationshipRole implements FieldConverter
|
||||
{
|
||||
public function convert(FieldDefs $fieldDefs, string $entityType): EntityDefs
|
||||
{
|
||||
$name = $fieldDefs->getName();
|
||||
|
||||
$attributeDefs = AttributeDefs::create($name)
|
||||
->withType(AttributeType::VARCHAR)
|
||||
->withNotStorable();
|
||||
|
||||
$attributeDefs = $this->addWhere($attributeDefs, $fieldDefs, $entityType);
|
||||
|
||||
return EntityDefs::create()
|
||||
->withAttribute($attributeDefs);
|
||||
}
|
||||
|
||||
private function addWhere(AttributeDefs $attributeDefs, FieldDefs $fieldDefs, string $entityType): AttributeDefs
|
||||
{
|
||||
$data = $fieldDefs->getParam('converterData');
|
||||
|
||||
if (!is_array($data)) {
|
||||
throw new RuntimeException("No `converterData` in field defs.");
|
||||
}
|
||||
|
||||
/** @var ?string $column */
|
||||
$column = $data['column'] ?? null;
|
||||
/** @var ?string $link */
|
||||
$link = $data['link'] ?? null;
|
||||
/** @var ?string $relationName */
|
||||
$relationName = $data['relationName'] ?? null;
|
||||
/** @var ?string $nearKey */
|
||||
$nearKey = $data['nearKey'] ?? null;
|
||||
|
||||
if (!$column || !$link || !$relationName || !$nearKey) {
|
||||
throw new RuntimeException("Bad `converterData`.");
|
||||
}
|
||||
|
||||
$midTable = ucfirst($relationName);
|
||||
|
||||
return $attributeDefs->withParamsMerged([
|
||||
'where' => [
|
||||
'=' => [
|
||||
'whereClause' => [
|
||||
'id=s' => [
|
||||
'from' => $midTable,
|
||||
'select' => [$nearKey],
|
||||
'whereClause' => [
|
||||
'deleted' => false,
|
||||
$column => '{value}',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'<>' => [
|
||||
'whereClause' => [
|
||||
'id!=s' => [
|
||||
'from' => $midTable,
|
||||
'select' => [$nearKey],
|
||||
'whereClause' => [
|
||||
'deleted' => false,
|
||||
$column => '{value}',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'IN' => [
|
||||
'whereClause' => [
|
||||
'id=s' => [
|
||||
'from' => $midTable,
|
||||
'select' => [$nearKey],
|
||||
'whereClause' => [
|
||||
'deleted' => false,
|
||||
$column => '{value}',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'NOT IN' => [
|
||||
'whereClause' => [
|
||||
'id!=s' => [
|
||||
'from' => $midTable,
|
||||
'select' => [$nearKey],
|
||||
'whereClause' => [
|
||||
'deleted' => false,
|
||||
$column => '{value}',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'LIKE' => [
|
||||
'whereClause' => [
|
||||
'id=s' => [
|
||||
'from' => $midTable,
|
||||
'select' => [$nearKey],
|
||||
'whereClause' => [
|
||||
'deleted' => false,
|
||||
"$column*" => '{value}',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'NOT LIKE' => [
|
||||
'whereClause' => [
|
||||
'id!=s' => [
|
||||
'from' => $midTable,
|
||||
'select' => [$nearKey],
|
||||
'whereClause' => [
|
||||
'deleted' => false,
|
||||
"$column*" => '{value}',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'IS NULL' => [
|
||||
'whereClause' => [
|
||||
'NOT' => [
|
||||
'EXISTS' => [
|
||||
'from' => $entityType,
|
||||
'fromAlias' => 'sq',
|
||||
'select' => ['id'],
|
||||
'leftJoins' => [
|
||||
[
|
||||
$link,
|
||||
'm',
|
||||
null,
|
||||
['onlyMiddle' => true]
|
||||
]
|
||||
],
|
||||
'whereClause' => [
|
||||
"m.$column!=" => null,
|
||||
'sq.id:' => lcfirst($entityType) . '.id',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'IS NOT NULL' => [
|
||||
'whereClause' => [
|
||||
'EXISTS' => [
|
||||
'from' => $entityType,
|
||||
'fromAlias' => 'sq',
|
||||
'select' => ['id'],
|
||||
'leftJoins' => [
|
||||
[
|
||||
$link,
|
||||
'm',
|
||||
null,
|
||||
['onlyMiddle' => true]
|
||||
]
|
||||
],
|
||||
'whereClause' => [
|
||||
"m.$column!=" => null,
|
||||
'sq.id:' => lcfirst($entityType) . '.id',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -186,11 +186,11 @@ class ArrayType
|
||||
{
|
||||
$maxLength = $validationValue ?? self::DEFAULT_MAX_ITEM_LENGTH;
|
||||
|
||||
/** @var string[] $value */
|
||||
/** @var mixed[] $value */
|
||||
$value = $entity->get($field) ?? [];
|
||||
|
||||
foreach ($value as $item) {
|
||||
if (mb_strlen($item) > $maxLength) {
|
||||
if (is_string($item) && mb_strlen($item) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,14 @@ class CurrencyType extends FloatType
|
||||
$currency = $entity->get($attribute);
|
||||
$currencyList = $this->config->get('currencyList') ?? [$this->config->get('defaultCurrency')];
|
||||
|
||||
if (
|
||||
$currency === null &&
|
||||
!$entity->has($field) &&
|
||||
$entity->isNew()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
$currency === null &&
|
||||
$entity->has($field) &&
|
||||
|
||||
@@ -95,7 +95,7 @@ class EnumType
|
||||
$value = $entity->get($field);
|
||||
|
||||
// For bc.
|
||||
// @todo Remove in v8.0.
|
||||
// @todo Remove in v9.0.
|
||||
if ($value === '') {
|
||||
$value = null;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use stdClass;
|
||||
|
||||
class IntType
|
||||
{
|
||||
@@ -40,6 +41,7 @@ class IntType
|
||||
|
||||
/**
|
||||
* @param mixed $validationValue
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function checkMax(Entity $entity, string $field, $validationValue): bool
|
||||
{
|
||||
@@ -56,6 +58,7 @@ class IntType
|
||||
|
||||
/**
|
||||
* @param mixed $validationValue
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function checkMin(Entity $entity, string $field, $validationValue): bool
|
||||
{
|
||||
@@ -70,6 +73,26 @@ class IntType
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @noinspection PhpUnused */
|
||||
public function rawCheckValid(stdClass $data, string $field): bool
|
||||
{
|
||||
if (!isset($data->$field)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$value = $data->$field;
|
||||
|
||||
if ($value === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_numeric($value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function isNotEmpty(Entity $entity, string $field): bool
|
||||
{
|
||||
return $entity->has($field) && $entity->get($field) !== null;
|
||||
|
||||
@@ -258,7 +258,7 @@ class LinkMultipleType
|
||||
);
|
||||
|
||||
// For bc.
|
||||
// @todo Remove in v8.0.
|
||||
// @todo Remove in v9.0.
|
||||
if ($value === '') {
|
||||
$value = null;
|
||||
}
|
||||
|
||||
@@ -30,19 +30,13 @@
|
||||
namespace Espo\Classes\Select\EmailFolder\AccessControlFilters;
|
||||
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
|
||||
use Espo\Core\Select\AccessControl\Filter;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
class Mandatory implements Filter
|
||||
{
|
||||
private $user;
|
||||
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
public function __construct(private User $user)
|
||||
{}
|
||||
|
||||
public function apply(SelectBuilder $queryBuilder): void
|
||||
{
|
||||
|
||||
@@ -37,12 +37,8 @@ use Espo\Entities\User;
|
||||
|
||||
class Mandatory implements Filter
|
||||
{
|
||||
private $user;
|
||||
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
public function __construct(private User $user)
|
||||
{}
|
||||
|
||||
public function apply(SelectBuilder $queryBuilder): void
|
||||
{
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
|
||||
use Espo\Core\Container;
|
||||
use Espo\Core\DataManager;
|
||||
use Espo\Core\Api\Request;
|
||||
@@ -44,6 +44,9 @@ use Espo\Entities\User;
|
||||
|
||||
class Admin
|
||||
{
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function __construct(
|
||||
private Container $container,
|
||||
private Config $config,
|
||||
@@ -53,12 +56,14 @@ class Admin
|
||||
private ScheduledJob $scheduledJob,
|
||||
private DataManager $dataManager
|
||||
) {
|
||||
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function postActionRebuild(): bool
|
||||
{
|
||||
$this->dataManager->rebuild();
|
||||
@@ -66,6 +71,9 @@ class Admin
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function postActionClearCache(): bool
|
||||
{
|
||||
$this->dataManager->clearCache();
|
||||
@@ -81,24 +89,28 @@ class Admin
|
||||
return $this->scheduledJob->getAvailableList();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
* @param string $data
|
||||
* @return array{
|
||||
* @return object{
|
||||
* id: string,
|
||||
* version: string,
|
||||
* }
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @todo Use Request.
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionUploadUpgradePackage($params, $data): array
|
||||
public function postActionUploadUpgradePackage(Request $request): object
|
||||
{
|
||||
if ($this->config->get('restrictedMode')) {
|
||||
if (!$this->user->isSuperAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
if (
|
||||
$this->config->get('restrictedMode') &&
|
||||
!$this->user->isSuperAdmin()
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$data = $request->getBodyContents();
|
||||
|
||||
if (!$data) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$upgradeManager = new UpgradeManager($this->container);
|
||||
@@ -106,7 +118,7 @@ class Admin
|
||||
$upgradeId = $upgradeManager->upload($data);
|
||||
$manifest = $upgradeManager->getManifest();
|
||||
|
||||
return [
|
||||
return (object) [
|
||||
'id' => $upgradeId,
|
||||
'version' => $manifest['version'],
|
||||
];
|
||||
@@ -120,10 +132,11 @@ class Admin
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if ($this->config->get('restrictedMode')) {
|
||||
if (!$this->user->isSuperAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
if (
|
||||
$this->config->get('restrictedMode') &&
|
||||
!$this->user->isSuperAdmin()
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$upgradeManager = new UpgradeManager($this->container);
|
||||
@@ -134,33 +147,37 @@ class Admin
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* message: string,
|
||||
* command: string,
|
||||
* @return object{
|
||||
* message: string,
|
||||
* command: string,
|
||||
* }
|
||||
*/
|
||||
public function actionCronMessage(): array
|
||||
public function getActionCronMessage(): object
|
||||
{
|
||||
return $this->scheduledJob->getSetupMessage();
|
||||
return (object) $this->scheduledJob->getSetupMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{id: string, type: string, message: string}>
|
||||
* @return array<int, array{
|
||||
* id: string,
|
||||
* type: string,
|
||||
* message: string,
|
||||
* }>
|
||||
*/
|
||||
public function actionAdminNotificationList(): array
|
||||
public function getActionAdminNotificationList(): array
|
||||
{
|
||||
return $this->adminNotificationManager->getNotificationList();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* php: array<string, array<string, mixed>>,
|
||||
* database: array<string, array<string, mixed>>,
|
||||
* permission: array<string, array<string, mixed>>,
|
||||
* @return object{
|
||||
* php: array<string, array<string, mixed>>,
|
||||
* database: array<string, array<string, mixed>>,
|
||||
* permission: array<string, array<string, mixed>>,
|
||||
* }
|
||||
*/
|
||||
public function actionSystemRequirementList(): array
|
||||
public function getActionSystemRequirementList(): object
|
||||
{
|
||||
return $this->systemRequirements->getAllRequiredList();
|
||||
return (object) $this->systemRequirements->getAllRequiredList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,9 +78,15 @@ class EmailFolder extends RecordBase
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getActionListAll(): stdClass
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function getActionListAll(Request $request): stdClass
|
||||
{
|
||||
$list = $this->getEmailFolderService()->listAll();
|
||||
$userId = $request->getQueryParam('userId');
|
||||
|
||||
$list = $this->getEmailFolderService()->listAll($userId);
|
||||
|
||||
return (object) ['list' => $list];
|
||||
}
|
||||
|
||||
@@ -31,12 +31,19 @@ namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Tools\EntityManager\EntityManager as EntityManagerTool;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Tools\ExportCustom\ExportCustom;
|
||||
use Espo\Tools\ExportCustom\Params as ExportCustomParams;
|
||||
use Espo\Tools\ExportCustom\Service as ExportCustomService;
|
||||
use Espo\Tools\LinkManager\LinkManager;
|
||||
use stdClass;
|
||||
|
||||
use const FILTER_SANITIZE_STRING;
|
||||
|
||||
class EntityManager
|
||||
{
|
||||
@@ -45,9 +52,10 @@ class EntityManager
|
||||
*/
|
||||
public function __construct(
|
||||
private User $user,
|
||||
private EntityManagerTool $entityManagerTool
|
||||
private EntityManagerTool $entityManagerTool,
|
||||
private LinkManager $linkManager,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {
|
||||
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
@@ -71,8 +79,8 @@ class EntityManager
|
||||
$name = $data['name'];
|
||||
$type = $data['type'];
|
||||
|
||||
$name = filter_var($name, \FILTER_SANITIZE_STRING);
|
||||
$type = filter_var($type, \FILTER_SANITIZE_STRING);
|
||||
$name = filter_var($name, FILTER_SANITIZE_STRING);
|
||||
$type = filter_var($type, FILTER_SANITIZE_STRING);
|
||||
|
||||
if (!is_string($name) || !is_string($type)) {
|
||||
throw new BadRequest();
|
||||
@@ -155,7 +163,7 @@ class EntityManager
|
||||
|
||||
$name = $data['name'];
|
||||
|
||||
$name = filter_var($name, \FILTER_SANITIZE_STRING);
|
||||
$name = filter_var($name, FILTER_SANITIZE_STRING);
|
||||
|
||||
if (!is_string($name)) {
|
||||
throw new BadRequest();
|
||||
@@ -183,7 +191,7 @@ class EntityManager
|
||||
|
||||
$name = $data['name'];
|
||||
|
||||
$name = filter_var($name, \FILTER_SANITIZE_STRING);
|
||||
$name = filter_var($name, FILTER_SANITIZE_STRING);
|
||||
|
||||
if (!is_string($name)) {
|
||||
throw new BadRequest();
|
||||
@@ -226,11 +234,11 @@ class EntityManager
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$params[$item] = filter_var($data[$item], \FILTER_SANITIZE_STRING);
|
||||
$params[$item] = filter_var($data[$item], FILTER_SANITIZE_STRING);
|
||||
}
|
||||
|
||||
foreach ($additionalParamList as $item) {
|
||||
$params[$item] = filter_var($data[$item] ?? null, \FILTER_SANITIZE_STRING);
|
||||
$params[$item] = filter_var($data[$item] ?? null, FILTER_SANITIZE_STRING);
|
||||
}
|
||||
|
||||
$params['labelForeign'] = $params['labelForeign'] ?? $params['linkForeign'];
|
||||
@@ -259,6 +267,14 @@ class EntityManager
|
||||
$params['foreignLinkEntityTypeList'] = $data['foreignLinkEntityTypeList'];
|
||||
}
|
||||
|
||||
if (array_key_exists('layout', $data)) {
|
||||
$params['layout'] = $data['layout'];
|
||||
}
|
||||
|
||||
if (array_key_exists('layoutForeign', $data)) {
|
||||
$params['layoutForeign'] = $data['layoutForeign'];
|
||||
}
|
||||
|
||||
/** @var array{
|
||||
* linkType: string,
|
||||
* entity: string,
|
||||
@@ -272,14 +288,20 @@ class EntityManager
|
||||
* linkMultipleFieldForeign?: bool,
|
||||
* audited?: bool,
|
||||
* auditedForeign?: bool,
|
||||
* layout?: string,
|
||||
* layoutForeign?: string,
|
||||
* } $params
|
||||
*/
|
||||
|
||||
$this->entityManagerTool->createLink($params);
|
||||
$this->linkManager->create($params);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Error
|
||||
*/
|
||||
public function postActionUpdateLink(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -299,7 +321,7 @@ class EntityManager
|
||||
|
||||
foreach ($paramList as $item) {
|
||||
if (array_key_exists($item, $data)) {
|
||||
$params[$item] = filter_var($data[$item], \FILTER_SANITIZE_STRING);
|
||||
$params[$item] = filter_var($data[$item], FILTER_SANITIZE_STRING);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,6 +348,14 @@ class EntityManager
|
||||
$params['foreignLinkEntityTypeList'] = $data['foreignLinkEntityTypeList'];
|
||||
}
|
||||
|
||||
if (array_key_exists('layout', $data)) {
|
||||
$params['layout'] = $data['layout'];
|
||||
}
|
||||
|
||||
if (array_key_exists('auditedForeign', $data)) {
|
||||
$params['layoutForeign'] = $data['layoutForeign'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array{
|
||||
* entity: string,
|
||||
@@ -340,14 +370,20 @@ class EntityManager
|
||||
* auditedForeign?: bool,
|
||||
* parentEntityTypeList?: string[],
|
||||
* foreignLinkEntityTypeList?: string[],
|
||||
* layout?: string,
|
||||
* layoutForeign?: string,
|
||||
* } $params
|
||||
*/
|
||||
|
||||
$this->entityManagerTool->updateLink($params);
|
||||
$this->linkManager->update($params);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Error
|
||||
*/
|
||||
public function postActionRemoveLink(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -362,7 +398,7 @@ class EntityManager
|
||||
$params = [];
|
||||
|
||||
foreach ($paramList as $item) {
|
||||
$params[$item] = filter_var($data[$item], \FILTER_SANITIZE_STRING);
|
||||
$params[$item] = filter_var($data[$item], FILTER_SANITIZE_STRING);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -372,7 +408,7 @@ class EntityManager
|
||||
* } $params
|
||||
*/
|
||||
|
||||
$this->entityManagerTool->deleteLink($params);
|
||||
$this->linkManager->delete($params);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -400,6 +436,29 @@ class EntityManager
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionResetFormulaToDefault(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$scope = $data->scope ?? null;
|
||||
$type = $data->type ?? null;
|
||||
|
||||
if (!$scope || !$type) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$this->entityManagerTool->resetFormulaToDefault($scope, $type);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Error
|
||||
*/
|
||||
public function postActionResetToDefault(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -412,4 +471,45 @@ class EntityManager
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionExportCustom(Request $request): stdClass
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$name = $data->name ?? null;
|
||||
$version = $data->version ?? null;
|
||||
$author = $data->author ?? null;
|
||||
$module = $data->module ?? null;
|
||||
$description = $data->description ?? null;
|
||||
|
||||
if (
|
||||
!is_string($name) ||
|
||||
!is_string($version) ||
|
||||
!is_string($author) ||
|
||||
!is_string($module) ||
|
||||
!is_string($description) && !is_null($description)
|
||||
) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$params = new ExportCustomParams(
|
||||
name: $name,
|
||||
module: $module,
|
||||
version: $version,
|
||||
author: $author,
|
||||
description: $description
|
||||
);
|
||||
|
||||
$export = $this->injectableFactory->create(ExportCustom::class);
|
||||
$service = $this->injectableFactory->create(ExportCustomService::class);
|
||||
|
||||
$service->storeToConfig($params);
|
||||
|
||||
$result = $export->process($params);
|
||||
|
||||
return (object) ['id' => $result->getAttachmentId()];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,25 +29,26 @@
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Tools\Layout\CustomLayoutService;
|
||||
use Espo\Tools\Layout\LayoutDefs;
|
||||
use Espo\Tools\Layout\Service as Service;
|
||||
use Espo\Entities\User;
|
||||
use stdClass;
|
||||
|
||||
class Layout
|
||||
{
|
||||
private User $user;
|
||||
private Service $service;
|
||||
|
||||
public function __construct(User $user, Service $service)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->service = $service;
|
||||
}
|
||||
public function __construct(
|
||||
private User $user,
|
||||
private Service $service,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
@@ -103,7 +104,7 @@ class Layout
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @return array<int, mixed>|stdClass|null
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
* @throws NotFound
|
||||
@@ -125,7 +126,7 @@ class Layout
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @return array<int, mixed>|stdClass|null
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
@@ -147,4 +148,75 @@ class Layout
|
||||
|
||||
return $this->service->getOriginal($scope, $name, $setId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function postActionCreate(Request $request): bool
|
||||
{
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$body = $request->getParsedBody();
|
||||
|
||||
$scope = $body->scope ?? null;
|
||||
$name = $body->name ?? null;
|
||||
$type = $body->type ?? null;
|
||||
$label = $body->label ?? null;
|
||||
|
||||
if (
|
||||
!is_string($scope) ||
|
||||
!is_string($name) ||
|
||||
!is_string($type) ||
|
||||
!is_string($label) ||
|
||||
!$scope ||
|
||||
!$name ||
|
||||
!$type ||
|
||||
!$label
|
||||
) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$defs = new LayoutDefs($scope, $name, $type, $label);
|
||||
|
||||
$service = $this->injectableFactory->create(CustomLayoutService::class);
|
||||
|
||||
$service->create($defs);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionDelete(Request $request): bool
|
||||
{
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$body = $request->getParsedBody();
|
||||
|
||||
$scope = $body->scope ?? null;
|
||||
$name = $body->name ?? null;
|
||||
|
||||
if (
|
||||
!is_string($scope) ||
|
||||
!is_string($name) ||
|
||||
!$scope ||
|
||||
!$name
|
||||
) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$service = $this->injectableFactory->create(CustomLayoutService::class);
|
||||
|
||||
$service->delete($scope, $name);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\TemplateFileManager;
|
||||
use Espo\Core\ApplicationState;
|
||||
@@ -38,16 +39,20 @@ use Espo\Core\Api\Request;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
* @todo Move to a service class.
|
||||
*/
|
||||
class TemplateManager
|
||||
{
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private TemplateFileManager $templateFileManager,
|
||||
private ApplicationState $applicationState
|
||||
private ApplicationState $applicationState,
|
||||
private Config $config
|
||||
) {
|
||||
|
||||
if (!$this->applicationState->isAdmin()) {
|
||||
@@ -55,6 +60,9 @@ class TemplateManager
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function getActionGetTemplate(Request $request): stdClass
|
||||
{
|
||||
$name = $request->getQueryParam('name');
|
||||
@@ -66,7 +74,6 @@ class TemplateManager
|
||||
$scope = $request->getQueryParam('scope');
|
||||
|
||||
$module = $this->metadata->get(['app', 'templates', $name, 'module']);
|
||||
|
||||
$hasSubject = !$this->metadata->get(['app', 'templates', $name, 'noSubject']);
|
||||
|
||||
$templateFileManager = $this->templateFileManager;
|
||||
@@ -82,6 +89,10 @@ class TemplateManager
|
||||
return $returnData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function postActionSaveTemplate(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -89,9 +100,18 @@ class TemplateManager
|
||||
$scope = null;
|
||||
|
||||
if (empty($data->name)) {
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
if (
|
||||
$data->name === 'passwordChangeLink' &&
|
||||
$this->config->get('restrictedMode') &&
|
||||
!$this->applicationState->getUser()->isSuperAdmin()
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if (!empty($data->scope)) {
|
||||
$scope = $data->scope;
|
||||
}
|
||||
@@ -109,6 +129,9 @@ class TemplateManager
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionResetTemplate(Request $request): stdClass
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -124,7 +147,6 @@ class TemplateManager
|
||||
}
|
||||
|
||||
$module = $this->metadata->get(['app', 'templates', $data->name, 'module']);
|
||||
|
||||
$hasSubject = !$this->metadata->get(['app', 'templates', $data->name, 'noSubject']);
|
||||
|
||||
$templateFileManager = $this->templateFileManager;
|
||||
|
||||
@@ -27,9 +27,8 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EntityManager\Hooks;
|
||||
namespace Espo\Core\Acl\Exceptions;
|
||||
|
||||
class PersonType extends BasePlusType
|
||||
{
|
||||
use RuntimeException;
|
||||
|
||||
}
|
||||
class NotAvailable extends RuntimeException {}
|
||||
@@ -35,10 +35,10 @@ use Espo\Core\Action\Params;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
use Espo\Core\Record\ActionHistory\Action;
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\ObjectUtil;
|
||||
use Espo\Entities\ActionHistoryRecord;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\EmailAddress;
|
||||
use Espo\Entities\PhoneNumber;
|
||||
@@ -82,6 +82,8 @@ class Merger
|
||||
|
||||
$entity->set($clonedData);
|
||||
|
||||
$this->unsetNotActualAttributes($entity);
|
||||
|
||||
if (!$service->checkAssignment($entity)) {
|
||||
throw new Forbidden("Assignment permission failure.");
|
||||
}
|
||||
@@ -135,7 +137,7 @@ class Merger
|
||||
foreach ($sourceEntityList as $sourceEntity) {
|
||||
$this->entityManager->removeEntity($sourceEntity);
|
||||
|
||||
$service->processActionHistoryRecord(ActionHistoryRecord::ACTION_DELETE, $sourceEntity);
|
||||
$service->processActionHistoryRecord(Action::DELETE, $sourceEntity);
|
||||
}
|
||||
|
||||
if ($hasPhoneNumber) {
|
||||
@@ -150,7 +152,7 @@ class Merger
|
||||
|
||||
$this->entityManager->saveEntity($entity);
|
||||
|
||||
$service->processActionHistoryRecord(ActionHistoryRecord::ACTION_UPDATE, $entity);
|
||||
$service->processActionHistoryRecord(Action::UPDATE, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -364,4 +366,20 @@ class Merger
|
||||
|
||||
$data->emailAddressData = $emailAddressData;
|
||||
}
|
||||
|
||||
private function unsetNotActualAttributes(Entity $entity): void
|
||||
{
|
||||
$fieldDefsList = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entity->getEntityType())
|
||||
->getFieldList();
|
||||
|
||||
foreach ($fieldDefsList as $fieldDefs) {
|
||||
$field = $fieldDefs->getName();
|
||||
|
||||
if ($fieldDefs->getType() === 'link' && $entity->isAttributeChanged($field . 'Id')) {
|
||||
$entity->clear($field . 'Name');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ class ControllerActionProcessor
|
||||
|
||||
if (!method_exists($controller, $primaryActionMethodName)) {
|
||||
throw new NotFoundSilent(
|
||||
"Action {$requestMethod} '{$actionName}' does not exist in controller '{$controllerName}'.");
|
||||
"Action $requestMethod '$actionName' does not exist in controller '$controllerName'.");
|
||||
}
|
||||
|
||||
if ($this->useShortParamList($controller, $primaryActionMethodName)) {
|
||||
@@ -187,11 +187,11 @@ class ControllerActionProcessor
|
||||
$className = $this->classFinder->find('Controllers', $name);
|
||||
|
||||
if (!$className) {
|
||||
throw new NotFound("Controller '{$name}' does not exist.");
|
||||
throw new NotFound("Controller '$name' does not exist.");
|
||||
}
|
||||
|
||||
if (!class_exists($className)) {
|
||||
throw new NotFound("Class not found for controller '{$name}'.");
|
||||
throw new NotFound("Class not found for controller '$name'.");
|
||||
}
|
||||
|
||||
return $className;
|
||||
|
||||
@@ -121,11 +121,9 @@ class BindingContainer
|
||||
$key &&
|
||||
$this->data->hasContext($className, $key)
|
||||
) {
|
||||
// @todo For v7.6. Uncomment, then remove the return statement below.
|
||||
/*$binding = $this->data->getContext($className, $key);
|
||||
$binding = $this->data->getContext($className, $key);
|
||||
|
||||
$notMatching =
|
||||
$type &&
|
||||
$type instanceof ReflectionNamedType &&
|
||||
!$type->isBuiltin() &&
|
||||
$binding->getType() === Binding::VALUE &&
|
||||
@@ -133,15 +131,12 @@ class BindingContainer
|
||||
|
||||
if (!$notMatching) {
|
||||
return $binding;
|
||||
}*/
|
||||
|
||||
return $this->data->getContext($className, $key);
|
||||
}
|
||||
}
|
||||
|
||||
$dependencyClassName = null;
|
||||
|
||||
if (
|
||||
$type &&
|
||||
$type instanceof ReflectionNamedType &&
|
||||
!$type->isBuiltin()
|
||||
) {
|
||||
|
||||
@@ -66,7 +66,7 @@ class ContainerConfiguration implements Configuration
|
||||
|
||||
if (!$className) {
|
||||
/** @deprecated */
|
||||
/** @todo Remove in 8.0. */
|
||||
/** @todo Remove in v9.0. */
|
||||
$className = $this->metadata->get(['app', 'loaders', ucfirst($name)]);
|
||||
}
|
||||
} catch (Exception) {}
|
||||
|
||||
@@ -40,6 +40,7 @@ use Espo\Core\Api\ErrorOutput;
|
||||
use Espo\Core\Api\RequestWrapper;
|
||||
use Espo\Core\Api\ResponseWrapper;
|
||||
use Espo\Core\Api\AuthBuilderFactory;
|
||||
use Espo\Core\Portal\Utils\Url;
|
||||
use Espo\Core\Utils\Route;
|
||||
use Espo\Core\Utils\ClientManager;
|
||||
use Espo\Core\ApplicationRunners\EntryPoint as EntryPointRunner;
|
||||
@@ -87,6 +88,14 @@ class Starter
|
||||
throw new BadRequest("No 'entryPoint' param.");
|
||||
}
|
||||
|
||||
$portalId = Url::getPortalIdFromEnv();
|
||||
|
||||
if ($portalId && !$final) {
|
||||
$this->runThroughPortal($portalId, $entryPoint);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$responseWrapped = new ResponseWrapper(new Response());
|
||||
|
||||
try {
|
||||
@@ -206,7 +215,11 @@ class Starter
|
||||
{
|
||||
$app = new PortalApplication($portalId);
|
||||
|
||||
$app->setClientBasePath($this->clientManager->getBasePath());
|
||||
$clientManager = $app->getContainer()
|
||||
->getByClass(ClientManager::class);
|
||||
|
||||
$clientManager->setBasePath($this->clientManager->getBasePath());
|
||||
$clientManager->setApiUrl('api/v1/portal-access/' . $portalId);
|
||||
|
||||
$params = RunnerParams::fromArray([
|
||||
'entryPoint' => $entryPoint,
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
namespace Espo\Core\Field\Currency;
|
||||
|
||||
/**
|
||||
* @deprecated Since v7.1.0. Use `Espo\Core\Currency\Converter`.
|
||||
* @deprecated As of v7.1.0. Use `Espo\Core\Currency\Converter`.
|
||||
* @todo Remove in v9.0.
|
||||
*/
|
||||
class CurrencyConverter extends \Espo\Core\Currency\Converter {}
|
||||
|
||||
@@ -248,17 +248,7 @@ class FieldValidationManager
|
||||
bool $throw
|
||||
): array {
|
||||
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
$validationList = array_unique(array_merge(
|
||||
$this->getValidationList($entityType, $field),
|
||||
$this->getMandatoryValidationList($entityType, $field)
|
||||
));
|
||||
|
||||
$validationList = array_filter(
|
||||
$validationList,
|
||||
fn ($type) => !in_array($field, $params->getTypeSkipFieldList($type))
|
||||
);
|
||||
$validationList = $this->getAllValidationList($entity->getEntityType(), $field, $params);
|
||||
|
||||
$failureList = [];
|
||||
|
||||
@@ -269,7 +259,7 @@ class FieldValidationManager
|
||||
continue;
|
||||
}
|
||||
|
||||
$failure = new Failure($entityType, $field, $type);
|
||||
$failure = new Failure($entity->getEntityType(), $field, $type);
|
||||
|
||||
$failureList[] = $failure;
|
||||
|
||||
@@ -287,6 +277,32 @@ class FieldValidationManager
|
||||
return array_merge($failureList, $additionalFailureList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getAllValidationList(string $entityType, string $field, FieldValidationParams $params): array
|
||||
{
|
||||
$validationList = array_unique(array_merge(
|
||||
$this->getValidationList($entityType, $field),
|
||||
$this->getMandatoryValidationList($entityType, $field)
|
||||
));
|
||||
|
||||
/** @var string[] $suppressList */
|
||||
$suppressList = $this->metadata->get("entityDefs.$entityType.fields.$field.suppressValidationList") ?? [];
|
||||
|
||||
$validationList = array_filter(
|
||||
$validationList,
|
||||
fn ($type) => !in_array($type, $suppressList)
|
||||
);
|
||||
|
||||
$validationList = array_filter(
|
||||
$validationList,
|
||||
fn ($type) => !in_array($field, $params->getTypeSkipFieldList($type))
|
||||
);
|
||||
|
||||
return array_values($validationList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $validationValue
|
||||
*/
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
namespace Espo\Core\Formula\Exceptions;
|
||||
|
||||
/**
|
||||
* Too few function arguments passsed.
|
||||
* Too few function arguments passed.
|
||||
*/
|
||||
class TooFewArguments extends Error
|
||||
{
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<?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\Formula\Functions\RecordGroup;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Formula\EvaluatedArgumentList;
|
||||
use Espo\Core\Formula\Exceptions\BadArgumentType;
|
||||
use Espo\Core\Formula\Exceptions\Error as FormulaError;
|
||||
use Espo\Core\Formula\Exceptions\TooFewArguments;
|
||||
use Espo\Core\Formula\Func;
|
||||
use Espo\Core\Select\SelectBuilderFactory;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Query\Part\Order;
|
||||
|
||||
class FindManyType implements Func
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private SelectBuilderFactory $selectBuilderFactory
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function process(EvaluatedArgumentList $arguments): array
|
||||
{
|
||||
if (count($arguments) < 4) {
|
||||
throw TooFewArguments::create(4);
|
||||
}
|
||||
|
||||
$entityType = $arguments[0];
|
||||
$limit = $arguments[1];
|
||||
$orderBy = $arguments[2];
|
||||
$order = $arguments[3] ?? Order::ASC;
|
||||
|
||||
if (!is_string($entityType)) {
|
||||
throw BadArgumentType::create(1, 'string');
|
||||
}
|
||||
|
||||
if (!is_int($limit)) {
|
||||
throw BadArgumentType::create(2, 'int');
|
||||
}
|
||||
|
||||
if ($orderBy !== null && !is_string($orderBy)) {
|
||||
throw BadArgumentType::create(3, 'string|null');
|
||||
}
|
||||
|
||||
if (!is_bool($order) && !is_string($orderBy)) {
|
||||
throw BadArgumentType::create(4, 'string|bool');
|
||||
}
|
||||
|
||||
$builder = $this->selectBuilderFactory
|
||||
->create()
|
||||
->from($entityType);
|
||||
|
||||
$whereClause = [];
|
||||
|
||||
if (count($arguments) <= 5) {
|
||||
$filter = null;
|
||||
|
||||
if (count($arguments) === 5) {
|
||||
$filter = $arguments[4];
|
||||
}
|
||||
|
||||
if ($filter && !is_string($filter)) {
|
||||
throw BadArgumentType::create(5, 'string');
|
||||
}
|
||||
|
||||
if ($filter) {
|
||||
$builder->withPrimaryFilter($filter);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$i = 4;
|
||||
|
||||
while ($i < count($arguments) - 1) {
|
||||
$key = $arguments[$i];
|
||||
$value = $arguments[$i + 1];
|
||||
|
||||
$whereClause[] = [$key => $value];
|
||||
|
||||
$i = $i + 2;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$queryBuilder = $builder->buildQueryBuilder();
|
||||
}
|
||||
catch (BadRequest|Error|Forbidden $e) {
|
||||
throw new FormulaError($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
|
||||
if (!empty($whereClause)) {
|
||||
$queryBuilder->where($whereClause);
|
||||
}
|
||||
|
||||
if ($orderBy) {
|
||||
$queryBuilder->order($orderBy, $order);
|
||||
}
|
||||
|
||||
$queryBuilder
|
||||
->select(['id'])
|
||||
->limit(0, $limit);
|
||||
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository($entityType)
|
||||
->clone($queryBuilder->build())
|
||||
->find();
|
||||
|
||||
return array_map(
|
||||
fn (Entity $entity) => $entity->getId(),
|
||||
iterator_to_array($collection)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -29,11 +29,12 @@
|
||||
|
||||
namespace Espo\Core\Formula\Functions\RecordGroup;
|
||||
|
||||
use Espo\Core\Formula\{
|
||||
Functions\BaseFunction,
|
||||
ArgumentList,
|
||||
};
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Formula\ArgumentList;
|
||||
use Espo\Core\Formula\Exceptions\Error as FormulaError;
|
||||
use Espo\Core\Formula\Functions\BaseFunction;
|
||||
use Espo\Core\Di;
|
||||
|
||||
class FindOneType extends BaseFunction implements
|
||||
@@ -87,7 +88,12 @@ class FindOneType extends BaseFunction implements
|
||||
}
|
||||
}
|
||||
|
||||
$queryBuilder = $builder->buildQueryBuilder();
|
||||
try {
|
||||
$queryBuilder = $builder->buildQueryBuilder();
|
||||
}
|
||||
catch (BadRequest|Error|Forbidden $e) {
|
||||
throw new FormulaError($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
|
||||
if (!empty($whereClause)) {
|
||||
$queryBuilder->where($whereClause);
|
||||
|
||||
@@ -30,12 +30,8 @@
|
||||
namespace Espo\Core\Formula\Functions\RecordGroup;
|
||||
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
|
||||
use Espo\Core\Formula\{
|
||||
Functions\BaseFunction,
|
||||
ArgumentList,
|
||||
};
|
||||
|
||||
use Espo\Core\Formula\ArgumentList;
|
||||
use Espo\Core\Formula\Functions\BaseFunction;
|
||||
use Espo\Core\Di;
|
||||
|
||||
class FindRelatedManyType extends BaseFunction implements
|
||||
@@ -98,7 +94,7 @@ class FindRelatedManyType extends BaseFunction implements
|
||||
$entity = $entityManager->getEntity($entityType, $id);
|
||||
|
||||
if (!$entity) {
|
||||
$this->log("record\\findRelatedMany: Entity {$entity} {$id} not found.", 'notice');
|
||||
$this->log("record\\findRelatedMany: Entity $entityType $id not found.", 'notice');
|
||||
|
||||
return [];
|
||||
}
|
||||
@@ -123,19 +119,19 @@ class FindRelatedManyType extends BaseFunction implements
|
||||
$relationType = $entity->getRelationParam($link, 'type');
|
||||
|
||||
if (in_array($relationType, ['belongsTo', 'hasOne', 'belongsToParent'])) {
|
||||
$this->throwError("Not supported link type '{$relationType}'.");
|
||||
$this->throwError("Not supported link type '$relationType'.");
|
||||
}
|
||||
|
||||
$foreignEntityType = $entity->getRelationParam($link, 'entity');
|
||||
|
||||
if (!$foreignEntityType) {
|
||||
$this->throwError("Bad or not supported link '{$link}'.");
|
||||
$this->throwError("Bad or not supported link '$link'.");
|
||||
}
|
||||
|
||||
$foreignLink = $entity->getRelationParam($link, 'foreign');
|
||||
|
||||
if (!$foreignLink) {
|
||||
$this->throwError("Not supported link '{$link}'.");
|
||||
$this->throwError("Not supported link '$link'.");
|
||||
}
|
||||
|
||||
$builder = $this->selectBuilderFactory
|
||||
|
||||
@@ -171,7 +171,7 @@ class InjectableFactory
|
||||
|
||||
$obj = $class->newInstanceArgs($injectionList);
|
||||
|
||||
// @todo Remove in 8.0.
|
||||
// @todo Remove in v9.0.
|
||||
if ($class->implementsInterface(Injectable::class)) {
|
||||
$this->applyInjectable($class, $obj);
|
||||
|
||||
@@ -476,7 +476,7 @@ class InjectableFactory
|
||||
/**
|
||||
* @deprecated
|
||||
* @param ReflectionClass<object> $class
|
||||
* @todo Remove in 8.0.
|
||||
* @todo Remove in v9.0.
|
||||
*/
|
||||
private function applyInjectable(ReflectionClass $class, object $obj): void
|
||||
{
|
||||
|
||||
@@ -212,9 +212,12 @@ class JobRunner
|
||||
|
||||
private function runJobWithClassName(JobEntity $jobEntity): void
|
||||
{
|
||||
/** @var class-string<Job|JobDataLess> $className */
|
||||
$className = $jobEntity->getClassName();
|
||||
|
||||
if (!$className) {
|
||||
throw new RuntimeException("No className in job {$jobEntity->getId()}.");
|
||||
}
|
||||
|
||||
$job = $this->jobFactory->createByClassName($className);
|
||||
|
||||
$this->runJob($job, $jobEntity);
|
||||
@@ -222,9 +225,16 @@ class JobRunner
|
||||
|
||||
/**
|
||||
* @param Job|JobDataLess $job
|
||||
* @internal Native type is not used for bc.
|
||||
*/
|
||||
private function runJob($job, JobEntity $jobEntity): void
|
||||
{
|
||||
if ($job instanceof JobDataLess) {
|
||||
$job->run();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$data = Data::create($jobEntity->getData())
|
||||
->withTargetId($jobEntity->getTargetId())
|
||||
->withTargetType($jobEntity->getTargetType());
|
||||
|
||||
@@ -60,7 +60,7 @@ class JobScheduler
|
||||
/**
|
||||
* A class name of the job. Should implement the `Job` interface.
|
||||
*
|
||||
* @param class-string<Job> $className
|
||||
* @param class-string<Job|JobDataLess> $className
|
||||
*/
|
||||
public function setClassName(string $className): self
|
||||
{
|
||||
@@ -70,8 +70,11 @@ class JobScheduler
|
||||
|
||||
$class = new ReflectionClass($className);
|
||||
|
||||
if (!$class->implementsInterface(Job::class)) {
|
||||
throw new RuntimeException("Class '{$className}' does not implement 'Job' interface.");
|
||||
if (
|
||||
!$class->implementsInterface(Job::class) &&
|
||||
!$class->implementsInterface(JobDataLess::class)
|
||||
) {
|
||||
throw new RuntimeException("Class '{$className}' does not implement 'Job' or 'JobDataLess' interface.");
|
||||
}
|
||||
|
||||
$this->className = $className;
|
||||
|
||||
@@ -29,17 +29,15 @@
|
||||
|
||||
namespace Espo\Core\Mail\Parsers;
|
||||
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\Attachment;
|
||||
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Core\Mail\Message;
|
||||
use Espo\Core\Mail\Parser;
|
||||
use Espo\Core\Mail\Message\Part;
|
||||
use Espo\Core\Mail\Message\MailMimeParser\Part as WrapperPart;
|
||||
|
||||
use Espo\ORM\EntityManager;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
use ZBateson\MailMimeParser\Header\AddressHeader;
|
||||
use ZBateson\MailMimeParser\MailMimeParser as WrappeeParser;
|
||||
@@ -63,20 +61,20 @@ class MailMimeParser implements Parser
|
||||
'webp' => 'image/webp',
|
||||
];
|
||||
|
||||
private EntityManager $entityManager;
|
||||
private ?WrappeeParser $parser = null;
|
||||
|
||||
/**
|
||||
* @var array<string, ParserMessage>
|
||||
*/
|
||||
private const FIELD_BODY = 'body';
|
||||
private const FIELD_ATTACHMENTS = 'attachments';
|
||||
|
||||
private const DISPOSITION_INLINE = 'inline';
|
||||
|
||||
/** @var array<string, ParserMessage> */
|
||||
private array $messageHash = [];
|
||||
|
||||
public function __construct(EntityManager $entityManager)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
public function __construct(private EntityManager $entityManager)
|
||||
{}
|
||||
|
||||
protected function getParser(): WrappeeParser
|
||||
private function getParser(): WrappeeParser
|
||||
{
|
||||
if (!$this->parser) {
|
||||
$this->parser = new WrappeeParser();
|
||||
@@ -85,7 +83,7 @@ class MailMimeParser implements Parser
|
||||
return $this->parser;
|
||||
}
|
||||
|
||||
protected function loadContent(Message $message): void
|
||||
private function loadContent(Message $message): void
|
||||
{
|
||||
$raw = $message->getFullRawContent();
|
||||
|
||||
@@ -98,7 +96,7 @@ class MailMimeParser implements Parser
|
||||
/**
|
||||
* @return ParserMessage
|
||||
*/
|
||||
protected function getMessage(Message $message)
|
||||
private function getMessage(Message $message)
|
||||
{
|
||||
$key = spl_object_hash($message);
|
||||
|
||||
@@ -291,7 +289,7 @@ class MailMimeParser implements Parser
|
||||
|
||||
$attachmentPartList = $this->getMessage($message)->getAllAttachmentParts();
|
||||
|
||||
$inlineIds = [];
|
||||
$inlineAttachmentMap = [];
|
||||
|
||||
foreach ($attachmentPartList as $attachmentPart) {
|
||||
if (!$attachmentPart instanceof MimePart) {
|
||||
@@ -334,54 +332,73 @@ class MailMimeParser implements Parser
|
||||
$contentId = trim($contentId, '<>');
|
||||
}
|
||||
|
||||
if ($disposition === 'inline') {
|
||||
if ($disposition === self::DISPOSITION_INLINE) {
|
||||
$attachment->setRole(Attachment::ROLE_INLINE_ATTACHMENT);
|
||||
$attachment->setTargetField('body');
|
||||
$attachment->setTargetField(self::FIELD_BODY);
|
||||
}
|
||||
else {
|
||||
$disposition = 'attachment';
|
||||
|
||||
$attachment->setRole(Attachment::ROLE_ATTACHMENT);
|
||||
$attachment->setTargetField('attachments');
|
||||
$attachment->setTargetField(self::FIELD_ATTACHMENTS);
|
||||
}
|
||||
|
||||
$attachment->setContents($content);
|
||||
|
||||
$this->entityManager->saveEntity($attachment);
|
||||
|
||||
if ($disposition === 'attachment') {
|
||||
$email->addLinkMultipleId('attachments', $attachment->getId());
|
||||
if ($attachment->getRole() === Attachment::ROLE_ATTACHMENT) {
|
||||
$email->addLinkMultipleId(self::FIELD_ATTACHMENTS, $attachment->getId());
|
||||
|
||||
if ($contentId) {
|
||||
$inlineIds[$contentId] = $attachment->getId();
|
||||
$inlineAttachmentMap[$contentId] = $attachment;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// inline disposition
|
||||
// Inline disposition.
|
||||
|
||||
if ($contentId) {
|
||||
$inlineIds[$contentId] = $attachment->getId();
|
||||
$inlineAttachmentMap[$contentId] = $attachment;
|
||||
|
||||
$inlineAttachmentList[] = $attachment;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$email->addLinkMultipleId('attachments', $attachment->getId());
|
||||
// No ID found, fallback to attachment.
|
||||
$attachment
|
||||
->setRole(Attachment::ROLE_ATTACHMENT)
|
||||
->setTargetField(self::FIELD_ATTACHMENTS);
|
||||
|
||||
$this->entityManager->saveEntity($attachment);
|
||||
|
||||
$email->addLinkMultipleId(self::FIELD_ATTACHMENTS, $attachment->getId());
|
||||
}
|
||||
|
||||
$body = $email->getBody();
|
||||
|
||||
if (!empty($body)) {
|
||||
foreach ($inlineIds as $cid => $attachmentId) {
|
||||
if ($body) {
|
||||
foreach ($inlineAttachmentMap as $cid => $attachment) {
|
||||
if (str_contains($body, 'cid:' . $cid)) {
|
||||
$body = str_replace('cid:' . $cid, '?entryPoint=attachment&id=' . $attachmentId, $body);
|
||||
$body = str_replace(
|
||||
'cid:' . $cid,
|
||||
'?entryPoint=attachment&id=' . $attachment->getId(),
|
||||
$body
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$email->addLinkMultipleId('attachments', $attachmentId);
|
||||
// Fallback to attachment.
|
||||
if ($attachment->getRole() === Attachment::ROLE_INLINE_ATTACHMENT) {
|
||||
$attachment
|
||||
->setRole(Attachment::ROLE_ATTACHMENT)
|
||||
->setTargetField(self::FIELD_ATTACHMENTS);
|
||||
|
||||
$this->entityManager->saveEntity($attachment);
|
||||
|
||||
$email->addLinkMultipleId(self::FIELD_ATTACHMENTS, $attachment->getId());
|
||||
}
|
||||
}
|
||||
|
||||
$email->setBody($body);
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Core\MassAction\Actions;
|
||||
|
||||
use Espo\Core\Record\ActionHistory\Action;
|
||||
use Espo\Entities\ActionHistoryRecord;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Core\Acl;
|
||||
@@ -97,7 +98,7 @@ class MassDelete implements MassAction
|
||||
|
||||
$count++;
|
||||
|
||||
$service->processActionHistoryRecord(ActionHistoryRecord::ACTION_DELETE, $entity);
|
||||
$service->processActionHistoryRecord(Action::DELETE, $entity);
|
||||
}
|
||||
|
||||
$result = [
|
||||
|
||||
@@ -56,12 +56,8 @@ class DefaultAssignmentNotificator implements AssignmentNotificator
|
||||
if ($entity->hasLinkMultipleField('assignedUsers')) {
|
||||
/** @var string[] $userIdList */
|
||||
$userIdList = $entity->getLinkMultipleIdList('assignedUsers');
|
||||
/** @var ?string[] $fetchedAssignedUserIdList */
|
||||
$fetchedAssignedUserIdList = $entity->getFetched('assignedUsersIds');
|
||||
|
||||
if (!is_array($fetchedAssignedUserIdList)) {
|
||||
$fetchedAssignedUserIdList = [];
|
||||
}
|
||||
/** @var string[] $fetchedAssignedUserIdList */
|
||||
$fetchedAssignedUserIdList = $entity->getFetched('assignedUsersIds') ?? [];
|
||||
|
||||
foreach ($userIdList as $userId) {
|
||||
if (in_array($userId, $fetchedAssignedUserIdList)) {
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Espo\Core\Notificators;
|
||||
|
||||
/**
|
||||
* @deprecated As of v6.0.
|
||||
* @todo Remove in v8.0.
|
||||
* @todo Remove in v9.0.
|
||||
*/
|
||||
class Base extends DefaultNotificator
|
||||
{
|
||||
|
||||
@@ -32,22 +32,18 @@ namespace Espo\Core\Notificators;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Core\Notification\AssignmentNotificator\Params;
|
||||
|
||||
use Espo\Core\Notification\DefaultAssignmentNotificator;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
|
||||
/**
|
||||
* @deprecated As of v7.0. Use plain classes that implement `Espo\Core\Notification\AssignmentNotificator`.
|
||||
* @todo Remove in v8.0.
|
||||
* @todo Remove in v9.0.
|
||||
*/
|
||||
class DefaultNotificator
|
||||
{
|
||||
protected $entityType; /** @phpstan-ignore-line */
|
||||
|
||||
protected $user; /** @phpstan-ignore-line */
|
||||
|
||||
protected $entityManager; /** @phpstan-ignore-line */
|
||||
|
||||
private $base; /** @phpstan-ignore-line */
|
||||
|
||||
public function __construct(User $user, EntityManager $entityManager, DefaultAssignmentNotificator $base)
|
||||
|
||||
138
application/Espo/Core/ORM/ClassNameProvider.php
Normal file
138
application/Espo/Core/ORM/ClassNameProvider.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?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\ORM;
|
||||
|
||||
use Espo\Core\ORM\Entity as BaseEntity;
|
||||
use Espo\Core\Repositories\Database as DatabaseRepository;
|
||||
use Espo\Core\Utils\ClassFinder;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Entity as Entity;
|
||||
use Espo\ORM\Repository\Repository as Repository;
|
||||
|
||||
class ClassNameProvider
|
||||
{
|
||||
/** @var class-string<Entity> */
|
||||
private const DEFAULT_ENTITY_CLASS_NAME = BaseEntity::class;
|
||||
/** @var class-string<Repository<Entity>> */
|
||||
private const DEFAULT_REPOSITORY_CLASS_NAME = DatabaseRepository::class;
|
||||
|
||||
/** @var array<string, class-string<Entity>> */
|
||||
private array $entityCache = [];
|
||||
|
||||
/** @var array<string, class-string<Repository<Entity>>> */
|
||||
private array $repositoryCache = [];
|
||||
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private ClassFinder $classFinder
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param string $entityType
|
||||
* @return class-string<Entity>
|
||||
*/
|
||||
public function getEntityClassName(string $entityType): string
|
||||
{
|
||||
if (!array_key_exists($entityType, $this->entityCache)) {
|
||||
$this->entityCache[$entityType] = $this->findEntityClassName($entityType);
|
||||
}
|
||||
|
||||
return $this->entityCache[$entityType];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $entityType
|
||||
* @return class-string<Repository<Entity>>
|
||||
*/
|
||||
public function getRepositoryClassName(string $entityType): string
|
||||
{
|
||||
if (!array_key_exists($entityType, $this->entityCache)) {
|
||||
$this->repositoryCache[$entityType] = $this->findRepositoryClassName($entityType);
|
||||
}
|
||||
|
||||
return $this->repositoryCache[$entityType];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $entityType
|
||||
* @return class-string<Entity>
|
||||
*/
|
||||
private function findEntityClassName(string $entityType): string
|
||||
{
|
||||
/** @var ?class-string<Entity> $className */
|
||||
$className = $this->classFinder->find('Entities', $entityType);
|
||||
|
||||
if ($className) {
|
||||
return $className;
|
||||
}
|
||||
|
||||
/** @var ?string $template */
|
||||
$template = $this->metadata->get(['scopes', $entityType, 'type']);
|
||||
|
||||
if ($template) {
|
||||
/** @var ?class-string<Entity> $className */
|
||||
$className = $this->metadata->get(['app', 'entityTemplates', $template, 'entityClassName']);
|
||||
}
|
||||
|
||||
if ($className) {
|
||||
return $className;
|
||||
}
|
||||
|
||||
return self::DEFAULT_ENTITY_CLASS_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $entityType
|
||||
* @return class-string<Repository<Entity>>
|
||||
*/
|
||||
private function findRepositoryClassName(string $entityType): string
|
||||
{
|
||||
/** @var ?class-string<Repository<Entity>> $className */
|
||||
$className = $this->classFinder->find('Repositories', $entityType);
|
||||
|
||||
if ($className) {
|
||||
return $className;
|
||||
}
|
||||
|
||||
/** @var ?string $template */
|
||||
$template = $this->metadata->get(['scopes', $entityType, 'type']);
|
||||
|
||||
if ($template) {
|
||||
/** @var ?class-string<Repository<Entity>> $className */
|
||||
$className = $this->metadata->get(['app', 'entityTemplates', $template, 'repositoryClassName']);
|
||||
}
|
||||
|
||||
if ($className) {
|
||||
return $className;
|
||||
}
|
||||
|
||||
return self::DEFAULT_REPOSITORY_CLASS_NAME;
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,6 @@ use RuntimeException;
|
||||
class DatabaseParamsFactory
|
||||
{
|
||||
private const DEFAULT_PLATFORM = 'Mysql';
|
||||
private const DEFAULT_CHARSET = 'utf8';
|
||||
|
||||
public function __construct(private Config $config) {}
|
||||
|
||||
@@ -55,7 +54,7 @@ class DatabaseParamsFactory
|
||||
->withName($config->get('database.dbname'))
|
||||
->withUsername($config->get('database.user'))
|
||||
->withPassword($config->get('database.password'))
|
||||
->withCharset($config->get('database.charset') ?? self::DEFAULT_CHARSET)
|
||||
->withCharset($config->get('database.charset'))
|
||||
->withPlatform($config->get('database.platform'))
|
||||
->withSslCa($config->get('database.sslCA'))
|
||||
->withSslCert($config->get('database.sslCert'))
|
||||
|
||||
@@ -33,9 +33,6 @@ use Espo\Core\Binding\Binder;
|
||||
use Espo\Core\Binding\BindingContainer;
|
||||
use Espo\Core\Binding\BindingData;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\ORM\Entity as BaseEntity;
|
||||
use Espo\Core\Utils\ClassFinder;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityFactory as EntityFactoryInterface;
|
||||
use Espo\ORM\EntityManager;
|
||||
@@ -49,20 +46,11 @@ class EntityFactory implements EntityFactoryInterface
|
||||
private ?ValueAccessorFactory $valueAccessorFactory = null;
|
||||
|
||||
public function __construct(
|
||||
private ClassFinder $classFinder,
|
||||
private ClassNameProvider $classNameProvider,
|
||||
private Helper $helper,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return ?class-string<Entity>
|
||||
*/
|
||||
private function getClassName(string $entityType): ?string
|
||||
{
|
||||
/** @var ?class-string<Entity> */
|
||||
return $this->classFinder->find('Entities', $entityType);
|
||||
}
|
||||
|
||||
public function setEntityManager(EntityManager $entityManager): void
|
||||
{
|
||||
if ($this->entityManager) {
|
||||
@@ -85,10 +73,6 @@ class EntityFactory implements EntityFactoryInterface
|
||||
{
|
||||
$className = $this->getClassName($entityType);
|
||||
|
||||
if (!$className) {
|
||||
$className = BaseEntity::class;
|
||||
}
|
||||
|
||||
if (!$this->entityManager) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
@@ -96,7 +80,7 @@ class EntityFactory implements EntityFactoryInterface
|
||||
$defs = $this->entityManager->getMetadata()->get($entityType);
|
||||
|
||||
if (is_null($defs)) {
|
||||
throw new RuntimeException("Entity '{$entityType}' is not defined in metadata.");
|
||||
throw new RuntimeException("Entity '$entityType' is not defined in metadata.");
|
||||
}
|
||||
|
||||
$bindingContainer = $this->getBindingContainer($className, $entityType, $defs);
|
||||
@@ -104,6 +88,15 @@ class EntityFactory implements EntityFactoryInterface
|
||||
return $this->injectableFactory->createWithBinding($className, $bindingContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<Entity>
|
||||
*/
|
||||
private function getClassName(string $entityType): string
|
||||
{
|
||||
/** @var class-string<Entity> */
|
||||
return $this->classNameProvider->getEntityClassName($entityType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<Entity> $className
|
||||
* @param array<string, mixed> $defs
|
||||
|
||||
@@ -45,7 +45,7 @@ class MetadataDataProvider implements MetadataDataProviderInterface
|
||||
$data = $this->ormMetadataData->getData();
|
||||
|
||||
foreach (array_keys($data) as $entityType) {
|
||||
$data[$entityType]['vFields'] = $this->metadata->get(['entityDefs', $entityType, 'fields']) ?? [];
|
||||
$data[$entityType]['fields'] = $this->metadata->get(['entityDefs', $entityType, 'fields']) ?? [];
|
||||
}
|
||||
|
||||
return $data;
|
||||
|
||||
@@ -32,41 +32,23 @@ namespace Espo\Core\ORM;
|
||||
use Espo\Core\Binding\BindingContainerBuilder;
|
||||
use Espo\Core\Binding\ContextualBinder;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Repositories\Database as DatabaseRepository;
|
||||
use Espo\Core\Utils\ClassFinder;
|
||||
use Espo\ORM\Entity as OrmEntity;
|
||||
use Espo\ORM\Entity as Entity;
|
||||
use Espo\ORM\EntityFactory as EntityFactoryInterface;
|
||||
use Espo\ORM\Repository\Repository;
|
||||
use Espo\ORM\Repository\RepositoryFactory as RepositoryFactoryInterface;
|
||||
|
||||
class RepositoryFactory implements RepositoryFactoryInterface
|
||||
{
|
||||
/** @var class-string<Repository<OrmEntity>> */
|
||||
protected $defaultClassName = DatabaseRepository::class;
|
||||
|
||||
public function __construct(
|
||||
protected EntityFactoryInterface $entityFactory,
|
||||
protected InjectableFactory $injectableFactory,
|
||||
protected ClassFinder $classFinder
|
||||
private EntityFactoryInterface $entityFactory,
|
||||
private InjectableFactory $injectableFactory,
|
||||
private ClassNameProvider $classNameProvider
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return ?class-string<Repository<OrmEntity>>
|
||||
*/
|
||||
protected function getClassName(string $entityType): ?string
|
||||
{
|
||||
/** @var ?class-string<Repository<OrmEntity>> */
|
||||
return $this->classFinder->find('Repositories', $entityType);
|
||||
}
|
||||
|
||||
public function create(string $entityType): Repository
|
||||
{
|
||||
$className = $this->getClassName($entityType);
|
||||
|
||||
if (!$className || !class_exists($className)) {
|
||||
$className = $this->defaultClassName;
|
||||
}
|
||||
|
||||
return $this->injectableFactory->createWithBinding(
|
||||
$className,
|
||||
BindingContainerBuilder::create()
|
||||
@@ -81,4 +63,13 @@ class RepositoryFactory implements RepositoryFactoryInterface
|
||||
->build()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<Repository<Entity>>
|
||||
*/
|
||||
private function getClassName(string $entityType): string
|
||||
{
|
||||
/** @var class-string<Repository<Entity>> */
|
||||
return $this->classNameProvider->getRepositoryClassName($entityType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,9 +47,14 @@ class Url
|
||||
return explode('/', $url)[count(explode('/', $scriptNameModified)) - 1] ?? null;
|
||||
}
|
||||
|
||||
public static function getPortalIdFromEnv(): ?string
|
||||
{
|
||||
return $_SERVER['ESPO_PORTAL_ID'] ?? null;
|
||||
}
|
||||
|
||||
public static function detectPortalId(): ?string
|
||||
{
|
||||
$portalId = $_SERVER['ESPO_PORTAL_ID'] ?? null;
|
||||
$portalId = self::getPortalIdFromEnv();
|
||||
|
||||
if ($portalId) {
|
||||
return $portalId;
|
||||
|
||||
52
application/Espo/Core/Rebuild/Actions/AddSystemData.php
Normal file
52
application/Espo/Core/Rebuild/Actions/AddSystemData.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?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\Rebuild\Actions;
|
||||
|
||||
use Espo\Core\Rebuild\RebuildAction;
|
||||
use Espo\Entities\SystemData;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
class AddSystemData implements RebuildAction
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager
|
||||
) {}
|
||||
|
||||
public function process(): void
|
||||
{
|
||||
$entity = $this->entityManager->getEntityById(SystemData::ENTITY_TYPE, SystemData::ONLY_ID);
|
||||
|
||||
if ($entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->entityManager->createEntity(SystemData::ENTITY_TYPE, ['id' => SystemData::ONLY_ID]);
|
||||
}
|
||||
}
|
||||
@@ -41,9 +41,11 @@ use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Crm\Entities\Account;
|
||||
use Espo\Modules\Crm\Entities\Contact;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Defs\RelationDefs;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Type\RelationType;
|
||||
|
||||
/**
|
||||
* Check access for record linking.
|
||||
@@ -57,6 +59,7 @@ class LinkCheck
|
||||
* @param string[] $noEditAccessRequiredLinkList
|
||||
*/
|
||||
public function __construct(
|
||||
private Defs $ormDefs,
|
||||
private EntityManager $entityManager,
|
||||
private Acl $acl,
|
||||
private Metadata $metadata,
|
||||
@@ -71,7 +74,7 @@ class LinkCheck
|
||||
*
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function process(Entity $entity): void
|
||||
public function processFields(Entity $entity): void
|
||||
{
|
||||
$this->processLinkMultiple($entity);
|
||||
}
|
||||
@@ -133,7 +136,7 @@ class LinkCheck
|
||||
in_array($name, $this->acl->getScopeForbiddenLinkList($entityType, AclTable::ACTION_EDIT))
|
||||
) {
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"No access to link {$name}.",
|
||||
"No access to link $name.",
|
||||
ErrorBody::create()
|
||||
->withMessageTranslation('cannotRelateForbiddenLink', null, ['link' => $name])
|
||||
->encode()
|
||||
@@ -172,7 +175,7 @@ class LinkCheck
|
||||
|
||||
if (!$foreignEntity) {
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"Can't relate with non-existing record.",
|
||||
"Can't relate with non-existing record. entity type: $entityType, link: $link.",
|
||||
ErrorBody::create()
|
||||
->withMessageTranslation(
|
||||
'cannotRelateNonExisting', null, ['foreignEntityType' => $foreignEntityType])
|
||||
@@ -206,7 +209,7 @@ class LinkCheck
|
||||
|
||||
if (!$this->acl->check($entity, $action)) {
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"No record access for link operation ({$entityType}:{$link}).",
|
||||
"No record access for link operation ($entityType:$link).",
|
||||
ErrorBody::create()
|
||||
->withMessageTranslation('noAccessToRecord', null, ['action' => $action])
|
||||
->encode()
|
||||
@@ -214,6 +217,16 @@ class LinkCheck
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check unlink access to a specific link.
|
||||
*
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function processUnlink(Entity $entity, string $link): void
|
||||
{
|
||||
$this->processLink($entity, $link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check link access for a specific foreign entity.
|
||||
* @throws Forbidden
|
||||
@@ -224,6 +237,16 @@ class LinkCheck
|
||||
$this->linkEntityAccessCheck($entity, $foreignEntity, $link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check unlink access for a specific foreign entity.
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function processUnlinkForeign(Entity $entity, string $link, Entity $foreignEntity): void
|
||||
{
|
||||
$this->processLinkForeign($entity, $link, $foreignEntity);
|
||||
$this->processUnlinkForeignRequired($entity, $link, $foreignEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
@@ -306,7 +329,7 @@ class LinkCheck
|
||||
$body->withMessageTranslation('noAccessToForeignRecord', null, ['action' => $action]);
|
||||
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"No foreign record access for link operation ({$entityType}:{$link}).",
|
||||
"No foreign record access for link operation ($entityType:$link).",
|
||||
$body->encode()
|
||||
);
|
||||
}
|
||||
@@ -329,7 +352,7 @@ class LinkCheck
|
||||
}
|
||||
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"No access for link operation ({$entityType}:{$link}).",
|
||||
"No access for link operation ($entityType:$link).",
|
||||
ErrorBody::create()
|
||||
->withMessageTranslation('noLinkAccess')
|
||||
->encode()
|
||||
@@ -359,4 +382,64 @@ class LinkCheck
|
||||
|
||||
return $checker;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function processUnlinkForeignRequired(Entity $entity, string $link, Entity $foreignEntity): void
|
||||
{
|
||||
$relationDefs = $this->ormDefs
|
||||
->getEntity($entity->getEntityType())
|
||||
->tryGetRelation($link);
|
||||
|
||||
if (!$relationDefs) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!$relationDefs->hasForeignEntityType() ||
|
||||
!$relationDefs->hasForeignRelationName()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$foreignLink = $relationDefs->getForeignRelationName();
|
||||
|
||||
$foreignRelationDefs = $this->ormDefs
|
||||
->getEntity($foreignEntity->getEntityType())
|
||||
->tryGetRelation($foreignLink);
|
||||
|
||||
if (!$foreignRelationDefs) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!in_array($foreignRelationDefs->getType(), [
|
||||
RelationType::BELONGS_TO,
|
||||
RelationType::HAS_ONE,
|
||||
RelationType::BELONGS_TO_PARENT,
|
||||
])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$foreignFieldDefs = $this->ormDefs
|
||||
->getEntity($foreignEntity->getEntityType())
|
||||
->tryGetField($foreignLink);
|
||||
|
||||
if (!$foreignFieldDefs) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$foreignFieldDefs->getParam('required')) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"Can't unlink required field ({$foreignEntity->getEntityType()}:$foreignLink}).",
|
||||
ErrorBody::create()
|
||||
->withMessageTranslation('cannotUnrelateRequiredLink')
|
||||
->encode()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
38
application/Espo/Core/Record/ActionHistory/Action.php
Normal file
38
application/Espo/Core/Record/ActionHistory/Action.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?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\Record\ActionHistory;
|
||||
|
||||
class Action
|
||||
{
|
||||
public const CREATE = 'create';
|
||||
public const READ = 'read';
|
||||
public const UPDATE = 'update';
|
||||
public const DELETE = 'delete';
|
||||
}
|
||||
45
application/Espo/Core/Record/ActionHistory/ActionLogger.php
Normal file
45
application/Espo/Core/Record/ActionHistory/ActionLogger.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?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\Record\ActionHistory;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* Logs actions users do with records.
|
||||
*/
|
||||
interface ActionLogger
|
||||
{
|
||||
/**
|
||||
* Log an action.
|
||||
*
|
||||
* @param Action::* $action
|
||||
*/
|
||||
public function log(string $action, Entity $entity): void;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?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\Record\ActionHistory;
|
||||
|
||||
use Espo\Core\Field\LinkParent;
|
||||
use Espo\Entities\ActionHistoryRecord;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
class DefaultActionLogger implements ActionLogger
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private User $user
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function log(string $action, Entity $entity): void
|
||||
{
|
||||
$historyRecord = $this->entityManager
|
||||
->getRepositoryByClass(ActionHistoryRecord::class)
|
||||
->getNew();
|
||||
|
||||
$historyRecord
|
||||
->setAction($action)
|
||||
->setUserId($this->user->getId())
|
||||
->setAuthTokenId($this->user->get('authTokenId'))
|
||||
->setAuthLogRecordId($this->user->get('authLogRecordId'))
|
||||
->setIpAddress($this->user->get('ipAddress'))
|
||||
->setTarget(LinkParent::createFromEntity($entity));
|
||||
|
||||
$this->entityManager->saveEntity($historyRecord);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,9 @@
|
||||
|
||||
namespace Espo\Core\Record\Hook;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\Record\CreateParams;
|
||||
|
||||
@@ -39,6 +42,9 @@ interface CreateHook
|
||||
{
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function process(Entity $entity, CreateParams $params): void;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
|
||||
namespace Espo\Core\Record\Hook;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\Record\DeleteParams;
|
||||
|
||||
@@ -39,6 +42,9 @@ interface DeleteHook
|
||||
{
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function process(Entity $entity, DeleteParams $params): void;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
|
||||
namespace Espo\Core\Record\Hook;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Espo\Core\Record\UpdateParams;
|
||||
@@ -40,6 +43,9 @@ interface UpdateHook
|
||||
{
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function process(Entity $entity, UpdateParams $params): void;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
|
||||
namespace Espo\Core\Record;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Record\Hook\CreateHook;
|
||||
use Espo\Core\Record\Hook\DeleteHook;
|
||||
use Espo\Core\Record\Hook\LinkHook;
|
||||
@@ -44,6 +47,11 @@ class HookManager
|
||||
public function __construct(private Provider $provider)
|
||||
{}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function processBeforeCreate(Entity $entity, CreateParams $params): void
|
||||
{
|
||||
foreach ($this->getBeforeCreateHookList($entity->getEntityType()) as $hook) {
|
||||
@@ -58,6 +66,11 @@ class HookManager
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function processBeforeUpdate(Entity $entity, UpdateParams $params): void
|
||||
{
|
||||
foreach ($this->getBeforeUpdateHookList($entity->getEntityType()) as $hook) {
|
||||
@@ -65,6 +78,11 @@ class HookManager
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function processBeforeDelete(Entity $entity, DeleteParams $params): void
|
||||
{
|
||||
foreach ($this->getBeforeDeleteHookList($entity->getEntityType()) as $hook) {
|
||||
|
||||
@@ -40,9 +40,10 @@ use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Exceptions\NotFoundSilent;
|
||||
use Espo\Core\Field\LinkParent;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\Record\Access\LinkCheck;
|
||||
use Espo\Core\Record\ActionHistory\Action;
|
||||
use Espo\Core\Record\ActionHistory\ActionLogger;
|
||||
use Espo\Core\Record\Formula\Processor as FormulaProcessor;
|
||||
use Espo\Core\Utils\Json;
|
||||
use Espo\Core\Acl;
|
||||
@@ -66,10 +67,13 @@ use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Query\Part\WhereClause;
|
||||
use Espo\Tools\Stream\Service as StreamService;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Entities\ActionHistoryRecord;
|
||||
|
||||
use stdClass;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use RuntimeException;
|
||||
|
||||
use const E_USER_DEPRECATED;
|
||||
|
||||
/**
|
||||
* The layer between a controller and ORM repository. For CRUD and other operations with records.
|
||||
@@ -139,8 +143,6 @@ class Service implements Crud,
|
||||
protected $nonAdminReadOnlyLinkList = [];
|
||||
/** @var string[] */
|
||||
protected $onlyAdminLinkList = [];
|
||||
/** @var array<string, array<string, mixed>> */
|
||||
protected $linkParams = [];
|
||||
/** @var array<string, string[]> */
|
||||
protected $linkMandatorySelectAttributeList = [];
|
||||
/** @var string[] */
|
||||
@@ -166,14 +168,13 @@ class Service implements Crud,
|
||||
/** @var bool */
|
||||
protected $forceSelectAllAttributes = false;
|
||||
/** @var string[] */
|
||||
protected $validateSkipFieldList = [];
|
||||
/**
|
||||
* @todo Move to metadata.
|
||||
* @var string[]
|
||||
*/
|
||||
protected $validateRequiredSkipFieldList = [];
|
||||
/** @var string[] */
|
||||
protected $duplicateIgnoreAttributeList = [];
|
||||
/**
|
||||
* @var string[]
|
||||
* @deprecated As of v8.0. Use `suppressValidationList` metadata parameter.
|
||||
* @todo Remove in v9.0.
|
||||
*/
|
||||
protected $validateSkipFieldList = [];
|
||||
|
||||
/** @var Acl */
|
||||
protected $acl = null;
|
||||
@@ -188,6 +189,7 @@ class Service implements Crud,
|
||||
private ?ListLoadProcessor $listLoadProcessor = null;
|
||||
private ?DuplicateFinder $duplicateFinder = null;
|
||||
private ?LinkCheck $linkCheck = null;
|
||||
private ?ActionLogger $actionLogger = null;
|
||||
|
||||
protected const MAX_SELECT_TEXT_ATTRIBUTE_LENGTH = 10000;
|
||||
|
||||
@@ -207,7 +209,7 @@ class Service implements Crud,
|
||||
/**
|
||||
* Add an action-history record.
|
||||
*
|
||||
* @param ActionHistoryRecord::ACTION_* $action
|
||||
* @param Action::* $action
|
||||
*/
|
||||
public function processActionHistoryRecord(string $action, Entity $entity): void
|
||||
{
|
||||
@@ -219,18 +221,16 @@ class Service implements Crud,
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var ActionHistoryRecord $historyRecord */
|
||||
$historyRecord = $this->entityManager->getNewEntity(ActionHistoryRecord::ENTITY_TYPE);
|
||||
$this->getActionLogger()->log($action, $entity);
|
||||
}
|
||||
|
||||
$historyRecord
|
||||
->setAction($action)
|
||||
->setUserId($this->user->getId())
|
||||
->setAuthTokenId($this->user->get('authTokenId'))
|
||||
->setAuthLogRecordId($this->user->get('authLogRecordId'))
|
||||
->setIpAddress($this->user->get('ipAddress'))
|
||||
->setTarget(LinkParent::createFromEntity($entity));
|
||||
private function getActionLogger(): ActionLogger
|
||||
{
|
||||
if (!$this->actionLogger) {
|
||||
$this->actionLogger = $this->injectableFactory->createResolved(ActionLogger::class);
|
||||
}
|
||||
|
||||
$this->entityManager->saveEntity($historyRecord);
|
||||
return $this->actionLogger;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -239,7 +239,7 @@ class Service implements Crud,
|
||||
* @param non-empty-string $id
|
||||
* @return TEntity
|
||||
* @throws NotFoundSilent If not found.
|
||||
* @throws ForbiddenSilent If no read access.
|
||||
* @throws Forbidden If no read access.
|
||||
*/
|
||||
public function read(string $id, ReadParams $params): Entity
|
||||
{
|
||||
@@ -258,7 +258,7 @@ class Service implements Crud,
|
||||
}
|
||||
|
||||
$this->recordHookManager->processBeforeRead($entity, $params);
|
||||
$this->processActionHistoryRecord(ActionHistoryRecord::ACTION_READ, $entity);
|
||||
$this->processActionHistoryRecord(Action::READ, $entity);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
@@ -266,12 +266,31 @@ class Service implements Crud,
|
||||
/**
|
||||
* Get an entity by ID. Access control check is performed.
|
||||
*
|
||||
* @throws ForbiddenSilent If no read access.
|
||||
* @throws Forbidden If no read access.
|
||||
* @return ?TEntity
|
||||
*/
|
||||
public function getEntity(string $id): ?Entity
|
||||
{
|
||||
$entity = $this->getRepository()->getById($id);
|
||||
try {
|
||||
$query = $this->selectBuilderFactory
|
||||
->create()
|
||||
->from($this->entityType)
|
||||
->withSearchParams(
|
||||
SearchParams::create()->withSelect(['*'])
|
||||
)
|
||||
->withAdditionalApplierClassNameList(
|
||||
$this->createSelectApplierClassNameListProvider()->get($this->entityType)
|
||||
)
|
||||
->build();
|
||||
}
|
||||
catch (BadRequest|Error $e) {
|
||||
throw new RuntimeException($e->getMessage());
|
||||
}
|
||||
|
||||
$entity = $this->getRepository()
|
||||
->clone($query)
|
||||
->where(['id' => $id])
|
||||
->findOne();
|
||||
|
||||
if (!$entity && $this->user->isAdmin()) {
|
||||
$entity = $this->getEntityEvenDeleted($id);
|
||||
@@ -354,8 +373,14 @@ class Service implements Crud,
|
||||
{
|
||||
$params = FieldValidationParams
|
||||
::create()
|
||||
->withSkipFieldList($this->validateSkipFieldList)
|
||||
->withTypeSkipFieldList('required', $this->validateRequiredSkipFieldList);
|
||||
->withSkipFieldList($this->validateSkipFieldList);
|
||||
|
||||
if (!empty($this->validateSkipFieldList)) {
|
||||
trigger_error(
|
||||
'$validateSkipFieldList is deprecated and will be removed in v9.0.',
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
}
|
||||
|
||||
$this->fieldValidationManager->process($entity, $data, $params);
|
||||
}
|
||||
@@ -497,6 +522,7 @@ class Service implements Crud,
|
||||
/**
|
||||
* @deprecated As of v7.0. Use filterCreateInput or filterUpdateInput. Or better don't extend the class.
|
||||
* Use entityAcl, app > acl, roles to restrict write access for specific fields.
|
||||
* @todo Remove in v9.0.
|
||||
* @param stdClass $data
|
||||
* @return void
|
||||
*/
|
||||
@@ -507,6 +533,7 @@ class Service implements Crud,
|
||||
/**
|
||||
* @deprecated As of v7.0. Use filterCreateInput or filterUpdateInput. Or better don't extend the class.
|
||||
* Use entityAcl, app > acl, roles to restrict write access for specific fields.
|
||||
* @todo Remove in v9.0.
|
||||
* @param stdClass $data
|
||||
* @return void
|
||||
*/
|
||||
@@ -577,6 +604,7 @@ class Service implements Crud,
|
||||
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
* @todo Move the logic to a class. Make customizable (recordDefs)?
|
||||
*/
|
||||
public function populateDefaults(Entity $entity, stdClass $data): void
|
||||
{
|
||||
@@ -664,7 +692,7 @@ class Service implements Crud,
|
||||
|
||||
$this->processValidation($entity, $data);
|
||||
$this->processAssignmentCheck($entity);
|
||||
$this->getLinkCheck()->process($entity);
|
||||
$this->getLinkCheck()->processFields($entity);
|
||||
|
||||
if (!$params->skipDuplicateCheck()) {
|
||||
$this->processDuplicateCheck($entity);
|
||||
@@ -681,7 +709,7 @@ class Service implements Crud,
|
||||
$this->afterCreateProcessDuplicating($entity, $params);
|
||||
$this->loadAdditionalFields($entity);
|
||||
$this->prepareEntityForOutput($entity);
|
||||
$this->processActionHistoryRecord(ActionHistoryRecord::ACTION_CREATE, $entity);
|
||||
$this->processActionHistoryRecord(Action::CREATE, $entity);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
@@ -731,7 +759,7 @@ class Service implements Crud,
|
||||
|
||||
$this->processValidation($entity, $data);
|
||||
$this->processAssignmentCheck($entity);
|
||||
$this->getLinkCheck()->process($entity);
|
||||
$this->getLinkCheck()->processFields($entity);
|
||||
|
||||
$checkForDuplicates =
|
||||
$this->metadata->get(['recordDefs', $this->entityType, 'updateDuplicateCheck']) ??
|
||||
@@ -748,8 +776,13 @@ class Service implements Crud,
|
||||
$this->entityManager->saveEntity($entity);
|
||||
|
||||
$this->afterUpdateEntity($entity, $data);
|
||||
|
||||
if ($this->metadata->get(['recordDefs', $this->entityType, 'loadAdditionalFieldsAfterUpdate'])) {
|
||||
$this->loadAdditionalFields($entity);
|
||||
}
|
||||
|
||||
$this->prepareEntityForOutput($entity);
|
||||
$this->processActionHistoryRecord(ActionHistoryRecord::ACTION_UPDATE, $entity);
|
||||
$this->processActionHistoryRecord(Action::UPDATE, $entity);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
@@ -785,7 +818,7 @@ class Service implements Crud,
|
||||
$this->beforeDeleteEntity($entity);
|
||||
$this->getRepository()->remove($entity);
|
||||
$this->afterDeleteEntity($entity);
|
||||
$this->processActionHistoryRecord(ActionHistoryRecord::ACTION_DELETE, $entity);
|
||||
$this->processActionHistoryRecord(Action::DELETE, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -851,7 +884,7 @@ class Service implements Crud,
|
||||
return RecordCollection::create($collection, $total);
|
||||
}
|
||||
|
||||
protected function createSelectApplierClassNameListProvider(): ApplierClassNameListProvider
|
||||
private function createSelectApplierClassNameListProvider(): ApplierClassNameListProvider
|
||||
{
|
||||
return $this->injectableFactory->create(ApplierClassNameListProvider::class);
|
||||
}
|
||||
@@ -859,7 +892,7 @@ class Service implements Crud,
|
||||
/**
|
||||
* @return TEntity|null
|
||||
*/
|
||||
protected function getEntityEvenDeleted(string $id): ?Entity
|
||||
private function getEntityEvenDeleted(string $id): ?Entity
|
||||
{
|
||||
$query = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
@@ -957,14 +990,11 @@ class Service implements Crud,
|
||||
->getRelation($link)
|
||||
->getForeignEntityType();
|
||||
|
||||
$linkParams = $this->linkParams[$link] ?? [];
|
||||
$skipAcl = $this->metadata
|
||||
->get("recordDefs.$this->entityType.relationships.$link.selectAccessControlDisabled") ?? false;
|
||||
|
||||
$skipAcl = $linkParams['skipAcl'] ?? false;
|
||||
|
||||
if (!$skipAcl) {
|
||||
if (!$this->acl->check($foreignEntityType, AclTable::ACTION_READ)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
if (!$skipAcl && !$this->acl->check($foreignEntityType, AclTable::ACTION_READ)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$recordService = $this->recordServiceContainer->get($foreignEntityType);
|
||||
@@ -990,7 +1020,10 @@ class Service implements Crud,
|
||||
|
||||
$selectBuilder
|
||||
->from($foreignEntityType)
|
||||
->withSearchParams($preparedSearchParams);
|
||||
->withSearchParams($preparedSearchParams)
|
||||
->withAdditionalApplierClassNameList(
|
||||
$this->createSelectApplierClassNameListProvider()->get($foreignEntityType)
|
||||
);
|
||||
|
||||
if (!$skipAcl) {
|
||||
$selectBuilder->withStrictAccessControl();
|
||||
@@ -1133,7 +1166,7 @@ class Service implements Crud,
|
||||
throw new LogicException("Only core entities are supported.");
|
||||
}
|
||||
|
||||
$this->getLinkCheck()->processLink($entity, $link);
|
||||
$this->getLinkCheck()->processUnlink($entity, $link);
|
||||
|
||||
if ($this->processUnlinkMethod($id, $link, $foreignId)) {
|
||||
return;
|
||||
@@ -1151,7 +1184,7 @@ class Service implements Crud,
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
$this->getLinkCheck()->processLinkForeign($entity, $link, $foreignEntity);
|
||||
$this->getLinkCheck()->processUnlinkForeign($entity, $link, $foreignEntity);
|
||||
|
||||
$this->recordHookManager->processBeforeUnlink($entity, $link, $foreignEntity);
|
||||
|
||||
@@ -1739,21 +1772,15 @@ class Service implements Crud,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @todo Remove in v7.6.
|
||||
* @param string $type
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getFieldByTypeList($type)
|
||||
{
|
||||
return $this->fieldUtil->getFieldByTypeList($this->entityType, $type);
|
||||
}
|
||||
|
||||
public function prepareSearchParams(SearchParams $searchParams): SearchParams
|
||||
{
|
||||
return $this
|
||||
->prepareSearchParamsSelect($searchParams)
|
||||
$searchParams = $this->prepareSearchParamsSelect($searchParams);
|
||||
|
||||
if ($searchParams->getSelect() === null) {
|
||||
$searchParams = $searchParams->withSelect(['*']);
|
||||
}
|
||||
|
||||
return $searchParams
|
||||
->withMaxTextAttributeLength(
|
||||
$this->getMaxSelectTextAttributeLength()
|
||||
);
|
||||
|
||||
@@ -31,6 +31,6 @@ namespace Espo\Core\Select\AccessControl;
|
||||
|
||||
/**
|
||||
* @deprecated Use `\Espo\Core\Select\AccessControl\FilterResolvers\Bypass` instead.
|
||||
* @todo Remove in v8.0.
|
||||
* @todo Remove in v9.0.
|
||||
*/
|
||||
class BypassFilterResolver extends \Espo\Core\Select\AccessControl\FilterResolvers\Bypass {}
|
||||
|
||||
@@ -115,6 +115,10 @@ class Applier
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($passedAttributeList === ['*']) {
|
||||
return ['*'];
|
||||
}
|
||||
|
||||
$attributeList = [];
|
||||
|
||||
if (!in_array('id', $passedAttributeList)) {
|
||||
|
||||
@@ -373,9 +373,7 @@ class SelectManager
|
||||
return;
|
||||
}
|
||||
|
||||
$relDefs = $this->getSeed()->getRelations();
|
||||
|
||||
$relationType = $seed->getRelationType($link);
|
||||
$relDefs = $this->entityManager->getMetadata()->get($this->entityType, ['relations']);
|
||||
|
||||
$defs = $relDefs[$link];
|
||||
|
||||
@@ -455,7 +453,7 @@ class SelectManager
|
||||
|
||||
public function applyInCategory(string $link, $value, array &$result)
|
||||
{
|
||||
$relDefs = $this->getSeed()->getRelations();
|
||||
$relDefs = $this->entityManager->getMetadata()->get($this->entityType, ['relations']);
|
||||
|
||||
if (empty($relDefs[$link])) {
|
||||
throw new Error("Can't apply inCategory for link {$link}.");
|
||||
|
||||
@@ -48,6 +48,7 @@ class Type
|
||||
public const STARTS_WITH = 'startsWith';
|
||||
public const ENDS_WITH = 'endsWith';
|
||||
public const CONTAINS = 'contains';
|
||||
public const NOT_CONTAINS = 'notContains';
|
||||
public const GREATER_THAN = 'greaterThan';
|
||||
public const LESS_THAN = 'lessThan';
|
||||
public const GREATER_THAN_OR_EQUALS = 'greaterThanOrEquals';
|
||||
@@ -55,6 +56,7 @@ class Type
|
||||
public const AFTER = 'after';
|
||||
public const BEFORE = 'before';
|
||||
public const BETWEEN = 'between';
|
||||
public const EVER = 'ever';
|
||||
public const ANY = 'any';
|
||||
public const NONE = 'none';
|
||||
public const IS_NULL = 'isNull';
|
||||
@@ -64,6 +66,7 @@ class Type
|
||||
public const TODAY = 'today';
|
||||
public const PAST = 'past';
|
||||
public const FUTURE = 'future';
|
||||
public const LAST_SEVEN_DAYS = 'lastSevenDays';
|
||||
public const LAST_X_DAYS = 'lastXDays';
|
||||
public const NEXT_X_DAYS = 'nextXDays';
|
||||
public const OLDER_THAN_X_DAYS = 'olderThanXDays';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,9 @@
|
||||
|
||||
namespace Espo\Core\Templates\Controllers;
|
||||
|
||||
/**
|
||||
* Do not remove. Used by exported custom modules.
|
||||
*/
|
||||
class Base extends \Espo\Core\Controllers\Record
|
||||
{
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
|
||||
namespace Espo\Core\Templates\Entities;
|
||||
|
||||
class Base extends \Espo\Core\ORM\Entity
|
||||
use Espo\Core\ORM\Entity;
|
||||
|
||||
class Base extends Entity
|
||||
{
|
||||
public const TEMPLATE_TYPE = 'Base';
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
|
||||
namespace Espo\Core\Templates\Entities;
|
||||
|
||||
class BasePlus extends \Espo\Core\ORM\Entity
|
||||
use Espo\Core\ORM\Entity;
|
||||
|
||||
class BasePlus extends Entity
|
||||
{
|
||||
public const TEMPLATE_TYPE = 'BasePlus';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\General"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\General"
|
||||
}
|
||||
@@ -40,7 +40,7 @@
|
||||
"childList": {
|
||||
"type": "jsonArray",
|
||||
"notStorable": true,
|
||||
"disabled": true
|
||||
"utility": true
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
@@ -85,7 +85,7 @@
|
||||
},
|
||||
"additionalTables": {
|
||||
"{entityType}Path": {
|
||||
"fields": {
|
||||
"attributes": {
|
||||
"id": {
|
||||
"type": "id",
|
||||
"dbType": "integer",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\Company"
|
||||
}
|
||||
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\General"
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
"aclPortalLevelList": ["all", "account", "contact", "own", "no"],
|
||||
"customizable": true,
|
||||
"importable": true,
|
||||
"notifications": true
|
||||
}
|
||||
"notifications": true,
|
||||
"duplicateCheckFieldList": ["name", "emailAddress"]
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
"type": "datetimeOptional",
|
||||
"view": "crm:views/meeting/fields/date-end",
|
||||
"required": true,
|
||||
"after": "dateStart"
|
||||
"after": "dateStart",
|
||||
"suppressValidationList": ["required"]
|
||||
},
|
||||
"isAllDay": {
|
||||
"type": "bool",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\Person"
|
||||
}
|
||||
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\General"
|
||||
}
|
||||
|
||||
@@ -8,5 +8,6 @@
|
||||
"customizable": true,
|
||||
"importable": true,
|
||||
"notifications": true,
|
||||
"hasPersonalData": true
|
||||
}
|
||||
"hasPersonalData": true,
|
||||
"duplicateCheckFieldList": ["name", "emailAddress"]
|
||||
}
|
||||
|
||||
@@ -33,10 +33,7 @@ use Espo\Services\Record;
|
||||
|
||||
/**
|
||||
* @extends Record<\Espo\Core\Templates\Entities\Event>
|
||||
* @deprecated Left for backward compatibility.
|
||||
*/
|
||||
class Event extends Record
|
||||
{
|
||||
protected $validateRequiredSkipFieldList = [
|
||||
'dateEnd'
|
||||
];
|
||||
}
|
||||
{}
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
|
||||
namespace Espo\Core\Utils\Acl;
|
||||
|
||||
use Espo\Core\Acl\Exceptions\NotAvailable;
|
||||
use Espo\Entities\Portal;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Core\AclManager;
|
||||
use Espo\Core\Portal\AclManagerContainer as PortalAclManagerContainer;
|
||||
use Espo\Core\ApplicationState;
|
||||
use RuntimeException;
|
||||
|
||||
class UserAclManagerProvider
|
||||
{
|
||||
@@ -49,6 +49,9 @@ class UserAclManagerProvider
|
||||
private ApplicationState $applicationState
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws NotAvailable
|
||||
*/
|
||||
public function get(User $user): AclManager
|
||||
{
|
||||
$key = $user->hasId() ? $user->getId() : spl_object_hash($user);
|
||||
@@ -60,6 +63,9 @@ class UserAclManagerProvider
|
||||
return $this->map[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotAvailable
|
||||
*/
|
||||
private function load(User $user): AclManager
|
||||
{
|
||||
$aclManager = $this->aclManager;
|
||||
@@ -72,7 +78,7 @@ class UserAclManagerProvider
|
||||
->findOne();
|
||||
|
||||
if (!$portal) {
|
||||
throw new RuntimeException("No portal for portal user '" . $user->getId() . "'.");
|
||||
throw new NotAvailable("No portal for portal user '" . $user->getId() . "'.");
|
||||
}
|
||||
|
||||
$aclManager = $this->portalAclManagerContainer->get($portal);
|
||||
|
||||
67
application/Espo/Core/Utils/Client/LoaderParamsProvider.php
Normal file
67
application/Espo/Core/Utils/Client/LoaderParamsProvider.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?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\Utils\Client;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
class LoaderParamsProvider
|
||||
{
|
||||
public function __construct(
|
||||
private Metadata $metadata
|
||||
) {}
|
||||
|
||||
public function getLibsConfig(): object
|
||||
{
|
||||
return (object) $this->metadata->get(['app', 'jsLibs'], []);
|
||||
}
|
||||
|
||||
public function getAliasMap(): object
|
||||
{
|
||||
$map = (object) [];
|
||||
|
||||
/** @var array<string, array<string, mixed>> $libs */
|
||||
$libs = $this->metadata->get(['app', 'jsLibs'], []);
|
||||
|
||||
foreach ($libs as $name => $item) {
|
||||
/** @var ?string[] $aliases */
|
||||
$aliases = $item['aliases'] ?? null;
|
||||
|
||||
$map->$name = 'lib!' . $name;
|
||||
|
||||
if ($aliases) {
|
||||
foreach ($aliases as $alias) {
|
||||
$map->$alias = 'lib!' . $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ namespace Espo\Core\Utils;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseWrapper;
|
||||
use Espo\Core\Utils\Client\DevModeJsFileListProvider;
|
||||
use Espo\Core\Utils\Client\LoaderParamsProvider;
|
||||
use Espo\Core\Utils\File\Manager as FileManager;
|
||||
|
||||
use Slim\Psr7\Response as Psr7Response;
|
||||
@@ -42,14 +43,16 @@ use Slim\ResponseEmitter;
|
||||
*/
|
||||
class ClientManager
|
||||
{
|
||||
protected string $mainHtmlFilePath = 'html/main.html';
|
||||
protected string $runScript = "app.start();";
|
||||
private string $mainHtmlFilePath = 'html/main.html';
|
||||
private string $runScript = 'app.start();';
|
||||
private string $favicon = 'client/img/favicon.ico';
|
||||
private string $favicon196 = 'client/img/favicon196x196.png';
|
||||
private string $basePath = '';
|
||||
private string $libsConfigPath = 'client/cfg/libs.json';
|
||||
private string $apiUrl = 'api/v1';
|
||||
|
||||
private string $nonce;
|
||||
|
||||
private const APP_DESCRIPTION = "EspoCRM - Open Source CRM application.";
|
||||
private const APP_DESCRIPTION = "EspoCRM – Open Source CRM application.";
|
||||
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
@@ -57,9 +60,9 @@ class ClientManager
|
||||
private Metadata $metadata,
|
||||
private FileManager $fileManager,
|
||||
private DevModeJsFileListProvider $devModeJsFileListProvider,
|
||||
private Module $module
|
||||
private Module $module,
|
||||
private LoaderParamsProvider $loaderParamsProvider
|
||||
) {
|
||||
|
||||
$this->nonce = Util::generateKey();
|
||||
}
|
||||
|
||||
@@ -73,15 +76,6 @@ class ClientManager
|
||||
return $this->basePath;
|
||||
}
|
||||
|
||||
protected function getCacheTimestamp(): int
|
||||
{
|
||||
if (!$this->config->get('useCache')) {
|
||||
return time();
|
||||
}
|
||||
|
||||
return $this->config->get('cacheTimestamp', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Move to a separate class.
|
||||
*/
|
||||
@@ -113,7 +107,7 @@ class ClientManager
|
||||
return;
|
||||
}
|
||||
|
||||
$scriptSrc = "script-src 'self' 'nonce-{$this->nonce}' 'unsafe-eval'";
|
||||
$scriptSrc = "script-src 'self' 'nonce-$this->nonce' 'unsafe-eval'";
|
||||
|
||||
$scriptSourceList = $this->config->get('clientCspScriptSourceList') ?? [];
|
||||
|
||||
@@ -157,86 +151,51 @@ class ClientManager
|
||||
*/
|
||||
public function render(?string $runScript = null, ?string $htmlFilePath = null, array $vars = []): string
|
||||
{
|
||||
if (is_null($runScript)) {
|
||||
$runScript = $this->runScript;
|
||||
}
|
||||
|
||||
if (is_null($htmlFilePath)) {
|
||||
$htmlFilePath = $this->mainHtmlFilePath;
|
||||
}
|
||||
$runScript ??= $this->runScript;
|
||||
$htmlFilePath ??= $this->mainHtmlFilePath;
|
||||
|
||||
$cacheTimestamp = $this->getCacheTimestamp();
|
||||
$jsFileList = $this->getJsFileList();
|
||||
$appTimestamp = $this->getAppTimestamp();
|
||||
|
||||
if ($this->config->get('isDeveloperMode')) {
|
||||
$useCache = $this->config->get('useCacheInDeveloperMode');
|
||||
$loaderCacheTimestamp = 'null';
|
||||
if ($this->isDeveloperMode()) {
|
||||
$useCache = $this->useCacheInDeveloperMode();
|
||||
$loaderCacheTimestamp = null;
|
||||
}
|
||||
else {
|
||||
$useCache = $this->config->get('useCache');
|
||||
$loaderCacheTimestamp = $cacheTimestamp;
|
||||
$useCache = $this->useCache();
|
||||
$loaderCacheTimestamp = $appTimestamp;
|
||||
}
|
||||
|
||||
$cssFileList = $this->metadata->get(['app', 'client', 'cssList'], []);
|
||||
$linkList = $this->metadata->get(['app', 'client', 'linkList'], []);
|
||||
$favicon196Path = $this->metadata->get(['app', 'client', 'favicon196']) ?? $this->favicon196;
|
||||
$faviconPath = $this->metadata->get(['app', 'client', 'favicon']) ?? $this->favicon;
|
||||
|
||||
$scriptsHtml = '';
|
||||
$scriptsHtml = implode('',
|
||||
array_map(fn ($file) => $this->getScriptItemHtml($file, $appTimestamp), $jsFileList)
|
||||
);
|
||||
|
||||
foreach ($jsFileList as $jsFile) {
|
||||
$src = $this->basePath . $jsFile . '?r=' . $cacheTimestamp;
|
||||
$additionalStyleSheetsHtml = implode('',
|
||||
array_map(fn ($file) => $this->getCssItemHtml($file, $appTimestamp), $cssFileList)
|
||||
);
|
||||
|
||||
$scriptsHtml .= "\n " .
|
||||
"<script type=\"text/javascript\" src=\"{$src}\" data-base-path=\"{$this->basePath}\"></script>";
|
||||
}
|
||||
|
||||
$additionalStyleSheetsHtml = '';
|
||||
|
||||
foreach ($cssFileList as $cssFile) {
|
||||
$src = $this->basePath . $cssFile . '?r=' . $cacheTimestamp;
|
||||
|
||||
$additionalStyleSheetsHtml .= "\n <link rel=\"stylesheet\" href=\"{$src}\">";
|
||||
}
|
||||
|
||||
$linksHtml = '';
|
||||
|
||||
foreach ($linkList as $item) {
|
||||
$href = $this->basePath . $item['href'];
|
||||
|
||||
if (empty($item['noTimestamp'])) {
|
||||
$href .= '?r=' . $cacheTimestamp;
|
||||
}
|
||||
|
||||
$as = $item['as'] ?? '';
|
||||
$rel = $item['rel'] ?? '';
|
||||
$type = $item['type'] ?? '';
|
||||
$additionalPlaceholder = '';
|
||||
|
||||
if (!empty($item['crossorigin'])) {
|
||||
$additionalPlaceholder .= ' crossorigin';
|
||||
}
|
||||
|
||||
$linksHtml .= "\n " .
|
||||
"<link rel=\"{$rel}\" href=\"{$href}\" as=\"{$as}\" as=\"{$type}\"{$additionalPlaceholder}>";
|
||||
}
|
||||
|
||||
$favicon196Path = $this->metadata->get(['app', 'client', 'favicon196']) ??
|
||||
'client/img/favicon196x196.png';
|
||||
|
||||
$faviconPath = $this->metadata->get(['app', 'client', 'favicon']) ?? 'client/img/favicon.ico';
|
||||
$linksHtml = implode('',
|
||||
array_map(fn ($item) => $this->getLinkItemHtml($item, $appTimestamp), $linkList)
|
||||
);
|
||||
|
||||
$internalModuleList = array_map(
|
||||
function (string $moduleName): string {
|
||||
return Util::fromCamelCase($moduleName, '-');
|
||||
},
|
||||
fn ($moduleName) => Util::fromCamelCase($moduleName, '-'),
|
||||
$this->module->getInternalList()
|
||||
);
|
||||
|
||||
$data = [
|
||||
'applicationId' => 'espocrm-application-id',
|
||||
'apiUrl' => 'api/v1',
|
||||
'apiUrl' => $this->apiUrl,
|
||||
'applicationName' => $this->config->get('applicationName', 'EspoCRM'),
|
||||
'cacheTimestamp' => $cacheTimestamp,
|
||||
'loaderCacheTimestamp' => $loaderCacheTimestamp,
|
||||
'appTimestamp' => $appTimestamp,
|
||||
'loaderCacheTimestamp' => Json::encode($loaderCacheTimestamp),
|
||||
'stylesheet' => $this->themeManager->getStylesheet(),
|
||||
'runScript' => $runScript,
|
||||
'basePath' => $this->basePath,
|
||||
@@ -248,16 +207,24 @@ class ClientManager
|
||||
'favicon196Path' => $favicon196Path,
|
||||
'faviconPath' => $faviconPath,
|
||||
'ajaxTimeout' => $this->config->get('ajaxTimeout') ?? 60000,
|
||||
'libsConfigPath' => $this->libsConfigPath,
|
||||
'internalModuleList' => Json::encode($internalModuleList),
|
||||
'bundledModuleList' => Json::encode($this->getBundledModuleList()),
|
||||
'applicationDescription' => $this->config->get('applicationDescription') ?? self::APP_DESCRIPTION,
|
||||
'nonce' => $this->nonce,
|
||||
'loaderParams' => Json::encode([
|
||||
'basePath' => $this->basePath,
|
||||
'cacheTimestamp' => $loaderCacheTimestamp,
|
||||
'internalModuleList' => $internalModuleList,
|
||||
'transpiledModuleList' => $this->getTranspiledModuleList(),
|
||||
'libsConfig' => $this->loaderParamsProvider->getLibsConfig(),
|
||||
'aliasMap' => $this->loaderParamsProvider->getAliasMap(),
|
||||
]),
|
||||
];
|
||||
|
||||
$html = $this->fileManager->getContents($htmlFilePath);
|
||||
|
||||
foreach ($vars as $key => $value) {
|
||||
$html = str_replace('{{'.$key.'}}', $value, $html);
|
||||
$html = str_replace('{{' . $key . '}}', $value, $html);
|
||||
}
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
@@ -265,7 +232,7 @@ class ClientManager
|
||||
continue;
|
||||
}
|
||||
|
||||
$html = str_replace('{{'.$key.'}}', $value, $html);
|
||||
$html = str_replace('{{' . $key . '}}', $value, $html);
|
||||
}
|
||||
|
||||
return $html;
|
||||
@@ -276,10 +243,10 @@ class ClientManager
|
||||
*/
|
||||
private function getJsFileList(): array
|
||||
{
|
||||
if ($this->config->get('isDeveloperMode')) {
|
||||
if ($this->isDeveloperMode()) {
|
||||
return array_merge(
|
||||
$this->getDeveloperModeBundleLibFileList(),
|
||||
$this->metadata->get(['app', 'client', 'developerModeScriptList']) ?? [],
|
||||
$this->getDeveloperModeBundleLibFileList(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -293,4 +260,128 @@ class ClientManager
|
||||
{
|
||||
return $this->devModeJsFileListProvider->get();
|
||||
}
|
||||
|
||||
private function isDeveloperMode(): bool
|
||||
{
|
||||
return (bool) $this->config->get('isDeveloperMode');
|
||||
}
|
||||
|
||||
private function useCache(): bool
|
||||
{
|
||||
return (bool) $this->config->get('useCache');
|
||||
}
|
||||
|
||||
private function useCacheInDeveloperMode(): bool
|
||||
{
|
||||
return (bool) $this->config->get('useCacheInDeveloperMode');
|
||||
}
|
||||
|
||||
private function getCacheTimestamp(): int
|
||||
{
|
||||
if (!$this->useCache()) {
|
||||
return time();
|
||||
}
|
||||
|
||||
return $this->config->get('cacheTimestamp', 0);
|
||||
}
|
||||
|
||||
private function getAppTimestamp(): int
|
||||
{
|
||||
if (!$this->useCache()) {
|
||||
return time();
|
||||
}
|
||||
|
||||
return $this->config->get('appTimestamp', 0);
|
||||
}
|
||||
|
||||
private function getScriptItemHtml(string $file, int $appTimestamp): string
|
||||
{
|
||||
$src = $this->basePath . $file . '?r=' . $appTimestamp;
|
||||
|
||||
return $this->getTabHtml() .
|
||||
"<script type=\"text/javascript\" src=\"$src\" data-base-path=\"$this->basePath\"></script>";
|
||||
}
|
||||
|
||||
private function getCssItemHtml(string $file, int $appTimestamp): string
|
||||
{
|
||||
$src = $this->basePath . $file . '?r=' . $appTimestamp;
|
||||
|
||||
return $this->getTabHtml() . "<link rel=\"stylesheet\" href=\"$src\">";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* href: string,
|
||||
* noTimestamp?: bool,
|
||||
* as?: string,
|
||||
* rel?: string,
|
||||
* type?: string,
|
||||
* crossorigin?: bool,
|
||||
* } $item
|
||||
*/
|
||||
private function getLinkItemHtml(array $item, int $appTimestamp): string
|
||||
{
|
||||
$href = $this->basePath . $item['href'];
|
||||
|
||||
if (empty($item['noTimestamp'])) {
|
||||
$href .= '?r=' . $appTimestamp;
|
||||
}
|
||||
|
||||
$as = $item['as'] ?? '';
|
||||
$rel = $item['rel'] ?? '';
|
||||
$type = $item['type'] ?? '';
|
||||
$part = '';
|
||||
|
||||
if ($item['crossorigin'] ?? false) {
|
||||
$part .= ' crossorigin';
|
||||
}
|
||||
|
||||
return $this->getTabHtml() .
|
||||
"<link rel=\"$rel\" href=\"$href\" as=\"$as\" as=\"$type\"$part>";
|
||||
}
|
||||
|
||||
private function getTabHtml(): string
|
||||
{
|
||||
return "\n ";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getTranspiledModuleList(): array
|
||||
{
|
||||
$modules = array_values(array_filter(
|
||||
$this->module->getList(),
|
||||
fn ($item) => $this->module->get([$item, 'jsTranspiled'])
|
||||
));
|
||||
|
||||
return array_map(
|
||||
fn ($item) => Util::fromCamelCase($item, '-'),
|
||||
$modules
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getBundledModuleList(): array
|
||||
{
|
||||
$modules = array_values(array_filter(
|
||||
$this->module->getList(),
|
||||
fn ($item) => $this->module->get([$item, 'bundled'])
|
||||
));
|
||||
|
||||
return array_map(
|
||||
fn ($item) => Util::fromCamelCase($item, '-'),
|
||||
$modules
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 8.0.0
|
||||
*/
|
||||
public function setApiUrl(string $apiUrl): void
|
||||
{
|
||||
$this->apiUrl = $apiUrl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,16 +29,7 @@
|
||||
|
||||
namespace Espo\Core\Utils\Database;
|
||||
|
||||
use Espo\Core\Utils\Config;
|
||||
|
||||
class ConfigDataProvider
|
||||
interface ConfigDataProvider
|
||||
{
|
||||
private const DEFAULT_PLATFORM = 'Mysql';
|
||||
|
||||
public function __construct(private Config $config) {}
|
||||
|
||||
public function getPlatform(): string
|
||||
{
|
||||
return $this->config->get('database.platform') ?? self::DEFAULT_PLATFORM;
|
||||
}
|
||||
public function getPlatform(): string;
|
||||
}
|
||||
|
||||
@@ -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\Core\Utils\Database;
|
||||
|
||||
use Espo\Core\Utils\Config;
|
||||
|
||||
class DefaultConfigDataProvider implements ConfigDataProvider
|
||||
{
|
||||
private const DEFAULT_PLATFORM = 'Mysql';
|
||||
|
||||
public function __construct(private Config $config) {}
|
||||
|
||||
public function getPlatform(): string
|
||||
{
|
||||
return $this->config->get('database.platform') ?? self::DEFAULT_PLATFORM;
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ class PostgresqlDetailsProvider implements DetailsProvider
|
||||
|
||||
public function getServerVersion(): string
|
||||
{
|
||||
return (string) $this->getParam('version');
|
||||
return (string) $this->getFullDatabaseVersion();
|
||||
}
|
||||
|
||||
public function getParam(string $name): ?string
|
||||
|
||||
@@ -92,15 +92,8 @@ class Converter
|
||||
/** @var array<string, mixed> */
|
||||
private array $idParams = [];
|
||||
|
||||
/**
|
||||
* Permitted entityDefs parameters which will be copied to ormMetadata.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private array $permittedEntityOptions = [
|
||||
'indexes',
|
||||
'additionalTables',
|
||||
];
|
||||
/** @var string[] */
|
||||
private array $copyEntityProperties = ['indexes'];
|
||||
|
||||
private IndexHelper $indexHelper;
|
||||
|
||||
@@ -167,11 +160,13 @@ class Converter
|
||||
$ormMetadata,
|
||||
$this->createEntityTypesFromRelations($entityType, $entityOrmMetadata)
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($entityDefs as $entityMetadata) {
|
||||
/** @var array<string, array<string, mixed>> $ormMetadata */
|
||||
$ormMetadata = Util::merge(
|
||||
$ormMetadata,
|
||||
$this->createAdditionalEntityTypes($entityOrmMetadata)
|
||||
$this->obtainAdditionalTablesOrmMetadata($entityMetadata)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -194,17 +189,17 @@ class Converter
|
||||
$ormMetadata = [];
|
||||
|
||||
$ormMetadata[$entityType] = [
|
||||
'fields' => [],
|
||||
'attributes' => [],
|
||||
'relations' => [],
|
||||
];
|
||||
|
||||
foreach ($this->permittedEntityOptions as $optionName) {
|
||||
foreach ($this->copyEntityProperties as $optionName) {
|
||||
if (isset($entityMetadata[$optionName])) {
|
||||
$ormMetadata[$entityType][$optionName] = $entityMetadata[$optionName];
|
||||
}
|
||||
}
|
||||
|
||||
$ormMetadata[$entityType]['fields'] = $this->convertFields($entityType, $entityMetadata);
|
||||
$ormMetadata[$entityType]['attributes'] = $this->convertFields($entityType, $entityMetadata);
|
||||
|
||||
$ormMetadata = $this->correctFields($entityType, $ormMetadata);
|
||||
|
||||
@@ -224,7 +219,7 @@ class Converter
|
||||
$ormMetadata[$entityType]['collection']['orderBy'] = $collectionDefs['orderByColumn'];
|
||||
}
|
||||
else if (array_key_exists('orderBy', $collectionDefs)) {
|
||||
if (array_key_exists($collectionDefs['orderBy'], $ormMetadata[$entityType]['fields'])) {
|
||||
if (array_key_exists($collectionDefs['orderBy'], $ormMetadata[$entityType]['attributes'])) {
|
||||
$ormMetadata[$entityType]['collection']['orderBy'] = $collectionDefs['orderBy'];
|
||||
}
|
||||
}
|
||||
@@ -245,15 +240,18 @@ class Converter
|
||||
*/
|
||||
private function afterFieldsProcess(array $ormMetadata): array
|
||||
{
|
||||
foreach ($ormMetadata as $entityType => &$entityParams) {
|
||||
foreach ($entityParams['fields'] as $attribute => &$attributeParams) {
|
||||
foreach ($ormMetadata as /*$entityType =>*/ &$entityParams) {
|
||||
if (empty($entityParams['attributes'])) {
|
||||
print_r($entityParams);
|
||||
}
|
||||
foreach ($entityParams['attributes'] as $attribute => &$attributeParams) {
|
||||
|
||||
// Remove fields without type.
|
||||
if (
|
||||
!isset($attributeParams['type']) &&
|
||||
(!isset($attributeParams['notStorable']) || $attributeParams['notStorable'] === false)
|
||||
) {
|
||||
unset($entityParams['fields'][$attribute]);
|
||||
unset($entityParams['attributes'][$attribute]);
|
||||
|
||||
continue;
|
||||
}
|
||||
@@ -316,7 +314,7 @@ class Converter
|
||||
private function afterProcess(array $ormMetadata): array
|
||||
{
|
||||
foreach ($ormMetadata as $entityType => &$entityParams) {
|
||||
foreach ($entityParams['fields'] as $attribute => &$attributeParams) {
|
||||
foreach ($entityParams['attributes'] as $attribute => &$attributeParams) {
|
||||
$attributeType = $attributeParams['type'] ?? null;
|
||||
|
||||
switch ($attributeType) {
|
||||
@@ -337,7 +335,7 @@ class Converter
|
||||
*/
|
||||
private function obtainForeignType(array $data, string $entityType, string $attribute): ?string
|
||||
{
|
||||
$params = $data[$entityType]['fields'][$attribute] ?? [];
|
||||
$params = $data[$entityType]['attributes'][$attribute] ?? [];
|
||||
|
||||
$foreign = $params['foreign'] ?? null;
|
||||
$relation = $params['relation'] ?? null;
|
||||
@@ -354,7 +352,7 @@ class Converter
|
||||
return null;
|
||||
}
|
||||
|
||||
$foreignParams = $data[$foreignEntityType]['fields'][$foreign] ?? [];
|
||||
$foreignParams = $data[$foreignEntityType]['attributes'][$foreign] ?? [];
|
||||
|
||||
return $foreignParams['type'] ?? null;
|
||||
}
|
||||
@@ -397,7 +395,7 @@ class Converter
|
||||
|
||||
$fieldTypeMetadata = $this->metadataHelper->getFieldDefsByType($attributeParams);
|
||||
|
||||
$fieldDefs = $this->convertField($entityType, $attribute, $attributeParams, $fieldTypeMetadata);
|
||||
$fieldDefs = $this->convertField($attributeParams, $fieldTypeMetadata);
|
||||
|
||||
if ($fieldDefs !== false) {
|
||||
if (isset($output[$attribute]) && !in_array($attribute, $unmergedFields)) {
|
||||
@@ -442,23 +440,25 @@ class Converter
|
||||
{
|
||||
$entityMetadata = $ormMetadata[$entityType];
|
||||
|
||||
foreach ($entityMetadata['fields'] as $field => $fieldParams) {
|
||||
$fieldType = $fieldParams['type'] ?? null;
|
||||
foreach ($entityMetadata['attributes'] as $field => $itemParams) {
|
||||
$type = $itemParams['type'] ?? null;
|
||||
|
||||
if (!$fieldType) {
|
||||
if (!$type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var ?class-string<FieldConverter> $className */
|
||||
$className = $this->metadata->get(['fields', $fieldType, 'converterClassName']);
|
||||
$className =
|
||||
$this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'converterClassName']) ??
|
||||
$this->metadata->get(['fields', $type, 'converterClassName']);
|
||||
|
||||
if ($className) {
|
||||
$toUnset =
|
||||
!in_array('', $this->metadata->get(['fields', $fieldType, 'actualFields']) ?? []) &&
|
||||
!in_array('', $this->metadata->get(['fields', $fieldType, 'notActualFields']) ?? []);
|
||||
!in_array('', $this->metadata->get(['fields', $type, 'actualFields']) ?? []) &&
|
||||
!in_array('', $this->metadata->get(['fields', $type, 'notActualFields']) ?? []);
|
||||
|
||||
if ($toUnset) {
|
||||
$ormMetadata = Util::unsetInArray($ormMetadata, [$entityType => ['fields.' . $field]]);
|
||||
$ormMetadata = Util::unsetInArray($ormMetadata, [$entityType => ['attributes.' . $field]]);
|
||||
}
|
||||
|
||||
$converter = $this->injectableFactory->create($className);
|
||||
@@ -480,7 +480,7 @@ class Converter
|
||||
if ($defaultAttributes && array_key_exists($field, $defaultAttributes)) {
|
||||
$defaultMetadataPart = [
|
||||
$entityType => [
|
||||
'fields' => [
|
||||
'attributes' => [
|
||||
$field => [
|
||||
'default' => $defaultAttributes[$field],
|
||||
]
|
||||
@@ -499,19 +499,19 @@ class Converter
|
||||
|
||||
if ($scopeDefs['stream'] ?? false) {
|
||||
if (!isset($entityMetadata['fields']['isFollowed'])) {
|
||||
$ormMetadata[$entityType]['fields']['isFollowed'] = [
|
||||
$ormMetadata[$entityType]['attributes']['isFollowed'] = [
|
||||
'type' => Entity::VARCHAR,
|
||||
'notStorable' => true,
|
||||
'notExportable' => true,
|
||||
];
|
||||
|
||||
$ormMetadata[$entityType]['fields']['followersIds'] = [
|
||||
$ormMetadata[$entityType]['attributes']['followersIds'] = [
|
||||
'type' => Entity::JSON_ARRAY,
|
||||
'notStorable' => true,
|
||||
'notExportable' => true,
|
||||
];
|
||||
|
||||
$ormMetadata[$entityType]['fields']['followersNames'] = [
|
||||
$ormMetadata[$entityType]['attributes']['followersNames'] = [
|
||||
'type' => Entity::JSON_OBJECT,
|
||||
'notStorable' => true,
|
||||
'notExportable' => true,
|
||||
@@ -521,7 +521,7 @@ class Converter
|
||||
|
||||
// @todo Refactor.
|
||||
if ($this->metadata->get(['entityDefs', $entityType, 'optimisticConcurrencyControl'])) {
|
||||
$ormMetadata[$entityType]['fields']['versionNumber'] = [
|
||||
$ormMetadata[$entityType]['attributes']['versionNumber'] = [
|
||||
'type' => Entity::INT,
|
||||
'dbType' => Types::BIGINT,
|
||||
'notExportable' => true,
|
||||
@@ -537,8 +537,6 @@ class Converter
|
||||
* @return array<string, mixed>|false
|
||||
*/
|
||||
private function convertField(
|
||||
string $entityType,
|
||||
string $field,
|
||||
array $fieldParams,
|
||||
?array $fieldTypeMetadata = null
|
||||
) {
|
||||
@@ -742,8 +740,8 @@ class Converter
|
||||
|
||||
$defs['indexes'] ??= [];
|
||||
|
||||
if (isset($defs['fields'])) {
|
||||
$indexList = self::getEntityIndexListFromAttributes($defs['fields']);
|
||||
if (isset($defs['attributes'])) {
|
||||
$indexList = self::getEntityIndexListFromAttributes($defs['attributes']);
|
||||
|
||||
foreach ($indexList as $indexName => $indexParams) {
|
||||
if (!isset($defs['indexes'][$indexName])) {
|
||||
@@ -824,7 +822,7 @@ class Converter
|
||||
* @param array<string, mixed> $defs
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function createAdditionalEntityTypes(array $defs): array
|
||||
private function obtainAdditionalTablesOrmMetadata(array $defs): array
|
||||
{
|
||||
/** @var array<string, array<string, mixed>> $additionalDefs */
|
||||
$additionalDefs = $defs['additionalTables'] ?? [];
|
||||
@@ -840,6 +838,17 @@ class Converter
|
||||
$this->applyIndexes($additionalDefs, $itemEntityType);
|
||||
}
|
||||
|
||||
// For backward compatibility. Actual as of v8.0.
|
||||
// @todo Remove in v10.0.
|
||||
// @todo Add deprecation warning in v9.0. If 'fields' is set.
|
||||
foreach ($additionalDefs as &$entityDefs) {
|
||||
if (!isset($entityDefs['attributes'])) {
|
||||
$entityDefs['attributes'] = $entityDefs['fields'] ?? [];
|
||||
|
||||
unset($entityDefs['fields']);
|
||||
}
|
||||
}
|
||||
|
||||
return $additionalDefs;
|
||||
}
|
||||
|
||||
@@ -862,7 +871,7 @@ class Converter
|
||||
|
||||
$itemDefs = [
|
||||
'skipRebuild' => true,
|
||||
'fields' => [
|
||||
'attributes' => [
|
||||
'id' => [
|
||||
'type' => Entity::ID,
|
||||
'autoincrement' => true,
|
||||
@@ -876,7 +885,7 @@ class Converter
|
||||
|
||||
if (!$relationDefs->hasMidKey()) {
|
||||
throw new LogicException(
|
||||
"Bad manyMany relation {$name} in {$entityType}. Might be not defined on the other side.");
|
||||
"Bad manyMany relation $name in $entityType. Might be not defined on the other side.");
|
||||
}
|
||||
|
||||
$key1 = $relationDefs->getMidKey();
|
||||
@@ -885,7 +894,7 @@ class Converter
|
||||
$midKeys = [$key1, $key2];
|
||||
|
||||
foreach ($midKeys as $key) {
|
||||
$itemDefs['fields'][$key] = [
|
||||
$itemDefs['attributes'][$key] = [
|
||||
'type' => Entity::FOREIGN_ID,
|
||||
];
|
||||
}
|
||||
@@ -907,7 +916,7 @@ class Converter
|
||||
$columnDefs['default'] = $attributeDefs->getParam('default');
|
||||
}
|
||||
|
||||
$itemDefs['fields'][$columnName] = $columnDefs;
|
||||
$itemDefs['attributes'][$columnName] = $columnDefs;
|
||||
}
|
||||
|
||||
foreach ($relationDefs->getIndexList() as $indexDefs) {
|
||||
|
||||
@@ -125,8 +125,7 @@ class EntityDefs
|
||||
$attributesData[$name] = $attributeDefs->toAssoc();
|
||||
}
|
||||
|
||||
// @todo Change to attributes.
|
||||
$data['fields'] = $attributesData;
|
||||
$data['attributes'] = $attributesData;
|
||||
}
|
||||
|
||||
if (count($this->relations)) {
|
||||
|
||||
@@ -46,7 +46,7 @@ class HasMany implements LinkConverter
|
||||
{
|
||||
if (!$linkDefs->hasForeignRelationName() && $linkDefs->getParam('disabled')) {
|
||||
// For bc.
|
||||
// @todo Remove in v8.0.
|
||||
// @todo Remove in v9.0.
|
||||
return EntityDefs::create();
|
||||
}
|
||||
|
||||
|
||||
@@ -264,6 +264,10 @@ class DiffModifier
|
||||
->setNotnull(false)
|
||||
->setDefault(null);
|
||||
|
||||
if ($name === 'id') {
|
||||
$column->setNotnull(true);
|
||||
}
|
||||
|
||||
self::unsetChangedColumnProperty($tableDiff, $columnDiff, $name, 'autoincrement');
|
||||
|
||||
return true;
|
||||
|
||||
@@ -31,11 +31,16 @@ namespace Espo\Core\Utils;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
use DateTimeZone;
|
||||
use DateTime as DateTimeStd;
|
||||
use Espo\Core\Field\Date;
|
||||
use Espo\Core\Field\DateTime as DateTimeField;
|
||||
|
||||
use DateTime as DateTimeStd;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
|
||||
/**
|
||||
* Util for a date-time formatting and conversion.
|
||||
* Available as 'dateTime' service.
|
||||
@@ -58,8 +63,14 @@ class DateTime
|
||||
) {
|
||||
$this->dateFormat = $dateFormat ?? 'YYYY-MM-DD';
|
||||
$this->timeFormat = $timeFormat ?? 'HH:mm';
|
||||
$this->timezone = new DateTimeZone($timeZone ?? 'UTC');
|
||||
$this->language = $language ?? 'en_US';
|
||||
|
||||
try {
|
||||
$this->timezone = new DateTimeZone($timeZone ?? 'UTC');
|
||||
}
|
||||
catch (Exception $e) {
|
||||
throw new RuntimeException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +95,7 @@ class DateTime
|
||||
* @param string $string A system date.
|
||||
* @param string|null $format A target format. If not specified then the default format will be used.
|
||||
* @param string|null $language A language. If not specified then the default language will be used.
|
||||
* @throws RuntimeException If could not parse.
|
||||
* @throws RuntimeException If it could not parse.
|
||||
*/
|
||||
public function convertSystemDate(
|
||||
string $string,
|
||||
@@ -95,7 +106,7 @@ class DateTime
|
||||
$dateTime = DateTimeStd::createFromFormat('Y-m-d', $string);
|
||||
|
||||
if ($dateTime === false) {
|
||||
throw new RuntimeException("Could not parse date `{$string}`.");
|
||||
throw new RuntimeException("Could not parse date `$string`.");
|
||||
}
|
||||
|
||||
$carbon = Carbon::instance($dateTime);
|
||||
@@ -112,7 +123,7 @@ class DateTime
|
||||
* @param ?string $timezone A target timezone. If not specified then the default timezone will be used.
|
||||
* @param ?string $format A target format. If not specified then the default format will be used.
|
||||
* @param ?string $language A language. If not specified then the default language will be used.
|
||||
* @throws RuntimeException If could not parse.
|
||||
* @throws RuntimeException If it could not parse.
|
||||
*/
|
||||
public function convertSystemDateTime(
|
||||
string $string,
|
||||
@@ -128,10 +139,15 @@ class DateTime
|
||||
$dateTime = DateTimeStd::createFromFormat('Y-m-d H:i:s', $string);
|
||||
|
||||
if ($dateTime === false) {
|
||||
throw new RuntimeException("Could not parse date-time `{$string}`.");
|
||||
throw new RuntimeException("Could not parse date-time `$string`.");
|
||||
}
|
||||
|
||||
$tz = $timezone ? new DateTimeZone($timezone) : $this->timezone;
|
||||
try {
|
||||
$tz = $timezone ? new DateTimeZone($timezone) : $this->timezone;
|
||||
}
|
||||
catch (Exception $e) {
|
||||
throw new RuntimeException($e->getMessage());
|
||||
}
|
||||
|
||||
$dateTime->setTimezone($tz);
|
||||
|
||||
@@ -149,7 +165,12 @@ class DateTime
|
||||
*/
|
||||
public function getTodayString(?string $timezone = null, ?string $format = null): string
|
||||
{
|
||||
$tz = $timezone ? new DateTimeZone($timezone) : $this->timezone;
|
||||
try {
|
||||
$tz = $timezone ? new DateTimeZone($timezone) : $this->timezone;
|
||||
}
|
||||
catch (Exception $e) {
|
||||
throw new RuntimeException($e->getMessage());
|
||||
}
|
||||
|
||||
$dateTime = new DateTimeStd();
|
||||
$dateTime->setTimezone($tz);
|
||||
@@ -168,7 +189,12 @@ class DateTime
|
||||
*/
|
||||
public function getNowString(?string $timezone = null, ?string $format = null): string
|
||||
{
|
||||
$tz = $timezone ? new DateTimeZone($timezone) : $this->timezone;
|
||||
try {
|
||||
$tz = $timezone ? new DateTimeZone($timezone) : $this->timezone;
|
||||
}
|
||||
catch (Exception $e) {
|
||||
throw new RuntimeException($e->getMessage());
|
||||
}
|
||||
|
||||
$dateTime = new DateTimeStd();
|
||||
|
||||
@@ -219,6 +245,41 @@ class DateTime
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default time zone.
|
||||
*
|
||||
* @since 8.0.0
|
||||
*/
|
||||
public function getTimezone(): DateTimeZone
|
||||
{
|
||||
return $this->timezone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a today's date according the default time zone.
|
||||
*
|
||||
* @since 8.0.0
|
||||
*/
|
||||
public function getToday(): Date
|
||||
{
|
||||
$string = (new DateTimeImmutable)
|
||||
->setTimezone($this->timezone)
|
||||
->format(self::SYSTEM_DATE_FORMAT);
|
||||
|
||||
return Date::fromString($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a now date-time with the default time zone applied.
|
||||
*
|
||||
* @since 8.0.0
|
||||
*/
|
||||
public function getNow(): DateTimeField
|
||||
{
|
||||
return DateTimeField::createNow()
|
||||
->withTimezone($this->timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `SYSTEM_DATE_TIME_FORMAT constant`.
|
||||
*/
|
||||
|
||||
@@ -231,7 +231,7 @@ class Manager
|
||||
|
||||
if (is_array($path)) {
|
||||
// For backward compatibility.
|
||||
// @todo Remove support of arrays in v7.3.
|
||||
// @todo Remove support of arrays in v9.0.
|
||||
trigger_error(
|
||||
'Array parameter is deprecated for FileManager::getContents.',
|
||||
E_USER_DEPRECATED
|
||||
@@ -552,6 +552,10 @@ class Manager
|
||||
$permission = (int) base_convert((string) $defaultPermissions['dir'], 8, 10);
|
||||
}
|
||||
|
||||
if (is_dir($path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$umask = umask(0);
|
||||
|
||||
$result = mkdir($path, $permission);
|
||||
@@ -560,6 +564,11 @@ class Manager
|
||||
umask($umask);
|
||||
}
|
||||
|
||||
if (!$result && is_dir($path)) {
|
||||
// Dir can be created by a concurrent process.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!empty($defaultPermissions['user'])) {
|
||||
$this->getPermissionUtils()->chown($path);
|
||||
}
|
||||
@@ -568,7 +577,7 @@ class Manager
|
||||
$this->getPermissionUtils()->chgrp($path);
|
||||
}
|
||||
|
||||
return (bool) $result;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -67,6 +67,11 @@ class Metadata
|
||||
['app', 'api', 'routeMiddlewareClassNameListMap', self::ANY_KEY],
|
||||
['app', 'api', 'controllerMiddlewareClassNameListMap', self::ANY_KEY],
|
||||
['app', 'api', 'controllerActionMiddlewareClassNameListMap', self::ANY_KEY],
|
||||
['app', 'entityManager', 'createHookClassNameList'],
|
||||
['app', 'entityManager', 'deleteHookClassNameList'],
|
||||
['app', 'entityManager', 'updateHookClassNameList'],
|
||||
['app', 'linkManager', 'createHookClassNameList'],
|
||||
['app', 'linkManager', 'deleteHookClassNameList'],
|
||||
['recordDefs', self::ANY_KEY, 'readLoaderClassNameList'],
|
||||
['recordDefs', self::ANY_KEY, 'listLoaderClassNameList'],
|
||||
['recordDefs', self::ANY_KEY, 'saverClassNameList'],
|
||||
@@ -160,11 +165,11 @@ class Metadata
|
||||
/**
|
||||
* Get all metadata.
|
||||
*
|
||||
* @param bool $isJSON
|
||||
* @param bool $reload
|
||||
* @return array<string, mixed>|string
|
||||
* @/param bool $isJSON
|
||||
* @/param bool $reload
|
||||
* @/return array<string, mixed>|string
|
||||
*/
|
||||
public function getAll(bool $isJSON = false, bool $reload = false)
|
||||
/*public function getAll(bool $isJSON = false, bool $reload = false)
|
||||
{
|
||||
if ($reload) {
|
||||
$this->init($reload);
|
||||
@@ -177,7 +182,7 @@ class Metadata
|
||||
}
|
||||
|
||||
return $this->data;
|
||||
}
|
||||
}*/
|
||||
|
||||
private function objInit(bool $reload = false): void
|
||||
{
|
||||
@@ -231,71 +236,9 @@ class Metadata
|
||||
return Util::getValueByKey($objData, $key, $default);
|
||||
}
|
||||
|
||||
public function getAllForFrontend(): stdClass
|
||||
public function getAll(): stdClass
|
||||
{
|
||||
$data = $this->getObjData();
|
||||
|
||||
$frontendHiddenPathList = $this->get(['app', 'metadata', 'frontendHiddenPathList'], []);
|
||||
|
||||
foreach ($frontendHiddenPathList as $row) {
|
||||
$this->removeDataByPath($row, $data);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string[] $row
|
||||
* @param stdClass $data
|
||||
*/
|
||||
private function removeDataByPath($row, &$data): void
|
||||
{
|
||||
$p = &$data;
|
||||
$path = [&$p];
|
||||
|
||||
foreach ($row as $i => $item) {
|
||||
if (is_array($item)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($item === self::ANY_KEY) {
|
||||
foreach (get_object_vars($p) as &$v) {
|
||||
$this->removeDataByPath(
|
||||
array_slice($row, $i + 1),
|
||||
$v
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!property_exists($p, $item)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($i == count($row) - 1) {
|
||||
unset($p->$item);
|
||||
|
||||
$o = &$p;
|
||||
|
||||
for ($j = $i - 1; $j > 0; $j--) {
|
||||
if (is_object($o) && !count(get_object_vars($o))) {
|
||||
$o = &$path[$j];
|
||||
$k = $row[$j];
|
||||
|
||||
unset($o->$k);
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$p = &$p->$item;
|
||||
$path[] = &$p;
|
||||
}
|
||||
}
|
||||
return $this->getObjData();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -323,7 +266,7 @@ class Metadata
|
||||
}
|
||||
|
||||
if (isset($collectionItem->order)) {
|
||||
$collectionItem->asc = $collectionItem->order === 'asc' ? true : false;
|
||||
$collectionItem->asc = $collectionItem->order === 'asc';
|
||||
}
|
||||
else if (isset($collectionItem->asc)) {
|
||||
$collectionItem->order = $collectionItem->asc === true ? 'asc' : 'desc';
|
||||
@@ -372,7 +315,7 @@ class Metadata
|
||||
*/
|
||||
public function getCustom(string $key1, string $key2, $default = null)
|
||||
{
|
||||
$filePath = $this->customPath . "/{$key1}/{$key2}.json";
|
||||
$filePath = $this->customPath . "/$key1/$key2.json";
|
||||
|
||||
if (!$this->fileManager->isFile($filePath)) {
|
||||
return $default;
|
||||
@@ -402,7 +345,7 @@ class Metadata
|
||||
}
|
||||
}
|
||||
|
||||
$filePath = $this->customPath . "/{$key1}/{$key2}.json";
|
||||
$filePath = $this->customPath . "/$key1/$key2.json";
|
||||
|
||||
$changedData = Json::encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
@@ -464,7 +407,7 @@ class Metadata
|
||||
$unsetList = $unsets;
|
||||
|
||||
foreach ($unsetList as $unsetItem) {
|
||||
if (preg_match('/fields\.([^\.]+)/', $unsetItem, $matches) && isset($matches[1])) {
|
||||
if (preg_match('/fields\.([^.]+)/', $unsetItem, $matches) && isset($matches[1])) {
|
||||
$fieldName = $matches[1];
|
||||
$fieldPath = [$key1, $key2, 'fields', $fieldName];
|
||||
|
||||
@@ -557,7 +500,7 @@ class Metadata
|
||||
continue;
|
||||
}
|
||||
|
||||
$filePath = $path . "/{$key1}/{$key2}.json";
|
||||
$filePath = $path . "/$key1/$key2.json";
|
||||
|
||||
$result &= $this->fileManager->mergeJsonContents($filePath, $data);
|
||||
}
|
||||
@@ -571,13 +514,13 @@ class Metadata
|
||||
continue;
|
||||
}
|
||||
|
||||
$filePath = $path . "/{$key1}/{$key2}.json";
|
||||
$filePath = $path . "/$key1/$key2.json";
|
||||
|
||||
$rowResult = $this->fileManager->unsetJsonContents($filePath, $unsetData);
|
||||
|
||||
if (!$rowResult) {
|
||||
throw new LogicException(
|
||||
"Metadata items {$key1}.{$key2} can be deleted for custom code only."
|
||||
"Metadata items $key1.$key2 can be deleted for custom code only."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class PasswordHash
|
||||
/**
|
||||
* Get a salt from the config and normalize it.
|
||||
*/
|
||||
protected function getSalt(): string
|
||||
private function getSalt(): string
|
||||
{
|
||||
$salt = $this->config->get('passwordSalt');
|
||||
|
||||
@@ -74,7 +74,7 @@ class PasswordHash
|
||||
/**
|
||||
* Convert salt in format in accordance to $saltFormat.
|
||||
*/
|
||||
protected function normalizeSalt(string $salt): string
|
||||
private function normalizeSalt(string $salt): string
|
||||
{
|
||||
return str_replace("{0}", $salt, $this->saltFormat);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,15 @@ use Espo\Core\Utils\File\Manager as FileManager;
|
||||
|
||||
class SystemRequirements
|
||||
{
|
||||
private const PLATFORM_MYSQL = 'Mysql';
|
||||
private const PLATFORM_POSTGRESQL = 'Postgresql';
|
||||
|
||||
/** @var array<string, string> */
|
||||
private $pdoExtensionMap = [
|
||||
self::PLATFORM_MYSQL => 'pdo_mysql',
|
||||
self::PLATFORM_POSTGRESQL => 'pdo_pgsql',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private FileManager $fileManager,
|
||||
@@ -100,7 +109,37 @@ class SystemRequirements
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->getRequiredList('phpRequirements', $requiredList);
|
||||
$list = $this->getRequiredList('phpRequirements', $requiredList);
|
||||
|
||||
$pdoExtension = $this->getPdoExtension();
|
||||
|
||||
if ($pdoExtension) {
|
||||
$acceptable = $this->systemHelper->hasPhpExtension($pdoExtension);
|
||||
|
||||
$list[$pdoExtension] = [
|
||||
'type' => 'lib',
|
||||
'acceptable' => $acceptable,
|
||||
'actual' => $acceptable ? 'On' : 'Off',
|
||||
];
|
||||
}
|
||||
|
||||
uksort($list, function ($k1, $k2) use ($list) {
|
||||
$order = ['version', 'lib', 'param'];
|
||||
|
||||
$a = $list[$k1];
|
||||
$b = $list[$k2];
|
||||
|
||||
return array_search($a['type'], $order) - array_search($b['type'], $order);
|
||||
});
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
private function getPdoExtension(): ?string
|
||||
{
|
||||
$platform = $this->config->get('database.platform') ?? self::PLATFORM_MYSQL;
|
||||
|
||||
return $this->pdoExtensionMap[$platform] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -277,9 +316,10 @@ class SystemRequirements
|
||||
switch ($type) {
|
||||
case 'requiredMysqlVersion':
|
||||
case 'requiredMariadbVersion':
|
||||
case 'requiredPostgresqlVersion':
|
||||
/** @var string $data */
|
||||
|
||||
$actualVersion = $databaseHelper->getServerVersion();
|
||||
$actualVersion = $databaseHelper->getVersion();
|
||||
|
||||
$requiredVersion = $data;
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ class ThemeManager
|
||||
{
|
||||
private string $defaultName = 'Espo';
|
||||
private string $defaultStylesheet = 'client/css/espo/espo.css';
|
||||
private string $defaultLogoSrc = 'client/img/logo.svg';
|
||||
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
@@ -48,4 +49,9 @@ class ThemeManager
|
||||
{
|
||||
return $this->metadata->get(['themes', $this->getName(), 'stylesheet']) ?? $this->defaultStylesheet;
|
||||
}
|
||||
|
||||
public function getLogoSrc(): string
|
||||
{
|
||||
return $this->metadata->get(['themes', $this->getName(), 'logo']) ?? $this->defaultLogoSrc;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -638,7 +638,11 @@ class Util
|
||||
public static function generateUuid4(): string
|
||||
{
|
||||
try {
|
||||
$hex = bin2hex(random_bytes(16));
|
||||
$data = random_bytes(16);
|
||||
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
|
||||
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
|
||||
|
||||
$hex = bin2hex($data);
|
||||
}
|
||||
catch (Exception) {
|
||||
throw new RuntimeException("Could not generate UUID.");
|
||||
|
||||
@@ -31,18 +31,19 @@ namespace Espo\Entities;
|
||||
|
||||
use Espo\Core\Field\LinkParent;
|
||||
use Espo\Core\ORM\Entity;
|
||||
use Espo\Core\Record\ActionHistory\Action;
|
||||
|
||||
class ActionHistoryRecord extends Entity
|
||||
{
|
||||
public const ENTITY_TYPE = 'ActionHistoryRecord';
|
||||
|
||||
public const ACTION_READ = 'read';
|
||||
public const ACTION_UPDATE = 'update';
|
||||
public const ACTION_CREATE = 'create';
|
||||
public const ACTION_DELETE = 'delete';
|
||||
public const ACTION_CREATE = Action::CREATE;
|
||||
public const ACTION_READ = Action::READ;
|
||||
public const ACTION_UPDATE = Action::UPDATE;
|
||||
public const ACTION_DELETE = Action::DELETE;
|
||||
|
||||
/**
|
||||
* @param self::ACTION_* $action
|
||||
* @param Action::* $action
|
||||
*/
|
||||
public function setAction(string $action): self
|
||||
{
|
||||
|
||||
@@ -29,22 +29,16 @@
|
||||
|
||||
namespace Espo\Entities;
|
||||
|
||||
use Espo\Core\ORM\Entity;
|
||||
use Espo\Tools\Import\Params;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class Import extends \Espo\Core\ORM\Entity
|
||||
class Import extends Entity
|
||||
{
|
||||
public const ENTITY_TYPE = 'Import';
|
||||
|
||||
public const STATUS_STANDBY = 'Standby';
|
||||
|
||||
public const STATUS_IN_PROCESS = 'In Process';
|
||||
|
||||
public const STATUS_FAILED = 'Failed';
|
||||
|
||||
public const STATUS_PENDING = 'Pending';
|
||||
|
||||
public const STATUS_COMPLETE = 'Complete';
|
||||
|
||||
public function getStatus(): ?string
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
|
||||
namespace Espo\Entities;
|
||||
|
||||
use Espo\Core\Job\Job as JobJob;
|
||||
use Espo\Core\Job\Job\Status;
|
||||
use Espo\Core\Job\JobDataLess;
|
||||
use Espo\Core\ORM\Entity;
|
||||
use Espo\Core\Utils\DateTime as DateTimeUtil;
|
||||
|
||||
@@ -113,6 +115,8 @@ class Job extends Entity
|
||||
|
||||
/**
|
||||
* Get a class name.
|
||||
*
|
||||
* @return ?class-string<JobJob|JobDataLess>
|
||||
*/
|
||||
public function getClassName(): ?string
|
||||
{
|
||||
|
||||
@@ -29,7 +29,14 @@
|
||||
|
||||
namespace Espo\Entities;
|
||||
|
||||
class LayoutRecord extends \Espo\Core\ORM\Entity
|
||||
use Espo\Core\ORM\Entity;
|
||||
|
||||
class LayoutRecord extends Entity
|
||||
{
|
||||
public const ENTITY_TYPE = 'LayoutRecord';
|
||||
|
||||
public function getData(): mixed
|
||||
{
|
||||
return $this->get('data');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,17 @@
|
||||
|
||||
namespace Espo\Entities;
|
||||
|
||||
class LayoutSet extends \Espo\Core\ORM\Entity
|
||||
use Espo\Core\ORM\Entity;
|
||||
|
||||
class LayoutSet extends Entity
|
||||
{
|
||||
public const ENTITY_TYPE = 'LayoutSet';
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getLayoutList(): array
|
||||
{
|
||||
return $this->get('layoutList') ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user