mirror of
https://github.com/espocrm/espocrm.git
synced 2026-03-06 15:27:01 +00:00
Compare commits
801 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a013b7ea7 | ||
|
|
124214aee3 | ||
|
|
3bd6b3bd46 | ||
|
|
c536cee637 | ||
|
|
b1d2795347 | ||
|
|
0f9bce70cf | ||
|
|
6f1f72127f | ||
|
|
986edeb79e | ||
|
|
b3391df6e8 | ||
|
|
d8a114c90a | ||
|
|
3fa9f4bf91 | ||
|
|
6c4a139e66 | ||
|
|
d3a1db25c2 | ||
|
|
0fcd3cd780 | ||
|
|
61cb573eec | ||
|
|
5dd84bf9a1 | ||
|
|
5dad7c6bba | ||
|
|
3007d50299 | ||
|
|
11c0ac6987 | ||
|
|
2bb62883a4 | ||
|
|
5796d2fb85 | ||
|
|
dae318c9bb | ||
|
|
342764e9ff | ||
|
|
27b9e28e20 | ||
|
|
f8d5dd7fa6 | ||
|
|
7408f97fbd | ||
|
|
d1a9cf840f | ||
|
|
d593927b14 | ||
|
|
f62ebb8fe8 | ||
|
|
ede798dbe5 | ||
|
|
c3095b6a32 | ||
|
|
9eb5bfc9fe | ||
|
|
bf2c529884 | ||
|
|
7ce8150541 | ||
|
|
44de149923 | ||
|
|
8ecdc7b9d9 | ||
|
|
95c64359d2 | ||
|
|
44203e2178 | ||
|
|
d3d940d9c9 | ||
|
|
9d3efae141 | ||
|
|
e6eb3e7099 | ||
|
|
3ab2b37471 | ||
|
|
33e7f4e3ef | ||
|
|
6463eaa6f6 | ||
|
|
a066c0a187 | ||
|
|
176f65713f | ||
|
|
493afdf3d4 | ||
|
|
c85b6a37a6 | ||
|
|
81971d0eda | ||
|
|
8a6cc9ea35 | ||
|
|
4fd125ddfc | ||
|
|
1298a8382d | ||
|
|
bb9c352f55 | ||
|
|
045533d080 | ||
|
|
6a5ab5f738 | ||
|
|
c1b0e4fd17 | ||
|
|
e71928ffa7 | ||
|
|
de3f5de029 | ||
|
|
57ceee4d4c | ||
|
|
9a5cd7609b | ||
|
|
1382dca3e3 | ||
|
|
bc7a3e8839 | ||
|
|
c5d6f4c63f | ||
|
|
6776f813af | ||
|
|
6d18f2485d | ||
|
|
73753b364e | ||
|
|
68d6a14d04 | ||
|
|
c155d37790 | ||
|
|
6a6446d0b7 | ||
|
|
b0b4fe5570 | ||
|
|
6de8c3d1ca | ||
|
|
319f3f645a | ||
|
|
bedfbb0e6b | ||
|
|
148ffce6b7 | ||
|
|
85cb969780 | ||
|
|
c1a75ad051 | ||
|
|
7de49e9812 | ||
|
|
13c5d65f50 | ||
|
|
34ce9d68bf | ||
|
|
58dbadb869 | ||
|
|
0a9d901874 | ||
|
|
9ebad9aad8 | ||
|
|
e44b921143 | ||
|
|
6cb44e65a6 | ||
|
|
5398331956 | ||
|
|
de88ff0b6a | ||
|
|
1bbc92e460 | ||
|
|
e3ef9391ce | ||
|
|
b317f99196 | ||
|
|
512c45c9b5 | ||
|
|
5f4ae01c85 | ||
|
|
6c2bab44f6 | ||
|
|
b6f8a8bf9e | ||
|
|
ed3030606f | ||
|
|
274e46fa35 | ||
|
|
e95620c131 | ||
|
|
31cb11f0d1 | ||
|
|
fae905efb0 | ||
|
|
fcc1bada71 | ||
|
|
8a140b2303 | ||
|
|
9551590f92 | ||
|
|
0d8278aa0d | ||
|
|
c23a4e2085 | ||
|
|
fe77f2c14c | ||
|
|
5e06ad19ee | ||
|
|
8bce12aca3 | ||
|
|
d928ba7c3b | ||
|
|
25b0077adc | ||
|
|
ee0bcb7685 | ||
|
|
642141f574 | ||
|
|
90cdfb5e7e | ||
|
|
9967bc26d9 | ||
|
|
fef2d74ec2 | ||
|
|
9bbd262d05 | ||
|
|
555d8a1a24 | ||
|
|
f7f549fb05 | ||
|
|
d69c631a32 | ||
|
|
5e3a4c12d7 | ||
|
|
f75d0562ba | ||
|
|
ab745b3b03 | ||
|
|
f745d870f7 | ||
|
|
0ebae7734a | ||
|
|
b1b61963fc | ||
|
|
5282f01cda | ||
|
|
6fed836bee | ||
|
|
d39c8837fb | ||
|
|
870be0d1f0 | ||
|
|
a889163d23 | ||
|
|
1145d1c902 | ||
|
|
d641e5faaa | ||
|
|
57ca5ebf7b | ||
|
|
c2126e0680 | ||
|
|
45ba66c0ff | ||
|
|
a6af348a50 | ||
|
|
95fe5a47f5 | ||
|
|
29b3166da9 | ||
|
|
38ab3fbc44 | ||
|
|
b7ae677fb3 | ||
|
|
f54656dde1 | ||
|
|
fad1661b14 | ||
|
|
ed99c82137 | ||
|
|
c27a4fb609 | ||
|
|
216268f225 | ||
|
|
a5371d9a52 | ||
|
|
fdcf06efa9 | ||
|
|
755d48919f | ||
|
|
51bd72dc62 | ||
|
|
fe4a90ed97 | ||
|
|
7ee08a6713 | ||
|
|
0695fd87c0 | ||
|
|
ab8fe96919 | ||
|
|
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 | ||
|
|
ec664163e5 | ||
|
|
b130076313 | ||
|
|
372a9c9640 | ||
|
|
958a1e6634 | ||
|
|
306a8728b4 | ||
|
|
e9527a6bbf | ||
|
|
d261a019bd | ||
|
|
6465277fb0 | ||
|
|
fc8be6d56f | ||
|
|
cc32089a51 | ||
|
|
56dd0aa594 | ||
|
|
71d8327b32 | ||
|
|
e322e036a4 | ||
|
|
4f2651dd2b | ||
|
|
f53553e301 | ||
|
|
4c346bebff | ||
|
|
083a2c5235 | ||
|
|
7adfb6c1c0 | ||
|
|
190330c204 | ||
|
|
56f975c65e | ||
|
|
a79892028a | ||
|
|
3086b75616 | ||
|
|
9955da3521 | ||
|
|
0ba9130e96 | ||
|
|
0af14b93c5 | ||
|
|
191884d5af | ||
|
|
dd1aac89ce | ||
|
|
24203e8d07 | ||
|
|
e53f79be03 | ||
|
|
435dc2d818 | ||
|
|
39e446ecf2 | ||
|
|
5a013ddc88 | ||
|
|
14b595940f | ||
|
|
b6d4b96aa8 | ||
|
|
de4c5d641d | ||
|
|
f731419f86 | ||
|
|
0e2f00665c | ||
|
|
cae0f541b5 | ||
|
|
88dec452f6 | ||
|
|
28072ad24f | ||
|
|
857c252b14 | ||
|
|
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 | ||
|
|
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" />
|
||||
|
||||
1
.idea/inspectionProfiles/Project_Default.xml
generated
1
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -2,6 +2,7 @@
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="ES6ConvertLetToConst" enabled="true" level="WEAK WARNING" enabled_by_default="true" editorAttributes="INFO_ATTRIBUTES" />
|
||||
<inspection_tool class="ES6ConvertVarToLetConst" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="JSIgnoredPromiseFromCall" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PhpDocMissingThrowsInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
|
||||
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
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
namespace Espo\Core\Api;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
@@ -47,6 +48,7 @@ interface Action
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Conflict
|
||||
* @throws Error
|
||||
*/
|
||||
public function process(Request $request): Response;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -144,14 +144,17 @@ class Utils
|
||||
* Normalize options to LDAP client format
|
||||
*
|
||||
* @param array<string, mixed> $options
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function normalizeOptions(array $options): array
|
||||
{
|
||||
$options['useSsl'] = (bool) ($options['useSsl'] == 'SSL');
|
||||
$options['useStartTls'] = (bool) ($options['useStartTls'] == 'TLS');
|
||||
$options['accountCanonicalForm'] = $this->accountCanonicalFormMap[$options['accountCanonicalForm']];
|
||||
$useSsl = ($options['useSsl'] ?? null) == 'SSL';
|
||||
$useStartTls = ($options['useStartTls'] ?? null) == 'TLS';
|
||||
$accountCanonicalFormKey = $options['accountCanonicalForm'] ?? 'Dn';
|
||||
|
||||
$options['useSsl'] = $useSsl;
|
||||
$options['useStartTls'] = $useStartTls;
|
||||
$options['accountCanonicalForm'] = $this->accountCanonicalFormMap[$accountCanonicalFormKey] ?? 1;
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
@@ -112,6 +112,7 @@ class KeysProvider
|
||||
CURLOPT_TIMEOUT => self::REQUEST_TIMEOUT,
|
||||
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
|
||||
CURLOPT_CUSTOMREQUEST => 'GET',
|
||||
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP,
|
||||
]);
|
||||
|
||||
/** @var string|false $response */
|
||||
|
||||
@@ -229,6 +229,7 @@ class Login implements LoginInterface
|
||||
CURLOPT_CUSTOMREQUEST => 'POST',
|
||||
CURLOPT_POSTFIELDS => http_build_query($params),
|
||||
CURLOPT_HTTPHEADER => ['content-type: application/x-www-form-urlencoded'],
|
||||
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP,
|
||||
]);
|
||||
|
||||
/** @var string|false $response */
|
||||
|
||||
@@ -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()
|
||||
) {
|
||||
|
||||
@@ -53,7 +53,7 @@ class Help implements Command
|
||||
$commandList = array_filter(
|
||||
$fullCommandList,
|
||||
function ($item): bool {
|
||||
return (bool) $this->metadata->get(['app', 'consoleCommands', $item]);
|
||||
return (bool) $this->metadata->get(['app', 'consoleCommands', $item, 'listed']);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -48,6 +48,8 @@ class Record extends RecordBase
|
||||
* @throws BadRequest
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function getActionListLinked(Request $request): stdClass
|
||||
{
|
||||
@@ -167,6 +169,7 @@ class Record extends RecordBase
|
||||
* @throws BadRequest
|
||||
* @throws NotFoundSilent
|
||||
* @throws Forbidden
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function putActionFollow(Request $request): bool
|
||||
{
|
||||
@@ -186,6 +189,7 @@ class Record extends RecordBase
|
||||
*
|
||||
* @throws NotFoundSilent
|
||||
* @throws BadRequest
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function deleteActionUnfollow(Request $request): bool
|
||||
{
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -416,9 +416,8 @@ class Saver implements SaverInterface
|
||||
if ($emailAddressOld) {
|
||||
$this->entityManager
|
||||
->getRDBRepository($entity->getEntityType())
|
||||
->unrelate($entity, 'emailAddresses', $emailAddressOld, [
|
||||
SaveOption::SKIP_HOOKS => true,
|
||||
]);
|
||||
->getRelation($entity, 'emailAddresses')
|
||||
->unrelate($emailAddressOld, [SaveOption::SKIP_HOOKS => true]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -490,15 +489,15 @@ class Saver implements SaverInterface
|
||||
$emailAddressOld = $this->getByAddress($emailAddressValueOld);
|
||||
|
||||
if ($emailAddressOld) {
|
||||
$entityRepository->unrelate($entity, 'emailAddresses', $emailAddressOld, [
|
||||
SaveOption::SKIP_HOOKS => true,
|
||||
]);
|
||||
$entityRepository
|
||||
->getRelation($entity, 'emailAddresses')
|
||||
->unrelate($emailAddressOld, [SaveOption::SKIP_HOOKS => true]);
|
||||
}
|
||||
}
|
||||
|
||||
$entityRepository->relate($entity, 'emailAddresses', $emailAddressNew, null, [
|
||||
SaveOption::SKIP_HOOKS => true,
|
||||
]);
|
||||
$entityRepository
|
||||
->getRelation($entity, 'emailAddresses')
|
||||
->relate($emailAddressNew, null, [SaveOption::SKIP_HOOKS => true]);
|
||||
|
||||
if ($entity->has('emailAddressIsOptedOut')) {
|
||||
$this->markAddressOptedOut($emailAddressValue, (bool) $entity->get('emailAddressIsOptedOut'));
|
||||
|
||||
@@ -411,7 +411,7 @@ class Saver implements SaverInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$phoneNumberValue = trim($entity->get('phoneNumber'));
|
||||
$phoneNumberValue = trim($entity->get('phoneNumber') ?? '');
|
||||
|
||||
$entityRepository = $this->entityManager->getRDBRepository($entity->getEntityType());
|
||||
|
||||
@@ -425,8 +425,6 @@ class Saver implements SaverInterface
|
||||
])
|
||||
->findOne();
|
||||
|
||||
$isNewPhoneNumber = false;
|
||||
|
||||
if (!$phoneNumberNew) {
|
||||
$phoneNumberNew = $this->entityManager->getNewEntity(PhoneNumber::ENTITY_TYPE);
|
||||
|
||||
@@ -446,8 +444,6 @@ class Saver implements SaverInterface
|
||||
$phoneNumberNew->set('type', $defaultType);
|
||||
|
||||
$this->entityManager->saveEntity($phoneNumberNew);
|
||||
|
||||
$isNewPhoneNumber = true;
|
||||
}
|
||||
|
||||
$phoneNumberValueOld = $entity->getFetched('phoneNumber');
|
||||
@@ -456,15 +452,15 @@ class Saver implements SaverInterface
|
||||
$phoneNumberOld = $this->getByNumber($phoneNumberValueOld);
|
||||
|
||||
if ($phoneNumberOld) {
|
||||
$entityRepository->unrelate($entity, 'phoneNumbers', $phoneNumberOld, [
|
||||
SaveOption::SKIP_HOOKS => true,
|
||||
]);
|
||||
$entityRepository
|
||||
->getRelation($entity, 'phoneNumbers')
|
||||
->unrelate($phoneNumberOld, [SaveOption::SKIP_HOOKS => true]);
|
||||
}
|
||||
}
|
||||
|
||||
$entityRepository->relate($entity, 'phoneNumbers', $phoneNumberNew, null, [
|
||||
SaveOption::SKIP_HOOKS => true,
|
||||
]);
|
||||
$entityRepository
|
||||
->getRelation($entity, 'phoneNumbers')
|
||||
->relate($phoneNumberNew, null, [SaveOption::SKIP_HOOKS => true]);
|
||||
|
||||
if ($entity->has('phoneNumberIsOptedOut')) {
|
||||
$this->markNumberOptedOut($phoneNumberValue, (bool) $entity->get('phoneNumberIsOptedOut'));
|
||||
@@ -527,9 +523,9 @@ class Saver implements SaverInterface
|
||||
$phoneNumberOld = $this->getByNumber($phoneNumberValueOld);
|
||||
|
||||
if ($phoneNumberOld) {
|
||||
$entityRepository->unrelate($entity, 'phoneNumbers', $phoneNumberOld, [
|
||||
SaveOption::SKIP_HOOKS => true,
|
||||
]);
|
||||
$entityRepository
|
||||
->getRelation($entity, 'phoneNumbers')
|
||||
->unrelate($phoneNumberOld, [SaveOption::SKIP_HOOKS => true]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,11 +138,12 @@ class FieldValidationManager
|
||||
*/
|
||||
private function getMandatoryValidationList(string $entityType, string $field): array
|
||||
{
|
||||
/** @var ?string $fieldType */
|
||||
$fieldType = $this->fieldUtil->getEntityTypeFieldParam($entityType, $field, 'type');
|
||||
|
||||
return
|
||||
$this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'mandatoryValidationList']) ??
|
||||
$this->metadata->get(['fields', $fieldType, 'mandatoryValidationList']) ?? [];
|
||||
$this->metadata->get(['fields', $fieldType ?? '', 'mandatoryValidationList']) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,11 +151,12 @@ class FieldValidationManager
|
||||
*/
|
||||
private function getValidationList(string $entityType, string $field): array
|
||||
{
|
||||
/** @var ?string $fieldType */
|
||||
$fieldType = $this->fieldUtil->getEntityTypeFieldParam($entityType, $field, 'type');
|
||||
|
||||
return
|
||||
$this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'validationList']) ??
|
||||
$this->metadata->get(['fields', $fieldType, 'validationList']) ?? [];
|
||||
$this->metadata->get(['fields', $fieldType ?? '', 'validationList']) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -248,17 +250,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 +261,7 @@ class FieldValidationManager
|
||||
continue;
|
||||
}
|
||||
|
||||
$failure = new Failure($entityType, $field, $type);
|
||||
$failure = new Failure($entity->getEntityType(), $field, $type);
|
||||
|
||||
$failureList[] = $failure;
|
||||
|
||||
@@ -287,6 +279,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
|
||||
*/
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace Espo\Core\FieldValidation;
|
||||
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\FieldUtil;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use RuntimeException;
|
||||
@@ -40,7 +41,8 @@ class ValidatorFactory
|
||||
|
||||
public function __construct(
|
||||
private InjectableFactory $injectableFactory,
|
||||
private Metadata $metadata
|
||||
private Metadata $metadata,
|
||||
private FieldUtil $fieldUtil
|
||||
) {}
|
||||
|
||||
public function isCreatable(string $entityType, string $field, string $type): bool
|
||||
@@ -67,9 +69,12 @@ class ValidatorFactory
|
||||
*/
|
||||
private function getClassName(string $entityType, string $field, string $type): ?string
|
||||
{
|
||||
/** @var ?string $fieldType */
|
||||
$fieldType = $this->fieldUtil->getEntityTypeFieldParam($entityType, $field, 'type');
|
||||
|
||||
return
|
||||
$this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'validatorClassNameMap', $type]) ??
|
||||
$this->metadata->get(['fields', $field, 'validatorClassNameMap', $type]);
|
||||
$this->metadata->get(['fields', $fieldType ?? '', 'validatorClassNameMap', $type]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
namespace Espo\Core\Formula\Exceptions;
|
||||
|
||||
/**
|
||||
* Too few function arguments passsed.
|
||||
* Too few function arguments passed.
|
||||
*/
|
||||
class TooFewArguments extends Error
|
||||
{
|
||||
|
||||
@@ -29,26 +29,31 @@
|
||||
|
||||
namespace Espo\Core\Formula\Functions\JsonGroup;
|
||||
|
||||
use Espo\Core\Formula\{
|
||||
Functions\BaseFunction,
|
||||
ArgumentList,
|
||||
};
|
||||
use Espo\Core\Formula\ArgumentList;
|
||||
use Espo\Core\Formula\Exceptions\Error;
|
||||
use Espo\Core\Formula\Exceptions\ExecutionException;
|
||||
use Espo\Core\Formula\Exceptions\TooFewArguments;
|
||||
use Espo\Core\Formula\Functions\BaseFunction;
|
||||
|
||||
class RetrieveType extends BaseFunction
|
||||
{
|
||||
/**
|
||||
* @return mixed
|
||||
* @throws \Espo\Core\Formula\Exceptions\TooFewArguments
|
||||
* @throws \Espo\Core\Formula\Exceptions\Error
|
||||
* @throws TooFewArguments
|
||||
* @throws Error
|
||||
* @throws ExecutionException
|
||||
*/
|
||||
public function process(ArgumentList $args)
|
||||
{
|
||||
if (count($args) < 2) {
|
||||
if (count($args) < 1) {
|
||||
$this->throwTooFewArguments();
|
||||
}
|
||||
|
||||
$jsonString = $this->evaluate($args[0]);
|
||||
$path = $this->evaluate($args[1]);
|
||||
|
||||
$path = count($args) > 1 ?
|
||||
$this->evaluate($args[1]) :
|
||||
'';
|
||||
|
||||
if (!is_string($jsonString)) {
|
||||
$this->throwBadArgumentType(1, 'string');
|
||||
@@ -58,10 +63,6 @@ class RetrieveType extends BaseFunction
|
||||
$this->throwBadArgumentType(2, 'string');
|
||||
}
|
||||
|
||||
if ($path === '') {
|
||||
$this->throwBadArgumentValue(2);
|
||||
}
|
||||
|
||||
$item = json_decode($jsonString);
|
||||
|
||||
$pathArray = $this->splitPath($path);
|
||||
@@ -75,6 +76,10 @@ class RetrieveType extends BaseFunction
|
||||
*/
|
||||
private function splitPath(string $path): array
|
||||
{
|
||||
if ($path === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @var string[] $pathArray */
|
||||
$pathArray = preg_split('/(?<!\\\)\./', $path);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -85,6 +85,7 @@ class Sender
|
||||
private FileStorageManager $fileStorageManager
|
||||
) {
|
||||
|
||||
/** @noinspection PhpDeprecationInspection */
|
||||
$this->useGlobal();
|
||||
}
|
||||
|
||||
@@ -149,6 +150,7 @@ class Sender
|
||||
throw new InvalidArgumentException();
|
||||
}
|
||||
|
||||
/** @noinspection PhpDeprecationInspection */
|
||||
return $this->useSmtp($params);
|
||||
}
|
||||
|
||||
@@ -171,6 +173,7 @@ class Sender
|
||||
*/
|
||||
public function withEnvelopeOptions(array $options): self
|
||||
{
|
||||
/** @noinspection PhpDeprecationInspection */
|
||||
return $this->setEnvelopeOptions($options);
|
||||
}
|
||||
|
||||
@@ -252,8 +255,9 @@ class Sender
|
||||
$authMechanism = $params['authMechanism'] ?? $params['smtpAuthMechanism'] ?? null;
|
||||
|
||||
if ($authMechanism) {
|
||||
$authMechanism = preg_replace("([\.]{2,})", '', $authMechanism);
|
||||
$authMechanism = preg_replace("([.]{2,})", '', $authMechanism);
|
||||
|
||||
/** @noinspection SpellCheckingInspection */
|
||||
if (in_array($authMechanism, ['login', 'crammd5', 'plain'])) {
|
||||
$options['connectionClass'] = $authMechanism;
|
||||
}
|
||||
@@ -571,7 +575,7 @@ class Sender
|
||||
empty($messageId) ||
|
||||
!is_string($messageId) ||
|
||||
strlen($messageId) < 4 ||
|
||||
strpos($messageId, 'dummy:') === 0
|
||||
str_starts_with($messageId, 'dummy:')
|
||||
) {
|
||||
$messageId = $this->generateMessageId($email);
|
||||
|
||||
@@ -597,13 +601,17 @@ class Sender
|
||||
$email->set('dateSent', DateTime::createNow()->getString());
|
||||
}
|
||||
catch (Exception $e) {
|
||||
/** @noinspection PhpDeprecationInspection */
|
||||
$this->resetParams();
|
||||
/** @noinspection PhpDeprecationInspection */
|
||||
$this->useGlobal();
|
||||
|
||||
$this->handleException($e);
|
||||
}
|
||||
|
||||
/** @noinspection PhpDeprecationInspection */
|
||||
$this->resetParams();
|
||||
/** @noinspection PhpDeprecationInspection */
|
||||
$this->useGlobal();
|
||||
}
|
||||
|
||||
@@ -642,14 +650,14 @@ class Sender
|
||||
private function handleException(Exception $e): void
|
||||
{
|
||||
if ($e instanceof ProtocolRuntimeException) {
|
||||
$message = "Unknown error.";
|
||||
$message = "unknownError";
|
||||
|
||||
if (
|
||||
stripos($e->getMessage(), 'password') !== false ||
|
||||
stripos($e->getMessage(), 'credentials') !== false ||
|
||||
stripos($e->getMessage(), '5.7.8') !== false
|
||||
) {
|
||||
$message = 'Invalid credentials.';
|
||||
$message = 'invalidCredentials';
|
||||
}
|
||||
|
||||
$this->log->error("Email sending error: " . $e->getMessage());
|
||||
@@ -671,7 +679,7 @@ class Sender
|
||||
}
|
||||
else {
|
||||
$messageId =
|
||||
'' . md5($email->get('name')) . '/' .time() . '/' .
|
||||
md5($email->get('name')) . '/' .time() . '/' .
|
||||
$rand . '@espo';
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -44,6 +44,11 @@ class SaveOption
|
||||
* Import. Boolean.
|
||||
*/
|
||||
public const IMPORT = 'import';
|
||||
/**
|
||||
* Called from a Record service.
|
||||
* @since 8.0.1
|
||||
*/
|
||||
public const API = 'api';
|
||||
/**
|
||||
* Skip all additional processing. Boolean.
|
||||
*/
|
||||
|
||||
@@ -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,7 @@
|
||||
|
||||
namespace Espo\Core\Record\Duplicator;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Defs\FieldDefs;
|
||||
@@ -44,7 +45,8 @@ class EntityDuplicator
|
||||
public function __construct(
|
||||
private Defs $defs,
|
||||
private FieldDuplicatorFactory $fieldDuplicatorFactory,
|
||||
private FieldUtil $fieldUtil
|
||||
private FieldUtil $fieldUtil,
|
||||
private Metadata $metadata
|
||||
) {}
|
||||
|
||||
public function duplicate(Entity $entity): stdClass
|
||||
@@ -68,7 +70,7 @@ class EntityDuplicator
|
||||
$entityType = $entity->getEntityType();
|
||||
$field = $fieldDefs->getName();
|
||||
|
||||
if ($fieldDefs->getParam('duplicateIgnore')) {
|
||||
if ($this->toIgnoreField($entityType, $fieldDefs)) {
|
||||
$attributeList = $this->fieldUtil->getAttributeList($entityType, $field);
|
||||
|
||||
foreach ($attributeList as $attribute) {
|
||||
@@ -90,4 +92,20 @@ class EntityDuplicator
|
||||
$valueMap->$attribute = $value;
|
||||
}
|
||||
}
|
||||
|
||||
private function toIgnoreField(string $entityType, FieldDefs $fieldDefs): bool
|
||||
{
|
||||
$type = $fieldDefs->getType();
|
||||
|
||||
// @todo Use FieldType constants.
|
||||
if (in_array($type, ['autoincrement', 'number'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->metadata->get(['scopes', $entityType, 'statusField']) === $fieldDefs->getName()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (bool) $fieldDefs->getParam('duplicateIgnore');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,11 @@ 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\ORM\Repository\Option\SaveOption;
|
||||
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 +68,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 +144,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 +169,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 +190,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 +210,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 +222,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 +240,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 +259,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 +267,33 @@ 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(['*'])
|
||||
->withPrimaryFilter('one')
|
||||
)
|
||||
->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 +376,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 +525,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 +536,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 +607,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 +695,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);
|
||||
@@ -675,13 +706,13 @@ class Service implements Crud,
|
||||
|
||||
$this->beforeCreateEntity($entity, $data);
|
||||
|
||||
$this->entityManager->saveEntity($entity);
|
||||
$this->entityManager->saveEntity($entity, [SaveOption::API => true]);
|
||||
|
||||
$this->afterCreateEntity($entity, $data);
|
||||
$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 +762,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']) ??
|
||||
@@ -745,11 +776,16 @@ class Service implements Crud,
|
||||
$this->recordHookManager->processBeforeUpdate($entity, $params);
|
||||
$this->beforeUpdateEntity($entity, $data);
|
||||
|
||||
$this->entityManager->saveEntity($entity);
|
||||
$this->entityManager->saveEntity($entity, [SaveOption::API => true]);
|
||||
|
||||
$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 +821,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 +887,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 +895,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 +993,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 +1023,10 @@ class Service implements Crud,
|
||||
|
||||
$selectBuilder
|
||||
->from($foreignEntityType)
|
||||
->withSearchParams($preparedSearchParams);
|
||||
->withSearchParams($preparedSearchParams)
|
||||
->withAdditionalApplierClassNameList(
|
||||
$this->createSelectApplierClassNameListProvider()->get($foreignEntityType)
|
||||
);
|
||||
|
||||
if (!$skipAcl) {
|
||||
$selectBuilder->withStrictAccessControl();
|
||||
@@ -1133,7 +1169,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 +1187,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);
|
||||
|
||||
@@ -1351,7 +1387,7 @@ class Service implements Crud,
|
||||
}
|
||||
|
||||
if (!$id || !$link) {
|
||||
throw new BadRequest;
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$this->processForbiddenLinkEditCheck($link);
|
||||
@@ -1362,6 +1398,7 @@ class Service implements Crud,
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
// Not used link-check deliberately. Only edit access.
|
||||
if (!$this->acl->check($entity, AclTable::ACTION_EDIT)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
@@ -1373,7 +1410,7 @@ class Service implements Crud,
|
||||
}
|
||||
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
throw new LogicException("Only core entities are supported");
|
||||
throw new LogicException("Only core entities are supported.");
|
||||
}
|
||||
|
||||
$foreignEntityType = $entity->getRelationParam($link, 'entity');
|
||||
@@ -1739,21 +1776,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 {}
|
||||
|
||||
45
application/Espo/Core/Select/Primary/Filters/One.php
Normal file
45
application/Espo/Core/Select/Primary/Filters/One.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\Select\Primary\Filters;
|
||||
|
||||
use Espo\Core\Select\Primary\Filter;
|
||||
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
|
||||
|
||||
/**
|
||||
* A dummy filter 'one'. Applied only when reading a single record (from the detail view).
|
||||
* Can be detected in a custom AdditionalApplier to distinguish a read request from a find request.
|
||||
*/
|
||||
class One implements Filter
|
||||
{
|
||||
public const NAME = 'one';
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder): void
|
||||
{}
|
||||
}
|
||||
@@ -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,14 +29,17 @@
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* @todo Use WeakMap (User as a key).
|
||||
*/
|
||||
class UserAclManagerProvider
|
||||
{
|
||||
/** @var array<string, AclManager> */
|
||||
@@ -49,6 +52,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 +66,9 @@ class UserAclManagerProvider
|
||||
return $this->map[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotAvailable
|
||||
*/
|
||||
private function load(User $user): AclManager
|
||||
{
|
||||
$aclManager = $this->aclManager;
|
||||
@@ -72,7 +81,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)) {
|
||||
|
||||
@@ -56,6 +56,13 @@ class LinkMultiple implements FieldConverter
|
||||
'fieldType' => 'linkMultiple',
|
||||
]);
|
||||
|
||||
/** @var array<string, mixed> $defaults */
|
||||
$defaults = $fieldDefs->getParam('defaultAttributes') ?? [];
|
||||
|
||||
if (array_key_exists($idsName, $defaults)) {
|
||||
$idsDefs = $idsDefs->withDefault($defaults[$idsName]);
|
||||
}
|
||||
|
||||
$namesDefs = AttributeDefs::create($namesName)
|
||||
->withType(AttributeType::JSON_OBJECT)
|
||||
->withNotStorable()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user