mirror of
https://github.com/espocrm/espocrm.git
synced 2026-03-04 17:17:01 +00:00
Compare commits
714 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f512bbc9fa | ||
|
|
b8f5fe2b21 | ||
|
|
be73390fde | ||
|
|
17cd2bc543 | ||
|
|
74712ba931 | ||
|
|
0d22a238dd | ||
|
|
56e9170a6b | ||
|
|
b333fd6772 | ||
|
|
4eb469eb59 | ||
|
|
4ff0d3c654 | ||
|
|
4f1dd0673e | ||
|
|
caf05b5c26 | ||
|
|
2643ab7a17 | ||
|
|
6caea136a5 | ||
|
|
1d23d65910 | ||
|
|
8cc3de5807 | ||
|
|
9b022df709 | ||
|
|
aba061cb72 | ||
|
|
ae4d725595 | ||
|
|
6a76dc41d1 | ||
|
|
2b649c64a3 | ||
|
|
db2944cb15 | ||
|
|
3d40184373 | ||
|
|
e788c51ff3 | ||
|
|
d7fc389182 | ||
|
|
ecafbe823e | ||
|
|
8abde38ebf | ||
|
|
0fd7f768a9 | ||
|
|
9c38d68b65 | ||
|
|
f0de5dde53 | ||
|
|
2cdbf0ba76 | ||
|
|
34e4f4d7c2 | ||
|
|
61c0b58798 | ||
|
|
b2db0ea683 | ||
|
|
bd0fb02bb2 | ||
|
|
f0aed23416 | ||
|
|
65368519c7 | ||
|
|
53db6f22d6 | ||
|
|
5d47a987f4 | ||
|
|
a1f03e22d4 | ||
|
|
cddaa7b7a1 | ||
|
|
58dee36f1b | ||
|
|
ae24fc07ba | ||
|
|
13ca0db761 | ||
|
|
d2b20154de | ||
|
|
b176d866d0 | ||
|
|
07a9a79626 | ||
|
|
b60da458d5 | ||
|
|
40ffbc550f | ||
|
|
35076148cc | ||
|
|
3175ff9a76 | ||
|
|
81457b1e48 | ||
|
|
e9d5472d72 | ||
|
|
7c4bd371e7 | ||
|
|
98ed7e4dd3 | ||
|
|
8404ef1d83 | ||
|
|
0d99d84adf | ||
|
|
8492962923 | ||
|
|
99627f856f | ||
|
|
b917cc20f6 | ||
|
|
c582e88b7b | ||
|
|
9a00603600 | ||
|
|
550b5e3bb4 | ||
|
|
5ceb57a2cf | ||
|
|
16e96f6454 | ||
|
|
c162ee5337 | ||
|
|
66d34e7035 | ||
|
|
f57f7f72ed | ||
|
|
c911cfe310 | ||
|
|
223c8b53b4 | ||
|
|
add1154014 | ||
|
|
6ede731cfe | ||
|
|
0619cc0e3d | ||
|
|
06a686ad1a | ||
|
|
15aed06f9d | ||
|
|
012936dc7d | ||
|
|
0fb5b4a323 | ||
|
|
dc3cfb4c23 | ||
|
|
450e7894aa | ||
|
|
b8e7dbd147 | ||
|
|
a41c53d7f4 | ||
|
|
419faa2a47 | ||
|
|
1d3b181a1c | ||
|
|
e0d4ec4a28 | ||
|
|
f95d8cbdde | ||
|
|
e22c9151d1 | ||
|
|
f6ae58a05d | ||
|
|
cb3fc1c951 | ||
|
|
7f84fd2b4c | ||
|
|
518a67bf8a | ||
|
|
cc6af53eff | ||
|
|
7b27f62f10 | ||
|
|
16d69a1925 | ||
|
|
66fb39c98d | ||
|
|
9bfd68ad24 | ||
|
|
846665bd57 | ||
|
|
04f92584d4 | ||
|
|
3891422735 | ||
|
|
a806c3336d | ||
|
|
5310fd38bf | ||
|
|
507df94427 | ||
|
|
f55ea8ddbb | ||
|
|
865030914e | ||
|
|
e5baa91433 | ||
|
|
059eaccbe2 | ||
|
|
b432e1ad2c | ||
|
|
e9c25c1330 | ||
|
|
bc1a263b13 | ||
|
|
98ec83aa36 | ||
|
|
fe2b527561 | ||
|
|
57d57160dd | ||
|
|
c60964ac16 | ||
|
|
9d4c5a4d1b | ||
|
|
2e6cd93908 | ||
|
|
f9e4d35cad | ||
|
|
96ca03371e | ||
|
|
f3e4da9e0b | ||
|
|
bcde89b52e | ||
|
|
6becdc495d | ||
|
|
483130176c | ||
|
|
4481a3d10e | ||
|
|
0caf9a63fc | ||
|
|
82a08d3a6a | ||
|
|
a4bbf889ec | ||
|
|
a4beb96754 | ||
|
|
e4212161b5 | ||
|
|
f271ded65b | ||
|
|
5eff7ff3fe | ||
|
|
342542622f | ||
|
|
0be995a108 | ||
|
|
6486b0b4f8 | ||
|
|
c3f79087fd | ||
|
|
87caf8b58f | ||
|
|
4a94813509 | ||
|
|
8c9429d77a | ||
|
|
a0c3524056 | ||
|
|
beba210031 | ||
|
|
c50f28325a | ||
|
|
4efa575201 | ||
|
|
bf8bfb1d23 | ||
|
|
40eaa026b2 | ||
|
|
4c7d2d66f0 | ||
|
|
cfcf5bacea | ||
|
|
1f12365518 | ||
|
|
538a00b3eb | ||
|
|
ba623fe3f0 | ||
|
|
5a27041a54 | ||
|
|
c899cf4098 | ||
|
|
a99b84bb86 | ||
|
|
c0bede1670 | ||
|
|
868f80340b | ||
|
|
5f0b692bc4 | ||
|
|
73ad04c049 | ||
|
|
5d6cdc2fee | ||
|
|
fc712e0869 | ||
|
|
f049e3f482 | ||
|
|
19e517558b | ||
|
|
7c9a115574 | ||
|
|
93f51f7087 | ||
|
|
c9bb6dfdde | ||
|
|
5c14eda02a | ||
|
|
8ee0cb3af5 | ||
|
|
1404e2a90a | ||
|
|
b58d72e9fd | ||
|
|
be03a3e175 | ||
|
|
bf46d970b8 | ||
|
|
c36e405acd | ||
|
|
ebe4f78a62 | ||
|
|
5bda3e14d3 | ||
|
|
cac0684696 | ||
|
|
6b89707c6e | ||
|
|
efcdbbe30c | ||
|
|
ce4c86de9d | ||
|
|
d9b69ae61b | ||
|
|
412174e8bb | ||
|
|
2691a5dbaa | ||
|
|
eef34cec7c | ||
|
|
0449396678 | ||
|
|
5b456b9d44 | ||
|
|
adb44b1b0a | ||
|
|
4f5bd4b705 | ||
|
|
9370031805 | ||
|
|
8ee258272b | ||
|
|
fd5dfd769a | ||
|
|
fb6d6e7599 | ||
|
|
8c90cafcba | ||
|
|
36f3d3b479 | ||
|
|
3647fcb48e | ||
|
|
2368c3c8a7 | ||
|
|
60a041e454 | ||
|
|
5613938877 | ||
|
|
8817a1a19e | ||
|
|
5b4caedbef | ||
|
|
b431b1e095 | ||
|
|
dac295ea19 | ||
|
|
2c2ac08bf7 | ||
|
|
3b8e9d8e47 | ||
|
|
9682671de2 | ||
|
|
29559e571c | ||
|
|
cfb25d068d | ||
|
|
9b8c6bef99 | ||
|
|
168a1ea6e5 | ||
|
|
1515517bc3 | ||
|
|
7b20c9e448 | ||
|
|
008c9e6f7b | ||
|
|
d26edd74d1 | ||
|
|
6f3be6fdde | ||
|
|
603019116b | ||
|
|
0f7b5ea1f4 | ||
|
|
d62a83ba0c | ||
|
|
b873f850af | ||
|
|
16e45f6c4e | ||
|
|
5c34747a74 | ||
|
|
cd4099cf4b | ||
|
|
4ceb22c0ad | ||
|
|
cd76a63c2e | ||
|
|
6871416bc1 | ||
|
|
f401e08d0e | ||
|
|
c8dd17ffa7 | ||
|
|
766e8cbcf7 | ||
|
|
86ad6a262b | ||
|
|
614900443d | ||
|
|
0be7bbd57e | ||
|
|
5b45cc552f | ||
|
|
b6653f99be | ||
|
|
520b3c5e89 | ||
|
|
ebe2ca9e9d | ||
|
|
63740b17bb | ||
|
|
800a7af874 | ||
|
|
75bdd68779 | ||
|
|
0f9ac24f52 | ||
|
|
5ac39c53b3 | ||
|
|
3edc895edc | ||
|
|
ecc06c5917 | ||
|
|
c672e1b435 | ||
|
|
9165660286 | ||
|
|
60673c579f | ||
|
|
3967864df7 | ||
|
|
c76e288b6b | ||
|
|
a4549b0c12 | ||
|
|
3fc838a2b4 | ||
|
|
a81fbe6c92 | ||
|
|
34429bbcf5 | ||
|
|
1cb7d943ec | ||
|
|
a57af66b2b | ||
|
|
59c388a3f5 | ||
|
|
282c3e32fc | ||
|
|
e841d43a06 | ||
|
|
474d7b638c | ||
|
|
44c9d10cbb | ||
|
|
e863e3f56d | ||
|
|
432f5a5647 | ||
|
|
3545b560fe | ||
|
|
4e86ce2a77 | ||
|
|
4881787cf0 | ||
|
|
3172cdbd55 | ||
|
|
6601922c13 | ||
|
|
049d6bc4de | ||
|
|
9fddd6b1ba | ||
|
|
ea4762e3d0 | ||
|
|
cb44f0fe13 | ||
|
|
a8fa776713 | ||
|
|
cb82f0df0c | ||
|
|
c6fc914a4e | ||
|
|
c7096574ab | ||
|
|
c98a3ae40a | ||
|
|
39e4250998 | ||
|
|
6c44253798 | ||
|
|
4836bce5b0 | ||
|
|
184a889301 | ||
|
|
9b0609a7f1 | ||
|
|
824216035f | ||
|
|
e690231f05 | ||
|
|
22201f83b2 | ||
|
|
be4ab13f6d | ||
|
|
24186ae143 | ||
|
|
eba8cc630d | ||
|
|
e835e0d269 | ||
|
|
c420892800 | ||
|
|
ada7c067b3 | ||
|
|
8109c8460e | ||
|
|
aeddff23ad | ||
|
|
c93c05fe8c | ||
|
|
01d0020b28 | ||
|
|
aa78f4f9db | ||
|
|
7879b57ea3 | ||
|
|
a4794cac60 | ||
|
|
6bd6510ce9 | ||
|
|
88225eb590 | ||
|
|
ef1c1d2a24 | ||
|
|
f461f1968d | ||
|
|
2513edc53a | ||
|
|
40d03c6848 | ||
|
|
75d6a728b1 | ||
|
|
739d1dd3d1 | ||
|
|
97b338fc5d | ||
|
|
5350cf8e52 | ||
|
|
0c2959da80 | ||
|
|
37cba66ad7 | ||
|
|
03606ae644 | ||
|
|
292453e425 | ||
|
|
9639840b46 | ||
|
|
db6cdec9b5 | ||
|
|
b74c17c582 | ||
|
|
20a139ca0a | ||
|
|
4fd5555146 | ||
|
|
2e4cc5f0be | ||
|
|
983dc7c376 | ||
|
|
f461434fb4 | ||
|
|
907655bb09 | ||
|
|
41d6a0a099 | ||
|
|
573d14f854 | ||
|
|
c0719694ba | ||
|
|
49aea5aad1 | ||
|
|
076569027a | ||
|
|
2a7cc3aeb0 | ||
|
|
89dca6086d | ||
|
|
c00bbdb0fe | ||
|
|
60bdf881b1 | ||
|
|
ae9237c4b9 | ||
|
|
51a221809b | ||
|
|
d7d176c169 | ||
|
|
374413eaae | ||
|
|
96538a2b6f | ||
|
|
39a3a6dbd2 | ||
|
|
58d959fdef | ||
|
|
3de7238d7b | ||
|
|
66b2065d7c | ||
|
|
e4b012c40f | ||
|
|
adf5283375 | ||
|
|
1f5baa9d70 | ||
|
|
7d09306047 | ||
|
|
c3ae8cdd62 | ||
|
|
fef6ed5f34 | ||
|
|
778b212bd0 | ||
|
|
d675bdfdf2 | ||
|
|
0923414af1 | ||
|
|
d9b0dce5f4 | ||
|
|
b28bae20b7 | ||
|
|
a235ad1bc3 | ||
|
|
db0114b600 | ||
|
|
2aad48505e | ||
|
|
347493dbbd | ||
|
|
f09e69e9b1 | ||
|
|
2c410f4ecc | ||
|
|
5e26f8da37 | ||
|
|
8d6b86529b | ||
|
|
18859963e5 | ||
|
|
37828e5716 | ||
|
|
27bb69a4f7 | ||
|
|
c41c994772 | ||
|
|
5610569fe7 | ||
|
|
ba0ad4dfe9 | ||
|
|
049443f4cc | ||
|
|
ba76ea086c | ||
|
|
a7beec2548 | ||
|
|
49bc5a5d5d | ||
|
|
d9fd6bfc87 | ||
|
|
4d5f0fce6c | ||
|
|
e7114d9c4c | ||
|
|
8957a9ba5b | ||
|
|
a9ce82c8e9 | ||
|
|
85f1e05860 | ||
|
|
5bdf4cb099 | ||
|
|
c52b1f8cc3 | ||
|
|
5176aef78c | ||
|
|
8fc0ed81ff | ||
|
|
b84838bfbb | ||
|
|
2adf724458 | ||
|
|
fd8f546d1f | ||
|
|
c33de304dc | ||
|
|
c6afb86765 | ||
|
|
71e2af6c4c | ||
|
|
15b74e19d7 | ||
|
|
74eece5956 | ||
|
|
ada764bb7f | ||
|
|
e2e99009a3 | ||
|
|
8e879e2d73 | ||
|
|
7808586751 | ||
|
|
23e66d58d3 | ||
|
|
214b8baf8f | ||
|
|
756a913e94 | ||
|
|
720bdc7aaf | ||
|
|
b49d1e2e2c | ||
|
|
c0e76b05e2 | ||
|
|
ca0e7c781e | ||
|
|
50bcc77cd0 | ||
|
|
6decfd05a0 | ||
|
|
6bae680e4a | ||
|
|
3532f4136c | ||
|
|
af406bb473 | ||
|
|
78decd9b67 | ||
|
|
4d2a27d908 | ||
|
|
f2afdcba6f | ||
|
|
2d4c43e51a | ||
|
|
43124e3c06 | ||
|
|
7b80a0a60e | ||
|
|
74fc9007dd | ||
|
|
e0705293e1 | ||
|
|
29f3f7c330 | ||
|
|
ef2d83ab08 | ||
|
|
ba9520d6f5 | ||
|
|
1cbab04c35 | ||
|
|
e8deed9244 | ||
|
|
b3ff690497 | ||
|
|
a8f03abd1d | ||
|
|
9be41a4e37 | ||
|
|
0e0e81f5ae | ||
|
|
b020cc1c39 | ||
|
|
4eab7f6a88 | ||
|
|
8f213f819b | ||
|
|
4bd6f761e5 | ||
|
|
9dfcbb7c8e | ||
|
|
488981753a | ||
|
|
7622cdc1cf | ||
|
|
bf503db267 | ||
|
|
fe6ffe4555 | ||
|
|
028ad25fc6 | ||
|
|
a6d0a90c73 | ||
|
|
716cd2e729 | ||
|
|
034e06f636 | ||
|
|
dfa4627998 | ||
|
|
942b6f3305 | ||
|
|
fda03757fc | ||
|
|
6955ea581c | ||
|
|
950d98528a | ||
|
|
8b2d62d27a | ||
|
|
2b3d1e9ce0 | ||
|
|
15d5985ec4 | ||
|
|
bd94543b71 | ||
|
|
ab93196432 | ||
|
|
d8c5955ba4 | ||
|
|
0f64611843 | ||
|
|
7244204f35 | ||
|
|
6eee3c3432 | ||
|
|
3801de3449 | ||
|
|
d97fefebf8 | ||
|
|
06d55f2746 | ||
|
|
c54bc30bda | ||
|
|
f2138a4e23 | ||
|
|
8d0bb8fc12 | ||
|
|
57c5d4b15c | ||
|
|
41433ecbf5 | ||
|
|
e0a4c5b6a8 | ||
|
|
219a77f121 | ||
|
|
43c0299c25 | ||
|
|
7b65cba5e4 | ||
|
|
0e3b3c4193 | ||
|
|
35bbe00e17 | ||
|
|
673c18a5ce | ||
|
|
dc32b278f9 | ||
|
|
58c767c0a6 | ||
|
|
7678316b5a | ||
|
|
988622629a | ||
|
|
c090b4629b | ||
|
|
a9a3759d6e | ||
|
|
19b4e91e25 | ||
|
|
f0d98542e4 | ||
|
|
a32ed9c8f2 | ||
|
|
67635f0b20 | ||
|
|
5d90b4f070 | ||
|
|
c212eb1f20 | ||
|
|
5ae0b5a3f2 | ||
|
|
06d907d633 | ||
|
|
737c5ff2ba | ||
|
|
ba19934c06 | ||
|
|
8885bb87be | ||
|
|
690c873291 | ||
|
|
158cdd27bc | ||
|
|
51286a2c46 | ||
|
|
16538077ee | ||
|
|
3181e54aa6 | ||
|
|
8028a153af | ||
|
|
0a03750d26 | ||
|
|
e9b1a91180 | ||
|
|
8677dd48ad | ||
|
|
c3d309d07e | ||
|
|
63e78a8895 | ||
|
|
4b54a962b2 | ||
|
|
f3d2f0b453 | ||
|
|
fe638db414 | ||
|
|
e51be3fd85 | ||
|
|
4fd345cb88 | ||
|
|
41e2de18aa | ||
|
|
df567a0603 | ||
|
|
419fb93a91 | ||
|
|
c7473be8c7 | ||
|
|
311a01ef41 | ||
|
|
1a163907a3 | ||
|
|
2acdc00547 | ||
|
|
b622d8865c | ||
|
|
786fcfb580 | ||
|
|
eec8512f96 | ||
|
|
be62caef2b | ||
|
|
eabd52b186 | ||
|
|
101087680b | ||
|
|
372f1770e0 | ||
|
|
a44dcdc9ff | ||
|
|
d44b172602 | ||
|
|
241d7610ba | ||
|
|
6929b2d717 | ||
|
|
e743f61913 | ||
|
|
7f4e5dd01a | ||
|
|
8a1b58cf7c | ||
|
|
2a20766912 | ||
|
|
b49679eb90 | ||
|
|
b2ea073c16 | ||
|
|
54085a59c3 | ||
|
|
858658768d | ||
|
|
86421010b9 | ||
|
|
8675db36f3 | ||
|
|
f36a98a824 | ||
|
|
3c71ff05f3 | ||
|
|
6039939308 | ||
|
|
1ddea9938b | ||
|
|
5d3b515ef1 | ||
|
|
31b7db4033 | ||
|
|
6a7d59ba20 | ||
|
|
0ab0e46d67 | ||
|
|
2777fe49d9 | ||
|
|
1e246e8197 | ||
|
|
d8eec1068d | ||
|
|
0021e16962 | ||
|
|
b046da1d79 | ||
|
|
2ca8f3af6f | ||
|
|
b180ac2314 | ||
|
|
9cc646e3b8 | ||
|
|
a2fee68732 | ||
|
|
dc0d150000 | ||
|
|
76cc4a8d2c | ||
|
|
6e4c097dee | ||
|
|
af398cb5d4 | ||
|
|
3865bba926 | ||
|
|
3ad39d3337 | ||
|
|
1dfe1eb9bf | ||
|
|
7885a9cac3 | ||
|
|
bb63751332 | ||
|
|
083770f705 | ||
|
|
a65862325f | ||
|
|
9465ca46b2 | ||
|
|
62f05f370f | ||
|
|
8e408d93cb | ||
|
|
7c1d587ae0 | ||
|
|
cb42bfe8e7 | ||
|
|
32a340afe0 | ||
|
|
0ff8246a00 | ||
|
|
c9c58d373a | ||
|
|
af7aea720b | ||
|
|
dee488a9de | ||
|
|
88ec71400a | ||
|
|
10ad5a804a | ||
|
|
45f948d96c | ||
|
|
e5e6a32ce5 | ||
|
|
cf5202a065 | ||
|
|
f37a49ddaf | ||
|
|
4858f964c8 | ||
|
|
5b50215242 | ||
|
|
461e356419 | ||
|
|
7146f3d3aa | ||
|
|
43d289bc56 | ||
|
|
65c843b581 | ||
|
|
d08704c95c | ||
|
|
fb9100efdc | ||
|
|
ee2182638a | ||
|
|
8ba9fb4693 | ||
|
|
b427d8cade | ||
|
|
89de19fd2e | ||
|
|
2f4df63281 | ||
|
|
e12eade143 | ||
|
|
15b0d147d9 | ||
|
|
4901b8c5a0 | ||
|
|
8c4f3b1103 | ||
|
|
2225906eac | ||
|
|
895b3aa7c9 | ||
|
|
67b359ff18 | ||
|
|
62200d393e | ||
|
|
166522e4e6 | ||
|
|
cc3d7a9f20 | ||
|
|
81ce479293 | ||
|
|
afa6591903 | ||
|
|
e89cb2547e | ||
|
|
7b982acc3e | ||
|
|
e24fc19314 | ||
|
|
0d9473b1e9 | ||
|
|
db4ccbe590 | ||
|
|
f085d2140c | ||
|
|
4f2117453d | ||
|
|
919fff6cd5 | ||
|
|
b77f0a705e | ||
|
|
73699894be | ||
|
|
4282d1766b | ||
|
|
3ebb9ea8fc | ||
|
|
5635fddf3d | ||
|
|
9501e116cb | ||
|
|
c0997f1d97 | ||
|
|
106961ff03 | ||
|
|
fd2b9cc818 | ||
|
|
d90853ff73 | ||
|
|
a8bcd1dc2f | ||
|
|
1f4ae0f07c | ||
|
|
cf0a9fd808 | ||
|
|
036e10cbf7 | ||
|
|
0a22510566 | ||
|
|
44f14c1d29 | ||
|
|
e6796390db | ||
|
|
c11d52cef6 | ||
|
|
33e8b8b0c1 | ||
|
|
04dbe9129a | ||
|
|
8e1a4ba368 | ||
|
|
920b5244a8 | ||
|
|
cd57f6c600 | ||
|
|
7e6a32ab06 | ||
|
|
ee0dc257b2 | ||
|
|
a8e6421517 | ||
|
|
1c5d2bd756 | ||
|
|
1fc59b28aa | ||
|
|
ed603c5f98 | ||
|
|
0b7743419e | ||
|
|
81ff01e9be | ||
|
|
4845622949 | ||
|
|
122cd9c56d | ||
|
|
379562b0d7 | ||
|
|
c72d6d928a | ||
|
|
b09c1366e3 | ||
|
|
00e0427fdd | ||
|
|
85f888d60b | ||
|
|
f22856dd91 | ||
|
|
5673d253e4 | ||
|
|
27ce836d4a | ||
|
|
a490ded36a | ||
|
|
bd54195403 | ||
|
|
21d8e291e4 | ||
|
|
b4fa62b403 | ||
|
|
1f3d7063d2 | ||
|
|
e5709571e4 | ||
|
|
a80d847f44 | ||
|
|
9de7105cde | ||
|
|
cd7cc188e1 | ||
|
|
e68a5f1979 | ||
|
|
c3e16a896e | ||
|
|
eafd0c3840 | ||
|
|
d5b5bb31c7 | ||
|
|
eaab96732a | ||
|
|
0dfed7279e | ||
|
|
b0e973d8ba | ||
|
|
1d809c10b0 | ||
|
|
e0279f403f | ||
|
|
fa75da658c | ||
|
|
d23689aab7 | ||
|
|
d4cc305265 | ||
|
|
695d4b0cda | ||
|
|
2107d93cd2 | ||
|
|
fb256ca29f | ||
|
|
557bcb4ecd | ||
|
|
50a272473b | ||
|
|
0eb9544371 | ||
|
|
620fa607d4 | ||
|
|
304d216b0e | ||
|
|
ce2fe9d50c | ||
|
|
eadf7835db | ||
|
|
048f65c8f6 | ||
|
|
c30c4163a6 | ||
|
|
8d6db72516 | ||
|
|
71864056bc | ||
|
|
e375105a5f | ||
|
|
e066f4c6b0 | ||
|
|
b032aa4d8d | ||
|
|
342a18e0ac | ||
|
|
e27e9de701 | ||
|
|
b14b199d43 | ||
|
|
f77a7c32d5 | ||
|
|
9f84d3f233 | ||
|
|
be82244c6b | ||
|
|
7fed72d391 | ||
|
|
54d615b64d | ||
|
|
2dbbd1e23d | ||
|
|
cba8867389 | ||
|
|
32d47db15a | ||
|
|
2c65c9ff9f | ||
|
|
fa5d63253b | ||
|
|
b7ae252d3d | ||
|
|
f07b43abda | ||
|
|
276db37baf | ||
|
|
6adad84a29 | ||
|
|
f137298c49 | ||
|
|
f8839518d5 | ||
|
|
1ae39d1a40 | ||
|
|
94f920324e | ||
|
|
dbc3f7c7d6 | ||
|
|
94c00e2901 | ||
|
|
7de988104b | ||
|
|
03bc90968c | ||
|
|
a5401a22b9 | ||
|
|
4c1375d8b2 | ||
|
|
bab4a3b0d6 | ||
|
|
33e135f227 | ||
|
|
56b9d8d5c1 | ||
|
|
38ec88e302 | ||
|
|
10be208d8d | ||
|
|
38d227a948 | ||
|
|
1a8e4435e2 | ||
|
|
12febbdff5 | ||
|
|
ebea168350 | ||
|
|
704519f80b | ||
|
|
5f1000ddd3 | ||
|
|
2ee7e6cf4a | ||
|
|
2989975829 | ||
|
|
a4c068427d | ||
|
|
e1a76b9924 | ||
|
|
1f2efcd716 | ||
|
|
576dfe068f | ||
|
|
f76860cdfd | ||
|
|
7c549556ac | ||
|
|
a6b5f38aed |
@@ -28,6 +28,7 @@ module.exports = function (grunt) {
|
||||
'client/lib/handlebars.js',
|
||||
'client/lib/base64.js',
|
||||
'client/lib/jquery-ui.min.js',
|
||||
'client/lib/jquery.ui.touch-punch.min.js',
|
||||
'client/lib/moment.min.js',
|
||||
'client/lib/moment-timezone-with-data.min.js',
|
||||
'client/lib/jquery.timepicker.min.js',
|
||||
@@ -170,6 +171,7 @@ module.exports = function (grunt) {
|
||||
'html/**',
|
||||
'bootstrap.php',
|
||||
'cron.php',
|
||||
'daemon.php',
|
||||
'rebuild.php',
|
||||
'clear_cache.php',
|
||||
'upgrade.php',
|
||||
|
||||
16
README.md
16
README.md
@@ -9,7 +9,7 @@ Download the latest release from our [website](http://www.espocrm.com).
|
||||
### Requirements
|
||||
|
||||
* PHP 5.6 or above (with pdo, json, gd, openssl, zip, imap, mbstring, curl extensions);
|
||||
* MySQL 5.5.3 or above.
|
||||
* MySQL 5.5.3 or above, or MariaDB.
|
||||
|
||||
For more information about server configuration see [this article](https://www.espocrm.com/documentation/administration/server-configuration/).
|
||||
|
||||
@@ -17,10 +17,14 @@ For more information about server configuration see [this article](https://www.e
|
||||
|
||||
Documentation for administrators, users and developers is available [here](https://www.espocrm.com/documentation/).
|
||||
|
||||
### How to report bug
|
||||
### How to report a bug
|
||||
|
||||
Create an issue [here](https://github.com/espocrm/espocrm/issues) or post on our [forum](http://forum.espocrm.com/forum/bug-reports).
|
||||
|
||||
### How to install a stable version
|
||||
|
||||
[Download](https://www.espocrm.com/download/) the latest version. See the [instructions](https://www.espocrm.com/documentation/administration/installation/) about installation.
|
||||
|
||||
### How to get started (for developers)
|
||||
|
||||
1. Clone repository to your local computer.
|
||||
@@ -34,7 +38,7 @@ Now you can build. Build will create compiled css files.
|
||||
|
||||
To compose a proper config.php and populate database you can run install by opening `http(s)://{YOUR_CRM_URL}/install` location in a browser. Then open `data/config.php` file and add `isDeveloperMode => true`.
|
||||
|
||||
### How to build
|
||||
### How to build (for developers)
|
||||
|
||||
You need to have nodejs and Grunt CLI installed.
|
||||
|
||||
@@ -48,6 +52,12 @@ The build will be created in the `build` directory.
|
||||
|
||||
Before we can merge your pull request you need to accept our CLA [here](https://github.com/espocrm/cla). It's very simple to do.
|
||||
|
||||
Branches:
|
||||
|
||||
* hotfix/* – an upcoming maintenance release; fixes should be pushed to this branch;
|
||||
* master – an upcoming minor or major release; new features should be pushed to this branch;
|
||||
* stable – a last stable release.
|
||||
|
||||
### How to make a translation
|
||||
|
||||
Build po file with command:
|
||||
|
||||
@@ -34,6 +34,7 @@ use \Espo\ORM\Entity;
|
||||
|
||||
class Email extends \Espo\Core\Acl\Base
|
||||
{
|
||||
protected $ownerUserIdAttribute = 'usersIds';
|
||||
|
||||
public function checkEntityRead(EntityUser $user, Entity $entity, $data)
|
||||
{
|
||||
@@ -118,4 +119,3 @@ class Email extends \Espo\Core\Acl\Base
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,9 @@ class EmailAddress extends \Espo\Core\Acl\Base
|
||||
if (!$this->getAclManager()->check($user, $e, 'edit')) {
|
||||
$isFobidden = true;
|
||||
if (
|
||||
$e->get('isPortalUser') && $excludeEntity->getEntityType() === 'Contact' &&
|
||||
$e->getEntityType() === 'User' &&
|
||||
$e->isPortal() &&
|
||||
$excludeEntity->getEntityType() === 'Contact' &&
|
||||
$e->get('contactId') === $excludeEntity->id
|
||||
) {
|
||||
$isFobidden = false;
|
||||
|
||||
53
application/Espo/Acl/Import.php
Normal file
53
application/Espo/Acl/Import.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: http://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\Acl;
|
||||
|
||||
use \Espo\Entities\User as EntityUser;
|
||||
use \Espo\ORM\Entity;
|
||||
|
||||
class Import extends \Espo\Core\Acl\Base
|
||||
{
|
||||
|
||||
public function checkEntityRead(EntityUser $user, Entity $entity, $data)
|
||||
{
|
||||
if ($user->isAdmin()) return true;
|
||||
if ($user->id === $entity->get('createdById')) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function checkEntityDelete(EntityUser $user, Entity $entity, $data)
|
||||
{
|
||||
if ($user->isAdmin()) return true;
|
||||
if ($user->id === $entity->get('createdById')) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,10 @@ use \Espo\ORM\Entity;
|
||||
|
||||
class Note extends \Espo\Core\Acl\Base
|
||||
{
|
||||
protected $deleteThresholdPeriod = '1 month';
|
||||
|
||||
protected $editThresholdPeriod = '7 days';
|
||||
|
||||
public function checkIsOwner(EntityUser $user, Entity $entity)
|
||||
{
|
||||
if ($entity->get('type') === 'Post' && $user->id === $entity->get('createdById')) {
|
||||
@@ -41,5 +45,60 @@ class Note extends \Espo\Core\Acl\Base
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function checkEntityEdit(EntityUser $user, Entity $entity, $data)
|
||||
{
|
||||
if ($user->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->checkEntity($user, $entity, $data, 'edit')) {
|
||||
if ($this->checkIsOwner($user, $entity)) {
|
||||
$createdAt = $entity->get('createdAt');
|
||||
if ($createdAt) {
|
||||
$noteEditThresholdPeriod = '-' . $this->getConfig()->get('noteEditThresholdPeriod', $this->editThresholdPeriod);
|
||||
$dt = new \DateTime();
|
||||
$dt->modify($noteEditThresholdPeriod);
|
||||
try {
|
||||
if ($dt->format('U') > (new \DateTime($createdAt))->format('U')) {
|
||||
return false;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function checkEntityDelete(EntityUser $user, Entity $entity, $data)
|
||||
{
|
||||
if ($user->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->checkEntity($user, $entity, $data, 'delete')) {
|
||||
if ($this->checkIsOwner($user, $entity)) {
|
||||
$createdAt = $entity->get('createdAt');
|
||||
if ($createdAt) {
|
||||
$deleteThresholdPeriod = '-' . $this->getConfig()->get('noteDeleteThresholdPeriod', $this->deleteThresholdPeriod);
|
||||
$dt = new \DateTime();
|
||||
$dt->modify($deleteThresholdPeriod);
|
||||
try {
|
||||
if ($dt->format('U') > (new \DateTime($createdAt))->format('U')) {
|
||||
return false;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,9 @@ class PhoneNumber extends \Espo\Core\Acl\Base
|
||||
if (!$this->getAclManager()->check($user, $e, 'edit')) {
|
||||
$isFobidden = true;
|
||||
if (
|
||||
$e->get('isPortalUser') && $excludeEntity->getEntityType() === 'Contact' &&
|
||||
$e->getEntityType() === 'User' &&
|
||||
$e->isPortal() &&
|
||||
$excludeEntity->getEntityType() === 'Contact' &&
|
||||
$e->get('contactId') === $excludeEntity->id
|
||||
) {
|
||||
$isFobidden = false;
|
||||
|
||||
@@ -44,6 +44,9 @@ class User extends \Espo\Core\Acl\Base
|
||||
if (!$user->isAdmin()) {
|
||||
return false;
|
||||
}
|
||||
if ($entity->isSuperAdmin() && !$user->isSuperAdmin()) {
|
||||
return false;
|
||||
}
|
||||
return $this->checkEntity($user, $entity, $data, 'create');
|
||||
}
|
||||
|
||||
@@ -55,6 +58,12 @@ class User extends \Espo\Core\Acl\Base
|
||||
if (!$user->isAdmin()) {
|
||||
return false;
|
||||
}
|
||||
if ($entity->isSystem()) {
|
||||
return false;
|
||||
}
|
||||
if ($entity->isSuperAdmin() && !$user->isSuperAdmin()) {
|
||||
return false;
|
||||
}
|
||||
return parent::checkEntityDelete($user, $entity, $data);
|
||||
}
|
||||
|
||||
@@ -63,11 +72,17 @@ class User extends \Espo\Core\Acl\Base
|
||||
if ($entity->id === 'system') {
|
||||
return false;
|
||||
}
|
||||
if ($entity->isSystem()) {
|
||||
return false;
|
||||
}
|
||||
if (!$user->isAdmin()) {
|
||||
if ($user->id !== $entity->id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if ($entity->isSuperAdmin() && !$user->isSuperAdmin()) {
|
||||
return false;
|
||||
}
|
||||
return $this->checkEntity($user, $entity, $data, 'edit');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ use \Espo\ORM\Entity;
|
||||
|
||||
class Email extends \Espo\Core\AclPortal\Base
|
||||
{
|
||||
protected $ownerUserIdAttribute = 'usersIds';
|
||||
|
||||
public function checkEntityRead(EntityUser $user, Entity $entity, $data)
|
||||
{
|
||||
|
||||
@@ -69,7 +69,7 @@ class Admin extends \Espo\Core\Controllers\Base
|
||||
public function postActionUploadUpgradePackage($params, $data)
|
||||
{
|
||||
if ($this->getConfig()->get('restrictedMode')) {
|
||||
if (!$this->getUser()->get('isSuperAdmin')) {
|
||||
if (!$this->getUser()->isSuperAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ class Admin extends \Espo\Core\Controllers\Base
|
||||
public function postActionRunUpgrade($params, $data)
|
||||
{
|
||||
if ($this->getConfig()->get('restrictedMode')) {
|
||||
if (!$this->getUser()->get('isSuperAdmin')) {
|
||||
if (!$this->getUser()->isSuperAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
@@ -108,4 +108,10 @@ class Admin extends \Espo\Core\Controllers\Base
|
||||
$adminNotificationManager = new \Espo\Core\Utils\AdminNotificationManager($this->getContainer());
|
||||
return $adminNotificationManager->getNotificationList();
|
||||
}
|
||||
|
||||
public function actionSystemRequirementList($params)
|
||||
{
|
||||
$systemRequirementManager = new \Espo\Core\Utils\SystemRequirements($this->getContainer());
|
||||
return $systemRequirementManager->getAllRequiredList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,5 +34,27 @@ use \Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
class Attachment extends \Espo\Core\Controllers\Record
|
||||
{
|
||||
public function actionList($params, $data, $request)
|
||||
{
|
||||
if (!$this->getUser()->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
return parent::actionList($params, $data, $request);
|
||||
}
|
||||
|
||||
public function postActionGetAttachmentFromImageUrl($params, $data)
|
||||
{
|
||||
if (empty($data->url)) throw new BadRequest();
|
||||
if (empty($data->field)) throw new BadRequest('postActionGetAttachmentFromImageUrl: No field specified');
|
||||
|
||||
return $this->getRecordService()->getAttachmentFromImageUrl($data)->getValueMap();
|
||||
}
|
||||
|
||||
public function postActionGetCopiedAttachment($params, $data)
|
||||
{
|
||||
if (empty($data->id)) throw new BadRequest();
|
||||
if (empty($data->field)) throw new BadRequest('postActionGetCopiedAttachment copy: No field specified');
|
||||
|
||||
return $this->getRecordService()->getCopiedAttachment($data)->getValueMap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,11 +42,13 @@ class EmailAddress extends \Espo\Core\Controllers\Record
|
||||
throw new Forbidden();
|
||||
}
|
||||
$q = $request->get('q');
|
||||
$limit = intval($request->get('limit'));
|
||||
if (empty($limit) || $limit > 30) {
|
||||
$limit = 5;
|
||||
$maxSize = intval($request->get('maxSize'));
|
||||
if (empty($maxSize) || $maxSize > 50) {
|
||||
$maxSize = $this->getConfig()->get('recordsPerPage', 20);
|
||||
}
|
||||
return $this->getRecordService()->searchInAddressBook($q, $limit);
|
||||
|
||||
$onlyActual = $request->get('onlyActual') === 'true';
|
||||
|
||||
return $this->getRecordService()->searchInAddressBook($q, $maxSize, $onlyActual);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ class EntityManager extends \Espo\Core\Controllers\Base
|
||||
$params['iconClass'] = $data['iconClass'];
|
||||
}
|
||||
if (isset($data['fullTextSearch'])) {
|
||||
$params['fullTestSearch'] = $data['fullTextSearch'];
|
||||
$params['fullTextSearch'] = $data['fullTextSearch'];
|
||||
}
|
||||
|
||||
$params['kanbanViewMode'] = !empty($data['kanbanViewMode']);
|
||||
@@ -130,10 +130,6 @@ class EntityManager extends \Espo\Core\Controllers\Base
|
||||
$name = $data['name'];
|
||||
$name = filter_var($name, \FILTER_SANITIZE_STRING);
|
||||
|
||||
if (!empty($data['sortDirection'])) {
|
||||
$data['asc'] = $data['sortDirection'] === 'asc';
|
||||
}
|
||||
|
||||
$result = $this->getContainer()->get('entityManagerUtil')->update($name, $data);
|
||||
|
||||
if ($result) {
|
||||
|
||||
@@ -66,7 +66,7 @@ class Extension extends \Espo\Core\Controllers\Record
|
||||
throw new Forbidden();
|
||||
}
|
||||
if ($this->getConfig()->get('restrictedMode')) {
|
||||
if (!$this->getUser()->get('isSuperAdmin')) {
|
||||
if (!$this->getUser()->isSuperAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ class Extension extends \Espo\Core\Controllers\Record
|
||||
throw new Forbidden();
|
||||
}
|
||||
if ($this->getConfig()->get('restrictedMode')) {
|
||||
if (!$this->getUser()->get('isSuperAdmin')) {
|
||||
if (!$this->getUser()->isSuperAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
@@ -120,7 +120,7 @@ class Extension extends \Espo\Core\Controllers\Record
|
||||
throw BadRequest();
|
||||
}
|
||||
if ($this->getConfig()->get('restrictedMode')) {
|
||||
if (!$this->getUser()->get('isSuperAdmin')) {
|
||||
if (!$this->getUser()->isSuperAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class Import extends \Espo\Core\Controllers\Record
|
||||
{
|
||||
protected function checkControllerAccess()
|
||||
{
|
||||
if (!$this->getUser()->isAdmin()) {
|
||||
if (!$this->getAcl()->check('Import')) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
@@ -90,10 +90,9 @@ class Import extends \Espo\Core\Controllers\Record
|
||||
$attachment->set('type', 'text/csv');
|
||||
$attachment->set('role', 'Import File');
|
||||
$attachment->set('name', 'import-file.csv');
|
||||
$attachment->set('contents', $contents);
|
||||
$this->getEntityManager()->saveEntity($attachment);
|
||||
|
||||
$this->getFileStorageManager()->putContents($attachment, $contents);
|
||||
|
||||
return array(
|
||||
'attachmentId' => $attachment->id
|
||||
);
|
||||
@@ -127,7 +126,7 @@ class Import extends \Espo\Core\Controllers\Record
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
if (!isset($data->fieldDelimiter)) {
|
||||
if (!isset($data->delimiter)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
@@ -167,7 +166,7 @@ class Import extends \Espo\Core\Controllers\Record
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
if (!isset($data->fields)) {
|
||||
if (!isset($data->attributeList)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
@@ -178,7 +177,7 @@ class Import extends \Espo\Core\Controllers\Record
|
||||
|
||||
$importParams = array(
|
||||
'headerRow' => !empty($data->headerRow),
|
||||
'fieldDelimiter' => $data->fieldDelimiter,
|
||||
'delimiter' => $data->delimiter,
|
||||
'textQualifier' => $data->textQualifier,
|
||||
'dateFormat' => $data->dateFormat,
|
||||
'timeFormat' => $data->timeFormat,
|
||||
@@ -202,7 +201,7 @@ class Import extends \Espo\Core\Controllers\Record
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
return $this->getService('Import')->import($data->entityType, $data->fields, $attachmentId, $importParams);
|
||||
return $this->getService('Import')->import($data->entityType, $data->attributeList, $attachmentId, $importParams);
|
||||
}
|
||||
|
||||
public function postActionUnmarkAsDuplicate($params, $data)
|
||||
|
||||
@@ -35,12 +35,24 @@ class LastViewed extends \Espo\Core\Controllers\Base
|
||||
{
|
||||
public function getActionIndex($params, $data, $request)
|
||||
{
|
||||
$result = $this->getServiceFactory()->create('LastViewed')->get();
|
||||
$params = [];
|
||||
|
||||
return [
|
||||
'total' => $result['total'],
|
||||
'list' => isset($result['collection']) ? $result['collection']->toArray() : $result['list']
|
||||
$params['offset'] = $request->get('offset', 0);
|
||||
$params['maxSize'] = $request->get('maxSize');
|
||||
|
||||
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', \Espo\Core\Controllers\Record::MAX_SIZE_LIMIT);
|
||||
if (empty($params['maxSize'])) {
|
||||
$params['maxSize'] = $maxSizeLimit;
|
||||
}
|
||||
if (!empty($params['maxSize']) && $params['maxSize'] > $maxSizeLimit) {
|
||||
throw new Forbidden("Max size should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
|
||||
}
|
||||
|
||||
$result = $this->getServiceFactory()->create('LastViewed')->getList($params);
|
||||
|
||||
return (object) [
|
||||
'total' => $result->total,
|
||||
'list' => $result->collection->getValueMapList()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,15 +78,18 @@ class Layout extends \Espo\Core\Controllers\Base
|
||||
return $this->actionUpdate($params, $data, $request);
|
||||
}
|
||||
|
||||
public function actionResetToDefault($params, $data, $request)
|
||||
public function postActionResetToDefault($params, $data, $request)
|
||||
{
|
||||
if (!$request->isPost()) {
|
||||
throw new BadRequest();
|
||||
if (!$this->getUser()->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if (empty($data->scope) || empty($data->name)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$this->getContainer()->get('dataManager')->updateCacheTimestamp();
|
||||
|
||||
return $this->getContainer()->get('layout')->resetToDefault($data->scope, $data->name);
|
||||
}
|
||||
}
|
||||
|
||||
72
application/Espo/Controllers/LeadCapture.php
Normal file
72
application/Espo/Controllers/LeadCapture.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: http://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use \Espo\Core\Exceptions\Forbidden;
|
||||
use \Espo\Core\Exceptions\BadRequest;
|
||||
use \Espo\Core\Exceptions\NotFound;
|
||||
|
||||
class LeadCapture extends \Espo\Core\Controllers\Record
|
||||
{
|
||||
public function postActionLeadCapture($params, $data, $request, $response)
|
||||
{
|
||||
if (empty($params['apiKey'])) throw new BadRequest('No API key provided.');
|
||||
if (empty($data)) throw new BadRequest('No payload provided.');
|
||||
|
||||
$allowOrigin = $this->getConfig()->get('leadCaptureAllowOrigin', '*');
|
||||
$response->headers->set('Access-Control-Allow-Origin', $allowOrigin);
|
||||
|
||||
return $this->getRecordService()->leadCapture($params['apiKey'], $data);
|
||||
}
|
||||
|
||||
public function optionsActionLeadCapture($params, $data, $request, $response)
|
||||
{
|
||||
if (empty($params['apiKey'])) throw new BadRequest('No API key provided.');
|
||||
|
||||
if (!$this->getRecordService()->isApiKeyValid($params['apiKey'])) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
$allowOrigin = $this->getConfig()->get('leadCaptureAllowOrigin', '*');
|
||||
|
||||
$response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Accept');
|
||||
$response->headers->set('Access-Control-Allow-Origin', $allowOrigin);
|
||||
$response->headers->set('Access-Control-Allow-Methods', 'POST');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function postActionGenerateNewApiKey($params, $data, $request)
|
||||
{
|
||||
if (empty($data->id)) throw new BadRequest();
|
||||
|
||||
return $this->getRecordService()->generateNewApiKeyForEntity($data->id)->getValueMap();
|
||||
}
|
||||
}
|
||||
38
application/Espo/Controllers/LeadCaptureLogRecord.php
Normal file
38
application/Espo/Controllers/LeadCaptureLogRecord.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: http://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use \Espo\Core\Exceptions\Forbidden;
|
||||
use \Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
class LeadCaptureLogRecord extends \Espo\Core\Controllers\Record
|
||||
{
|
||||
|
||||
}
|
||||
@@ -88,7 +88,7 @@ class Preferences extends \Espo\Core\Controllers\Base
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
if ($this->getAcl()->getLevel('Preferences', 'read') === 'no') {
|
||||
if ($this->getAcl()->getLevel('Preferences', 'edit') === 'no') {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ class Preferences extends \Espo\Core\Controllers\Base
|
||||
|
||||
$entity->set('smtpEmailAddress', $user->get('emailAddress'));
|
||||
$entity->set('name', $user->get('name'));
|
||||
$entity->set('isPortalUser', $user->get('isPortalUser'));
|
||||
$entity->set('isPortalUser', $user->isPortal());
|
||||
|
||||
$entity->clear('smtpPassword');
|
||||
|
||||
@@ -142,5 +142,45 @@ class Preferences extends \Espo\Core\Controllers\Base
|
||||
|
||||
return $entity->getValueMap();
|
||||
}
|
||||
}
|
||||
|
||||
public function postActionResetDashboard($params, $data)
|
||||
{
|
||||
if (empty($data->id)) throw new BadRequest();
|
||||
|
||||
$userId = $data->id;
|
||||
|
||||
$this->handleUserAccess($userId);
|
||||
|
||||
$user = $this->getEntityManager()->getEntity('User', $userId);
|
||||
$preferences = $this->getEntityManager()->getEntity('Preferences', $userId);
|
||||
if (!$user) throw new NotFound();
|
||||
if (!$preferences) throw new NotFound();
|
||||
|
||||
if ($user->isPortal()) throw new Forbidden();
|
||||
|
||||
if ($this->getAcl()->getLevel('Preferences', 'edit') === 'no') {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$forbiddenAttributeList = $this->getAcl()->getScopeForbiddenAttributeList('Preferences', 'edit');
|
||||
|
||||
if (in_array('dashboardLayout', $forbiddenAttributeList)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$dashboardLayout = $this->getConfig()->get('dashboardLayout');
|
||||
$dashletsOptions = $this->getConfig()->get('dashletsOptions');
|
||||
|
||||
$preferences->set([
|
||||
'dashboardLayout' => $dashboardLayout,
|
||||
'dashletsOptions' => $dashletsOptions
|
||||
]);
|
||||
|
||||
$this->getEntityManager()->saveEntity($preferences);
|
||||
|
||||
return (object) [
|
||||
'dashboardLayout' => $preferences->get('dashboardLayout'),
|
||||
'dashletsOptions' => $preferences->get('dashletsOptions')
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,23 +35,12 @@ use \Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
class Settings extends \Espo\Core\Controllers\Base
|
||||
{
|
||||
|
||||
protected function getConfigData()
|
||||
{
|
||||
if ($this->getUser()->id == 'system') {
|
||||
$data = $this->getConfig()->getData();
|
||||
} else {
|
||||
$data = $this->getConfig()->getData($this->getUser()->isAdmin());
|
||||
}
|
||||
$data = $this->getServiceFactory()->create('Settings')->getConfigData();
|
||||
|
||||
$fieldDefs = $this->getMetadata()->get('entityDefs.Settings.fields');
|
||||
|
||||
foreach ($fieldDefs as $field => $d) {
|
||||
if ($d['type'] === 'password') {
|
||||
unset($data[$field]);
|
||||
}
|
||||
}
|
||||
|
||||
$data['jsLibs'] = $this->getMetadata()->get('app.jsLibs');
|
||||
$data->jsLibs = $this->getMetadata()->get('app.jsLibs');
|
||||
|
||||
return $data;
|
||||
}
|
||||
@@ -76,23 +65,7 @@ class Settings extends \Espo\Core\Controllers\Base
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
if (
|
||||
(isset($data->useCache) && $data->useCache !== $this->getConfig()->get('useCache'))
|
||||
||
|
||||
(isset($data->aclStrictMode) && $data->aclStrictMode !== $this->getConfig()->get('aclStrictMode'))
|
||||
) {
|
||||
$this->getContainer()->get('dataManager')->clearCache();
|
||||
}
|
||||
|
||||
$this->getConfig()->setData($data, $this->getUser()->isAdmin());
|
||||
$result = $this->getConfig()->save();
|
||||
if ($result === false) {
|
||||
throw new Error('Cannot save settings');
|
||||
}
|
||||
|
||||
if (isset($data->defaultCurrency) || isset($data->baseCurrency) || isset($data->currencyRates)) {
|
||||
$this->getContainer()->get('dataManager')->rebuildDatabase([]);
|
||||
}
|
||||
$this->getServiceFactory()->create('Settings')->setConfigData($data);
|
||||
|
||||
return $this->getConfigData();
|
||||
}
|
||||
|
||||
@@ -49,24 +49,59 @@ class Stream extends \Espo\Core\Controllers\Base
|
||||
|
||||
$service = $this->getService('Stream');
|
||||
|
||||
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', self::MAX_SIZE_LIMIT);
|
||||
if (empty($maxSize)) {
|
||||
$maxSize = self::MAX_SIZE_LIMIT;
|
||||
$maxSize = $maxSizeLimit;
|
||||
}
|
||||
if (!empty($maxSize) && $maxSize > self::MAX_SIZE_LIMIT) {
|
||||
throw new Forbidden();
|
||||
if (!empty($maxSize) && $maxSize > $maxSizeLimit) {
|
||||
throw new Forbidden("Max size should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
|
||||
}
|
||||
|
||||
$result = $service->find($scope, $id, array(
|
||||
$result = $service->find($scope, $id, [
|
||||
'offset' => $offset,
|
||||
'maxSize' => $maxSize,
|
||||
'after' => $after,
|
||||
'filter' => $filter
|
||||
));
|
||||
]);
|
||||
|
||||
return array(
|
||||
'total' => $result['total'],
|
||||
'list' => $result['collection']->toArray()
|
||||
);
|
||||
return (object) [
|
||||
'total' => $result->total,
|
||||
'list' => $result->collection->getValueMapList()
|
||||
];
|
||||
}
|
||||
|
||||
public function getActionListPosts($params, $data, $request)
|
||||
{
|
||||
$scope = $params['scope'];
|
||||
$id = isset($params['id']) ? $params['id'] : null;
|
||||
|
||||
$offset = intval($request->get('offset'));
|
||||
$maxSize = intval($request->get('maxSize'));
|
||||
$after = $request->get('after');
|
||||
|
||||
$where = $request->get('where');
|
||||
|
||||
$service = $this->getService('Stream');
|
||||
|
||||
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', self::MAX_SIZE_LIMIT);
|
||||
if (empty($maxSize)) {
|
||||
$maxSize = $maxSizeLimit;
|
||||
}
|
||||
if (!empty($maxSize) && $maxSize > $maxSizeLimit) {
|
||||
throw new Forbidden("Max size should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
|
||||
}
|
||||
|
||||
$result = $service->find($scope, $id, [
|
||||
'offset' => $offset,
|
||||
'maxSize' => $maxSize,
|
||||
'after' => $after,
|
||||
'filter' => 'posts',
|
||||
'where' => $where
|
||||
]);
|
||||
|
||||
return (object) [
|
||||
'total' => $result->total,
|
||||
'list' => $result->collection->getValueMapList()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
122
application/Espo/Controllers/TemplateManager.php
Normal file
122
application/Espo/Controllers/TemplateManager.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: http://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Utils as Utils;
|
||||
use \Espo\Core\Exceptions\NotFound;
|
||||
use \Espo\Core\Exceptions\Error;
|
||||
use \Espo\Core\Exceptions\Forbidden;
|
||||
use \Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
class TemplateManager extends \Espo\Core\Controllers\Base
|
||||
{
|
||||
protected function checkControllerAccess()
|
||||
{
|
||||
if (!$this->getUser()->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
|
||||
public function getActionGetTemplate($params, $data, $request)
|
||||
{
|
||||
$name = $request->get('name');
|
||||
if (empty($name)) throw new BadRequest();
|
||||
$scope = $request->get('scope');
|
||||
$module = null;
|
||||
$module = $this->getMetadata()->get(['app', 'templates', $name, 'module']);
|
||||
$hasSubject = !$this->getMetadata()->get(['app', 'templates', $name, 'noSubject']);
|
||||
|
||||
$templateFileManager = $this->getContainer()->get('templateFileManager');
|
||||
|
||||
$returnData = (object) [];
|
||||
$returnData->body = $templateFileManager->getTemplate($name, 'body', $scope, $module);
|
||||
|
||||
if ($hasSubject) {
|
||||
$returnData->subject = $templateFileManager->getTemplate($name, 'subject', $scope, $module);
|
||||
}
|
||||
|
||||
return $returnData;
|
||||
}
|
||||
|
||||
public function postActionSaveTemplate($params, $data)
|
||||
{
|
||||
$scope = null;
|
||||
if (empty($data->name)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
if (!empty($data->scope)) {
|
||||
$scope = $data->scope;
|
||||
}
|
||||
|
||||
$templateFileManager = $this->getContainer()->get('templateFileManager');
|
||||
|
||||
if (isset($data->subject)) {
|
||||
$templateFileManager->saveTemplate($data->name, 'subject', $data->subject, $scope);
|
||||
}
|
||||
|
||||
if (isset($data->body)) {
|
||||
$templateFileManager->saveTemplate($data->name, 'body', $data->body, $scope);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function postActionResetTemplate($params, $data)
|
||||
{
|
||||
$scope = null;
|
||||
if (empty($data->name)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
if (!empty($data->scope)) {
|
||||
$scope = $data->scope;
|
||||
}
|
||||
|
||||
$module = null;
|
||||
$module = $this->getMetadata()->get(['app', 'templates', $data->name, 'module']);
|
||||
$hasSubject = !$this->getMetadata()->get(['app', 'templates', $data->name, 'noSubject']);
|
||||
|
||||
$templateFileManager = $this->getContainer()->get('templateFileManager');
|
||||
|
||||
if ($hasSubject) {
|
||||
$templateFileManager->resetTemplate($data->name, 'subject', $scope);
|
||||
}
|
||||
|
||||
$templateFileManager->resetTemplate($data->name, 'body', $scope);
|
||||
|
||||
$returnData = (object) [];
|
||||
$returnData->body = $templateFileManager->getTemplate($data->name, 'body', $scope, $module);
|
||||
|
||||
if ($hasSubject) {
|
||||
$returnData->subject = $templateFileManager->getTemplate($data->name, 'subject', $scope, $module);
|
||||
}
|
||||
|
||||
return $returnData;
|
||||
}
|
||||
}
|
||||
@@ -105,5 +105,18 @@ class User extends \Espo\Core\Controllers\Record
|
||||
|
||||
return $this->getService('User')->passwordChangeRequest($userName, $emailAddress, $url);
|
||||
}
|
||||
}
|
||||
|
||||
public function actionCreateLink($params, $data, $request)
|
||||
{
|
||||
if (!$this->getUser()->isAdmin()) throw new Forbidden();
|
||||
|
||||
return parent::actionCreateLink($params, $data, $request);
|
||||
}
|
||||
|
||||
public function actionRemoveLink($params, $data, $request)
|
||||
{
|
||||
if (!$this->getUser()->isAdmin()) throw new Forbidden();
|
||||
|
||||
return parent::actionRemoveLink($params, $data, $request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,5 +133,19 @@ class Acl
|
||||
{
|
||||
return $this->getAclManager()->checkAssignmentPermission($this->getUser(), $target);
|
||||
}
|
||||
}
|
||||
|
||||
public function getScopeRestrictedFieldList($scope, $type)
|
||||
{
|
||||
return $this->getAclManager()->getScopeRestrictedFieldList($scope, $type);
|
||||
}
|
||||
|
||||
public function getScopeRestrictedAttributeList($scope, $type)
|
||||
{
|
||||
return $this->getAclManager()->getScopeRestrictedAttributeList($scope, $type);
|
||||
}
|
||||
|
||||
public function getScopeRestrictedLinkList($scope, $type)
|
||||
{
|
||||
return $this->getAclManager()->getScopeRestrictedLinkList($scope, $type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,10 @@ class Base implements Injectable
|
||||
|
||||
protected $injections = array();
|
||||
|
||||
protected $ownerUserIdAttribute = null;
|
||||
|
||||
protected $allowDeleteCreatedThresholdPeriod = '24 hours';
|
||||
|
||||
public function inject($name, $object)
|
||||
{
|
||||
$this->injections[$name] = $object;
|
||||
@@ -265,15 +269,26 @@ class Base implements Injectable
|
||||
&&
|
||||
$entity->has('createdById') && $entity->get('createdById') == $user->id
|
||||
) {
|
||||
$isDeletedAllowed = false;
|
||||
if (!$entity->has('assignedUserId')) {
|
||||
return true;
|
||||
$isDeletedAllowed = true;
|
||||
} else {
|
||||
if (!$entity->get('assignedUserId')) {
|
||||
return true;
|
||||
$isDeletedAllowed = true;
|
||||
} else if ($entity->get('assignedUserId') == $entity->get('createdById')) {
|
||||
$isDeletedAllowed = true;
|
||||
}
|
||||
if ($entity->get('assignedUserId') == $entity->get('createdById')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($isDeletedAllowed) {
|
||||
$createdAt = $entity->get('createdAt');
|
||||
if ($createdAt) {
|
||||
$deleteThresholdPeriod = $this->getConfig()->get('aclAllowDeleteCreatedThresholdPeriod', $this->allowDeleteCreatedThresholdPeriod);
|
||||
if (\Espo\Core\Utils\DateTime::isAfterThreshold($createdAt, $deleteThresholdPeriod)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -281,5 +296,23 @@ class Base implements Injectable
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getOwnerUserIdAttribute(Entity $entity)
|
||||
{
|
||||
if ($this->ownerUserIdAttribute) {
|
||||
return $this->ownerUserIdAttribute;
|
||||
}
|
||||
|
||||
if ($entity->hasLinkMultipleField('assignedUsers')) {
|
||||
return 'assignedUsersIds';
|
||||
}
|
||||
|
||||
if ($entity->hasAttribute('assignedUserId')) {
|
||||
return 'assignedUserId';
|
||||
}
|
||||
|
||||
if ($entity->hasAttribute('createdById')) {
|
||||
return 'createdById';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
175
application/Espo/Core/Acl/GlobalRestricton.php
Normal file
175
application/Espo/Core/Acl/GlobalRestricton.php
Normal file
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: http://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Acl;
|
||||
|
||||
class GlobalRestricton
|
||||
{
|
||||
protected $fieldTypeList = [
|
||||
'forbidden', // totally forbidden
|
||||
'internal', // reading forbidden, writing allowed
|
||||
'onlyAdmin', // forbidden for non admin users
|
||||
'readOnly', // read-only for all users
|
||||
'nonAdminReadOnly' // read-only for non-admin users
|
||||
];
|
||||
|
||||
protected $linkTypeList = [
|
||||
'forbidden', // totally forbidden
|
||||
'internal', // reading forbidden, writing allowed
|
||||
'onlyAdmin', // forbidden for non admin users
|
||||
'readOnly', // read-only for all users
|
||||
'nonAdminReadOnly' // read-only for non-admin users
|
||||
];
|
||||
|
||||
protected $cacheFilePath = 'data/cache/application/entityAcl.php';
|
||||
|
||||
private $metadata;
|
||||
|
||||
private $fileManager;
|
||||
|
||||
private $fieldManagerUtil;
|
||||
|
||||
private $data;
|
||||
|
||||
public function __construct(
|
||||
\Espo\Core\Utils\Metadata $metadata,
|
||||
\Espo\Core\Utils\File\Manager $fileManager,
|
||||
\Espo\Core\Utils\FieldManagerUtil $fieldManagerUtil
|
||||
)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
$this->fileManager = $fileManager;
|
||||
$this->fieldManagerUtil = $fieldManagerUtil;
|
||||
|
||||
if (!file_exists($this->cacheFilePath)) {
|
||||
$this->buildCacheFile();
|
||||
}
|
||||
|
||||
$this->data = include($this->cacheFilePath);
|
||||
}
|
||||
|
||||
protected function buildCacheFile()
|
||||
{
|
||||
$scopeList = array_keys($this->getMetadata()->get(['entityDefs'], []));
|
||||
|
||||
$data = (object) [];
|
||||
|
||||
foreach ($scopeList as $scope) {
|
||||
$fieldList = array_keys($this->getMetadata()->get(['entityDefs', $scope, 'fields'], []));
|
||||
$linkList = array_keys($this->getMetadata()->get(['entityDefs', $scope, 'links'], []));
|
||||
|
||||
$isNotEmpty = false;
|
||||
|
||||
$scopeData = (object) [
|
||||
'fields' => (object) [],
|
||||
'attributes' => (object) [],
|
||||
'links' => (object) []
|
||||
];
|
||||
|
||||
foreach ($this->fieldTypeList as $type) {
|
||||
$resultFieldList = [];
|
||||
$resultAttributeList = [];
|
||||
|
||||
foreach ($fieldList as $field) {
|
||||
if ($this->getMetadata()->get(['entityAcl', $scope, 'fields', $field, $type])) {
|
||||
$isNotEmpty = true;
|
||||
$resultFieldList[] = $field;
|
||||
$fieldAttributeList = $this->getFieldManagerUtil()->getAttributeList($scope, $field);
|
||||
foreach ($fieldAttributeList as $attribute) {
|
||||
$resultAttributeList[] = $attribute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scopeData->fields->$type = $resultFieldList;
|
||||
$scopeData->attributes->$type = $resultAttributeList;
|
||||
}
|
||||
foreach ($this->linkTypeList as $type) {
|
||||
$resultLinkList = [];
|
||||
foreach ($linkList as $link) {
|
||||
if ($this->getMetadata()->get(['entityAcl', $scope, 'links', $link, $type])) {
|
||||
$isNotEmpty = true;
|
||||
$resultLinkList[] = $link;
|
||||
}
|
||||
}
|
||||
$scopeData->links->$type = $resultLinkList;
|
||||
}
|
||||
|
||||
if ($isNotEmpty) {
|
||||
$data->$scope = $scopeData;
|
||||
}
|
||||
}
|
||||
|
||||
$this->data = $data;
|
||||
|
||||
$this->getFileManager()->putPhpContents($this->cacheFilePath, $data, true);
|
||||
}
|
||||
|
||||
protected function getMetadata()
|
||||
{
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
protected function getFileManager()
|
||||
{
|
||||
return $this->fileManager;
|
||||
}
|
||||
|
||||
protected function getFieldManagerUtil()
|
||||
{
|
||||
return $this->fieldManagerUtil;
|
||||
}
|
||||
|
||||
public function getScopeRestrictedFieldList($scope, $type)
|
||||
{
|
||||
if (!property_exists($this->data, $scope)) return [];
|
||||
if (!property_exists($this->data->$scope, 'fields')) return [];
|
||||
if (!property_exists($this->data->$scope->fields, $type)) return [];
|
||||
|
||||
return $this->data->$scope->fields->$type;
|
||||
}
|
||||
|
||||
public function getScopeRestrictedAttributeList($scope, $type)
|
||||
{
|
||||
if (!property_exists($this->data, $scope)) return [];
|
||||
if (!property_exists($this->data->$scope, 'attributes')) return [];
|
||||
if (!property_exists($this->data->$scope->attributes, $type)) return [];
|
||||
|
||||
return $this->data->$scope->attributes->$type;
|
||||
}
|
||||
|
||||
public function getScopeRestrictedLinkList($scope, $type)
|
||||
{
|
||||
if (!property_exists($this->data, $scope)) return [];
|
||||
if (!property_exists($this->data->$scope, 'links')) return [];
|
||||
if (!property_exists($this->data->$scope->links, $type)) return [];
|
||||
|
||||
return $this->data->$scope->links->$type;
|
||||
}
|
||||
}
|
||||
@@ -182,6 +182,15 @@ class Table
|
||||
return 'no';
|
||||
}
|
||||
|
||||
public function getHighestLevel($action)
|
||||
{
|
||||
if (in_array($action, $this->booleanActionList)) {
|
||||
return 'yes';
|
||||
} else {
|
||||
return 'all';
|
||||
}
|
||||
}
|
||||
|
||||
private function load()
|
||||
{
|
||||
$valuePermissionLists = (object)[];
|
||||
@@ -554,7 +563,7 @@ class Table
|
||||
|
||||
protected function applyAdditional(&$table, &$fieldTable, &$valuePermissionLists)
|
||||
{
|
||||
if ($this->getUser()->get('isPortalUser')) {
|
||||
if ($this->getUser()->isPortal()) {
|
||||
foreach ($this->getScopeList() as $scope) {
|
||||
$table->$scope = false;
|
||||
unset($fieldTable->$scope);
|
||||
|
||||
@@ -49,10 +49,18 @@ class AclManager
|
||||
|
||||
protected $userAclClassName = '\\Espo\\Core\\Acl';
|
||||
|
||||
protected $globalRestricton;
|
||||
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->metadata = $container->get('metadata');
|
||||
|
||||
$this->globalRestricton = new \Espo\Core\Acl\GlobalRestricton(
|
||||
$container->get('metadata'),
|
||||
$container->get('fileManager'),
|
||||
$container->get('fieldManagerUtil')
|
||||
);
|
||||
}
|
||||
|
||||
protected function getContainer()
|
||||
@@ -125,7 +133,7 @@ class AclManager
|
||||
public function getLevel(User $user, $scope, $action)
|
||||
{
|
||||
if ($user->isAdmin()) {
|
||||
return 'all';
|
||||
return $this->getTable($user)->getHighestLevel($action);
|
||||
}
|
||||
return $this->getTable($user)->getLevel($scope, $action);
|
||||
}
|
||||
@@ -241,16 +249,64 @@ class AclManager
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getGlobalRestrictionTypeList(User $user, $action = 'read')
|
||||
{
|
||||
$typeList = ['forbidden'];
|
||||
|
||||
if ($action === 'read') {
|
||||
$typeList[] = 'internal';
|
||||
}
|
||||
|
||||
if (!$user->isAdmin()) {
|
||||
$typeList[] = 'onlyAdmin';
|
||||
}
|
||||
|
||||
if ($action === 'edit') {
|
||||
$typeList[] = 'readOnly';
|
||||
if (!$user->isAdmin()) {
|
||||
$typeList[] = 'nonAdminReadOnly';
|
||||
}
|
||||
}
|
||||
|
||||
return $typeList;
|
||||
}
|
||||
|
||||
public function getScopeForbiddenAttributeList(User $user, $scope, $action = 'read', $thresholdLevel = 'no')
|
||||
{
|
||||
if ($user->isAdmin()) return [];
|
||||
return $this->getTable($user)->getScopeForbiddenAttributeList($scope, $action, $thresholdLevel);
|
||||
$list = [];
|
||||
|
||||
if (!$user->isAdmin()) {
|
||||
$list = $this->getTable($user)->getScopeForbiddenAttributeList($scope, $action, $thresholdLevel);
|
||||
}
|
||||
|
||||
if ($thresholdLevel === 'no') {
|
||||
$list = array_merge(
|
||||
$list,
|
||||
$this->getScopeRestrictedAttributeList($scope, $this->getGlobalRestrictionTypeList($user, $action))
|
||||
);
|
||||
$list = array_values($list);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
public function getScopeForbiddenFieldList(User $user, $scope, $action = 'read', $thresholdLevel = 'no')
|
||||
{
|
||||
if ($user->isAdmin()) return [];
|
||||
return $this->getTable($user)->getScopeForbiddenFieldList($scope, $action, $thresholdLevel);
|
||||
$list = [];
|
||||
|
||||
if (!$user->isAdmin()) {
|
||||
$list = $this->getTable($user)->getScopeForbiddenFieldList($scope, $action, $thresholdLevel);
|
||||
}
|
||||
|
||||
if ($thresholdLevel === 'no') {
|
||||
$list = array_merge(
|
||||
$list,
|
||||
$this->getScopeRestrictedFieldList($scope, $this->getGlobalRestrictionTypeList($user, $action))
|
||||
);
|
||||
$list = array_values($list);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
public function checkUserPermission(User $user, $target, $permissionType = 'userPermission')
|
||||
@@ -294,4 +350,46 @@ class AclManager
|
||||
$acl = new $className($this, $user);
|
||||
return $acl;
|
||||
}
|
||||
|
||||
public function getScopeRestrictedFieldList($scope, $type)
|
||||
{
|
||||
if (is_array($type)) {
|
||||
$typeList = $type;
|
||||
$list = [];
|
||||
foreach ($typeList as $type) {
|
||||
$list = array_merge($list, $this->globalRestricton->getScopeRestrictedFieldList($scope, $type));
|
||||
}
|
||||
$list = array_values($list);
|
||||
return $list;
|
||||
}
|
||||
return $this->globalRestricton->getScopeRestrictedFieldList($scope, $type);
|
||||
}
|
||||
|
||||
public function getScopeRestrictedAttributeList($scope, $type)
|
||||
{
|
||||
if (is_array($type)) {
|
||||
$typeList = $type;
|
||||
$list = [];
|
||||
foreach ($typeList as $type) {
|
||||
$list = array_merge($list, $this->globalRestricton->getScopeRestrictedAttributeList($scope, $type));
|
||||
}
|
||||
$list = array_values($list);
|
||||
return $list;
|
||||
}
|
||||
return $this->globalRestricton->getScopeRestrictedAttributeList($scope, $type);
|
||||
}
|
||||
|
||||
public function getScopeRestrictedLinkList($scope, $type)
|
||||
{
|
||||
if (is_array($type)) {
|
||||
$typeList = $type;
|
||||
$list = [];
|
||||
foreach ($typeList as $type) {
|
||||
$list = array_merge($list, $this->globalRestricton->getScopeRestrictedLinkList($scope, $type));
|
||||
}
|
||||
$list = array_values($list);
|
||||
return $list;
|
||||
}
|
||||
return $this->globalRestricton->getScopeRestrictedLinkList($scope, $type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,11 @@ class Application
|
||||
return $this->container;
|
||||
}
|
||||
|
||||
protected function getConfig()
|
||||
{
|
||||
return $this->getContainer()->get('config');
|
||||
}
|
||||
|
||||
public function run($name = 'default')
|
||||
{
|
||||
$this->routeHooks();
|
||||
@@ -134,6 +139,11 @@ class Application
|
||||
|
||||
public function runCron()
|
||||
{
|
||||
if ($this->getConfig()->get('cronDisabled')) {
|
||||
$GLOBALS['log']->warning("Cron is not run because it's disabled with 'cronDisabled' param.");
|
||||
return;
|
||||
}
|
||||
|
||||
$auth = $this->createAuth();
|
||||
$auth->useNoAuth();
|
||||
|
||||
@@ -141,6 +151,50 @@ class Application
|
||||
$cronManager->run();
|
||||
}
|
||||
|
||||
public function runDaemon()
|
||||
{
|
||||
$maxProcessNumber = $this->getConfig()->get('daemonMaxProcessNumber');
|
||||
$interval = $this->getConfig()->get('daemonInterval');
|
||||
$timeout = $this->getConfig()->get('daemonProcessTimeout');
|
||||
|
||||
if (!$maxProcessNumber || !$interval) {
|
||||
$GLOBALS['log']->error("Daemon config params are not set.");
|
||||
return;
|
||||
}
|
||||
|
||||
$processList = [];
|
||||
while (true) {
|
||||
$toSkip = false;
|
||||
$runningCount = 0;
|
||||
foreach ($processList as $i => $process) {
|
||||
if ($process->isRunning()) {
|
||||
$runningCount++;
|
||||
} else if ($process->isRunning()) {
|
||||
unset($processList[$i]);
|
||||
}
|
||||
}
|
||||
$processList = array_values($processList);
|
||||
if (count($runningCount) >= $maxProcessNumber) {
|
||||
$toSkip = true;
|
||||
}
|
||||
if (!$toSkip) {
|
||||
$process = new \Symfony\Component\Process\Process(['php', 'cron.php']);
|
||||
$process->setTimeout($timeout);
|
||||
$process->run();
|
||||
}
|
||||
sleep($interval);
|
||||
}
|
||||
}
|
||||
|
||||
public function runJob($id)
|
||||
{
|
||||
$auth = $this->createAuth();
|
||||
$auth->useNoAuth();
|
||||
|
||||
$cronManager = new \Espo\Core\CronManager($this->container);
|
||||
$cronManager->runJobById($id);
|
||||
}
|
||||
|
||||
public function runRebuild()
|
||||
{
|
||||
$dataManager = $this->getContainer()->get('dataManager');
|
||||
@@ -155,7 +209,7 @@ class Application
|
||||
|
||||
public function isInstalled()
|
||||
{
|
||||
$config = $this->getContainer()->get('config');
|
||||
$config = $this->getConfig();
|
||||
|
||||
if (file_exists($config->getConfigPath()) && $config->get('isInstalled')) {
|
||||
return true;
|
||||
@@ -224,7 +278,7 @@ class Application
|
||||
|
||||
try {
|
||||
$controllerManager = $this->getContainer()->get('controllerManager');
|
||||
$result = $controllerManager->process($controllerName, $actionName, $params, $data, $slim->request());
|
||||
$result = $controllerManager->process($controllerName, $actionName, $params, $data, $slim->request(), $slim->response());
|
||||
$container->get('output')->render($result);
|
||||
} catch (\Exception $e) {
|
||||
$container->get('output')->processError($e->getMessage(), $e->getCode(), false, $e);
|
||||
@@ -244,7 +298,7 @@ class Application
|
||||
|
||||
protected function getRouteList()
|
||||
{
|
||||
$routes = new \Espo\Core\Utils\Route($this->getContainer()->get('config'), $this->getMetadata(), $this->getContainer()->get('fileManager'));
|
||||
$routes = new \Espo\Core\Utils\Route($this->getConfig(), $this->getMetadata(), $this->getContainer()->get('fileManager'));
|
||||
|
||||
|
||||
return $routes->getAll();
|
||||
@@ -252,11 +306,11 @@ class Application
|
||||
|
||||
protected function initRoutes()
|
||||
{
|
||||
$crudList = array_keys($this->getContainer()->get('config')->get('crud'));
|
||||
$crudList = array_keys($this->getConfig()->get('crud'));
|
||||
|
||||
foreach ($this->getRouteList() as $route) {
|
||||
$method = strtolower($route['method']);
|
||||
if (!in_array($method, $crudList)) {
|
||||
if (!in_array($method, $crudList) && $method !== 'options') {
|
||||
$GLOBALS['log']->error('Route: Method ['.$method.'] does not exist. Please check your route ['.$route['route'].']');
|
||||
continue;
|
||||
}
|
||||
@@ -273,7 +327,7 @@ class Application
|
||||
|
||||
protected function initAutoloads()
|
||||
{
|
||||
$autoload = new \Espo\Core\Utils\Autoload($this->getContainer()->get('config'), $this->getMetadata(), $this->getContainer()->get('fileManager'));
|
||||
$autoload = new \Espo\Core\Utils\Autoload($this->getConfig(), $this->getMetadata(), $this->getContainer()->get('fileManager'));
|
||||
|
||||
try {
|
||||
$autoloadList = $autoload->getAll();
|
||||
@@ -328,9 +382,9 @@ class Application
|
||||
public function setupSystemUser()
|
||||
{
|
||||
$user = $this->getContainer()->get('entityManager')->getEntity('User', 'system');
|
||||
$user->set('isAdmin', true);
|
||||
$user->set('isAdmin', true); // TODO remove in 5.7
|
||||
$user->set('type', 'system');
|
||||
$this->getContainer()->setUser($user);
|
||||
$this->getContainer()->get('entityManager')->setUser($user);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Espo\Core;
|
||||
class Container
|
||||
{
|
||||
|
||||
private $data = array();
|
||||
private $data = [];
|
||||
|
||||
|
||||
/**
|
||||
@@ -178,7 +178,7 @@ class Container
|
||||
|
||||
protected function loadMailSender()
|
||||
{
|
||||
$className = $this->getServiceClassName('mailSernder', '\\Espo\\Core\\Mail\\Sender');
|
||||
$className = $this->getServiceClassName('mailSender', '\\Espo\\Core\\Mail\\Sender');
|
||||
return new $className(
|
||||
$this->get('config'),
|
||||
$this->get('entityManager')
|
||||
@@ -222,6 +222,13 @@ class Container
|
||||
);
|
||||
}
|
||||
|
||||
protected function loadNotificatorFactory()
|
||||
{
|
||||
return new \Espo\Core\NotificatorFactory(
|
||||
$this
|
||||
);
|
||||
}
|
||||
|
||||
protected function loadMetadata()
|
||||
{
|
||||
return new \Espo\Core\Utils\Metadata(
|
||||
@@ -247,6 +254,14 @@ class Container
|
||||
);
|
||||
}
|
||||
|
||||
protected function loadInternalAclManager()
|
||||
{
|
||||
$className = $this->getServiceClassName('acl', '\\Espo\\Core\\AclManager');
|
||||
return new $className(
|
||||
$this->get('container')
|
||||
);
|
||||
}
|
||||
|
||||
protected function loadAcl()
|
||||
{
|
||||
$className = $this->getServiceClassName('acl', '\\Espo\\Core\\Acl');
|
||||
|
||||
@@ -40,12 +40,16 @@ class ControllerManager
|
||||
|
||||
private $container;
|
||||
|
||||
private $controllersHash = null;
|
||||
|
||||
public function __construct(\Espo\Core\Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
|
||||
$this->config = $this->container->get('config');
|
||||
$this->metadata = $this->container->get('metadata');
|
||||
|
||||
$this->controllersHash = (object) [];
|
||||
}
|
||||
|
||||
protected function getConfig()
|
||||
@@ -58,7 +62,7 @@ class ControllerManager
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
public function process($controllerName, $actionName, $params, $data, $request)
|
||||
protected function getControllerClassName($controllerName)
|
||||
{
|
||||
$customClassName = '\\Espo\\Custom\\Controllers\\' . Util::normilizeClassName($controllerName);
|
||||
if (class_exists($customClassName)) {
|
||||
@@ -72,27 +76,48 @@ class ControllerManager
|
||||
}
|
||||
}
|
||||
|
||||
if ($data && stristr($request->getContentType(), 'application/json')) {
|
||||
$data = json_decode($data);
|
||||
}
|
||||
|
||||
if (!class_exists($controllerClassName)) {
|
||||
throw new NotFound("Controller '$controllerName' is not found");
|
||||
}
|
||||
|
||||
$controller = new $controllerClassName($this->container, $request->getMethod());
|
||||
return $controllerClassName;
|
||||
}
|
||||
|
||||
public function createController($name)
|
||||
{
|
||||
$controllerClassName = $this->getControllerClassName($name);
|
||||
$controller = new $controllerClassName($this->container);
|
||||
|
||||
return $controller;
|
||||
}
|
||||
|
||||
public function getController($name)
|
||||
{
|
||||
if (!property_exists($this->controllersHash, $name)) {
|
||||
$this->controllersHash->$name = $this->createController($name);
|
||||
}
|
||||
return $this->controllersHash->$name;
|
||||
}
|
||||
|
||||
public function processRequest(\Espo\Core\Controllers\Base $controller, $actionName, $params, $data, $request, $response = null)
|
||||
{
|
||||
if ($data && stristr($request->getContentType(), 'application/json')) {
|
||||
$data = json_decode($data);
|
||||
}
|
||||
|
||||
if ($actionName == 'index') {
|
||||
$actionName = $controllerClassName::$defaultAction;
|
||||
$actionName = $controller::$defaultAction;
|
||||
}
|
||||
|
||||
$requestMethod = $request->getMethod();
|
||||
|
||||
$actionNameUcfirst = ucfirst($actionName);
|
||||
|
||||
$beforeMethodName = 'before' . $actionNameUcfirst;
|
||||
$actionMethodName = 'action' . $actionNameUcfirst;
|
||||
$afterMethodName = 'after' . $actionNameUcfirst;
|
||||
|
||||
$fullActionMethodName = strtolower($request->getMethod()) . ucfirst($actionMethodName);
|
||||
$fullActionMethodName = strtolower($requestMethod) . ucfirst($actionMethodName);
|
||||
|
||||
if (method_exists($controller, $fullActionMethodName)) {
|
||||
$primaryActionMethodName = $fullActionMethodName;
|
||||
@@ -101,24 +126,24 @@ class ControllerManager
|
||||
}
|
||||
|
||||
if (!method_exists($controller, $primaryActionMethodName)) {
|
||||
throw new NotFound("Action '$actionName' (".$request->getMethod().") does not exist in controller '$controllerName'");
|
||||
throw new NotFound("Action {$requestMethod} '{$actionName}' does not exist in controller '".$controller->getName()."'.");
|
||||
}
|
||||
|
||||
// TODO Remove in 5.1.0
|
||||
if ($data instanceof \stdClass) {
|
||||
if ($this->getMetadata()->get(['app', 'deprecatedControllerActions', $controllerName, $primaryActionMethodName])) {
|
||||
if ($this->getMetadata()->get(['app', 'deprecatedControllerActions', $controller->getName(), $primaryActionMethodName])) {
|
||||
$data = get_object_vars($data);
|
||||
}
|
||||
}
|
||||
|
||||
if (method_exists($controller, $beforeMethodName)) {
|
||||
$controller->$beforeMethodName($params, $data, $request);
|
||||
$controller->$beforeMethodName($params, $data, $request, $response);
|
||||
}
|
||||
|
||||
$result = $controller->$primaryActionMethodName($params, $data, $request);
|
||||
$result = $controller->$primaryActionMethodName($params, $data, $request, $response);
|
||||
|
||||
if (method_exists($controller, $afterMethodName)) {
|
||||
$controller->$afterMethodName($params, $data, $request);
|
||||
$controller->$afterMethodName($params, $data, $request, $response);
|
||||
}
|
||||
|
||||
if (is_array($result) || is_bool($result) || $result instanceof \StdClass) {
|
||||
@@ -127,4 +152,10 @@ class ControllerManager
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function process($controllerName, $actionName, $params, $data, $request, $response = null)
|
||||
{
|
||||
$controller = $this->getController($controllerName);
|
||||
return $this->processRequest($controller, $actionName, $params, $data, $request, $response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,18 +38,12 @@ abstract class Base
|
||||
|
||||
private $container;
|
||||
|
||||
private $requestMethod;
|
||||
|
||||
public static $defaultAction = 'index';
|
||||
|
||||
public function __construct(Container $container, $requestMethod = null)
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
|
||||
if (isset($requestMethod)) {
|
||||
$this->setRequestMethod($requestMethod);
|
||||
}
|
||||
|
||||
if (empty($this->name)) {
|
||||
$name = get_class($this);
|
||||
if (preg_match('@\\\\([\w]+)$@', $name, $matches)) {
|
||||
@@ -61,6 +55,11 @@ abstract class Base
|
||||
$this->checkControllerAccess();
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
protected function checkControllerAccess()
|
||||
{
|
||||
return;
|
||||
@@ -71,21 +70,6 @@ abstract class Base
|
||||
return $this->container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get request method name (Uppercase)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getRequestMethod()
|
||||
{
|
||||
return $this->requestMethod;
|
||||
}
|
||||
|
||||
protected function setRequestMethod($requestMethod)
|
||||
{
|
||||
$this->requestMethod = strtoupper($requestMethod);
|
||||
}
|
||||
|
||||
protected function getUser()
|
||||
{
|
||||
return $this->container->get('user');
|
||||
|
||||
@@ -83,6 +83,8 @@ class Record extends Base
|
||||
|
||||
public function actionCreate($params, $data, $request)
|
||||
{
|
||||
if (!is_object($data)) throw new BadRequest();
|
||||
|
||||
if (!$request->isPost()) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
@@ -93,7 +95,7 @@ class Record extends Base
|
||||
|
||||
$service = $this->getRecordService();
|
||||
|
||||
if ($entity = $service->createEntity($data)) {
|
||||
if ($entity = $service->create($data)) {
|
||||
return $entity->getValueMap();
|
||||
}
|
||||
|
||||
@@ -102,6 +104,8 @@ class Record extends Base
|
||||
|
||||
public function actionUpdate($params, $data, $request)
|
||||
{
|
||||
if (!is_object($data)) throw new BadRequest();
|
||||
|
||||
if (!$request->isPut() && !$request->isPatch()) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
@@ -112,7 +116,7 @@ class Record extends Base
|
||||
|
||||
$id = $params['id'];
|
||||
|
||||
if ($entity = $this->getRecordService()->updateEntity($id, $data)) {
|
||||
if ($entity = $this->getRecordService()->update($id, $data)) {
|
||||
return $entity->getValueMap();
|
||||
}
|
||||
|
||||
@@ -125,35 +129,18 @@ class Record extends Base
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$where = $request->get('where');
|
||||
$offset = $request->get('offset');
|
||||
$maxSize = $request->get('maxSize');
|
||||
$asc = $request->get('asc', 'true') === 'true';
|
||||
$sortBy = $request->get('sortBy');
|
||||
$q = $request->get('q');
|
||||
$textFilter = $request->get('textFilter');
|
||||
|
||||
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', self::MAX_SIZE_LIMIT);
|
||||
if (empty($maxSize)) {
|
||||
$maxSize = $maxSizeLimit;
|
||||
}
|
||||
if (!empty($maxSize) && $maxSize > $maxSizeLimit) {
|
||||
throw new Forbidden("Max should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
|
||||
}
|
||||
|
||||
$params = array(
|
||||
'where' => $where,
|
||||
'offset' => $offset,
|
||||
'maxSize' => $maxSize,
|
||||
'asc' => $asc,
|
||||
'sortBy' => $sortBy,
|
||||
'q' => $q,
|
||||
'textFilter' => $textFilter
|
||||
);
|
||||
|
||||
$params = [];
|
||||
$this->fetchListParamsFromRequest($params, $request, $data);
|
||||
|
||||
$result = $this->getRecordService()->findEntities($params);
|
||||
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', self::MAX_SIZE_LIMIT);
|
||||
if (empty($params['maxSize'])) {
|
||||
$params['maxSize'] = $maxSizeLimit;
|
||||
}
|
||||
if (!empty($params['maxSize']) && $params['maxSize'] > $maxSizeLimit) {
|
||||
throw new Forbidden("Max size should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
|
||||
}
|
||||
|
||||
$result = $this->getRecordService()->find($params);
|
||||
|
||||
return array(
|
||||
'total' => $result['total'],
|
||||
@@ -167,34 +154,17 @@ class Record extends Base
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$where = $request->get('where');
|
||||
$offset = $request->get('offset');
|
||||
$maxSize = $request->get('maxSize');
|
||||
$asc = $request->get('asc', 'true') === 'true';
|
||||
$sortBy = $request->get('sortBy');
|
||||
$q = $request->get('q');
|
||||
$textFilter = $request->get('textFilter');
|
||||
$params = [];
|
||||
$this->fetchListParamsFromRequest($params, $request, $data);
|
||||
|
||||
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', self::MAX_SIZE_LIMIT);
|
||||
if (empty($maxSize)) {
|
||||
$maxSize = $maxSizeLimit;
|
||||
if (empty($params['maxSize'])) {
|
||||
$params['maxSize'] = $maxSizeLimit;
|
||||
}
|
||||
if (!empty($maxSize) && $maxSize > $maxSizeLimit) {
|
||||
throw new Forbidden("Max should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
|
||||
if (!empty($params['maxSize']) && $params['maxSize'] > $maxSizeLimit) {
|
||||
throw new Forbidden("Max size should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
|
||||
}
|
||||
|
||||
$params = array(
|
||||
'where' => $where,
|
||||
'offset' => $offset,
|
||||
'maxSize' => $maxSize,
|
||||
'asc' => $asc,
|
||||
'sortBy' => $sortBy,
|
||||
'q' => $q,
|
||||
'textFilter' => $textFilter
|
||||
);
|
||||
|
||||
$this->fetchListParamsFromRequest($params, $request, $data);
|
||||
|
||||
$result = $this->getRecordService()->getListKanban($params);
|
||||
|
||||
return (object) [
|
||||
@@ -206,19 +176,7 @@ class Record extends Base
|
||||
|
||||
protected function fetchListParamsFromRequest(&$params, $request, $data)
|
||||
{
|
||||
if ($request->get('primaryFilter')) {
|
||||
$params['primaryFilter'] = $request->get('primaryFilter');
|
||||
}
|
||||
if ($request->get('boolFilterList')) {
|
||||
$params['boolFilterList'] = $request->get('boolFilterList');
|
||||
}
|
||||
if ($request->get('filterList')) {
|
||||
$params['filterList'] = $request->get('filterList');
|
||||
}
|
||||
|
||||
if ($request->get('select')) {
|
||||
$params['select'] = explode(',', $request->get('select'));
|
||||
}
|
||||
\Espo\Core\Utils\ControllerUtil::fetchListParamsFromRequest($params, $request, $data);
|
||||
}
|
||||
|
||||
public function actionListLinked($params, $data, $request)
|
||||
@@ -226,35 +184,18 @@ class Record extends Base
|
||||
$id = $params['id'];
|
||||
$link = $params['link'];
|
||||
|
||||
$where = $request->get('where');
|
||||
$offset = $request->get('offset');
|
||||
$maxSize = $request->get('maxSize');
|
||||
$asc = $request->get('asc', 'true') === 'true';
|
||||
$sortBy = $request->get('sortBy');
|
||||
$q = $request->get('q');
|
||||
$textFilter = $request->get('textFilter');
|
||||
|
||||
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', self::MAX_SIZE_LIMIT);
|
||||
if (empty($maxSize)) {
|
||||
$maxSize = $maxSizeLimit;
|
||||
}
|
||||
if (!empty($maxSize) && $maxSize > $maxSizeLimit) {
|
||||
throw new Forbidden("Max should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
|
||||
}
|
||||
|
||||
$params = array(
|
||||
'where' => $where,
|
||||
'offset' => $offset,
|
||||
'maxSize' => $maxSize,
|
||||
'asc' => $asc,
|
||||
'sortBy' => $sortBy,
|
||||
'q' => $q,
|
||||
'textFilter' => $textFilter
|
||||
);
|
||||
|
||||
$params = [];
|
||||
$this->fetchListParamsFromRequest($params, $request, $data);
|
||||
|
||||
$result = $this->getRecordService()->findLinkedEntities($id, $link, $params);
|
||||
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', self::MAX_SIZE_LIMIT);
|
||||
if (empty($params['maxSize'])) {
|
||||
$params['maxSize'] = $maxSizeLimit;
|
||||
}
|
||||
if (!empty($params['maxSize']) && $params['maxSize'] > $maxSizeLimit) {
|
||||
throw new Forbidden("Max size should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
|
||||
}
|
||||
|
||||
$result = $this->getRecordService()->findLinked($id, $link, $params);
|
||||
|
||||
return array(
|
||||
'total' => $result['total'],
|
||||
@@ -270,7 +211,7 @@ class Record extends Base
|
||||
|
||||
$id = $params['id'];
|
||||
|
||||
if ($this->getRecordService()->deleteEntity($id)) {
|
||||
if ($this->getRecordService()->delete($id)) {
|
||||
return true;
|
||||
}
|
||||
throw new Error();
|
||||
@@ -278,6 +219,8 @@ class Record extends Base
|
||||
|
||||
public function actionExport($params, $data, $request)
|
||||
{
|
||||
if (!is_object($data)) throw new BadRequest();
|
||||
|
||||
if (!$request->isPost()) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
@@ -337,6 +280,10 @@ class Record extends Base
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
if ($this->getAcl()->get('massUpdatePermission') !== 'yes') {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$params = array();
|
||||
if (property_exists($data, 'where') && !empty($data->byWhere)) {
|
||||
$params['where'] = json_decode(json_encode($data->where), true);
|
||||
@@ -375,7 +322,7 @@ class Record extends Base
|
||||
$params['ids'] = $data->ids;
|
||||
}
|
||||
|
||||
return $this->getRecordService()->massRemove($params);
|
||||
return $this->getRecordService()->massDelete($params);
|
||||
}
|
||||
|
||||
public function actionCreateLink($params, $data, $request)
|
||||
@@ -402,7 +349,7 @@ class Record extends Base
|
||||
$selectData = json_decode(json_encode($data->selectData), true);
|
||||
}
|
||||
|
||||
return $this->getRecordService()->linkEntityMass($id, $link, $where, $selectData);
|
||||
return $this->getRecordService()->linkMass($id, $link, $where, $selectData);
|
||||
} else {
|
||||
$foreignIdList = array();
|
||||
if (isset($data->id)) {
|
||||
@@ -416,7 +363,7 @@ class Record extends Base
|
||||
|
||||
$result = false;
|
||||
foreach ($foreignIdList as $foreignId) {
|
||||
if ($this->getRecordService()->linkEntity($id, $link, $foreignId)) {
|
||||
if ($this->getRecordService()->link($id, $link, $foreignId)) {
|
||||
$result = true;
|
||||
}
|
||||
}
|
||||
@@ -453,7 +400,7 @@ class Record extends Base
|
||||
|
||||
$result = false;
|
||||
foreach ($foreignIdList as $foreignId) {
|
||||
if ($this->getRecordService()->unlinkEntity($id, $link, $foreignId)) {
|
||||
if ($this->getRecordService()->unlink($id, $link, $foreignId)) {
|
||||
$result = $result || true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,9 +28,12 @@
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core;
|
||||
|
||||
use \PDO;
|
||||
use Espo\Core\Utils\Json;
|
||||
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
class CronManager
|
||||
{
|
||||
@@ -48,8 +51,21 @@ class CronManager
|
||||
|
||||
private $cronScheduledJobUtil;
|
||||
|
||||
private $useProcessPool = false;
|
||||
|
||||
private $asSoonAsPossibleSchedulingList = [
|
||||
'*',
|
||||
'* *',
|
||||
'* * *',
|
||||
'* * * *',
|
||||
'* * * * *',
|
||||
'* * * * * *'
|
||||
];
|
||||
|
||||
const PENDING = 'Pending';
|
||||
|
||||
const READY = 'Ready';
|
||||
|
||||
const RUNNING = 'Running';
|
||||
|
||||
const SUCCESS = 'Success';
|
||||
@@ -70,6 +86,14 @@ class CronManager
|
||||
$this->scheduledJobUtil = $this->container->get('scheduledJob');
|
||||
$this->cronJobUtil = new \Espo\Core\Utils\Cron\Job($this->config, $this->entityManager);
|
||||
$this->cronScheduledJobUtil = new \Espo\Core\Utils\Cron\ScheduledJob($this->config, $this->entityManager);
|
||||
|
||||
if ($this->getConfig()->get('jobRunInParallel')) {
|
||||
if (\Spatie\Async\Pool::isSupported()) {
|
||||
$this->useProcessPool = true;
|
||||
} else {
|
||||
$GLOBALS['log']->warning("CronManager: useProcessPool requires pcntl and posix extensions.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getContainer()
|
||||
@@ -146,6 +170,16 @@ class CronManager
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function useProcessPool()
|
||||
{
|
||||
return $this->useProcessPool;
|
||||
}
|
||||
|
||||
public function setUseProcessPool($useProcessPool)
|
||||
{
|
||||
$this->useProcessPool = $useProcessPool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run Cron
|
||||
*
|
||||
@@ -160,17 +194,39 @@ class CronManager
|
||||
|
||||
$this->setLastRunTime(time());
|
||||
|
||||
$this->getCronJobUtil()->markFailedJobs();
|
||||
$this->getCronJobUtil()->markJobsFailed();
|
||||
$this->getCronJobUtil()->updateFailedJobAttempts();
|
||||
$this->createJobsFromScheduledJobs();
|
||||
$this->getCronJobUtil()->removePendingJobDuplicates();
|
||||
|
||||
$pendingJobList = $this->getCronJobUtil()->getPendingJobList();
|
||||
$this->processPendingJobs();
|
||||
}
|
||||
|
||||
public function processPendingJobs($queue = null, $limit = null, $poolDisabled = false, $noLock = false)
|
||||
{
|
||||
if (is_null($limit)) {
|
||||
$limit = intval($this->getConfig()->get('jobMaxPortion', 0));
|
||||
}
|
||||
|
||||
$pendingJobList = $this->getCronJobUtil()->getPendingJobList($queue, $limit);
|
||||
|
||||
$useProcessPool = $this->useProcessPool();
|
||||
|
||||
if ($poolDisabled) {
|
||||
$useProcessPool = false;
|
||||
}
|
||||
|
||||
if ($useProcessPool) {
|
||||
$pool = \Spatie\Async\Pool::create()
|
||||
->autoload(getcwd() . '/vendor/autoload.php')
|
||||
->concurrency($this->getConfig()->get('jobPoolConcurrencyNumber'))
|
||||
->timeout($this->getConfig()->get('jobPeriodForActiveProcess'));
|
||||
}
|
||||
|
||||
foreach ($pendingJobList as $job) {
|
||||
$skip = false;
|
||||
$this->getEntityManager()->getPdo()->query('LOCK TABLES `job` WRITE');
|
||||
if ($this->getCronJobUtil()->isJobPending($job->id)) {
|
||||
if (!$noLock) $this->lockJobTable();
|
||||
if ($noLock || $this->getCronJobUtil()->isJobPending($job->id)) {
|
||||
if ($job->get('scheduledJobId')) {
|
||||
if ($this->getCronJobUtil()->isScheduledJobRunning($job->get('scheduledJobId'), $job->get('targetId'), $job->get('targetType'))) {
|
||||
$skip = true;
|
||||
@@ -181,43 +237,102 @@ class CronManager
|
||||
}
|
||||
|
||||
if ($skip) {
|
||||
$this->getEntityManager()->getPdo()->query('UNLOCK TABLES');
|
||||
if (!$noLock) $this->unlockTables();
|
||||
continue;
|
||||
}
|
||||
|
||||
$job->set('status', self::RUNNING);
|
||||
$job->set('pid', $this->getCronJobUtil()->getPid());
|
||||
$this->getEntityManager()->saveEntity($job);
|
||||
$this->getEntityManager()->getPdo()->query('UNLOCK TABLES');
|
||||
$job->set('startedAt', date('Y-m-d H:i:s'));
|
||||
|
||||
$isSuccess = true;
|
||||
$skipLog = false;
|
||||
|
||||
try {
|
||||
if ($job->get('scheduledJobId')) {
|
||||
$this->runScheduledJob($job);
|
||||
} else {
|
||||
$this->runService($job);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$isSuccess = false;
|
||||
if ($e->getCode() === -1) {
|
||||
$job->set('attempts', 0);
|
||||
$skipLog = true;
|
||||
} else {
|
||||
$GLOBALS['log']->error('CronManager: Failed job running, job ['.$job->id.']. Error Details: '.$e->getMessage());
|
||||
}
|
||||
if ($useProcessPool) {
|
||||
$job->set('status', self::READY);
|
||||
} else {
|
||||
$job->set('status', self::RUNNING);
|
||||
$job->set('pid', \Espo\Core\Utils\System::getPid());
|
||||
}
|
||||
|
||||
$status = $isSuccess ? self::SUCCESS : self::FAILED;
|
||||
|
||||
$job->set('status', $status);
|
||||
$this->getEntityManager()->saveEntity($job);
|
||||
if (!$noLock) $this->unlockTables();
|
||||
|
||||
if ($job->get('scheduledJobId') && !$skipLog) {
|
||||
$this->getCronScheduledJobUtil()->addLogRecord($job->get('scheduledJobId'), $status, null, $job->get('targetId'), $job->get('targetType'));
|
||||
if ($useProcessPool) {
|
||||
$task = new \Espo\Core\Utils\Cron\JobTask($job->id);
|
||||
$pool->add($task);
|
||||
} else {
|
||||
$this->runJob($job);
|
||||
}
|
||||
}
|
||||
|
||||
if ($useProcessPool) {
|
||||
$pool->wait();
|
||||
}
|
||||
}
|
||||
|
||||
protected function lockJobTable()
|
||||
{
|
||||
$this->getEntityManager()->getPdo()->query('LOCK TABLES `job` WRITE');
|
||||
}
|
||||
|
||||
protected function unlockTables()
|
||||
{
|
||||
$this->getEntityManager()->getPdo()->query('UNLOCK TABLES');
|
||||
}
|
||||
|
||||
public function runJobById($id)
|
||||
{
|
||||
if (empty($id)) throw new Error();
|
||||
|
||||
$job = $this->getEntityManager()->getEntity('Job', $id);
|
||||
|
||||
if (!$job) throw new Error("Job {$id} not found.");
|
||||
|
||||
if ($job->get('status') !== self::READY) {
|
||||
throw new Error("Can't run job {$id} with no status Ready.");
|
||||
}
|
||||
|
||||
if (!$job->get('startedAt')) {
|
||||
$job->set('startedAt', date('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
$job->set('status', self::RUNNING);
|
||||
$job->set('pid', \Espo\Core\Utils\System::getPid());
|
||||
$this->getEntityManager()->saveEntity($job);
|
||||
|
||||
$this->runJob($job);
|
||||
}
|
||||
|
||||
public function runJob($job)
|
||||
{
|
||||
$isSuccess = true;
|
||||
$skipLog = false;
|
||||
|
||||
try {
|
||||
if ($job->get('scheduledJobId')) {
|
||||
$this->runScheduledJob($job);
|
||||
} else {
|
||||
$this->runService($job);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$isSuccess = false;
|
||||
if ($e->getCode() === -1) {
|
||||
$job->set('attempts', 0);
|
||||
$skipLog = true;
|
||||
} else {
|
||||
$GLOBALS['log']->error('CronManager: Failed job running, job ['.$job->id.']. Error Details: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$status = $isSuccess ? self::SUCCESS : self::FAILED;
|
||||
|
||||
$job->set('status', $status);
|
||||
|
||||
if ($isSuccess) {
|
||||
$job->set('executedAt', date('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
$this->getEntityManager()->saveEntity($job);
|
||||
|
||||
if ($job->get('scheduledJobId') && !$skipLog) {
|
||||
$this->getCronScheduledJobUtil()->addLogRecord($job->get('scheduledJobId'), $status, null, $job->get('targetId'), $job->get('targetType'));
|
||||
}
|
||||
}
|
||||
|
||||
protected function runScheduledJob($job)
|
||||
@@ -268,12 +383,6 @@ class CronManager
|
||||
$methodNameDeprecated = $job->get('method');
|
||||
$methodName = $job->get('methodName');
|
||||
|
||||
$isDeprecated = false;
|
||||
if (!$methodName) {
|
||||
$isDeprecated = true;
|
||||
$methodName = $methodNameDeprecated;
|
||||
}
|
||||
|
||||
if (!$methodName) {
|
||||
throw new Error('Job with empty methodName.');
|
||||
}
|
||||
@@ -284,40 +393,40 @@ class CronManager
|
||||
|
||||
$data = $job->get('data');
|
||||
|
||||
if ($isDeprecated) {
|
||||
$data = Json::decode(Json::encode($data), true);
|
||||
}
|
||||
|
||||
$service->$methodName($data, $job->get('targetId'), $job->get('targetType'));
|
||||
}
|
||||
|
||||
protected function createJobsFromScheduledJobs()
|
||||
{
|
||||
$activeScheduledJobList = $this->getCronScheduledJobUtil()->getActiveScheduledJobList();
|
||||
|
||||
$runningScheduledJobIdList = $this->getCronJobUtil()->getRunningScheduledJobIdList();
|
||||
|
||||
$createdJobIdList = array();
|
||||
$createdJobIdList = [];
|
||||
foreach ($activeScheduledJobList as $scheduledJob) {
|
||||
$scheduling = $scheduledJob->get('scheduling');
|
||||
$asSoonAsPossible = in_array($scheduling, $this->asSoonAsPossibleSchedulingList);
|
||||
|
||||
try {
|
||||
$cronExpression = \Cron\CronExpression::factory($scheduling);
|
||||
} catch (\Exception $e) {
|
||||
$GLOBALS['log']->error('CronManager (ScheduledJob ['.$scheduledJob->id.']): Scheduling string error - '. $e->getMessage() . '.');
|
||||
continue;
|
||||
if ($asSoonAsPossible) {
|
||||
$nextDate = date('Y-m-d H:i:s');
|
||||
} else {
|
||||
try {
|
||||
$cronExpression = \Cron\CronExpression::factory($scheduling);
|
||||
} catch (\Exception $e) {
|
||||
$GLOBALS['log']->error('CronManager (ScheduledJob ['.$scheduledJob->id.']): Scheduling string error - '. $e->getMessage() . '.');
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$nextDate = $cronExpression->getNextRunDate()->format('Y-m-d H:i:s');
|
||||
} catch (\Exception $e) {
|
||||
$GLOBALS['log']->error('CronManager (ScheduledJob ['.$scheduledJob->id.']): Unsupported CRON expression ['.$scheduling.']');
|
||||
continue;
|
||||
}
|
||||
|
||||
$existingJob = $this->getCronJobUtil()->getJobByScheduledJobIdOnMinute($scheduledJob->id, $nextDate);
|
||||
if ($existingJob) continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$nextDate = $cronExpression->getNextRunDate()->format('Y-m-d H:i:s');
|
||||
} catch (\Exception $e) {
|
||||
$GLOBALS['log']->error('CronManager (ScheduledJob ['.$scheduledJob->id.']): Unsupported CRON expression ['.$scheduling.']');
|
||||
continue;
|
||||
}
|
||||
|
||||
$existingJob = $this->getCronJobUtil()->getJobByScheduledJob($scheduledJob->id, $nextDate);
|
||||
if ($existingJob) continue;
|
||||
|
||||
$className = $this->getScheduledJobUtil()->get($scheduledJob->get('job'));
|
||||
if ($className) {
|
||||
if (method_exists($className, 'prepare')) {
|
||||
@@ -331,13 +440,25 @@ class CronManager
|
||||
continue;
|
||||
}
|
||||
|
||||
$pendingCount = $this->getCronJobUtil()->getPendingCountByScheduledJobId($scheduledJob->id);
|
||||
|
||||
if ($asSoonAsPossible) {
|
||||
if ($pendingCount > 0) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if ($pendingCount > 1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$jobEntity = $this->getEntityManager()->getEntity('Job');
|
||||
$jobEntity->set(array(
|
||||
$jobEntity->set([
|
||||
'name' => $scheduledJob->get('name'),
|
||||
'status' => self::PENDING,
|
||||
'scheduledJobId' => $scheduledJob->id,
|
||||
'executeTime' => $nextDate
|
||||
));
|
||||
]);
|
||||
$this->getEntityManager()->saveEntity($jobEntity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,18 +130,21 @@ class DataManager
|
||||
$metadata = $this->getContainer()->get('metadata');
|
||||
$entityManager = $this->getContainer()->get('entityManager');
|
||||
|
||||
$jobs = $metadata->get(['entityDefs', 'ScheduledJob', 'jobs'], array());
|
||||
$jobs = $metadata->get(['entityDefs', 'ScheduledJob', 'jobs'], []);
|
||||
|
||||
$systemJobNameList = [];
|
||||
|
||||
foreach ($jobs as $jobName => $defs) {
|
||||
if ($jobName && !empty($defs['isSystem']) && !empty($defs['scheduling'])) {
|
||||
$systemJobNameList[] = $jobName;
|
||||
if (!$entityManager->getRepository('ScheduledJob')->where(array(
|
||||
'job' => $jobName,
|
||||
'status' => 'Active',
|
||||
'scheduling' => $defs['scheduling']
|
||||
))->findOne()) {
|
||||
$job = $entityManager->getRepository('ScheduledJob')->where(array(
|
||||
$job = $entityManager->getRepository('ScheduledJob')->where([
|
||||
'job' => $jobName
|
||||
))->findOne();
|
||||
])->findOne();
|
||||
if ($job) {
|
||||
$entityManager->removeEntity($job);
|
||||
}
|
||||
@@ -150,28 +153,34 @@ class DataManager
|
||||
$name = $defs['name'];
|
||||
}
|
||||
$job = $entityManager->getEntity('ScheduledJob');
|
||||
$job->set(array(
|
||||
$job->set([
|
||||
'job' => $jobName,
|
||||
'status' => 'Active',
|
||||
'scheduling' => $defs['scheduling'],
|
||||
'isInternal' => true,
|
||||
'name' => $name
|
||||
));
|
||||
]);
|
||||
$entityManager->saveEntity($job);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$internalScheduledJobList = $entityManager->getRepository('ScheduledJob')->where([
|
||||
'isInternal' => true
|
||||
])->find();
|
||||
foreach ($internalScheduledJobList as $scheduledJob) {
|
||||
$jobName = $scheduledJob->get('job');
|
||||
if (!in_array($jobName, $systemJobNameList)) {
|
||||
$entityManager->getRepository('ScheduledJob')->deleteFromDb($scheduledJob->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update cache timestamp
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function updateCacheTimestamp()
|
||||
{
|
||||
$this->getContainer()->get('config')->updateCacheTimestamp();
|
||||
$this->getContainer()->get('config')->save();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -193,6 +202,12 @@ class DataManager
|
||||
|
||||
$config->set('fullTextSearchMinLength', $fullTextSearchMinLength);
|
||||
|
||||
$cryptKey = $config->get('cryptKey');
|
||||
if (!$cryptKey) {
|
||||
$cryptKey = \Espo\Core\Utils\Util::generateKey();
|
||||
$config->set('cryptKey', $cryptKey);
|
||||
}
|
||||
|
||||
$config->save();
|
||||
}
|
||||
}
|
||||
|
||||
36
application/Espo/Core/Exceptions/ServiceUnavailable.php
Normal file
36
application/Espo/Core/Exceptions/ServiceUnavailable.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: http://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\Exceptions;
|
||||
|
||||
class ServiceUnavailable extends \Exception
|
||||
{
|
||||
protected $code = 503;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: http://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\StringGroup;
|
||||
|
||||
use \Espo\Core\Exceptions\Error;
|
||||
|
||||
class TestType extends \Espo\Core\Formula\Functions\Base
|
||||
{
|
||||
public function process(\StdClass $item)
|
||||
{
|
||||
if (!property_exists($item, 'value') || !is_array($item->value)) {
|
||||
throw new Error('Value for \'String\\Test\' item is not an array.');
|
||||
}
|
||||
if (count($item->value) < 2) {
|
||||
throw new Error('Bad arguments passed to \'String\\Test\'.');
|
||||
}
|
||||
$string = $this->evaluate($item->value[0]);
|
||||
$regexp = $this->evaluate($item->value[1]);
|
||||
|
||||
if (!is_string($string)) {
|
||||
return false;
|
||||
}
|
||||
if (!is_string($regexp)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!preg_match($regexp, $string);
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,11 @@ class Htmlizer
|
||||
return $this->entityManager;
|
||||
}
|
||||
|
||||
protected function getMetadata()
|
||||
{
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
protected function format($value)
|
||||
{
|
||||
if (is_float($value)) {
|
||||
@@ -87,36 +92,74 @@ class Htmlizer
|
||||
return $value;
|
||||
}
|
||||
|
||||
protected function getDataFromEntity(Entity $entity, $skipLinks = false)
|
||||
protected function getDataFromEntity(Entity $entity, $skipLinks = false, $level = 0)
|
||||
{
|
||||
$data = $entity->toArray();
|
||||
|
||||
$fieldDefs = $entity->getFields();
|
||||
$fieldList = array_keys($fieldDefs);
|
||||
$attributeDefs = $entity->getAttributes();
|
||||
$attributeList = array_keys($attributeDefs);
|
||||
|
||||
$forbidenAttributeList = [];
|
||||
$forbiddenAttributeList = [];
|
||||
$skipAttributeList = [];
|
||||
$forbiddenLinkList = [];
|
||||
|
||||
if ($this->getAcl()) {
|
||||
$forbidenAttributeList = $this->getAcl()->getScopeForbiddenAttributeList($entity->getEntityType(), 'read');
|
||||
$forbiddenAttributeList = $this->getAcl()->getScopeForbiddenAttributeList($entity->getEntityType(), 'read');
|
||||
|
||||
$forbiddenAttributeList = array_merge(
|
||||
$forbiddenAttributeList,
|
||||
$this->getAcl()->getScopeRestrictedAttributeList($entity->getEntityType(), ['forbidden', 'internal', 'onlyAdmin'])
|
||||
);
|
||||
|
||||
$forbiddenLinkList = $this->getAcl()->getScopeRestrictedLinkList($entity->getEntityType(), ['forbidden', 'internal', 'onlyAdmin']);
|
||||
}
|
||||
|
||||
$relationList = $entity->getRelationList();
|
||||
|
||||
foreach ($fieldList as $field) {
|
||||
if (in_array($field, $forbidenAttributeList)) continue;
|
||||
if (!$skipLinks && $level === 0) {
|
||||
foreach ($relationList as $relation) {
|
||||
if (!$entity->hasLinkMultipleField($relation)) continue;
|
||||
|
||||
$type = $entity->getAttributeType($field);
|
||||
$collection = $entity->getLinkMultipleCollection($relation);
|
||||
$data[$relation] = $collection;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if ($value instanceof \Espo\ORM\EntityCollection) {
|
||||
$skipAttributeList[] = $key;
|
||||
$collection = $value;
|
||||
$list = [];
|
||||
foreach ($collection as $item) {
|
||||
$list[] = $this->getDataFromEntity($item, $skipLinks, $level + 1);
|
||||
}
|
||||
$data[$key] = $list;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($attributeList as $attribute) {
|
||||
if (in_array($attribute, $forbiddenAttributeList)) {
|
||||
unset($data[$attribute]);
|
||||
continue;
|
||||
}
|
||||
if (in_array($attribute, $skipAttributeList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $entity->getAttributeType($attribute);
|
||||
|
||||
if ($type == Entity::DATETIME) {
|
||||
if (!empty($data[$field])) {
|
||||
$data[$field] = $this->dateTime->convertSystemDateTime($data[$field]);
|
||||
if (!empty($data[$attribute])) {
|
||||
$data[$attribute] = $this->dateTime->convertSystemDateTime($data[$attribute]);
|
||||
}
|
||||
} else if ($type == Entity::DATE) {
|
||||
if (!empty($data[$field])) {
|
||||
$data[$field] = $this->dateTime->convertSystemDate($data[$field]);
|
||||
if (!empty($data[$attribute])) {
|
||||
$data[$attribute] = $this->dateTime->convertSystemDate($data[$attribute]);
|
||||
}
|
||||
} else if ($type == Entity::JSON_ARRAY) {
|
||||
if (!empty($data[$field])) {
|
||||
$list = $data[$field];
|
||||
if (!empty($data[$attribute])) {
|
||||
$list = $data[$attribute];
|
||||
|
||||
$newList = [];
|
||||
foreach ($list as $item) {
|
||||
$v = $item;
|
||||
@@ -133,42 +176,43 @@ class Htmlizer
|
||||
|
||||
$newList[] = $v;
|
||||
}
|
||||
$data[$field] = $newList;
|
||||
$data[$attribute] = $newList;
|
||||
}
|
||||
} else if ($type == Entity::JSON_OBJECT) {
|
||||
if (!empty($data[$field])) {
|
||||
$value = $data[$field];
|
||||
if (!empty($data[$attribute])) {
|
||||
$value = $data[$attribute];
|
||||
if ($value instanceof \StdClass) {
|
||||
$data[$field] = json_decode(json_encode($value, \JSON_PRESERVE_ZERO_FRACTION), true);
|
||||
$data[$attribute] = json_decode(json_encode($value, \JSON_PRESERVE_ZERO_FRACTION), true);
|
||||
}
|
||||
foreach ($data[$field] as $k => $w) {
|
||||
foreach ($data[$attribute] as $k => $w) {
|
||||
$keyRaw = $k . '_RAW';
|
||||
$data[$field][$keyRaw] = $data[$field][$k];
|
||||
$data[$field][$k] = $this->format($data[$field][$k]);
|
||||
$data[$attribute][$keyRaw] = $data[$attribute][$k];
|
||||
$data[$attribute][$k] = $this->format($data[$attribute][$k]);
|
||||
}
|
||||
}
|
||||
} else if ($type === Entity::PASSWORD) {
|
||||
unset($data[$field]);
|
||||
unset($data[$attribute]);
|
||||
}
|
||||
|
||||
if (array_key_exists($field, $data)) {
|
||||
$keyRaw = $field . '_RAW';
|
||||
$data[$keyRaw] = $data[$field];
|
||||
if (array_key_exists($attribute, $data)) {
|
||||
$keyRaw = $attribute . '_RAW';
|
||||
$data[$keyRaw] = $data[$attribute];
|
||||
|
||||
$fieldType = $this->getFieldType($entity->getEntityType(), $field);
|
||||
$fieldType = $this->getFieldType($entity->getEntityType(), $attribute);
|
||||
if ($fieldType === 'enum') {
|
||||
if ($this->language) {
|
||||
$data[$field] = $this->language->translateOption($data[$field], $field, $entity->getEntityType());
|
||||
$data[$attribute] = $this->language->translateOption($data[$attribute], $attribute, $entity->getEntityType());
|
||||
}
|
||||
}
|
||||
|
||||
$data[$field] = $this->format($data[$field]);
|
||||
$data[$attribute] = $this->format($data[$attribute]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$skipLinks) {
|
||||
$relationDefs = $entity->getRelations();
|
||||
foreach ($entity->getRelationList() as $relation) {
|
||||
if (in_array($relation, $forbiddenLinkList)) continue;
|
||||
if (
|
||||
!empty($relationDefs[$relation]['type'])
|
||||
&&
|
||||
@@ -180,7 +224,7 @@ class Htmlizer
|
||||
if (!$this->getAcl()->check($relatedEntity, 'read')) continue;
|
||||
}
|
||||
|
||||
$data[$relation] = $this->getDataFromEntity($relatedEntity, true);
|
||||
$data[$relation] = $this->getDataFromEntity($relatedEntity, true, $level + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,7 +232,7 @@ class Htmlizer
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function render(Entity $entity, $template, $id = null, $additionalData = array(), $skipLinks = false)
|
||||
public function render(Entity $entity, $template, $id = null, $additionalData = [], $skipLinks = false)
|
||||
{
|
||||
$code = \LightnCandy::compile($template, [
|
||||
'flags' => \LightnCandy::FLAG_HANDLEBARSJS,
|
||||
@@ -303,4 +347,4 @@ class Htmlizer
|
||||
if (!$this->metadata) return;
|
||||
return $this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'type']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,9 @@ namespace Espo\Core;
|
||||
|
||||
abstract class Injectable implements \Espo\Core\Interfaces\Injectable
|
||||
{
|
||||
protected $dependencyList = array();
|
||||
protected $dependencyList = [];
|
||||
|
||||
protected $injections = array();
|
||||
protected $injections = [];
|
||||
|
||||
public function inject($name, $object)
|
||||
{
|
||||
|
||||
@@ -51,8 +51,21 @@ class InjectableFactory
|
||||
foreach ($dependencyList as $name) {
|
||||
$service->inject($name, $this->container->get($name));
|
||||
}
|
||||
if (method_exists($service, 'prepare')) {
|
||||
$service->prepare();
|
||||
}
|
||||
return $service;
|
||||
}
|
||||
throw new Error("Class '$className' does not exist");
|
||||
}
|
||||
|
||||
protected function getMetadata()
|
||||
{
|
||||
return $this->getContainer()->get('metadata');
|
||||
}
|
||||
|
||||
protected function getContainer()
|
||||
{
|
||||
return $this->container;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@ class TemplateFileManager extends Base
|
||||
{
|
||||
$templateFileManager = new \Espo\Core\Utils\TemplateFileManager(
|
||||
$this->getContainer()->get('config'),
|
||||
$this->getContainer()->get('metadata')
|
||||
$this->getContainer()->get('metadata'),
|
||||
$this->getContainer()->get('fileManager')
|
||||
);
|
||||
|
||||
return $templateFileManager;
|
||||
|
||||
@@ -42,11 +42,14 @@ class Importer
|
||||
|
||||
private $filtersMatcher;
|
||||
|
||||
public function __construct($entityManager, $config)
|
||||
private $notificator = null;
|
||||
|
||||
public function __construct($entityManager, $config, $notificator = null)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->config = $config;
|
||||
$this->filtersMatcher = new FiltersMatcher();
|
||||
$this->notificator = $notificator;
|
||||
}
|
||||
|
||||
protected function getEntityManager()
|
||||
@@ -64,6 +67,11 @@ class Importer
|
||||
return $this->filtersMatcher;
|
||||
}
|
||||
|
||||
protected function getNotificator()
|
||||
{
|
||||
return $this->notificator;
|
||||
}
|
||||
|
||||
public function importMessage($parserType = 'ZendMail', $message, $assignedUserId = null, $teamsIdList = [], $userIdList = [], $filterList = [], $fetchOnlyHeader = false, $folderData = null)
|
||||
{
|
||||
$parser = $message->getParser();
|
||||
@@ -153,10 +161,14 @@ class Importer
|
||||
}
|
||||
}
|
||||
|
||||
$duplicate = null;
|
||||
|
||||
if ($duplicate = $this->findDuplicate($email)) {
|
||||
$duplicate = $this->getEntityManager()->getEntity('Email', $duplicate->id);
|
||||
$this->processDuplicate($duplicate, $assignedUserId, $userIdList, $folderData, $teamsIdList);
|
||||
return $duplicate;
|
||||
if ($duplicate->get('status') != 'Being Imported') {
|
||||
$duplicate = $this->getEntityManager()->getEntity('Email', $duplicate->id);
|
||||
$this->processDuplicate($duplicate, $assignedUserId, $userIdList, $folderData, $teamsIdList);
|
||||
return $duplicate;
|
||||
}
|
||||
}
|
||||
|
||||
if ($parser->checkMessageAttribute($message, 'date')) {
|
||||
@@ -280,11 +292,38 @@ class Importer
|
||||
}
|
||||
}
|
||||
|
||||
$this->getEntityManager()->getPdo()->query('LOCK TABLES `email` WRITE');
|
||||
if (!$duplicate) {
|
||||
$this->lockEmailTable();
|
||||
if ($duplicate = $this->findDuplicate($email)) {
|
||||
$this->unlockTables();
|
||||
if ($duplicate->get('status') != 'Being Imported') {
|
||||
$duplicate = $this->getEntityManager()->getEntity('Email', $duplicate->id);
|
||||
$this->processDuplicate($duplicate, $assignedUserId, $userIdList, $folderData, $teamsIdList);
|
||||
return $duplicate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($duplicate) {
|
||||
$duplicate->set([
|
||||
'from' => $email->get('from'),
|
||||
'to' => $email->get('to'),
|
||||
'cc' => $email->get('cc'),
|
||||
'bcc' => $email->get('bcc'),
|
||||
'replyTo' => $email->get('replyTo'),
|
||||
'name' => $email->get('name'),
|
||||
'dateSent' => $email->get('dateSent'),
|
||||
'body' => $email->get('body'),
|
||||
'bodyPlain' => $email->get('bodyPlain'),
|
||||
'parentType' => $email->get('parentType'),
|
||||
'parentId' => $email->get('parentId'),
|
||||
'isHtml' => $email->get('isHtml'),
|
||||
'messageId' => $email->get('messageId'),
|
||||
'fromString' => $email->get('fromString'),
|
||||
'replyToString' => $email->get('replyToString'),
|
||||
]);
|
||||
$this->getEntityManager()->getRepository('Email')->fillAccount($duplicate);
|
||||
|
||||
if ($duplicate = $this->findDuplicate($email)) {
|
||||
$this->getEntityManager()->getPdo()->query('UNLOCK TABLES');
|
||||
$duplicate = $this->getEntityManager()->getEntity('Email', $duplicate->id);
|
||||
$this->processDuplicate($duplicate, $assignedUserId, $userIdList, $folderData, $teamsIdList);
|
||||
return $duplicate;
|
||||
}
|
||||
@@ -292,13 +331,14 @@ class Importer
|
||||
if (!$email->get('messageId')) {
|
||||
$email->setDummyMessageId();
|
||||
}
|
||||
$email->set('status', 'Being Imported');
|
||||
|
||||
$this->getEntityManager()->saveEntity($email, [
|
||||
'skipAll' => true,
|
||||
'keepNew' => true
|
||||
]);
|
||||
|
||||
$this->getEntityManager()->getPdo()->query('UNLOCK TABLES');
|
||||
$this->unlockTables();
|
||||
|
||||
if ($parentFound) {
|
||||
$parentType = $email->get('parentType');
|
||||
@@ -317,19 +357,34 @@ class Importer
|
||||
}
|
||||
}
|
||||
|
||||
$this->getEntityManager()->saveEntity($email);
|
||||
$email->set('status', 'Archived');
|
||||
|
||||
$this->getEntityManager()->saveEntity($email, [
|
||||
'isBeingImported' => true
|
||||
]);
|
||||
|
||||
foreach ($inlineAttachmentList as $attachment) {
|
||||
$attachment->set(array(
|
||||
$attachment->set([
|
||||
'relatedId' => $email->id,
|
||||
'relatedType' => 'Email'
|
||||
));
|
||||
'relatedType' => 'Email',
|
||||
'field' => 'body'
|
||||
]);
|
||||
$this->getEntityManager()->saveEntity($attachment);
|
||||
}
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
protected function lockEmailTable()
|
||||
{
|
||||
$this->getEntityManager()->getPdo()->query('LOCK TABLES `email` WRITE');
|
||||
}
|
||||
|
||||
protected function unlockTables()
|
||||
{
|
||||
$this->getEntityManager()->getPdo()->query('UNLOCK TABLES');
|
||||
}
|
||||
|
||||
protected function findParent(Entity $email, $emailAddress)
|
||||
{
|
||||
$contact = $this->getEntityManager()->getRepository('Contact')->where(array(
|
||||
@@ -370,9 +425,9 @@ class Importer
|
||||
protected function findDuplicate(Entity $email)
|
||||
{
|
||||
if ($email->get('messageId')) {
|
||||
$duplicate = $this->getEntityManager()->getRepository('Email')->select(['id'])->where(array(
|
||||
$duplicate = $this->getEntityManager()->getRepository('Email')->select(['id', 'status'])->where([
|
||||
'messageId' => $email->get('messageId')
|
||||
))->findOne(['skipAdditionalSelectParams' => true]);
|
||||
])->findOne(['skipAdditionalSelectParams' => true]);
|
||||
if ($duplicate) {
|
||||
return $duplicate;
|
||||
}
|
||||
@@ -381,30 +436,94 @@ class Importer
|
||||
|
||||
protected function processDuplicate(Entity $duplicate, $assignedUserId, $userIdList, $folderData, $teamsIdList)
|
||||
{
|
||||
if ($duplicate->get('status') == 'Archived') {
|
||||
$this->getEntityManager()->getRepository('Email')->loadFromField($duplicate);
|
||||
$this->getEntityManager()->getRepository('Email')->loadToField($duplicate);
|
||||
}
|
||||
|
||||
$duplicate->loadLinkMultipleField('users');
|
||||
$fetchedUserIdList = $duplicate->getLinkMultipleIdList('users');
|
||||
$duplicate->setLinkMultipleIdList('users', []);
|
||||
|
||||
$processNoteAcl = false;
|
||||
|
||||
if ($assignedUserId) {
|
||||
$duplicate->addLinkMultipleId('users', $assignedUserId);
|
||||
if (!in_array($assignedUserId, $fetchedUserIdList)) {
|
||||
$processNoteAcl = true;
|
||||
$duplicate->addLinkMultipleId('users', $assignedUserId);
|
||||
}
|
||||
$duplicate->addLinkMultipleId('assignedUsers', $assignedUserId);
|
||||
}
|
||||
|
||||
if (!empty($userIdList)) {
|
||||
foreach ($userIdList as $uId) {
|
||||
$duplicate->addLinkMultipleId('users', $uId);
|
||||
if (!in_array($uId, $fetchedUserIdList)) {
|
||||
$processNoteAcl = true;
|
||||
$duplicate->addLinkMultipleId('users', $uId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($folderData) {
|
||||
foreach ($folderData as $uId => $folderId) {
|
||||
$duplicate->setLinkMultipleColumn('users', 'folderId', $uId, $folderId);
|
||||
if (!in_array($uId, $fetchedUserIdList)) {
|
||||
$duplicate->setLinkMultipleColumn('users', 'folderId', $uId, $folderId);
|
||||
} else {
|
||||
$this->getEntityManager()->getRepository('Email')->updateRelation($duplicate, 'users', $uId, [
|
||||
'folderId' => $folderId
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$duplicate->set('isBeingImported', true);
|
||||
|
||||
$this->getEntityManager()->saveEntity($duplicate);
|
||||
$this->getEntityManager()->getRepository('Email')->applyUsersFilters($duplicate);
|
||||
|
||||
$this->getEntityManager()->getRepository('Email')->processLinkMultipleFieldSave($duplicate, 'users', [
|
||||
'skipLinkMultipleRemove' => true,
|
||||
'skipLinkMultipleUpdate' => true
|
||||
]);
|
||||
|
||||
$this->getEntityManager()->getRepository('Email')->processLinkMultipleFieldSave($duplicate, 'assignedUsers', [
|
||||
'skipLinkMultipleRemove' => true,
|
||||
'skipLinkMultipleUpdate' => true
|
||||
]);
|
||||
|
||||
if ($notificator = $this->getNotificator()) {
|
||||
$notificator->process($duplicate, [
|
||||
'isBeingImported' => true
|
||||
]);
|
||||
}
|
||||
|
||||
$fetchedTeamIdList = $duplicate->getLinkMultipleIdList('teams');
|
||||
|
||||
if (!empty($teamsIdList)) {
|
||||
foreach ($teamsIdList as $teamId) {
|
||||
$this->getEntityManager()->getRepository('Email')->relate($duplicate, 'teams', $teamId);
|
||||
if (!in_array($teamId, $fetchedTeamIdList)) {
|
||||
$processNoteAcl = true;
|
||||
$this->getEntityManager()->getRepository('Email')->relate($duplicate, 'teams', $teamId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($duplicate->get('parentType') && $processNoteAcl) {
|
||||
$dt = new \DateTime();
|
||||
$dt->modify('+5 seconds');
|
||||
$executeAt = $dt->format('Y-m-d H:i:s');
|
||||
|
||||
$job = $this->getEntityManager()->getEntity('Job');
|
||||
$job->set([
|
||||
'serviceName' => 'Note',
|
||||
'methodName' => 'processNoteAclJob',
|
||||
'data' => [
|
||||
'targetType' => 'Email',
|
||||
'targetId' => $duplicate->id
|
||||
],
|
||||
'executeAt' => $executeAt,
|
||||
'queue' => 'q1'
|
||||
]);
|
||||
$this->getEntityManager()->saveEntity($job);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
@@ -47,7 +47,7 @@ use Zend\Mime\Mime;
|
||||
|
||||
class XQueueItemId implements Header\HeaderInterface
|
||||
{
|
||||
protected $fieldName = 'X-QueueItemId';
|
||||
protected $fieldName = 'X-Queue-Item-Id';
|
||||
|
||||
protected $id = null;
|
||||
|
||||
@@ -57,7 +57,7 @@ class XQueueItemId implements Header\HeaderInterface
|
||||
$value = Header\HeaderWrap::mimeDecodeValue($value);
|
||||
|
||||
if (strtolower($name) !== 'x-queue-item-id') {
|
||||
throw new Header\Exception\InvalidArgumentException('Invalid header line for Message-ID string');
|
||||
throw new Header\Exception\InvalidArgumentException('Invalid header line for x-queue-item-id string');
|
||||
}
|
||||
|
||||
$header = new static();
|
||||
|
||||
@@ -176,7 +176,10 @@ class Sender
|
||||
} else {
|
||||
$fromName = $config->get('outboundEmailFromName');
|
||||
}
|
||||
|
||||
$message->addFrom(trim($email->get('from')), $fromName);
|
||||
|
||||
$fromAddress = trim($email->get('from'));
|
||||
} else {
|
||||
if (!empty($params['fromAddress'])) {
|
||||
$fromAddress = $params['fromAddress'];
|
||||
@@ -194,12 +197,16 @@ class Sender
|
||||
}
|
||||
|
||||
$message->addFrom($fromAddress, $fromName);
|
||||
}
|
||||
|
||||
if (!$email->get('from')) {
|
||||
$email->set('from', $fromAddress);
|
||||
}
|
||||
|
||||
$fromString = '<' . $fromAddress . '>';
|
||||
if ($fromName) {
|
||||
$fromString = $fromName . ' ' . $fromString;
|
||||
}
|
||||
$email->set('fromString', $fromString);
|
||||
|
||||
$sender = new \Zend\Mail\Header\Sender();
|
||||
$sender->setAddress($email->get('from'));
|
||||
$message->getHeaders()->addHeader($sender);
|
||||
|
||||
58
application/Espo/Core/NotificatorFactory.php
Normal file
58
application/Espo/Core/NotificatorFactory.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: http://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;
|
||||
|
||||
use \Espo\Core\Exceptions\Error;
|
||||
|
||||
use \Espo\Core\Utils\Util;
|
||||
use \Espo\Core\InjectableFactory;
|
||||
|
||||
class NotificatorFactory extends InjectableFactory
|
||||
{
|
||||
public function create($entityType)
|
||||
{
|
||||
$normalizedName = Util::normilizeClassName($entityType);
|
||||
|
||||
$className = '\\Espo\\Custom\\Notificators\\' . $normalizedName;
|
||||
if (!class_exists($className)) {
|
||||
$moduleName = $this->getMetadata()->getScopeModuleName($entityType);
|
||||
if ($moduleName) {
|
||||
$className = '\\Espo\\Modules\\' . $moduleName . '\\Notificators\\' . $normalizedName;
|
||||
} else {
|
||||
$className = '\\Espo\\Notificators\\' . $normalizedName;
|
||||
}
|
||||
if (!class_exists($className)) {
|
||||
$className = '\\Espo\\Core\\Notificators\\Base';
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createByClassName($className);
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ class Base implements Injectable
|
||||
return $this->injections['user'];
|
||||
}
|
||||
|
||||
public function process(Entity $entity)
|
||||
public function process(Entity $entity, array $options = [])
|
||||
{
|
||||
if ($entity->hasLinkMultipleField('assignedUsers')) {
|
||||
$userIdList = $entity->getLinkMultipleIdList('assignedUsers');
|
||||
|
||||
@@ -33,12 +33,22 @@ class Entity extends \Espo\ORM\Entity
|
||||
{
|
||||
public function hasLinkMultipleField($field)
|
||||
{
|
||||
return $this->hasAttribute($field . 'Ids');
|
||||
return
|
||||
$this->hasRelation($field) &&
|
||||
$this->getAttributeParam($field . 'Ids', 'isLinkMultipleIdList');
|
||||
}
|
||||
|
||||
public function hasLinkField($field)
|
||||
{
|
||||
return $this->hasAttribute($field . 'Id');
|
||||
return $this->hasAttribute($field . 'Id') && $this->hasRelation($field);
|
||||
}
|
||||
|
||||
public function hasLinkParentField($field)
|
||||
{
|
||||
return
|
||||
$this->hasAttributeType($field . 'Type') == 'foreignType' &&
|
||||
$this->hasAttribute($field . 'Id') &&
|
||||
$this->hasRelation($field);
|
||||
}
|
||||
|
||||
public function loadParentNameField($field)
|
||||
@@ -64,15 +74,28 @@ class Entity extends \Espo\ORM\Entity
|
||||
}
|
||||
}
|
||||
|
||||
public function loadLinkMultipleField($field, $columns = null)
|
||||
public function getLinkMultipleCollection($field)
|
||||
{
|
||||
if (!$this->hasRelation($field) || !$this->hasAttribute($field . 'Ids')) return;
|
||||
if (!$this->hasLinkMultipleField($field)) return;
|
||||
|
||||
$defs = array();
|
||||
if (!empty($columns)) {
|
||||
$defs['additionalColumns'] = $columns;
|
||||
$defs = $this->getRelationSelectParams($field);
|
||||
|
||||
$columnAttribute = $field . 'Columns';
|
||||
if ($this->hasAttribute($columnAttribute) && $this->getAttributeParam($columnAttribute, 'columns')) {
|
||||
$defs['additionalColumns'] = $this->getAttributeParam($columnAttribute, 'columns');
|
||||
}
|
||||
|
||||
$collection = $this->get($field, $defs);
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
protected function getRelationSelectParams($link)
|
||||
{
|
||||
$field = $link;
|
||||
|
||||
$defs = [];
|
||||
|
||||
$idsAttribute = $field . 'Ids';
|
||||
|
||||
$foreignEntityType = $this->getRelationParam($field, 'entity');
|
||||
@@ -103,6 +126,19 @@ class Entity extends \Espo\ORM\Entity
|
||||
}
|
||||
}
|
||||
|
||||
return $defs;
|
||||
}
|
||||
|
||||
public function loadLinkMultipleField($field, $columns = null)
|
||||
{
|
||||
if (!$this->hasRelation($field) || !$this->hasAttribute($field . 'Ids')) return;
|
||||
|
||||
$defs = $this->getRelationSelectParams($field);
|
||||
|
||||
if (!empty($columns)) {
|
||||
$defs['additionalColumns'] = $columns;
|
||||
}
|
||||
|
||||
$defs['select'] = ['id', 'name'];
|
||||
|
||||
$hasType = false;
|
||||
@@ -136,8 +172,10 @@ class Entity extends \Espo\ORM\Entity
|
||||
}
|
||||
}
|
||||
|
||||
$idsAttribute = $field . 'Ids';
|
||||
|
||||
$this->set($idsAttribute, $ids);
|
||||
if (!$this->hasFetched($idsAttribute)) {
|
||||
if (!$this->isNew() && !$this->hasFetched($idsAttribute)) {
|
||||
$this->setFetched($idsAttribute, $ids);
|
||||
}
|
||||
|
||||
@@ -170,7 +208,13 @@ class Entity extends \Espo\ORM\Entity
|
||||
$entityName = $entity->get('name');
|
||||
}
|
||||
|
||||
$this->set($field . 'Id', $entityId);
|
||||
$idAttribute = $field . 'Id';
|
||||
|
||||
if (!$this->isNew() && !$this->hasFetched($idAttribute)) {
|
||||
$this->setFetched($idAttribute, $entityId);
|
||||
}
|
||||
|
||||
$this->set($idAttribute, $entityId);
|
||||
$this->set($field . 'Name', $entityName);
|
||||
}
|
||||
|
||||
@@ -295,4 +339,3 @@ class Entity extends \Espo\ORM\Entity
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
|
||||
protected $processFieldsBeforeSaveDisabled = false;
|
||||
|
||||
protected $processFieldsAfterRemoveDisabled = false;
|
||||
|
||||
protected function addDependency($name)
|
||||
{
|
||||
$this->dependencies[] = $name;
|
||||
@@ -124,20 +126,17 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
return;
|
||||
}
|
||||
|
||||
$defs = $metadata->get('entityDefs.' . $entityType);
|
||||
$defs = $metadata->get(['entityDefs', $entityType]);
|
||||
|
||||
foreach ($defs['fields'] as $field => $d) {
|
||||
if (isset($d['type']) && $d['type'] == 'currency') {
|
||||
if (!empty($d['notStorable'])) {
|
||||
continue;
|
||||
}
|
||||
if (empty($params['customJoin'])) {
|
||||
$params['customJoin'] = '';
|
||||
}
|
||||
$alias = Util::toUnderScore($field) . "_currency_alias";
|
||||
$params['customJoin'] .= "
|
||||
LEFT JOIN currency AS `{$alias}` ON {$alias}.id = ".Util::toUnderScore($entityType).".".Util::toUnderScore($field)."_currency
|
||||
";
|
||||
if (!empty($d['notStorable'])) continue;
|
||||
if (empty($params['leftJoins'])) $params['leftJoins'] = [];
|
||||
$alias = $field . 'CurrencyRate';
|
||||
|
||||
$params['leftJoins'][] = ['Currency', $alias, [
|
||||
$alias . '.id:' => $field . 'Currency'
|
||||
]];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,45 +144,23 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
|
||||
protected function handleEmailAddressParams(&$params)
|
||||
{
|
||||
$entityType = $this->entityType;
|
||||
|
||||
$defs = $this->getEntityManager()->getMetadata()->get($entityType);
|
||||
$defs = $this->getEntityManager()->getMetadata()->get($this->entityType);
|
||||
if (!empty($defs['relations']) && array_key_exists('emailAddresses', $defs['relations'])) {
|
||||
if (empty($params['leftJoins'])) {
|
||||
$params['leftJoins'] = array();
|
||||
}
|
||||
if (empty($params['whereClause'])) {
|
||||
$params['whereClause'] = array();
|
||||
}
|
||||
if (empty($params['joinConditions'])) {
|
||||
$params['joinConditions'] = array();
|
||||
}
|
||||
$params['leftJoins'][] = 'emailAddresses';
|
||||
$params['joinConditions']['emailAddresses'] = array(
|
||||
if (empty($params['leftJoins'])) $params['leftJoins'] = [];
|
||||
$params['leftJoins'][] = ['emailAddresses', null, [
|
||||
'primary' => 1
|
||||
);
|
||||
]];
|
||||
}
|
||||
}
|
||||
|
||||
protected function handlePhoneNumberParams(&$params)
|
||||
{
|
||||
$entityType = $this->entityType;
|
||||
|
||||
$defs = $this->getEntityManager()->getMetadata()->get($entityType);
|
||||
$defs = $this->getEntityManager()->getMetadata()->get($this->entityType);
|
||||
if (!empty($defs['relations']) && array_key_exists('phoneNumbers', $defs['relations'])) {
|
||||
if (empty($params['leftJoins'])) {
|
||||
$params['leftJoins'] = array();
|
||||
}
|
||||
if (empty($params['whereClause'])) {
|
||||
$params['whereClause'] = array();
|
||||
}
|
||||
if (empty($params['joinConditions'])) {
|
||||
$params['joinConditions'] = array();
|
||||
}
|
||||
$params['leftJoins'][] = 'phoneNumbers';
|
||||
$params['joinConditions']['phoneNumbers'] = array(
|
||||
if (empty($params['leftJoins'])) $params['leftJoins'] = [];
|
||||
$params['leftJoins'][] = ['phoneNumbers', null, [
|
||||
'primary' => 1
|
||||
);
|
||||
]];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +183,11 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
protected function afterRemove(Entity $entity, array $options = array())
|
||||
{
|
||||
parent::afterRemove($entity, $options);
|
||||
|
||||
if (!$this->processFieldsAfterRemoveDisabled) {
|
||||
$this->processArrayFieldsRemove($entity);
|
||||
}
|
||||
|
||||
if (!$this->hooksDisabled && empty($options['skipHooks'])) {
|
||||
$this->getEntityManager()->getHookManager()->process($this->entityType, 'afterRemove', $entity, $options);
|
||||
}
|
||||
@@ -274,7 +256,7 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
}
|
||||
}
|
||||
|
||||
protected function afterSave(Entity $entity, array $options = array())
|
||||
protected function afterSave(Entity $entity, array $options = [])
|
||||
{
|
||||
if (!empty($this->restoreData)) {
|
||||
$entity->set($this->restoreData);
|
||||
@@ -285,8 +267,10 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
if (!$this->processFieldsAfterSaveDisabled) {
|
||||
$this->processEmailAddressSave($entity);
|
||||
$this->processPhoneNumberSave($entity);
|
||||
$this->processSpecifiedRelationsSave($entity);
|
||||
$this->processSpecifiedRelationsSave($entity, $options);
|
||||
$this->processFileFieldsSave($entity);
|
||||
$this->processArrayFieldsSave($entity);
|
||||
$this->processWysiwygFieldsSave($entity);
|
||||
}
|
||||
|
||||
if (!$this->hooksDisabled && empty($options['skipHooks'])) {
|
||||
@@ -405,6 +389,57 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
}
|
||||
}
|
||||
|
||||
protected function processArrayFieldsSave(Entity $entity)
|
||||
{
|
||||
foreach ($entity->getAttributes() as $attribute => $defs) {
|
||||
if (!isset($defs['type']) || $defs['type'] !== Entity::JSON_ARRAY) continue;
|
||||
if (!$entity->has($attribute)) continue;
|
||||
if (!$entity->isAttributeChanged($attribute)) continue;
|
||||
if (!$entity->getAttributeParam($attribute, 'storeArrayValues')) continue;
|
||||
if ($entity->getAttributeParam($attribute, 'notStorable')) continue;
|
||||
$this->getEntityManager()->getRepository('ArrayValue')->storeEntityAttribute($entity, $attribute);
|
||||
}
|
||||
}
|
||||
|
||||
protected function processWysiwygFieldsSave(Entity $entity)
|
||||
{
|
||||
if (!$entity->isNew()) return;
|
||||
|
||||
$fieldsDefs = $this->getMetadata()->get(['entityDefs', $entity->getEntityType(), 'fields'], []);
|
||||
foreach ($fieldsDefs as $field => $defs) {
|
||||
if (!empty($defs['type']) && $defs['type'] === 'wysiwyg') {
|
||||
$content = $entity->get($field);
|
||||
if (!$content) continue;
|
||||
if (preg_match_all("/\?entryPoint=attachment&id=([^&=\"']+)/", $content, $matches)) {
|
||||
if (!empty($matches[1]) && is_array($matches[1])) {
|
||||
foreach ($matches[1] as $id) {
|
||||
$attachment = $this->getEntityManager()->getEntity('Attachment', $id);
|
||||
if ($attachment) {
|
||||
if (!$attachment->get('relatedId') && !$attachment->get('sourceId')) {
|
||||
$attachment->set([
|
||||
'relatedId' => $entity->id,
|
||||
'relatedType' => $entity->getEntityType()
|
||||
]);
|
||||
$this->getEntityManager()->saveEntity($attachment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function processArrayFieldsRemove(Entity $entity)
|
||||
{
|
||||
foreach ($entity->getAttributes() as $attribute => $defs) {
|
||||
if (!isset($defs['type']) || $defs['type'] !== Entity::JSON_ARRAY) continue;
|
||||
if (!$entity->getAttributeParam($attribute, 'storeArrayValues')) continue;
|
||||
if ($entity->getAttributeParam($attribute, 'notStorable')) continue;
|
||||
$this->getEntityManager()->getRepository('ArrayValue')->deleteEntityAttribute($entity, $attribute);
|
||||
}
|
||||
}
|
||||
|
||||
protected function processEmailAddressSave(Entity $entity)
|
||||
{
|
||||
if ($entity->hasRelation('emailAddresses') && $entity->hasAttribute('emailAddress')) {
|
||||
@@ -419,108 +454,146 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
}
|
||||
}
|
||||
|
||||
protected function processSpecifiedRelationsSave(Entity $entity)
|
||||
public function processLinkMultipleFieldSave(Entity $entity, $link, array $options = [])
|
||||
{
|
||||
$name = $link;
|
||||
|
||||
$idListAttribute = $link . 'Ids';
|
||||
$columnsAttribute = $link . 'Columns';
|
||||
|
||||
if ($this->getMetadata()->get("entityDefs." . $entity->getEntityType() . ".fields.{$name}.noSave")) {
|
||||
return;
|
||||
}
|
||||
|
||||
$skipCreate = false;
|
||||
$skipRemove = false;
|
||||
$skipUpdate = false;
|
||||
if (!empty($options['skipLinkMultipleCreate'])) $skipCreate = true;
|
||||
if (!empty($options['skipLinkMultipleRemove'])) $skipRemove = true;
|
||||
if (!empty($options['skipLinkMultipleUpdate'])) $skipUpdate = true;
|
||||
|
||||
if ($entity->isNew()) {
|
||||
$skipRemove = true;
|
||||
$skipUpdate = true;
|
||||
}
|
||||
|
||||
if ($entity->has($idListAttribute)) {
|
||||
$specifiedIdList = $entity->get($idListAttribute);
|
||||
} else if ($entity->has($columnsAttribute)) {
|
||||
$skipRemove = true;
|
||||
$specifiedIdList = [];
|
||||
foreach ($entity->get($columnsAttribute) as $id => $d) {
|
||||
$specifiedIdList[] = $id;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_array($specifiedIdList)) return;
|
||||
|
||||
$toRemoveIdList = [];
|
||||
$existingIdList = [];
|
||||
$toUpdateIdList = [];
|
||||
$toCreateIdList = [];
|
||||
$existingColumnsData = (object)[];
|
||||
|
||||
$defs = [];
|
||||
$columns = $this->getMetadata()->get("entityDefs." . $entity->getEntityType() . ".fields.{$name}.columns");
|
||||
if (!empty($columns)) {
|
||||
$columnData = $entity->get($columnsAttribute);
|
||||
$defs['additionalColumns'] = $columns;
|
||||
}
|
||||
|
||||
if (!$skipRemove && !$skipUpdate) {
|
||||
$foreignEntityList = $entity->get($name, $defs);
|
||||
if ($foreignEntityList) {
|
||||
foreach ($foreignEntityList as $foreignEntity) {
|
||||
$existingIdList[] = $foreignEntity->id;
|
||||
if (!empty($columns)) {
|
||||
$data = (object)[];
|
||||
foreach ($columns as $columnName => $columnField) {
|
||||
$foreignId = $foreignEntity->id;
|
||||
$data->$columnName = $foreignEntity->get($columnField);
|
||||
}
|
||||
$existingColumnsData->$foreignId = $data;
|
||||
if (!$entity->isNew()) {
|
||||
$entity->setFetched($columnsAttribute, $existingColumnsData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$entity->isNew()) {
|
||||
if ($entity->has($idListAttribute) && !$entity->hasFetched($idListAttribute)) {
|
||||
$entity->setFetched($idListAttribute, $existingIdList);
|
||||
}
|
||||
if ($entity->has($columnsAttribute) && !empty($columns)) {
|
||||
$entity->setFetched($columnsAttribute, $existingColumnsData);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($existingIdList as $id) {
|
||||
if (!in_array($id, $specifiedIdList)) {
|
||||
if (!$skipRemove) {
|
||||
$toRemoveIdList[] = $id;
|
||||
}
|
||||
} else {
|
||||
if (!$skipUpdate && !empty($columns)) {
|
||||
foreach ($columns as $columnName => $columnField) {
|
||||
if (isset($columnData->$id) && is_object($columnData->$id)) {
|
||||
if (
|
||||
property_exists($columnData->$id, $columnName)
|
||||
&&
|
||||
(
|
||||
!property_exists($existingColumnsData->$id, $columnName)
|
||||
||
|
||||
$columnData->$id->$columnName !== $existingColumnsData->$id->$columnName
|
||||
)
|
||||
) {
|
||||
$toUpdateIdList[] = $id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$skipCreate) {
|
||||
foreach ($specifiedIdList as $id) {
|
||||
if (!in_array($id, $existingIdList)) {
|
||||
$toCreateIdList[] = $id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($toCreateIdList as $id) {
|
||||
$data = null;
|
||||
if (!empty($columns) && isset($columnData->$id)) {
|
||||
$data = $columnData->$id;
|
||||
}
|
||||
$this->relate($entity, $name, $id, $data);
|
||||
}
|
||||
|
||||
foreach ($toRemoveIdList as $id) {
|
||||
$this->unrelate($entity, $name, $id);
|
||||
}
|
||||
|
||||
foreach ($toUpdateIdList as $id) {
|
||||
$data = $columnData->$id;
|
||||
$this->updateRelation($entity, $name, $id, $data);
|
||||
}
|
||||
}
|
||||
|
||||
protected function processSpecifiedRelationsSave(Entity $entity, array $options = [])
|
||||
{
|
||||
$relationTypeList = [$entity::HAS_MANY, $entity::MANY_MANY, $entity::HAS_CHILDREN];
|
||||
foreach ($entity->getRelations() as $name => $defs) {
|
||||
if (in_array($defs['type'], $relationTypeList)) {
|
||||
$fieldName = $name . 'Ids';
|
||||
$columnsFieldsName = $name . 'Columns';
|
||||
|
||||
|
||||
if ($entity->has($fieldName) || $entity->has($columnsFieldsName)) {
|
||||
if ($this->getMetadata()->get("entityDefs." . $entity->getEntityType() . ".fields.{$name}.noSave")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($entity->has($fieldName)) {
|
||||
$specifiedIds = $entity->get($fieldName);
|
||||
} else {
|
||||
$specifiedIds = array();
|
||||
foreach ($entity->get($columnsFieldsName) as $id => $d) {
|
||||
$specifiedIds[] = $id;
|
||||
}
|
||||
}
|
||||
if (is_array($specifiedIds)) {
|
||||
$toRemoveIds = array();
|
||||
$existingIds = array();
|
||||
$toUpdateIds = array();
|
||||
$existingColumnsData = new \stdClass();
|
||||
|
||||
$defs = array();
|
||||
$columns = $this->getMetadata()->get("entityDefs." . $entity->getEntityType() . ".fields.{$name}.columns");
|
||||
if (!empty($columns)) {
|
||||
$columnData = $entity->get($columnsFieldsName);
|
||||
$defs['additionalColumns'] = $columns;
|
||||
}
|
||||
|
||||
$foreignCollection = $entity->get($name, $defs);
|
||||
if ($foreignCollection) {
|
||||
foreach ($foreignCollection as $foreignEntity) {
|
||||
$existingIds[] = $foreignEntity->id;
|
||||
if (!empty($columns)) {
|
||||
$data = new \stdClass();
|
||||
foreach ($columns as $columnName => $columnField) {
|
||||
$foreignId = $foreignEntity->id;
|
||||
$data->$columnName = $foreignEntity->get($columnField);
|
||||
}
|
||||
$existingColumnsData->$foreignId = $data;
|
||||
$entity->setFetched($columnsFieldsName, $existingColumnsData);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if ($entity->has($fieldName)) {
|
||||
$entity->setFetched($fieldName, $existingIds);
|
||||
}
|
||||
if ($entity->has($columnsFieldsName) && !empty($columns)) {
|
||||
$entity->setFetched($columnsFieldsName, $existingColumnsData);
|
||||
}
|
||||
|
||||
foreach ($existingIds as $id) {
|
||||
if (!in_array($id, $specifiedIds)) {
|
||||
$toRemoveIds[] = $id;
|
||||
} else {
|
||||
if (!empty($columns)) {
|
||||
foreach ($columns as $columnName => $columnField) {
|
||||
if (isset($columnData->$id) && is_object($columnData->$id)) {
|
||||
if (
|
||||
property_exists($columnData->$id, $columnName)
|
||||
&&
|
||||
(
|
||||
!property_exists($existingColumnsData->$id, $columnName)
|
||||
||
|
||||
$columnData->$id->$columnName !== $existingColumnsData->$id->$columnName
|
||||
)
|
||||
) {
|
||||
$toUpdateIds[] = $id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($specifiedIds as $id) {
|
||||
if (!in_array($id, $existingIds)) {
|
||||
$data = null;
|
||||
if (!empty($columns) && isset($columnData->$id)) {
|
||||
$data = $columnData->$id;
|
||||
}
|
||||
$this->relate($entity, $name, $id, $data);
|
||||
}
|
||||
}
|
||||
foreach ($toRemoveIds as $id) {
|
||||
$this->unrelate($entity, $name, $id);
|
||||
}
|
||||
if (!empty($columns)) {
|
||||
foreach ($toUpdateIds as $id) {
|
||||
$data = $columnData->$id;
|
||||
$this->updateRelation($entity, $name, $id, $data);
|
||||
}
|
||||
}
|
||||
}
|
||||
$idListAttribute = $name . 'Ids';
|
||||
$columnsAttribute = $name . 'Columns';
|
||||
if ($entity->has($idListAttribute) || $entity->has($columnsAttribute)) {
|
||||
$this->processLinkMultipleFieldSave($entity, $name, $options);
|
||||
}
|
||||
} else if ($defs['type'] === $entity::HAS_ONE) {
|
||||
if (empty($defs['entity']) || empty($defs['foreignKey'])) continue;
|
||||
@@ -531,35 +604,37 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
|
||||
|
||||
$foreignEntityType = $defs['entity'];
|
||||
$foreignKey = $defs['foreignKey'];
|
||||
$idFieldName = $name . 'Id';
|
||||
$nameFieldName = $name . 'Name';
|
||||
$idAttribute = $name . 'Id';
|
||||
|
||||
if (!$entity->has($idFieldName)) continue;
|
||||
if (!$entity->has($idAttribute)) continue;
|
||||
|
||||
$where = array();
|
||||
$where = [];
|
||||
$where[$foreignKey] = $entity->id;
|
||||
$previousForeignEntity = $this->getEntityManager()->getRepository($foreignEntityType)->where($where)->findOne();
|
||||
if ($previousForeignEntity) {
|
||||
$entity->setFetched($idFieldName, $previousForeignEntity->id);
|
||||
if ($previousForeignEntity->id !== $entity->get($idFieldName)) {
|
||||
if (!$entity->isNew()) {
|
||||
$entity->setFetched($idAttribute, $previousForeignEntity->id);
|
||||
}
|
||||
if ($previousForeignEntity->id !== $entity->get($idAttribute)) {
|
||||
$previousForeignEntity->set($foreignKey, null);
|
||||
$this->getEntityManager()->saveEntity($previousForeignEntity);
|
||||
}
|
||||
} else {
|
||||
$entity->setFetched($idFieldName, null);
|
||||
if (!$entity->isNew()) {
|
||||
$entity->setFetched($idAttribute, null);
|
||||
}
|
||||
}
|
||||
|
||||
if ($entity->get($idFieldName)) {
|
||||
$newForeignEntity = $this->getEntityManager()->getEntity($foreignEntityType, $entity->get($idFieldName));
|
||||
if ($entity->get($idAttribute)) {
|
||||
$newForeignEntity = $this->getEntityManager()->getEntity($foreignEntityType, $entity->get($idAttribute));
|
||||
if ($newForeignEntity) {
|
||||
$newForeignEntity->set($foreignKey, $entity->id);
|
||||
$this->getEntityManager()->saveEntity($newForeignEntity);
|
||||
} else {
|
||||
$entity->set($idFieldName, null);
|
||||
$entity->set($idAttribute, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -264,8 +264,7 @@ class AclManager extends \Espo\Core\AclManager
|
||||
|
||||
protected function checkUserIsNotPortal($user)
|
||||
{
|
||||
return !$user->get('isPortalUser');
|
||||
return !$user->isPortal();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -61,10 +61,15 @@ class Event extends \Espo\Core\ORM\Repositories\RDB
|
||||
$pdo->query($sql);
|
||||
}
|
||||
|
||||
protected function afterSave(Entity $entity, array $options = array())
|
||||
protected function afterSave(Entity $entity, array $options = [])
|
||||
{
|
||||
parent::afterSave($entity, $options);
|
||||
$this->processReminderAfterSave($entity, $options);
|
||||
|
||||
parent::afterSave($entity, $options);
|
||||
}
|
||||
|
||||
protected function processReminderAfterSave(Entity $entity, array $options = [])
|
||||
{
|
||||
if (
|
||||
$entity->isNew() ||
|
||||
$entity->isAttributeChanged('assignedUserId') ||
|
||||
|
||||
@@ -136,8 +136,11 @@ class Base
|
||||
|
||||
protected function order($sortBy, $desc = false, &$result)
|
||||
{
|
||||
if (!empty($sortBy)) {
|
||||
if (is_string($desc)) {
|
||||
$desc = $desc === strtolower('desc');
|
||||
}
|
||||
|
||||
if (!empty($sortBy)) {
|
||||
$result['orderBy'] = $sortBy;
|
||||
$type = $this->getMetadata()->get(['entityDefs', $this->getEntityType(), 'fields', $sortBy, 'type']);
|
||||
if (in_array($type, ['link', 'file', 'image'])) {
|
||||
@@ -150,7 +153,7 @@ class Base
|
||||
} else {
|
||||
$orderPart = 'DESC';
|
||||
}
|
||||
$result['orderBy'] = [[$sortBy . 'Country', $orderPart], [$sortBy . 'City', $orderPart], [$sortBy . '_eet', $orderPart]];
|
||||
$result['orderBy'] = [[$sortBy . 'Country', $orderPart], [$sortBy . 'City', $orderPart], [$sortBy . 'Street', $orderPart]];
|
||||
return;
|
||||
} else if ($type === 'enum') {
|
||||
$list = $this->getMetadata()->get(['entityDefs', $this->getEntityType(), 'fields', $sortBy, 'options']);
|
||||
@@ -222,6 +225,8 @@ class Base
|
||||
$whereClause = $this->convertWhere($where, false, $result);
|
||||
|
||||
$result['whereClause'] = array_merge($result['whereClause'], $whereClause);
|
||||
|
||||
$this->applyLeftJoinsFromWhere($where, $result);
|
||||
}
|
||||
|
||||
public function convertWhere(array $where, $ignoreAdditionaFilterTypes = false, &$result = null)
|
||||
@@ -423,7 +428,7 @@ class Base
|
||||
protected function prepareResult(&$result)
|
||||
{
|
||||
if (empty($result)) {
|
||||
$result = array();
|
||||
$result = [];
|
||||
}
|
||||
if (empty($result['joins'])) {
|
||||
$result['joins'] = [];
|
||||
@@ -699,14 +704,26 @@ class Base
|
||||
|
||||
public function getSelectParams(array $params, $withAcl = false, $checkWherePermission = false)
|
||||
{
|
||||
$result = array();
|
||||
$result = [];
|
||||
$this->prepareResult($result);
|
||||
|
||||
if (!empty($params['sortBy'])) {
|
||||
if (!array_key_exists('asc', $params)) {
|
||||
$params['asc'] = true;
|
||||
if (!empty($params['orderBy'])) {
|
||||
$isDesc = false;
|
||||
if (isset($params['order'])) {
|
||||
$isDesc = $params['order'] === 'desc';
|
||||
}
|
||||
$this->order($params['sortBy'], !$params['asc'], $result);
|
||||
$this->order($params['orderBy'], $isDesc, $result);
|
||||
} else if (!empty($params['sortBy'])) {
|
||||
if (isset($params['order'])) {
|
||||
$isDesc = $params['order'] === 'desc';
|
||||
} else if (isset($params['asc'])) {
|
||||
$isDesc = $params['asc'] !== true;
|
||||
}
|
||||
$this->order($params['sortBy'], $isDesc, $result);
|
||||
} else if (!empty($params['order'])) {
|
||||
$orderBy = $this->getMetadata()->get(['entityDefs', $this->getEntityType(), 'collection', 'orderBy']);
|
||||
$isDesc = $params['order'] === 'desc';
|
||||
$this->order($orderBy, $isDesc, $result);
|
||||
}
|
||||
|
||||
if (!isset($params['offset'])) {
|
||||
@@ -839,10 +856,12 @@ class Base
|
||||
case 'today':
|
||||
$where['type'] = 'between';
|
||||
$dt->setTime(0, 0, 0);
|
||||
$dtTo = clone $dt;
|
||||
$dtTo->modify('+1 day -1 second');
|
||||
$dt->setTimezone(new \DateTimeZone('UTC'));
|
||||
$dtTo->setTimezone(new \DateTimeZone('UTC'));
|
||||
$from = $dt->format($format);
|
||||
$dt->modify('+1 day -1 second');
|
||||
$to = $dt->format($format);
|
||||
$to = $dtTo->format($format);
|
||||
$where['value'] = [$from, $to];
|
||||
break;
|
||||
case 'past':
|
||||
@@ -927,12 +946,13 @@ class Base
|
||||
break;
|
||||
case 'on':
|
||||
$where['type'] = 'between';
|
||||
|
||||
$dt = new \DateTime($value, new \DateTimeZone($timeZone));
|
||||
$dtTo = clone $dt;
|
||||
$dtTo->modify('+1 day -1 second');
|
||||
$dt->setTimezone(new \DateTimeZone('UTC'));
|
||||
$dtTo->setTimezone(new \DateTimeZone('UTC'));
|
||||
$from = $dt->format($format);
|
||||
$dt->modify('+1 day -1 second');
|
||||
$to = $dt->format($format);
|
||||
$to = $dtTo->format($format);
|
||||
$where['value'] = [$from, $to];
|
||||
break;
|
||||
case 'before':
|
||||
@@ -944,6 +964,7 @@ class Base
|
||||
case 'after':
|
||||
$where['type'] = 'after';
|
||||
$dt = new \DateTime($value, new \DateTimeZone($timeZone));
|
||||
$dt->modify('+1 day -1 second');
|
||||
$dt->setTimezone(new \DateTimeZone('UTC'));
|
||||
$where['value'] = $dt->format($format);
|
||||
break;
|
||||
@@ -972,7 +993,7 @@ class Base
|
||||
|
||||
protected function getWherePart($item, &$result = null)
|
||||
{
|
||||
$part = array();
|
||||
$part = [];
|
||||
|
||||
$attribute = null;
|
||||
if (!empty($item['field'])) { // for backward compatibility
|
||||
@@ -1004,165 +1025,199 @@ class Base
|
||||
if (!array_key_exists('value', $item)) {
|
||||
$item['value'] = null;
|
||||
}
|
||||
$value = $item['value'];
|
||||
|
||||
if (!empty($item['type'])) {
|
||||
switch ($item['type']) {
|
||||
$type = $item['type'];
|
||||
|
||||
switch ($type) {
|
||||
case 'or':
|
||||
case 'and':
|
||||
case 'not':
|
||||
if (is_array($item['value'])) {
|
||||
$arr = array();
|
||||
foreach ($item['value'] as $i) {
|
||||
if (is_array($value)) {
|
||||
$arr = [];
|
||||
foreach ($value as $i) {
|
||||
$a = $this->getWherePart($i, $result);
|
||||
foreach ($a as $left => $right) {
|
||||
if (!empty($right) || is_null($right) || $right === '' || $right === 0 || $right === false) {
|
||||
$arr[] = array($left => $right);
|
||||
$arr[] = [$left => $right];
|
||||
}
|
||||
}
|
||||
}
|
||||
$part[strtoupper($item['type'])] = $arr;
|
||||
$part[strtoupper($type)] = $arr;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'like':
|
||||
$part[$attribute . '*'] = $item['value'];
|
||||
$part[$attribute . '*'] = $value;
|
||||
break;
|
||||
|
||||
case 'notLike':
|
||||
$part[$attribute . '!*'] = $item['value'];
|
||||
$part[$attribute . '!*'] = $value;
|
||||
break;
|
||||
|
||||
case 'equals':
|
||||
case 'on':
|
||||
$part[$attribute . '='] = $item['value'];
|
||||
$part[$attribute . '='] = $value;
|
||||
break;
|
||||
|
||||
case 'startsWith':
|
||||
$part[$attribute . '*'] = $item['value'] . '%';
|
||||
$part[$attribute . '*'] = $value . '%';
|
||||
break;
|
||||
|
||||
case 'endsWith':
|
||||
$part[$attribute . '*'] = '%' . $item['value'];
|
||||
$part[$attribute . '*'] = '%' . $value;
|
||||
break;
|
||||
|
||||
case 'contains':
|
||||
$part[$attribute . '*'] = '%' . $item['value'] . '%';
|
||||
$part[$attribute . '*'] = '%' . $value . '%';
|
||||
break;
|
||||
|
||||
case 'notContains':
|
||||
$part[$attribute . '!*'] = '%' . $item['value'] . '%';
|
||||
$part[$attribute . '!*'] = '%' . $value . '%';
|
||||
break;
|
||||
|
||||
case 'notEquals':
|
||||
case 'notOn':
|
||||
$part[$attribute . '!='] = $item['value'];
|
||||
$part[$attribute . '!='] = $value;
|
||||
break;
|
||||
|
||||
case 'greaterThan':
|
||||
case 'after':
|
||||
$part[$attribute . '>'] = $item['value'];
|
||||
$part[$attribute . '>'] = $value;
|
||||
break;
|
||||
|
||||
case 'lessThan':
|
||||
case 'before':
|
||||
$part[$attribute . '<'] = $item['value'];
|
||||
$part[$attribute . '<'] = $value;
|
||||
break;
|
||||
|
||||
case 'greaterThanOrEquals':
|
||||
$part[$attribute . '>='] = $item['value'];
|
||||
$part[$attribute . '>='] = $value;
|
||||
break;
|
||||
|
||||
case 'lessThanOrEquals':
|
||||
$part[$attribute . '<='] = $item['value'];
|
||||
$part[$attribute . '<='] = $value;
|
||||
break;
|
||||
|
||||
case 'in':
|
||||
$part[$attribute . '='] = $item['value'];
|
||||
$part[$attribute . '='] = $value;
|
||||
break;
|
||||
|
||||
case 'notIn':
|
||||
$part[$attribute . '!='] = $item['value'];
|
||||
$part[$attribute . '!='] = $value;
|
||||
break;
|
||||
|
||||
case 'isNull':
|
||||
$part[$attribute . '='] = null;
|
||||
break;
|
||||
|
||||
case 'isNotNull':
|
||||
case 'ever':
|
||||
$part[$attribute . '!='] = null;
|
||||
break;
|
||||
|
||||
case 'isTrue':
|
||||
$part[$attribute . '='] = true;
|
||||
break;
|
||||
|
||||
case 'isFalse':
|
||||
$part[$attribute . '='] = false;
|
||||
break;
|
||||
|
||||
case 'today':
|
||||
$part[$attribute . '='] = date('Y-m-d');
|
||||
break;
|
||||
|
||||
case 'past':
|
||||
$part[$attribute . '<'] = date('Y-m-d');
|
||||
break;
|
||||
|
||||
case 'future':
|
||||
$part[$attribute . '>='] = date('Y-m-d');
|
||||
break;
|
||||
|
||||
case 'lastSevenDays':
|
||||
$dt1 = new \DateTime();
|
||||
$dt2 = clone $dt1;
|
||||
$dt2->modify('-7 days');
|
||||
$part['AND'] = array(
|
||||
$part['AND'] = [
|
||||
$attribute . '>=' => $dt2->format('Y-m-d'),
|
||||
$attribute . '<=' => $dt1->format('Y-m-d'),
|
||||
);
|
||||
];
|
||||
break;
|
||||
|
||||
case 'lastXDays':
|
||||
$dt1 = new \DateTime();
|
||||
$dt2 = clone $dt1;
|
||||
$number = strval(intval($item['value']));
|
||||
$number = strval(intval($value));
|
||||
|
||||
$dt2->modify('-'.$number.' days');
|
||||
$part['AND'] = array(
|
||||
$part['AND'] = [
|
||||
$attribute . '>=' => $dt2->format('Y-m-d'),
|
||||
$attribute . '<=' => $dt1->format('Y-m-d'),
|
||||
);
|
||||
];
|
||||
break;
|
||||
|
||||
case 'nextXDays':
|
||||
$dt1 = new \DateTime();
|
||||
$dt2 = clone $dt1;
|
||||
$number = strval(intval($item['value']));
|
||||
$number = strval(intval($value));
|
||||
$dt2->modify('+'.$number.' days');
|
||||
$part['AND'] = array(
|
||||
$part['AND'] = [
|
||||
$attribute . '>=' => $dt1->format('Y-m-d'),
|
||||
$attribute . '<=' => $dt2->format('Y-m-d'),
|
||||
);
|
||||
];
|
||||
break;
|
||||
|
||||
case 'olderThanXDays':
|
||||
$dt1 = new \DateTime();
|
||||
$number = strval(intval($item['value']));
|
||||
$number = strval(intval($value));
|
||||
$dt1->modify('-'.$number.' days');
|
||||
$part[$attribute . '<'] = $dt1->format('Y-m-d');
|
||||
break;
|
||||
|
||||
case 'afterXDays':
|
||||
$dt1 = new \DateTime();
|
||||
$number = strval(intval($item['value']));
|
||||
$number = strval(intval($value));
|
||||
$dt1->modify('+'.$number.' days');
|
||||
$part[$attribute . '>'] = $dt1->format('Y-m-d');
|
||||
break;
|
||||
|
||||
case 'currentMonth':
|
||||
$dt = new \DateTime();
|
||||
$part['AND'] = array(
|
||||
$part['AND'] = [
|
||||
$attribute . '>=' => $dt->modify('first day of this month')->format('Y-m-d'),
|
||||
$attribute . '<' => $dt->add(new \DateInterval('P1M'))->format('Y-m-d'),
|
||||
);
|
||||
];
|
||||
break;
|
||||
|
||||
case 'lastMonth':
|
||||
$dt = new \DateTime();
|
||||
$part['AND'] = array(
|
||||
$part['AND'] = [
|
||||
$attribute . '>=' => $dt->modify('first day of last month')->format('Y-m-d'),
|
||||
$attribute . '<' => $dt->add(new \DateInterval('P1M'))->format('Y-m-d'),
|
||||
);
|
||||
];
|
||||
break;
|
||||
|
||||
case 'nextMonth':
|
||||
$dt = new \DateTime();
|
||||
$part['AND'] = array(
|
||||
$part['AND'] = [
|
||||
$attribute . '>=' => $dt->modify('first day of next month')->format('Y-m-d'),
|
||||
$attribute . '<' => $dt->add(new \DateInterval('P1M'))->format('Y-m-d'),
|
||||
);
|
||||
];
|
||||
break;
|
||||
|
||||
case 'currentQuarter':
|
||||
$dt = new \DateTime();
|
||||
$quarter = ceil($dt->format('m') / 3);
|
||||
$dt->modify('first day of January this year');
|
||||
$part['AND'] = array(
|
||||
$part['AND'] = [
|
||||
$attribute . '>=' => $dt->add(new \DateInterval('P'.(($quarter - 1) * 3).'M'))->format('Y-m-d'),
|
||||
$attribute . '<' => $dt->add(new \DateInterval('P3M'))->format('Y-m-d'),
|
||||
);
|
||||
];
|
||||
break;
|
||||
|
||||
case 'lastQuarter':
|
||||
$dt = new \DateTime();
|
||||
$quarter = ceil($dt->format('m') / 3);
|
||||
@@ -1172,33 +1227,80 @@ class Base
|
||||
$quarter = 4;
|
||||
$dt->modify('-1 year');
|
||||
}
|
||||
$part['AND'] = array(
|
||||
$part['AND'] = [
|
||||
$attribute . '>=' => $dt->add(new \DateInterval('P'.(($quarter - 1) * 3).'M'))->format('Y-m-d'),
|
||||
$attribute . '<' => $dt->add(new \DateInterval('P3M'))->format('Y-m-d'),
|
||||
);
|
||||
];
|
||||
break;
|
||||
|
||||
case 'currentYear':
|
||||
$dt = new \DateTime();
|
||||
$part['AND'] = array(
|
||||
$part['AND'] = [
|
||||
$attribute . '>=' => $dt->modify('first day of January this year')->format('Y-m-d'),
|
||||
$attribute . '<' => $dt->add(new \DateInterval('P1Y'))->format('Y-m-d'),
|
||||
);
|
||||
];
|
||||
break;
|
||||
|
||||
case 'lastYear':
|
||||
$dt = new \DateTime();
|
||||
$part['AND'] = array(
|
||||
$part['AND'] = [
|
||||
$attribute . '>=' => $dt->modify('first day of January last year')->format('Y-m-d'),
|
||||
$attribute . '<' => $dt->add(new \DateInterval('P1Y'))->format('Y-m-d'),
|
||||
);
|
||||
];
|
||||
break;
|
||||
|
||||
case 'currentFiscalYear':
|
||||
case 'lastFiscalYear':
|
||||
$dtToday = new \DateTime();
|
||||
$dt = new \DateTime();
|
||||
$fiscalYearShift = $this->getConfig()->get('fiscalYearShift', 0);
|
||||
$dt->modify('first day of January this year')->modify('+' . $fiscalYearShift . ' months');
|
||||
if (intval($dtToday->format('m')) < $fiscalYearShift + 1) {
|
||||
$dt->modify('-1 year');
|
||||
}
|
||||
if ($type === 'lastFiscalYear') {
|
||||
$dt->modify('-1 year');
|
||||
}
|
||||
$part['AND'] = [
|
||||
$attribute . '>=' => $dt->format('Y-m-d'),
|
||||
$attribute . '<' => $dt->add(new \DateInterval('P1Y'))->format('Y-m-d')
|
||||
];
|
||||
break;
|
||||
|
||||
case 'currentFiscalQuarter':
|
||||
case 'lastFiscalQuarter':
|
||||
$dtToday = new \DateTime();
|
||||
$dt = new \DateTime();
|
||||
$fiscalYearShift = $this->getConfig()->get('fiscalYearShift', 0);
|
||||
$dt->modify('first day of January this year')->modify('+' . $fiscalYearShift . ' months');
|
||||
$month = intval($dtToday->format('m'));
|
||||
$quarterShift = floor(($month - $fiscalYearShift - 1) / 3);
|
||||
if ($quarterShift) {
|
||||
if ($quarterShift >= 0) {
|
||||
$dt->add(new \DateInterval('P'.($quarterShift * 3).'M'));
|
||||
} else {
|
||||
$quarterShift *= -1;
|
||||
$dt->sub(new \DateInterval('P'.($quarterShift * 3).'M'));
|
||||
}
|
||||
}
|
||||
if ($type === 'lastFiscalQuarter') {
|
||||
$dt->modify('-3 months');
|
||||
}
|
||||
$part['AND'] = [
|
||||
$attribute . '>=' => $dt->format('Y-m-d'),
|
||||
$attribute . '<' => $dt->add(new \DateInterval('P3M'))->format('Y-m-d')
|
||||
];
|
||||
break;
|
||||
|
||||
case 'between':
|
||||
if (is_array($item['value'])) {
|
||||
$part['AND'] = array(
|
||||
$attribute . '>=' => $item['value'][0],
|
||||
$attribute . '<=' => $item['value'][1],
|
||||
);
|
||||
if (is_array($value)) {
|
||||
$part['AND'] = [
|
||||
$attribute . '>=' => $value[0],
|
||||
$attribute . '<=' => $value[1],
|
||||
];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'columnLike':
|
||||
case 'columnIn':
|
||||
case 'columnIsNull':
|
||||
@@ -1208,30 +1310,30 @@ class Base
|
||||
$alias = $link . 'Filter' . strval(rand(10000, 99999));
|
||||
$this->setDistinct(true, $result);
|
||||
$this->addLeftJoin([$link, $alias], $result);
|
||||
$value = $item['value'];
|
||||
$columnKey = $alias . 'Middle.' . $column;
|
||||
if ($item['type'] === 'columnIn') {
|
||||
if ($type === 'columnIn') {
|
||||
$part[$columnKey] = $value;
|
||||
} else if ($item['type'] === 'columnNotIn') {
|
||||
} else if ($type === 'columnNotIn') {
|
||||
$part[$columnKey . '!='] = $value;
|
||||
} else if ($item['type'] === 'columnIsNull') {
|
||||
} else if ($type === 'columnIsNull') {
|
||||
$part[$columnKey] = null;
|
||||
} else if ($item['type'] === 'columnIsNotNull') {
|
||||
} else if ($type === 'columnIsNotNull') {
|
||||
$part[$columnKey . '!='] = null;
|
||||
} else if ($item['type'] === 'columnLike') {
|
||||
} else if ($type === 'columnLike') {
|
||||
$part[$columnKey . '*'] = $value;
|
||||
} else if ($item['type'] === 'columnStartsWith') {
|
||||
} else if ($type === 'columnStartsWith') {
|
||||
$part[$columnKey . '*'] = $value . '%';
|
||||
} else if ($item['type'] === 'columnEndsWith') {
|
||||
} else if ($type === 'columnEndsWith') {
|
||||
$part[$columnKey . '*'] = '%' . $value;
|
||||
} else if ($item['type'] === 'columnContains') {
|
||||
} else if ($type === 'columnContains') {
|
||||
$part[$columnKey . '*'] = '%' . $value . '%';
|
||||
} else if ($item['type'] === 'columnEquals') {
|
||||
} else if ($type === 'columnEquals') {
|
||||
$part[$columnKey . '='] = $value;
|
||||
} else if ($item['type'] === 'columnNotEquals') {
|
||||
} else if ($type === 'columnNotEquals') {
|
||||
$part[$columnKey . '!='] = $value;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'isNotLinked':
|
||||
if (!$result) break;
|
||||
$alias = $attribute . 'IsNotLinkedFilter' . strval(rand(10000, 99999));
|
||||
@@ -1239,6 +1341,7 @@ class Base
|
||||
$this->setDistinct(true, $result);
|
||||
$this->addLeftJoin([$attribute, $alias], $result);
|
||||
break;
|
||||
|
||||
case 'isLinked':
|
||||
if (!$result) break;
|
||||
$alias = $attribute . 'IsLinkedFilter' . strval(rand(10000, 99999));
|
||||
@@ -1246,6 +1349,7 @@ class Base
|
||||
$this->setDistinct(true, $result);
|
||||
$this->addLeftJoin([$attribute, $alias], $result);
|
||||
break;
|
||||
|
||||
case 'linkedWith':
|
||||
$seed = $this->getSeed();
|
||||
$link = $attribute;
|
||||
@@ -1253,9 +1357,7 @@ class Base
|
||||
|
||||
$alias = $link . 'Filter' . strval(rand(10000, 99999));
|
||||
|
||||
$value = $item['value'];
|
||||
|
||||
if (is_null($value)) break;
|
||||
if (is_null($value) || !$value && !is_array($value)) break;
|
||||
|
||||
$relationType = $seed->getRelationType($link);
|
||||
|
||||
@@ -1284,13 +1386,12 @@ class Base
|
||||
}
|
||||
$this->setDistinct(true, $result);
|
||||
break;
|
||||
|
||||
case 'notLinkedWith':
|
||||
$seed = $this->getSeed();
|
||||
$link = $attribute;
|
||||
if (!$seed->hasRelation($link)) break;
|
||||
|
||||
$value = $item['value'];
|
||||
|
||||
if (is_null($value)) break;
|
||||
|
||||
$relationType = $seed->getRelationType($link);
|
||||
@@ -1316,12 +1417,64 @@ class Base
|
||||
$part[$key . '!='] = $value;
|
||||
}
|
||||
} else if ($relationType == 'hasOne') {
|
||||
$this->addLeftJoin([$link, alias], $result);
|
||||
$this->addLeftJoin([$link, $alias], $result);
|
||||
$part[$alias . '.id!='] = $value;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
$this->setDistinct(true, $result);
|
||||
break;
|
||||
|
||||
case 'arrayAnyOf':
|
||||
case 'arrayNoneOf':
|
||||
case 'arrayIsEmpty':
|
||||
case 'arrayIsNotEmpty':
|
||||
$arrayValueAlias = 'arrayFilter' . strval(rand(10000, 99999));
|
||||
$arrayAttribute = $attribute;
|
||||
$arrayEntityType = $this->getEntityType();
|
||||
$idPart = 'id';
|
||||
|
||||
if (strpos($attribute, '.') > 0) {
|
||||
list($arrayAttributeLink, $arrayAttribute) = explode('.', $attribute);
|
||||
$seed = $this->getSeed();
|
||||
$arrayEntityType = $seed->getRelationParam($arrayAttributeLink, 'entity');
|
||||
$idPart = $arrayAttributeLink . '.id';
|
||||
}
|
||||
|
||||
if ($type === 'arrayAnyOf') {
|
||||
if (is_null($value) || !$value && !is_array($value)) break;
|
||||
$this->addLeftJoin(['ArrayValue', $arrayValueAlias, [
|
||||
$arrayValueAlias . '.entityId:' => $idPart,
|
||||
$arrayValueAlias . '.entityType' => $arrayEntityType,
|
||||
$arrayValueAlias . '.attribute' => $arrayAttribute
|
||||
]], $result);
|
||||
$part[$arrayValueAlias . '.value'] = $value;
|
||||
} else if ($type === 'arrayNoneOf') {
|
||||
if (is_null($value) || !$value && !is_array($value)) break;
|
||||
$this->addLeftJoin(['ArrayValue', $arrayValueAlias, [
|
||||
$arrayValueAlias . '.entityId:' => $idPart,
|
||||
$arrayValueAlias . '.entityType' => $arrayEntityType,
|
||||
$arrayValueAlias . '.attribute' => $arrayAttribute,
|
||||
$arrayValueAlias . '.value=' => $value
|
||||
]], $result);
|
||||
$part[$arrayValueAlias . '.id'] = null;
|
||||
} else if ($type === 'arrayIsEmpty') {
|
||||
$this->addLeftJoin(['ArrayValue', $arrayValueAlias, [
|
||||
$arrayValueAlias . '.entityId:' => $idPart,
|
||||
$arrayValueAlias . '.entityType' => $arrayEntityType,
|
||||
$arrayValueAlias . '.attribute' => $arrayAttribute
|
||||
]], $result);
|
||||
$part[$arrayValueAlias . '.id'] = null;
|
||||
} else if ($type === 'arrayIsNotEmpty') {
|
||||
$this->addLeftJoin(['ArrayValue', $arrayValueAlias, [
|
||||
$arrayValueAlias . '.entityId:' => $idPart,
|
||||
$arrayValueAlias . '.entityType' => $arrayEntityType,
|
||||
$arrayValueAlias . '.attribute' => $arrayAttribute
|
||||
]], $result);
|
||||
$part[$arrayValueAlias . '.id!='] = null;
|
||||
}
|
||||
|
||||
$this->setDistinct(true, $result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1565,6 +1718,12 @@ class Base
|
||||
$useFullTextSearch = false;
|
||||
}
|
||||
|
||||
if ($isAuxiliaryUse) {
|
||||
if (mb_strpos($textFilter, '@') !== false) {
|
||||
$useFullTextSearch = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($useFullTextSearch) {
|
||||
$textFilter = str_replace(['(', ')'], '', $textFilter);
|
||||
|
||||
@@ -1582,10 +1741,11 @@ class Base
|
||||
$function = 'MATCH_NATURAL_LANGUAGE';
|
||||
} else {
|
||||
$function = 'MATCH_BOOLEAN';
|
||||
|
||||
$textFilter = str_replace('@', '*', $textFilter);
|
||||
}
|
||||
|
||||
$textFilter = str_replace('"*', '"', $textFilter);
|
||||
$textFilter = str_replace('*"', '"', $textFilter);
|
||||
|
||||
while (strpos($textFilter, '**')) {
|
||||
$textFilter = str_replace('**', '*', $textFilter);
|
||||
$textFilter = trim($textFilter);
|
||||
@@ -1812,4 +1972,119 @@ class Base
|
||||
{
|
||||
$this->filterFollowed($result);
|
||||
}
|
||||
|
||||
public function mergeSelectParams($selectParams1, $selectParams2)
|
||||
{
|
||||
if (!$selectParams2) {
|
||||
return $selectParams1;
|
||||
}
|
||||
if (!isset($selectParams1['whereClause'])) {
|
||||
$selectParams1['whereClause'] = [];
|
||||
}
|
||||
if (!empty($selectParams2['whereClause'])) {
|
||||
$selectParams1['whereClause'][] = $selectParams2['whereClause'];
|
||||
}
|
||||
|
||||
if (!isset($selectParams1['havingClause'])) {
|
||||
$selectParams1['havingClause'] = [];
|
||||
}
|
||||
if (!empty($selectParams2['havingClause'])) {
|
||||
$selectParams1['havingClause'][] = $selectParams2['havingClause'];
|
||||
}
|
||||
|
||||
if (!empty($selectParams2['leftJoins'])) {
|
||||
foreach ($selectParams2['leftJoins'] as $item) {
|
||||
$this->addLeftJoin($item, $selectParams1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($selectParams2['joins'])) {
|
||||
foreach ($selectParams2['joins'] as $item) {
|
||||
$this->addJoin($item, $selectParams1);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($selectParams2['select'])) {
|
||||
$selectParams1['select'] = $selectParams2['select'];
|
||||
}
|
||||
|
||||
if (isset($selectParams2['customJoin'])) {
|
||||
if (!isset($selectParams1['customJoin'])) {
|
||||
$selectParams1['customJoin'] = '';
|
||||
}
|
||||
$selectParams1['customJoin'] .= ' ' . $selectParams2['customJoin'];
|
||||
}
|
||||
|
||||
if (isset($selectParams2['customWhere'])) {
|
||||
if (!isset($selectParams1['customWhere'])) {
|
||||
$selectParams1['customWhere'] = '';
|
||||
}
|
||||
$selectParams1['customWhere'] .= ' ' . $selectParams2['customWhere'];
|
||||
}
|
||||
|
||||
if (isset($selectParams2['additionalSelectColumns'])) {
|
||||
if (!isset($selectParams1['additionalSelectColumns'])) {
|
||||
$selectParams1['additionalSelectColumns'] = [];
|
||||
}
|
||||
foreach ($selectParams2['additionalSelectColumns'] as $key => $item) {
|
||||
$selectParams1['additionalSelectColumns'][$key] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($selectParams2['joinConditions'])) {
|
||||
if (!isset($selectParams1['joinConditions'])) {
|
||||
$selectParams1['joinConditions'] = [];
|
||||
}
|
||||
foreach ($selectParams2['joinConditions'] as $key => $item) {
|
||||
$selectParams1['joinConditions'][$key] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($selectParams2['orderBy'])) {
|
||||
$selectParams1['orderBy'] = $selectParams2['orderBy'];
|
||||
}
|
||||
if (isset($selectParams2['order'])) {
|
||||
$selectParams1['order'] = $selectParams2['order'];
|
||||
}
|
||||
|
||||
if (!empty($selectParams2['distinct'])) {
|
||||
$selectParams1['distinct'] = true;
|
||||
}
|
||||
|
||||
return $selectParams1;
|
||||
}
|
||||
|
||||
protected function applyLeftJoinsFromWhere($where, &$result)
|
||||
{
|
||||
if (!is_array($where)) return;
|
||||
|
||||
foreach ($where as $item) {
|
||||
$this->applyLeftJoinsFromWhereItem($item, $result);
|
||||
}
|
||||
}
|
||||
|
||||
protected function applyLeftJoinsFromWhereItem($item, &$result)
|
||||
{
|
||||
if (!empty($item['type'])) {
|
||||
if (in_array($item['type'], ['or', 'and', 'not', 'having'])) {
|
||||
if (!array_key_exists('value', $item) || !is_array($item['value'])) return;
|
||||
foreach ($item['value'] as $listItem) {
|
||||
$this->applyLeftJoinsFromWhereItem($listItem, $result);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$attibute = null;
|
||||
if (!empty($item['attribute'])) $attibute = $item['attribute'];
|
||||
if (!$attibute) return;
|
||||
|
||||
$attributeType = $this->getSeed()->getAttributeType($attibute);
|
||||
if ($attributeType === 'foreign') {
|
||||
$relation = $this->getSeed()->getAttributeParam($attibute, 'relation');
|
||||
if ($relation) {
|
||||
$this->addLeftJoin($relation, $result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,9 +108,11 @@ class ServiceFactory
|
||||
foreach ($dependencies as $name) {
|
||||
$service->inject($name, $this->container->get($name));
|
||||
}
|
||||
if (method_exists($service, 'prepare')) {
|
||||
$service->prepare();
|
||||
}
|
||||
return $service;
|
||||
}
|
||||
throw new Error("Class '$className' does not exist.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ abstract class Base implements Injectable
|
||||
'config',
|
||||
'entityManager',
|
||||
'user',
|
||||
'serviceFactory'
|
||||
);
|
||||
|
||||
protected $injections = array();
|
||||
@@ -55,6 +56,10 @@ abstract class Base implements Injectable
|
||||
{
|
||||
}
|
||||
|
||||
public function prepare()
|
||||
{
|
||||
}
|
||||
|
||||
protected function getInjection($name)
|
||||
{
|
||||
return $this->injections[$name];
|
||||
@@ -91,5 +96,9 @@ abstract class Base implements Injectable
|
||||
{
|
||||
return $this->getInjection('user');
|
||||
}
|
||||
}
|
||||
|
||||
protected function getServiceFactory()
|
||||
{
|
||||
return $this->getInjection('serviceFactory');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
}
|
||||
},
|
||||
"collection": {
|
||||
"sortBy": "createdAt",
|
||||
"asc": false
|
||||
"orderBy": "createdAt",
|
||||
"order": "desc"
|
||||
},
|
||||
"indexes": {
|
||||
"name": {
|
||||
|
||||
@@ -75,8 +75,8 @@
|
||||
}
|
||||
},
|
||||
"collection": {
|
||||
"sortBy": "createdAt",
|
||||
"asc": false
|
||||
"orderBy": "createdAt",
|
||||
"order": "desc"
|
||||
},
|
||||
"indexes": {
|
||||
"name": {
|
||||
|
||||
@@ -72,8 +72,8 @@
|
||||
}
|
||||
},
|
||||
"collection": {
|
||||
"sortBy": "parent",
|
||||
"asc": true
|
||||
"orderBy": "parent",
|
||||
"order": "asc"
|
||||
},
|
||||
"indexes": {
|
||||
"name": {
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"type": "text"
|
||||
},
|
||||
"website": {
|
||||
"type": "url"
|
||||
"type": "url",
|
||||
"strip": true
|
||||
},
|
||||
"emailAddress": {
|
||||
"type": "email"
|
||||
@@ -135,8 +136,8 @@
|
||||
}
|
||||
},
|
||||
"collection": {
|
||||
"sortBy": "createdAt",
|
||||
"asc": false
|
||||
"orderBy": "createdAt",
|
||||
"order": "desc"
|
||||
},
|
||||
"indexes": {
|
||||
"name": {
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"type": "enum",
|
||||
"options": ["Planned", "Held", "Not Held"],
|
||||
"default": "Planned",
|
||||
"view": "views/fields/enum-styled",
|
||||
"style": {
|
||||
"Held": "success"
|
||||
},
|
||||
@@ -101,8 +100,8 @@
|
||||
}
|
||||
},
|
||||
"collection": {
|
||||
"sortBy": "dateStart",
|
||||
"asc": false
|
||||
"orderBy": "dateStart",
|
||||
"order": "desc"
|
||||
},
|
||||
"indexes": {
|
||||
"dateStartStatus": {
|
||||
|
||||
@@ -126,8 +126,8 @@
|
||||
}
|
||||
},
|
||||
"collection": {
|
||||
"sortBy": "createdAt",
|
||||
"asc": false
|
||||
"orderBy": "createdAt",
|
||||
"order": "desc"
|
||||
},
|
||||
"indexes": {
|
||||
"firstName": {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"links": {
|
||||
"meetings": "Meetings",
|
||||
"calls": "Anrufe",
|
||||
"tasks": "Aufgaben"
|
||||
},
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"website": "Webseite"
|
||||
},
|
||||
"links": {
|
||||
"meetings": "Meetings",
|
||||
"calls": "Anrufe",
|
||||
"tasks": "Aufgaben"
|
||||
},
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"dateStart": "Startdatum",
|
||||
"dateEnd": "Enddatum",
|
||||
"duration": "Dauer",
|
||||
"status": "Status",
|
||||
"reminders": "Erinnerungen"
|
||||
},
|
||||
"links": {
|
||||
@@ -20,7 +19,6 @@
|
||||
"labels": {
|
||||
"Create {entityType}": "{entityTypeTranslated} erstellen",
|
||||
"Schedule {entityType}": "{entityTypeTranslated} planen",
|
||||
"Log {entityType}": "Log {entityTypeTranslated}",
|
||||
"Set Held": "Auf durchgeführt setzen",
|
||||
"Set Not Held": "Auf nicht durchgeführt setzen"
|
||||
},
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"address": "Adresse"
|
||||
},
|
||||
"links": {
|
||||
"meetings": "Meetings",
|
||||
"calls": "Anrufe",
|
||||
"tasks": "Aufgaben"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"links": {
|
||||
"meetings": "Juntas",
|
||||
"meetings": "Presentaciones",
|
||||
"calls": "Llamadas",
|
||||
"tasks": "Tareas"
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"website": "Sitio Web"
|
||||
},
|
||||
"links": {
|
||||
"meetings": "Juntas",
|
||||
"meetings": "Presentaciones",
|
||||
"calls": "Llamadas",
|
||||
"tasks": "Tareas"
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"address": "Dirección"
|
||||
},
|
||||
"links": {
|
||||
"meetings": "Juntas",
|
||||
"meetings": "Presentaciones",
|
||||
"calls": "Llamadas",
|
||||
"tasks": "Tareas"
|
||||
},
|
||||
|
||||
5
application/Espo/Core/Templates/i18n/fa_IR/Base.json
Normal file
5
application/Espo/Core/Templates/i18n/fa_IR/Base.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"labels": {
|
||||
"Create {entityType}": "ایجاد {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
10
application/Espo/Core/Templates/i18n/fa_IR/BasePlus.json
Normal file
10
application/Espo/Core/Templates/i18n/fa_IR/BasePlus.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"links": {
|
||||
"meetings": "جلسات",
|
||||
"calls": "تماس ها",
|
||||
"tasks": "وظایف"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "ایجاد {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
15
application/Espo/Core/Templates/i18n/fa_IR/Company.json
Normal file
15
application/Espo/Core/Templates/i18n/fa_IR/Company.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"fields": {
|
||||
"billingAddress": "آدرس صورتحساب",
|
||||
"shippingAddress": "آدرس حمل و نقل",
|
||||
"website": "وب سایت"
|
||||
},
|
||||
"links": {
|
||||
"meetings": "جلسات",
|
||||
"calls": "تماس ها",
|
||||
"tasks": "وظایف"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "ایجاد {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
34
application/Espo/Core/Templates/i18n/fa_IR/Event.json
Normal file
34
application/Espo/Core/Templates/i18n/fa_IR/Event.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"fields": {
|
||||
"parent": "پدر",
|
||||
"dateStart": "تاریخ شروع",
|
||||
"dateEnd": "تاریخ پایان",
|
||||
"duration": "مدت زمان",
|
||||
"status": "وضعیت",
|
||||
"reminders": "یادآوری ها"
|
||||
},
|
||||
"links": {
|
||||
"parent": "پدر"
|
||||
},
|
||||
"options": {
|
||||
"status": {
|
||||
"Planned": "برنامه ریزی شده",
|
||||
"Held": "برگزار شد",
|
||||
"Not Held": "برگزار نشد"
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "ایجاد {entityTypeTranslated}",
|
||||
"Set Held": "تنظیم برگذاری",
|
||||
"Set Not Held": "تنظیم عدم برگذاری"
|
||||
},
|
||||
"massActions": {
|
||||
"setHeld": "تنظیم برگذاری",
|
||||
"setNotHeld": "تنظیم عدم برگذاری"
|
||||
},
|
||||
"presetFilters": {
|
||||
"planned": "برنامه ریزی شده",
|
||||
"held": "برگزار شد",
|
||||
"todays": "امروزه"
|
||||
}
|
||||
}
|
||||
13
application/Espo/Core/Templates/i18n/fa_IR/Person.json
Normal file
13
application/Espo/Core/Templates/i18n/fa_IR/Person.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"fields": {
|
||||
"address": "آدرس"
|
||||
},
|
||||
"links": {
|
||||
"meetings": "جلسات",
|
||||
"calls": "تماس ها",
|
||||
"tasks": "وظایف"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "ایجاد {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
5
application/Espo/Core/Templates/i18n/lv_LV/Base.json
Normal file
5
application/Espo/Core/Templates/i18n/lv_LV/Base.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"labels": {
|
||||
"Create {entityType}": "Izveidot {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
10
application/Espo/Core/Templates/i18n/lv_LV/BasePlus.json
Normal file
10
application/Espo/Core/Templates/i18n/lv_LV/BasePlus.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"links": {
|
||||
"meetings": "Tikšanās",
|
||||
"calls": "Zvani",
|
||||
"tasks": "Uzdevumi"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "Izveidot {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
15
application/Espo/Core/Templates/i18n/lv_LV/Company.json
Normal file
15
application/Espo/Core/Templates/i18n/lv_LV/Company.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"fields": {
|
||||
"billingAddress": "Norēķinu adrese",
|
||||
"shippingAddress": "Piegādes adrese",
|
||||
"website": "Vietne"
|
||||
},
|
||||
"links": {
|
||||
"meetings": "Tikšanās",
|
||||
"calls": "Zvani",
|
||||
"tasks": "Uzdevumi"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "Izveidot {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
36
application/Espo/Core/Templates/i18n/lv_LV/Event.json
Normal file
36
application/Espo/Core/Templates/i18n/lv_LV/Event.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"fields": {
|
||||
"parent": "Primārais elements",
|
||||
"dateStart": "Sākuma datums",
|
||||
"dateEnd": "Beigu datums",
|
||||
"duration": "Ilgums",
|
||||
"status": "Statuss",
|
||||
"reminders": "Atgādinājumi"
|
||||
},
|
||||
"links": {
|
||||
"parent": "Primārais elements"
|
||||
},
|
||||
"options": {
|
||||
"status": {
|
||||
"Planned": "Plānots",
|
||||
"Held": "Noticis",
|
||||
"Not Held": "Nenoticis"
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "Izveidot {entityTypeTranslated}",
|
||||
"Schedule {entityType}": "Ieplānot {entityTypeTranslated}",
|
||||
"Log {entityType}": "Reģistrēt {entityTypeTranslated}",
|
||||
"Set Held": "Iestatīt kā notikušu",
|
||||
"Set Not Held": "Iestatīt kā nenotikušu"
|
||||
},
|
||||
"massActions": {
|
||||
"setHeld": "Iestatīt kā notikušu",
|
||||
"setNotHeld": "Iestatīt kā nenotikušu"
|
||||
},
|
||||
"presetFilters": {
|
||||
"planned": "Ieplānots",
|
||||
"held": "Noticis",
|
||||
"todays": "Šodienas"
|
||||
}
|
||||
}
|
||||
13
application/Espo/Core/Templates/i18n/lv_LV/Person.json
Normal file
13
application/Espo/Core/Templates/i18n/lv_LV/Person.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"fields": {
|
||||
"address": "Adrese"
|
||||
},
|
||||
"links": {
|
||||
"meetings": "Tikšanās",
|
||||
"calls": "Zvani",
|
||||
"tasks": "Uzdevumi"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "Izveidot {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"links": {
|
||||
"meetings": "Meetings",
|
||||
"calls": "Calls",
|
||||
"tasks": "Tasks"
|
||||
"meetings": "Встречи",
|
||||
"calls": "Звонки",
|
||||
"tasks": "Задачи"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "Создать {entityTypeTranslated}"
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
"fields": {
|
||||
"billingAddress": "Платежный адрес",
|
||||
"shippingAddress": "Адрес доставки",
|
||||
"website": "Website"
|
||||
"website": "Сайт"
|
||||
},
|
||||
"links": {
|
||||
"meetings": "Meetings",
|
||||
"calls": "Calls",
|
||||
"tasks": "Tasks"
|
||||
"meetings": "Встречи",
|
||||
"calls": "Звонки",
|
||||
"tasks": "Задачи"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "Создать {entityTypeTranslated}"
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
{
|
||||
"fields": {
|
||||
"parent": "Parent",
|
||||
"dateStart": "Date Start",
|
||||
"dateEnd": "Date End",
|
||||
"duration": "Duration",
|
||||
"status": "Status",
|
||||
"reminders": "Reminders"
|
||||
"parent": "Источник",
|
||||
"dateStart": "Дата начала",
|
||||
"dateEnd": "Дата окончания",
|
||||
"duration": "Длительность",
|
||||
"status": "Статус",
|
||||
"reminders": "Напоминания"
|
||||
},
|
||||
"links": {
|
||||
"parent": "Parent"
|
||||
"parent": "Источник"
|
||||
},
|
||||
"options": {
|
||||
"status": {
|
||||
"Planned": "Planned",
|
||||
"Held": "Held",
|
||||
"Not Held": "Not Held"
|
||||
"Planned": "Запланировано",
|
||||
"Held": "Состоялось",
|
||||
"Not Held": "Не состоялся"
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "Создать {entityTypeTranslated}",
|
||||
"Schedule {entityType}": "Schedule {entityTypeTranslated}",
|
||||
"Log {entityType}": "Log {entityTypeTranslated}",
|
||||
"Set Held": "Set Held",
|
||||
"Set Not Held": "Set Not Held"
|
||||
"Schedule {entityType}": "Запланировать {entityTypeTranslated}",
|
||||
"Set Held": "Состоялось",
|
||||
"Set Not Held": "Не состоялось"
|
||||
},
|
||||
"massActions": {
|
||||
"setHeld": "Set Held",
|
||||
"setNotHeld": "Set Not Held"
|
||||
"setHeld": "Состоялось",
|
||||
"setNotHeld": "Не состоялось"
|
||||
},
|
||||
"presetFilters": {
|
||||
"planned": "Запланировано",
|
||||
"held": "Held",
|
||||
"held": "Состоялся",
|
||||
"todays": "Сегодняшние"
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,9 @@
|
||||
"address": "Адрес"
|
||||
},
|
||||
"links": {
|
||||
"meetings": "Meetings",
|
||||
"calls": "Calls",
|
||||
"tasks": "Tasks"
|
||||
"meetings": "Встречи",
|
||||
"calls": "Звонки",
|
||||
"tasks": "Задачи"
|
||||
},
|
||||
"labels": {
|
||||
"Create {entityType}": "Создать {entityTypeTranslated}"
|
||||
|
||||
@@ -164,7 +164,7 @@ abstract class Base
|
||||
throw new Error('Another installation process is currently running.');
|
||||
}
|
||||
|
||||
$this->processId = uniqid();
|
||||
$this->processId = Util::generateId();
|
||||
|
||||
return $this->processId;
|
||||
}
|
||||
@@ -583,6 +583,17 @@ abstract class Base
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getManifestParam($name, $default = null)
|
||||
{
|
||||
$manifest = $this->getManifest();
|
||||
|
||||
if (array_key_exists($name, $manifest)) {
|
||||
return $manifest[$name];
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unzip a package archieve
|
||||
*
|
||||
@@ -631,7 +642,13 @@ abstract class Base
|
||||
|
||||
protected function systemRebuild()
|
||||
{
|
||||
return $this->getContainer()->get('dataManager')->rebuild();
|
||||
try {
|
||||
return $this->getContainer()->get('dataManager')->rebuild();
|
||||
} catch (\Exception $e) {
|
||||
$GLOBALS['log']->error('Database rebuild failure, details: '.$e->getMessage().'.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -720,4 +737,4 @@ abstract class Base
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,14 +109,18 @@ class Install extends \Espo\Core\Upgrades\Actions\Base
|
||||
|
||||
$this->afterRunAction();
|
||||
|
||||
$this->clearCache();
|
||||
$this->finalize();
|
||||
|
||||
/* delete unziped files */
|
||||
$this->deletePackageFiles();
|
||||
|
||||
$this->finalize();
|
||||
if ($this->getManifestParam('skipBackup')) {
|
||||
$this->getFileManager()->removeInDir($this->getPath('backupPath'), true);
|
||||
}
|
||||
|
||||
$GLOBALS['log']->debug('Installation process ['.$processId.']: end run.');
|
||||
|
||||
$this->clearCache();
|
||||
}
|
||||
|
||||
protected function restoreFiles()
|
||||
|
||||
@@ -59,16 +59,15 @@ class Uninstall extends \Espo\Core\Upgrades\Actions\Base
|
||||
|
||||
$backupPath = $this->getPath('backupPath');
|
||||
if (file_exists($backupPath)) {
|
||||
|
||||
/* copy core files */
|
||||
if (!$this->copyFiles()) {
|
||||
$this->throwErrorAndRemovePackage('Cannot copy files.');
|
||||
}
|
||||
}
|
||||
|
||||
/* remove extension files, saved in fileList */
|
||||
if (!$this->deleteFiles('delete', true)) {
|
||||
$this->throwErrorAndRemovePackage('Permission denied to delete files.');
|
||||
}
|
||||
/* remove extension files, saved in fileList */
|
||||
if (!$this->deleteFiles('delete', true)) {
|
||||
$this->throwErrorAndRemovePackage('Permission denied to delete files.');
|
||||
}
|
||||
|
||||
if (!isset($data['skipSystemRebuild']) || !$data['skipSystemRebuild']) {
|
||||
@@ -84,14 +83,14 @@ class Uninstall extends \Espo\Core\Upgrades\Actions\Base
|
||||
|
||||
$this->afterRunAction();
|
||||
|
||||
$this->clearCache();
|
||||
|
||||
/* delete backup files */
|
||||
$this->deletePackageFiles();
|
||||
|
||||
$this->finalize();
|
||||
|
||||
$GLOBALS['log']->debug('Uninstallation process ['.$processId.']: end run.');
|
||||
|
||||
$this->clearCache();
|
||||
}
|
||||
|
||||
protected function restoreFiles()
|
||||
|
||||
@@ -48,32 +48,49 @@ class Auth extends \Slim\Middleware
|
||||
|
||||
function call()
|
||||
{
|
||||
$req = $this->app->request();
|
||||
$request = $this->app->request();
|
||||
|
||||
$uri = $req->getResourceUri();
|
||||
$httpMethod = $req->getMethod();
|
||||
$uri = $request->getResourceUri();
|
||||
$httpMethod = $request->getMethod();
|
||||
|
||||
$authUsername = $req->headers('PHP_AUTH_USER');
|
||||
$authPassword = $req->headers('PHP_AUTH_PW');
|
||||
$username = $request->headers('PHP_AUTH_USER');
|
||||
$password = $request->headers('PHP_AUTH_PW');
|
||||
|
||||
$espoAuth = $req->headers('HTTP_ESPO_AUTHORIZATION');
|
||||
if (isset($espoAuth)) {
|
||||
list($authUsername, $authPassword) = explode(':', base64_decode($espoAuth), 2);
|
||||
}
|
||||
$authenticationMethod = null;
|
||||
|
||||
if (!isset($authUsername)) {
|
||||
if (!empty($_COOKIE['auth-username']) && !empty($_COOKIE['auth-token'])) {
|
||||
$authUsername = $_COOKIE['auth-username'];
|
||||
$authPassword = $_COOKIE['auth-token'];
|
||||
$espoAuthorizationHeader = $request->headers('Http-Espo-Authorization');
|
||||
if (isset($espoAuthorizationHeader)) {
|
||||
list($username, $password) = explode(':', base64_decode($espoAuthorizationHeader), 2);
|
||||
} else {
|
||||
$hmacAuthorizationHeader = $request->headers('X-Hmac-Authorization');
|
||||
if ($hmacAuthorizationHeader) {
|
||||
$authenticationMethod = 'Hmac';
|
||||
list($username, $password) = explode(':', base64_decode($hmacAuthorizationHeader), 2);
|
||||
} else {
|
||||
$apiKeyHeader = $request->headers('X-Api-Key');
|
||||
if ($apiKeyHeader) {
|
||||
$authenticationMethod = 'ApiKey';
|
||||
$username = $apiKeyHeader;
|
||||
$password = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$espoCgiAuth = $req->headers('HTTP_ESPO_CGI_AUTH');
|
||||
if (empty($espoCgiAuth)) {
|
||||
$espoCgiAuth = $req->headers('REDIRECT_HTTP_ESPO_CGI_AUTH');
|
||||
if (!isset($username)) {
|
||||
if (!empty($_COOKIE['auth-username']) && !empty($_COOKIE['auth-token'])) {
|
||||
$username = $_COOKIE['auth-username'];
|
||||
$password = $_COOKIE['auth-token'];
|
||||
}
|
||||
}
|
||||
if (!isset($authUsername) && !isset($authPassword) && !empty($espoCgiAuth)) {
|
||||
list($authUsername, $authPassword) = explode(':' , base64_decode(substr($espoCgiAuth, 6)));
|
||||
|
||||
if (!isset($username) && !isset($password)) {
|
||||
$espoCgiAuth = $request->headers('Http-Espo-Cgi-Auth');
|
||||
if (empty($espoCgiAuth)) {
|
||||
$espoCgiAuth = $request->headers('Redirect-Http-Espo-Cgi-Auth');
|
||||
}
|
||||
if (!empty($espoCgiAuth)) {
|
||||
list($username, $password) = explode(':' , base64_decode(substr($espoCgiAuth, 6)));
|
||||
}
|
||||
}
|
||||
|
||||
if (is_null($this->authRequired)) {
|
||||
@@ -83,9 +100,9 @@ class Auth extends \Slim\Middleware
|
||||
$routeConditions = $routes[0]->getConditions();
|
||||
if (isset($routeConditions['auth']) && $routeConditions['auth'] === false) {
|
||||
|
||||
if ($authUsername && $authPassword) {
|
||||
if ($username && $password) {
|
||||
try {
|
||||
$isAuthenticated = $this->auth->login($authUsername, $authPassword);
|
||||
$isAuthenticated = $this->auth->login($username, $password);
|
||||
} catch (\Exception $e) {
|
||||
$this->processException($e);
|
||||
return;
|
||||
@@ -109,9 +126,9 @@ class Auth extends \Slim\Middleware
|
||||
}
|
||||
}
|
||||
|
||||
if ($authUsername && $authPassword) {
|
||||
if ($username) {
|
||||
try {
|
||||
$isAuthenticated = $this->auth->login($authUsername, $authPassword);
|
||||
$isAuthenticated = $this->auth->login($username, $password, $authenticationMethod);
|
||||
} catch (\Exception $e) {
|
||||
$this->processException($e);
|
||||
return;
|
||||
@@ -154,9 +171,8 @@ class Auth extends \Slim\Middleware
|
||||
{
|
||||
$request = $this->app->request();
|
||||
|
||||
$httpXRequestedWith = $request->headers('HTTP_X_REQUESTED_WITH');
|
||||
|
||||
if (isset($httpXRequestedWith) && strtolower($httpXRequestedWith) == 'xmlhttprequest') {
|
||||
$httpXRequestedWith = $request->headers('Http-X-Requested-With');
|
||||
if ($httpXRequestedWith && strtolower($httpXRequestedWith) == 'xmlhttprequest') {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
83
application/Espo/Core/Utils/ApiKey.php
Normal file
83
application/Espo/Core/Utils/ApiKey.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: http://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;
|
||||
|
||||
class ApiKey
|
||||
{
|
||||
private $config;
|
||||
|
||||
public function __construct(\Espo\Core\Utils\Config $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
protected function getConfig()
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
public static function hash($secretKey, $string = '')
|
||||
{
|
||||
return hash_hmac('sha256', $string, $secretKey, true);
|
||||
}
|
||||
|
||||
public function getSecretKeyForUserId($id)
|
||||
{
|
||||
$apiSecretKeys = $this->getConfig()->get('apiSecretKeys');
|
||||
if (!$apiSecretKeys) return;
|
||||
if (!is_object($apiSecretKeys)) return;
|
||||
if (!isset($apiSecretKeys->$id)) return;
|
||||
return $apiSecretKeys->$id;
|
||||
}
|
||||
|
||||
public function storeSecretKeyForUserId($id, $secretKey)
|
||||
{
|
||||
$apiSecretKeys = $this->getConfig()->get('apiSecretKeys');
|
||||
if (!is_object($apiSecretKeys)) {
|
||||
$apiSecretKeys = (object)[];
|
||||
}
|
||||
$apiSecretKeys->$id = $secretKey;
|
||||
|
||||
$this->getConfig()->set('apiSecretKeys', $apiSecretKeys);
|
||||
$this->getConfig()->save();
|
||||
}
|
||||
|
||||
public function removeSecretKeyForUserId($id)
|
||||
{
|
||||
$apiSecretKeys = $this->getConfig()->get('apiSecretKeys');
|
||||
if (!is_object($apiSecretKeys)) {
|
||||
$apiSecretKeys = (object)[];
|
||||
}
|
||||
unset($apiSecretKeys->$id);
|
||||
|
||||
$this->getConfig()->set('apiSecretKeys', $apiSecretKeys);
|
||||
$this->getConfig()->save();
|
||||
}
|
||||
}
|
||||
@@ -38,8 +38,6 @@ class Auth
|
||||
{
|
||||
protected $container;
|
||||
|
||||
protected $authentication;
|
||||
|
||||
protected $allowAnyAccess;
|
||||
|
||||
const ACCESS_CRM_ONLY = 0;
|
||||
@@ -60,10 +58,6 @@ class Auth
|
||||
|
||||
$this->allowAnyAccess = $allowAnyAccess;
|
||||
|
||||
$authenticationMethod = $this->getConfig()->get('authenticationMethod', 'Espo');
|
||||
$authenticationClassName = "\\Espo\\Core\\Utils\\Authentication\\" . $authenticationMethod;
|
||||
$this->authentication = new $authenticationClassName($this->getConfig(), $this->getEntityManager(), $this);
|
||||
|
||||
$this->request = $container->get('slim')->request();
|
||||
}
|
||||
|
||||
@@ -72,6 +66,25 @@ class Auth
|
||||
return $this->container;
|
||||
}
|
||||
|
||||
protected function getDefaultAuthenticationMethod()
|
||||
{
|
||||
return $this->getConfig()->get('authenticationMethod', 'Espo');
|
||||
}
|
||||
|
||||
protected function getAuthentication($authenticationMethod)
|
||||
{
|
||||
$authenticationMethod = preg_replace('/[^a-zA-Z0-9]+/', '', $authenticationMethod);
|
||||
|
||||
$authenticationClassName = "\\Espo\\Custom\\Core\\Utils\\Authentication\\" . $authenticationMethod;
|
||||
if (!class_exists($authenticationClassName)) {
|
||||
$authenticationClassName = "\\Espo\\Core\\Utils\\Authentication\\" . $authenticationMethod;
|
||||
}
|
||||
|
||||
$authentication = new $authenticationClassName($this->getConfig(), $this->getEntityManager(), $this);
|
||||
|
||||
return $authentication;
|
||||
}
|
||||
|
||||
protected function setPortal(Portal $portal)
|
||||
{
|
||||
$this->portal = $portal;
|
||||
@@ -119,23 +132,29 @@ class Auth
|
||||
$this->getContainer()->setUser($user);
|
||||
}
|
||||
|
||||
public function login($username, $password)
|
||||
public function login($username, $password = null, $authenticationMethod = null)
|
||||
{
|
||||
$isByTokenOnly = false;
|
||||
if ($this->request->headers->get('HTTP_ESPO_AUTHORIZATION_BY_TOKEN') === 'true') {
|
||||
$isByTokenOnly = true;
|
||||
|
||||
if (!$authenticationMethod) {
|
||||
if ($this->request->headers->get('Http-Espo-Authorization-By-Token') === 'true') {
|
||||
$isByTokenOnly = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isByTokenOnly) {
|
||||
$this->checkFailedAttemptsLimit($username);
|
||||
}
|
||||
|
||||
$authToken = $this->getEntityManager()->getRepository('AuthToken')->where([
|
||||
'token' => $password
|
||||
])->findOne();
|
||||
|
||||
$authToken = null;
|
||||
$authTokenIsFound = false;
|
||||
|
||||
if (!$authenticationMethod) {
|
||||
$authToken = $this->getEntityManager()->getRepository('AuthToken')->where([
|
||||
'token' => $password
|
||||
])->findOne();
|
||||
}
|
||||
|
||||
if ($authToken) {
|
||||
$authTokenIsFound = true;
|
||||
}
|
||||
@@ -168,38 +187,52 @@ class Auth
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $this->authentication->login($username, $password, $authToken);
|
||||
if (!$authenticationMethod) {
|
||||
$authenticationMethod = $this->getDefaultAuthenticationMethod();
|
||||
}
|
||||
|
||||
$authentication = $this->getAuthentication($authenticationMethod);
|
||||
|
||||
$params = [
|
||||
'isPortal' => $this->isPortal()
|
||||
];
|
||||
|
||||
$user = $authentication->login($username, $password, $authToken, $params, $this->request);
|
||||
|
||||
$authLogRecord = null;
|
||||
|
||||
if (!$authTokenIsFound) {
|
||||
$authLogRecord = $this->createAuthLogRecord($username, $user);
|
||||
$authLogRecord = $this->createAuthLogRecord($username, $user, $authenticationMethod);
|
||||
}
|
||||
|
||||
if (!$user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$user->isAdmin() && $this->getConfig()->get('maintenanceMode')) {
|
||||
throw new \Espo\Core\Exceptions\ServiceUnavailable("Application is in maintenance mode.");
|
||||
}
|
||||
|
||||
if (!$user->isActive()) {
|
||||
$GLOBALS['log']->info("AUTH: Trying to login as user '".$user->get('userName')."' which is not active.");
|
||||
$this->logDenied($authLogRecord, 'INACTIVE_USER');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$user->isAdmin() && !$this->isPortal() && $user->get('isPortalUser')) {
|
||||
if (!$user->isAdmin() && !$this->isPortal() && $user->isPortal()) {
|
||||
$GLOBALS['log']->info("AUTH: Trying to login to crm as a portal user '".$user->get('userName')."'.");
|
||||
$this->logDenied($authLogRecord, 'IS_PORTAL_USER');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$user->isAdmin() && $this->isPortal() && !$user->get('isPortalUser')) {
|
||||
if ($this->isPortal() && !$user->isPortal()) {
|
||||
$GLOBALS['log']->info("AUTH: Trying to login to portal as user '".$user->get('userName')."' which is not portal user.");
|
||||
$this->logDenied($authLogRecord, 'IS_NOT_PORTAL_USER');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->isPortal()) {
|
||||
if (!$user->isAdmin() && !$this->getEntityManager()->getRepository('Portal')->isRelated($this->getPortal(), 'users', $user)) {
|
||||
if (!$this->getEntityManager()->getRepository('Portal')->isRelated($this->getPortal(), 'users', $user)) {
|
||||
$GLOBALS['log']->info("AUTH: Trying to login to portal as user '".$user->get('userName')."' which is portal user but does not belongs to portal.");
|
||||
$this->logDenied($authLogRecord, 'USER_IS_NOT_IN_PORTAL');
|
||||
return;
|
||||
@@ -214,7 +247,7 @@ class Auth
|
||||
$this->getEntityManager()->setUser($user);
|
||||
$this->getContainer()->setUser($user);
|
||||
|
||||
if ($this->request->headers->get('HTTP_ESPO_AUTHORIZATION')) {
|
||||
if ($this->request->headers->get('Http-Espo-Authorization')) {
|
||||
if (!$authToken) {
|
||||
$authToken = $this->getEntityManager()->getEntity('AuthToken');
|
||||
$token = $this->generateToken();
|
||||
@@ -309,7 +342,7 @@ class Auth
|
||||
}
|
||||
}
|
||||
|
||||
protected function createAuthLogRecord($username, $user)
|
||||
protected function createAuthLogRecord($username, $user, $authenticationMethod = null)
|
||||
{
|
||||
if ($username === '**logout') return;
|
||||
|
||||
@@ -320,7 +353,8 @@ class Auth
|
||||
'ipAddress' => $_SERVER['REMOTE_ADDR'],
|
||||
'requestTime' => $_SERVER['REQUEST_TIME_FLOAT'],
|
||||
'requestMethod' => $this->request->getMethod(),
|
||||
'requestUrl' => $this->request->getUrl() . $this->request->getPath()
|
||||
'requestUrl' => $this->request->getUrl() . $this->request->getPath(),
|
||||
'authenticationMethod' => $authenticationMethod
|
||||
]);
|
||||
|
||||
if ($this->isPortal()) {
|
||||
|
||||
50
application/Espo/Core/Utils/Authentication/ApiKey.php
Normal file
50
application/Espo/Core/Utils/Authentication/ApiKey.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: http://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\Authentication;
|
||||
|
||||
use \Espo\Core\Exceptions\Error;
|
||||
|
||||
class ApiKey extends Base
|
||||
{
|
||||
public function login($username, $password, $authToken = null, $params = [], $request)
|
||||
{
|
||||
$apiKey = $username;
|
||||
|
||||
$user = $this->getEntityManager()->getRepository('User')->findOne([
|
||||
'whereClause' => [
|
||||
'type' => 'api',
|
||||
'apiKey' => $apiKey,
|
||||
'authMethod' => 'ApiKey'
|
||||
]
|
||||
]);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
@@ -33,22 +33,24 @@ use \Espo\Core\Exceptions\Error;
|
||||
|
||||
class Espo extends Base
|
||||
{
|
||||
public function login($username, $password, \Espo\Entities\AuthToken $authToken = null)
|
||||
public function login($username, $password, \Espo\Entities\AuthToken $authToken = null, $params = [], $request)
|
||||
{
|
||||
if (!$password) return;
|
||||
|
||||
if ($authToken) {
|
||||
$hash = $authToken->get('hash');
|
||||
} else {
|
||||
$hash = $this->getPasswordHash()->hash($password);
|
||||
}
|
||||
|
||||
$user = $this->getEntityManager()->getRepository('User')->findOne(array(
|
||||
'whereClause' => array(
|
||||
$user = $this->getEntityManager()->getRepository('User')->findOne([
|
||||
'whereClause' => [
|
||||
'userName' => $username,
|
||||
'password' => $hash
|
||||
)
|
||||
));
|
||||
'password' => $hash,
|
||||
'type!=' => ['api', 'system']
|
||||
]
|
||||
]);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
67
application/Espo/Core/Utils/Authentication/Hmac.php
Normal file
67
application/Espo/Core/Utils/Authentication/Hmac.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: http://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\Authentication;
|
||||
|
||||
use \Espo\Core\Exceptions\Error;
|
||||
|
||||
class Hmac extends Base
|
||||
{
|
||||
public function login($username, $password, $authToken = null, $params = [], $request)
|
||||
{
|
||||
$apiKey = $username;
|
||||
$hash = $password;
|
||||
|
||||
$user = $this->getEntityManager()->getRepository('User')->findOne([
|
||||
'whereClause' => [
|
||||
'type' => 'api',
|
||||
'apiKey' => $apiKey,
|
||||
'authMethod' => 'Hmac'
|
||||
]
|
||||
]);
|
||||
|
||||
if (!$user) return;
|
||||
|
||||
if ($user) {
|
||||
$apiKeyUtil = new \Espo\Core\Utils\ApiKey($this->getConfig());
|
||||
$secretKey = $apiKeyUtil->getSecretKeyForUserId($user->id);
|
||||
if (!$secretKey) return;
|
||||
|
||||
$string = $request->getMethod() . ' ' . $request->getResourceUri();
|
||||
|
||||
if ($hash === \Espo\Core\Utils\ApiKey::hash($secretKey, $string)) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ use Espo\Core\Utils\Config;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
use Espo\Core\Utils\Auth;
|
||||
|
||||
class LDAP extends Base
|
||||
class LDAP extends Espo
|
||||
{
|
||||
private $utils;
|
||||
|
||||
@@ -64,6 +64,16 @@ class LDAP extends Base
|
||||
'defaultTeamId' => 'userDefaultTeamId',
|
||||
);
|
||||
|
||||
/**
|
||||
* User field name => option name
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $portalUserFieldMap = array(
|
||||
'portalsIds' => 'portalUserPortalsIds',
|
||||
'portalRolesIds' => 'portalUserRolesIds',
|
||||
);
|
||||
|
||||
public function __construct(Config $config, EntityManager $entityManager, Auth $auth)
|
||||
{
|
||||
parent::__construct($config, $entityManager, $auth);
|
||||
@@ -100,12 +110,23 @@ class LDAP extends Base
|
||||
*
|
||||
* @return \Espo\Entities\User | null
|
||||
*/
|
||||
public function login($username, $password, \Espo\Entities\AuthToken $authToken = null)
|
||||
public function login($username, $password, \Espo\Entities\AuthToken $authToken = null, $params = [], $request)
|
||||
{
|
||||
if (!$password) return;
|
||||
|
||||
$isPortal = !empty($params['isPortal']);
|
||||
|
||||
if ($authToken) {
|
||||
return $this->loginByToken($username, $authToken);
|
||||
}
|
||||
|
||||
if ($isPortal) {
|
||||
$useLdapAuthForPortalUser = $this->getUtils()->getOption('portalUserLdapAuth');
|
||||
if (!$useLdapAuthForPortalUser) {
|
||||
return parent::login($username, $password, $authToken, $params, $request);
|
||||
}
|
||||
}
|
||||
|
||||
$ldapClient = $this->getLdapClient();
|
||||
|
||||
/* Login LDAP system user (ldapUsername, ldapPassword) */
|
||||
@@ -151,16 +172,16 @@ class LDAP extends Base
|
||||
}
|
||||
}
|
||||
|
||||
$user = $this->getEntityManager()->getRepository('User')->findOne(array(
|
||||
'whereClause' => array(
|
||||
$user = $this->getEntityManager()->getRepository('User')->findOne([
|
||||
'whereClause' => [
|
||||
'userName' => $username,
|
||||
),
|
||||
));
|
||||
'type!=' => ['api', 'system']
|
||||
]
|
||||
]);
|
||||
|
||||
$isCreateUser = $this->getUtils()->getOption('createEspoUser');
|
||||
if (!isset($user) && $isCreateUser) {
|
||||
if (!isset($user) && $this->getUtils()->getOption('createEspoUser')) {
|
||||
$userData = $ldapClient->getEntry($userDn);
|
||||
$user = $this->createUser($userData);
|
||||
$user = $this->createUser($userData, $isPortal);
|
||||
}
|
||||
|
||||
return $user;
|
||||
@@ -184,7 +205,7 @@ class LDAP extends Base
|
||||
$user = $this->getEntityManager()->getEntity('User', $userId);
|
||||
|
||||
$tokenUsername = $user->get('userName');
|
||||
if ($username != $tokenUsername) {
|
||||
if (strtolower($username) != strtolower($tokenUsername)) {
|
||||
$GLOBALS['log']->alert('Unauthorized access attempt for user ['.$username.'] from IP ['.$_SERVER['REMOTE_ADDR'].']');
|
||||
return null;
|
||||
}
|
||||
@@ -209,13 +230,13 @@ class LDAP extends Base
|
||||
{
|
||||
$hash = $this->getPasswordHash()->hash($password);
|
||||
|
||||
$user = $this->getEntityManager()->getRepository('User')->findOne(array(
|
||||
'whereClause' => array(
|
||||
$user = $this->getEntityManager()->getRepository('User')->findOne([
|
||||
'whereClause' => [
|
||||
'userName' => $username,
|
||||
'password' => $hash,
|
||||
'isAdmin' => 1
|
||||
)
|
||||
));
|
||||
'type' => ['admin', 'super-admin']
|
||||
]
|
||||
]);
|
||||
|
||||
return $user;
|
||||
}
|
||||
@@ -224,10 +245,11 @@ class LDAP extends Base
|
||||
* Create Espo user with data gets from LDAP server
|
||||
*
|
||||
* @param array $userData LDAP entity data
|
||||
* @param boolean $isPortal Is portal user
|
||||
*
|
||||
* @return \Espo\Entities\User
|
||||
*/
|
||||
protected function createUser(array $userData)
|
||||
protected function createUser(array $userData, $isPortal = false)
|
||||
{
|
||||
$GLOBALS['log']->info('Creating new user ...');
|
||||
$data = array();
|
||||
@@ -246,7 +268,13 @@ class LDAP extends Base
|
||||
}
|
||||
|
||||
//set user fields
|
||||
$userFields = $this->loadFields('user');
|
||||
if ($isPortal) {
|
||||
$userFields = $this->loadFields('portalUser');
|
||||
$userFields['type'] = 'portal';
|
||||
} else {
|
||||
$userFields = $this->loadFields('user');
|
||||
}
|
||||
|
||||
foreach ($userFields as $fieldName => $fieldValue) {
|
||||
$data[$fieldName] = $fieldValue;
|
||||
}
|
||||
@@ -328,4 +356,4 @@ class LDAP extends Base
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,9 @@ class Utils
|
||||
'userTeamsIds' => 'ldapUserTeamsIds',
|
||||
'userDefaultTeamId' => 'ldapUserDefaultTeamId',
|
||||
'userObjectClass' => 'ldapUserObjectClass',
|
||||
'portalUserLdapAuth' => 'ldapPortalUserLdapAuth',
|
||||
'portalUserPortalsIds' => 'ldapPortalUserPortalsIds',
|
||||
'portalUserRolesIds' => 'ldapPortalUserRolesIds',
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -86,6 +89,9 @@ class Utils
|
||||
'userLoginFilter',
|
||||
'userTeamsIds',
|
||||
'userDefaultTeamId',
|
||||
'portalUserLdapAuth',
|
||||
'portalUserPortalsIds',
|
||||
'portalUserRolesIds',
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -163,7 +169,7 @@ class Utils
|
||||
*/
|
||||
public function getOption($name, $returns = null)
|
||||
{
|
||||
if (isset($this->options)) {
|
||||
if (!isset($this->options)) {
|
||||
$this->getOptions();
|
||||
}
|
||||
|
||||
@@ -187,4 +193,4 @@ class Utils
|
||||
return $zendOptions;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,12 +31,6 @@ namespace Espo\Core\Utils;
|
||||
|
||||
class Config
|
||||
{
|
||||
/**
|
||||
* Path of default config file
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $defaultConfigPath = 'application/Espo/Core/defaults/config.php';
|
||||
|
||||
private $systemConfigPath = 'application/Espo/Core/defaults/systemConfig.php';
|
||||
@@ -45,13 +39,7 @@ class Config
|
||||
|
||||
private $cacheTimestamp = 'cacheTimestamp';
|
||||
|
||||
/**
|
||||
* Array of admin items
|
||||
*
|
||||
* @access protected
|
||||
* @var array
|
||||
*/
|
||||
protected $adminItems = array();
|
||||
protected $adminItems = [];
|
||||
|
||||
protected $associativeArrayAttributeList = [
|
||||
'currencyRates',
|
||||
@@ -61,21 +49,16 @@ class Config
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Contains content of config
|
||||
*
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
private $data;
|
||||
|
||||
private $changedData = array();
|
||||
private $removeData = array();
|
||||
private $changedData = [];
|
||||
|
||||
private $removeData = [];
|
||||
|
||||
private $fileManager;
|
||||
|
||||
|
||||
public function __construct(\Espo\Core\Utils\File\Manager $fileManager) //TODO
|
||||
public function __construct(\Espo\Core\Utils\File\Manager $fileManager)
|
||||
{
|
||||
$this->fileManager = $fileManager;
|
||||
}
|
||||
@@ -228,11 +211,6 @@ class Config
|
||||
return $this->getFileManager()->getPhpContents($this->defaultConfigPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an Object of all configs
|
||||
* @param boolean $reload
|
||||
* @return array()
|
||||
*/
|
||||
protected function loadConfig($reload = false)
|
||||
{
|
||||
if (!$reload && isset($this->data) && !empty($this->data)) {
|
||||
@@ -249,50 +227,25 @@ class Config
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function getAllData()
|
||||
{
|
||||
return (object) $this->loadConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get config acording to restrictions for a user
|
||||
*
|
||||
* @param $isAdmin
|
||||
* @return array
|
||||
*/
|
||||
public function getData($isAdmin = null)
|
||||
{
|
||||
$data = $this->loadConfig();
|
||||
|
||||
$restrictedConfig = $data;
|
||||
foreach($this->getRestrictItems($isAdmin) as $name) {
|
||||
if (isset($restrictedConfig[$name])) {
|
||||
unset($restrictedConfig[$name]);
|
||||
}
|
||||
}
|
||||
|
||||
return $restrictedConfig;
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set JSON data acording to restrictions for a user
|
||||
*
|
||||
* @param $isAdmin
|
||||
* @return bool
|
||||
*/
|
||||
public function setData($data, $isAdmin = null)
|
||||
public function setData($data)
|
||||
{
|
||||
$restrictItems = $this->getRestrictItems($isAdmin);
|
||||
|
||||
if (is_object($data)) {
|
||||
$data = get_object_vars($data);
|
||||
}
|
||||
|
||||
$values = array();
|
||||
foreach ($data as $key => $item) {
|
||||
if (!in_array($key, $restrictItems)) {
|
||||
$values[$key] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->set($values);
|
||||
return $this->set($data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -303,9 +256,9 @@ class Config
|
||||
*/
|
||||
public function updateCacheTimestamp($onlyValue = false)
|
||||
{
|
||||
$timestamp = array(
|
||||
$this->cacheTimestamp => time(),
|
||||
);
|
||||
$timestamp = [
|
||||
$this->cacheTimestamp => time()
|
||||
];
|
||||
|
||||
if ($onlyValue) {
|
||||
return $timestamp;
|
||||
@@ -314,45 +267,24 @@ class Config
|
||||
return $this->set($timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get admin items
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
protected function getRestrictItems($onlySystemItems = null)
|
||||
public function getAdminOnlyItemList()
|
||||
{
|
||||
$data = $this->loadConfig();
|
||||
|
||||
if ($onlySystemItems) {
|
||||
return $data['systemItems'];
|
||||
}
|
||||
|
||||
if (empty($this->adminItems)) {
|
||||
$this->adminItems = array_merge($data['systemItems'], $data['adminItems']);
|
||||
}
|
||||
|
||||
if ($onlySystemItems === false) {
|
||||
return $this->adminItems;
|
||||
}
|
||||
|
||||
return array_merge($this->adminItems, $data['userItems']);
|
||||
return $this->get('adminItems', []);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if an item is allowed to get and save
|
||||
*
|
||||
* @param $name
|
||||
* @param $isAdmin
|
||||
* @return bool
|
||||
*/
|
||||
protected function isAllowed($name, $isAdmin = false)
|
||||
public function getSuperAdminOnlyItemList()
|
||||
{
|
||||
if (in_array($name, $this->getRestrictItems($isAdmin))) {
|
||||
return false;
|
||||
}
|
||||
return $this->get('superAdminItems', []);
|
||||
}
|
||||
|
||||
return true;
|
||||
public function getSystemOnlyItemList()
|
||||
{
|
||||
return $this->get('systemItems', []);
|
||||
}
|
||||
|
||||
public function getUserOnlyItemList()
|
||||
{
|
||||
return $this->get('userItems', []);
|
||||
}
|
||||
|
||||
public function getSiteUrl()
|
||||
|
||||
72
application/Espo/Core/Utils/ControllerUtil.php
Normal file
72
application/Espo/Core/Utils/ControllerUtil.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: http://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;
|
||||
|
||||
class ControllerUtil
|
||||
{
|
||||
public static function fetchListParamsFromRequest(&$params, $request, $data)
|
||||
{
|
||||
$params['where'] = $request->get('where');
|
||||
$params['maxSize'] = $request->get('maxSize');
|
||||
$params['offset'] = $request->get('offset');
|
||||
|
||||
if ($request->get('orderBy')) {
|
||||
$params['orderBy'] = $request->get('orderBy');
|
||||
} else if ($request->get('sortBy')) {
|
||||
$params['orderBy'] = $request->get('sortBy');
|
||||
}
|
||||
|
||||
if ($request->get('order')) {
|
||||
$params['order'] = $request->get('order');
|
||||
} else if ($request->get('asc')) {
|
||||
$params['order'] = $request->get('asc') === 'true' ? 'asc' : 'desc';
|
||||
}
|
||||
|
||||
if ($request->get('q')) {
|
||||
$params['q'] = trim($request->get('q'));
|
||||
}
|
||||
if ($request->get('textFilter')) {
|
||||
$params['textFilter'] = $request->get('textFilter');
|
||||
}
|
||||
if ($request->get('primaryFilter')) {
|
||||
$params['primaryFilter'] = $request->get('primaryFilter');
|
||||
}
|
||||
if ($request->get('boolFilterList')) {
|
||||
$params['boolFilterList'] = $request->get('boolFilterList');
|
||||
}
|
||||
if ($request->get('filterList')) {
|
||||
$params['filterList'] = $request->get('filterList');
|
||||
}
|
||||
|
||||
if ($request->get('select')) {
|
||||
$params['select'] = explode(',', $request->get('select'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,10 @@ class Job
|
||||
|
||||
private $cronScheduledJob;
|
||||
|
||||
const NOT_EXISTING_PROCESS_PERIOD = 300;
|
||||
|
||||
const READY_NOT_STARTED_PERIOD = 60;
|
||||
|
||||
public function __construct(Config $config, EntityManager $entityManager)
|
||||
{
|
||||
$this->config = $config;
|
||||
@@ -74,10 +78,8 @@ class Job
|
||||
])->findOne();
|
||||
}
|
||||
|
||||
public function getPendingJobList()
|
||||
public function getPendingJobList($queue = null, $limit = 0)
|
||||
{
|
||||
$limit = intval($this->getConfig()->get('jobMaxPortion', 0));
|
||||
|
||||
$selectParams = [
|
||||
'select' => [
|
||||
'id',
|
||||
@@ -87,15 +89,15 @@ class Job
|
||||
'targetId',
|
||||
'targetType',
|
||||
'methodName',
|
||||
'method', // TODO remove deprecated
|
||||
'serviceName',
|
||||
'data'
|
||||
],
|
||||
'whereClause' => [
|
||||
'status' => CronManager::PENDING,
|
||||
'executeTime<=' => date('Y-m-d H:i:s')
|
||||
'executeTime<=' => date('Y-m-d H:i:s'),
|
||||
'queue' => $queue
|
||||
],
|
||||
'orderBy' => 'executeTime'
|
||||
'orderBy' => 'number'
|
||||
];
|
||||
if ($limit) {
|
||||
$selectParams['offset'] = 0;
|
||||
@@ -109,7 +111,7 @@ class Job
|
||||
{
|
||||
$where = [
|
||||
'scheduledJobId' => $scheduledJobId,
|
||||
'status' => CronManager::RUNNING
|
||||
'status' => [CronManager::RUNNING, CronManager::READY]
|
||||
];
|
||||
if ($targetId && $targetType) {
|
||||
$where['targetId'] = $targetId;
|
||||
@@ -127,7 +129,7 @@ class Job
|
||||
$query = "
|
||||
SELECT scheduled_job_id FROM job
|
||||
WHERE
|
||||
`status` = 'Running' AND
|
||||
(`status` = 'Running' OR `status` = 'Ready') AND
|
||||
scheduled_job_id IS NOT NULL AND
|
||||
target_id IS NULL AND
|
||||
deleted = 0
|
||||
@@ -145,14 +147,13 @@ class Job
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Jobs by ScheduledJobId and date
|
||||
*
|
||||
* @param string $scheduledJobId
|
||||
* @param string $time
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getJobByScheduledJob($scheduledJobId, $time)
|
||||
public function getJobByScheduledJobIdOnMinute($scheduledJobId, $time)
|
||||
{
|
||||
$dateObj = new \DateTime($time);
|
||||
$timeWithoutSeconds = $dateObj->format('Y-m-d H:i:');
|
||||
@@ -176,69 +177,141 @@ class Job
|
||||
return $scheduledJob;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark pending jobs (all jobs that exceeded jobPeriod)
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function markFailedJobs()
|
||||
public function getPendingCountByScheduledJobId($scheduledJobId)
|
||||
{
|
||||
$this->markFailedJobsByPeriod('jobPeriodForActiveProcess');
|
||||
$this->markFailedJobsByPeriod('jobPeriod');
|
||||
$countPending = $this->getEntityManager()->getRepository('Job')->where([
|
||||
'scheduledJobId' => $scheduledJobId,
|
||||
'status' => CronManager::PENDING
|
||||
])->count();
|
||||
|
||||
return $countPending;
|
||||
}
|
||||
|
||||
protected function markFailedJobsByPeriod($period)
|
||||
public function markJobsFailed()
|
||||
{
|
||||
$time = time() - $this->getConfig()->get($period);
|
||||
$this->markJobsFailedByNotExistingProcesses();
|
||||
$this->markJobsFailedReadyNotStarted();
|
||||
$this->markJobsFailedByPeriod(true);
|
||||
$this->markJobsFailedByPeriod();
|
||||
}
|
||||
|
||||
$pdo = $this->getEntityManager()->getPDO();
|
||||
protected function markJobsFailedByNotExistingProcesses()
|
||||
{
|
||||
$timeThreshold = time() - $this->getConfig()->get('jobPeriodForNotExistingProcess', self::NOT_EXISTING_PROCESS_PERIOD);
|
||||
$dateTimeThreshold = date('Y-m-d H:i:s', $timeThreshold);
|
||||
|
||||
$select = "
|
||||
SELECT id, scheduled_job_id, execute_time, target_id, target_type, pid FROM `job`
|
||||
WHERE
|
||||
`status` = '" . CronManager::RUNNING ."' AND execute_time < '".date('Y-m-d H:i:s', $time)."'
|
||||
";
|
||||
$sth = $pdo->prepare($select);
|
||||
$sth->execute();
|
||||
$runningJobList = $this->getEntityManager()->getRepository('Job')->select([
|
||||
'id',
|
||||
'scheduledJobId',
|
||||
'executeTime',
|
||||
'targetId',
|
||||
'targetType',
|
||||
'pid',
|
||||
'startedAt'
|
||||
])->where([
|
||||
'status' => CronManager::RUNNING,
|
||||
'startedAt<' => $dateTimeThreshold
|
||||
])->find();
|
||||
|
||||
$jobData = array();
|
||||
|
||||
switch ($period) {
|
||||
case 'jobPeriod':
|
||||
while ($row = $sth->fetch(PDO::FETCH_ASSOC)) {
|
||||
if (empty($row['pid']) || !System::isProcessActive($row['pid'])) {
|
||||
$jobData[$row['id']] = $row;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jobPeriodForActiveProcess':
|
||||
while ($row = $sth->fetch(PDO::FETCH_ASSOC)) {
|
||||
$jobData[$row['id']] = $row;
|
||||
}
|
||||
break;
|
||||
$failedJobList = [];
|
||||
foreach ($runningJobList as $job) {
|
||||
if ($job->get('pid') && !System::isProcessActive($job->get('pid'))) {
|
||||
$failedJobList[] = $job;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($jobData)) {
|
||||
$jobQuotedIdList = [];
|
||||
foreach ($jobData as $jobId => $job) {
|
||||
$jobQuotedIdList[] = $pdo->quote($jobId);
|
||||
}
|
||||
$this->markJobListFailed($failedJobList);
|
||||
}
|
||||
|
||||
$update = "
|
||||
UPDATE job
|
||||
SET `status` = '" . CronManager::FAILED . "', attempts = 0
|
||||
WHERE id IN (".implode(", ", $jobQuotedIdList).")
|
||||
";
|
||||
protected function markJobsFailedReadyNotStarted()
|
||||
{
|
||||
$timeThreshold = time() - $this->getConfig()->get('jobPeriodForReadyNotStarted', SELF::READY_NOT_STARTED_PERIOD);
|
||||
$dateTimeThreshold = date('Y-m-d H:i:s', $timeThreshold);
|
||||
|
||||
$sth = $pdo->prepare($update);
|
||||
$sth->execute();
|
||||
$failedJobList = $this->getEntityManager()->getRepository('Job')->select([
|
||||
'id',
|
||||
'scheduledJobId',
|
||||
'executeTime',
|
||||
'targetId',
|
||||
'targetType',
|
||||
'pid',
|
||||
'startedAt'
|
||||
])->where([
|
||||
'status' => CronManager::READY,
|
||||
'startedAt<' => $dateTimeThreshold
|
||||
])->find();
|
||||
|
||||
$cronScheduledJob = $this->getCronScheduledJob();
|
||||
foreach ($jobData as $jobId => $job) {
|
||||
if (!empty($job['scheduled_job_id'])) {
|
||||
$cronScheduledJob->addLogRecord($job['scheduled_job_id'], CronManager::FAILED, $job['execute_time'], $job['target_id'], $job['target_type']);
|
||||
$this->markJobListFailed($failedJobList);
|
||||
}
|
||||
|
||||
protected function markJobsFailedByPeriod($isForActiveProcesses = false)
|
||||
{
|
||||
$period = 'jobPeriod';
|
||||
if ($isForActiveProcesses) {
|
||||
$period = 'jobPeriodForActiveProcess';
|
||||
}
|
||||
|
||||
$timeThreshold = time() - $this->getConfig()->get($period, 7800);
|
||||
$dateTimeThreshold = date('Y-m-d H:i:s', $timeThreshold);
|
||||
|
||||
$runningJobList = $this->getEntityManager()->getRepository('Job')->select([
|
||||
'id',
|
||||
'scheduledJobId',
|
||||
'executeTime',
|
||||
'targetId',
|
||||
'targetType',
|
||||
'pid',
|
||||
'startedAt'
|
||||
])->where([
|
||||
'status' => CronManager::RUNNING,
|
||||
'executeTime<' => $dateTimeThreshold
|
||||
])->find();
|
||||
|
||||
$failedJobList = [];
|
||||
foreach ($runningJobList as $job) {
|
||||
if (!$isForActiveProcesses) {
|
||||
if (!$job->get('pid') || !System::isProcessActive($job->get('pid'))) {
|
||||
$failedJobList[] = $job;
|
||||
}
|
||||
} else {
|
||||
$failedJobList[] = $job;
|
||||
}
|
||||
}
|
||||
|
||||
$this->markJobListFailed($failedJobList);
|
||||
}
|
||||
|
||||
protected function markJobListFailed($jobList)
|
||||
{
|
||||
if (!count($jobList)) return;
|
||||
|
||||
$jobIdList = [];
|
||||
foreach ($jobList as $job) {
|
||||
$jobIdList[] = $job->id;
|
||||
}
|
||||
|
||||
$quotedIdList = [];
|
||||
foreach ($jobIdList as $id) {
|
||||
$quotedIdList[] = $this->getEntityManager()->getPDO()->quote($id);
|
||||
}
|
||||
|
||||
$sql = "
|
||||
UPDATE job
|
||||
SET `status` = '" . CronManager::FAILED . "', attempts = 0
|
||||
WHERE id IN (".implode(", ", $quotedIdList).")
|
||||
";
|
||||
|
||||
$this->getEntityManager()->getPDO()->query($sql);
|
||||
|
||||
foreach ($jobList as $job) {
|
||||
if ($job->get('scheduledJobId')) {
|
||||
$this->getCronScheduledJob()->addLogRecord(
|
||||
$job->get('scheduledJobId'),
|
||||
CronManager::FAILED,
|
||||
$job->get('startedAt'),
|
||||
$job->get('targetId'),
|
||||
$job->get('targetType')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -273,7 +346,6 @@ class Job
|
||||
foreach ($duplicateJobList as $row) {
|
||||
if (!empty($row['scheduled_job_id'])) {
|
||||
|
||||
/* no possibility to use limit in update or subqueries */
|
||||
$query = "
|
||||
SELECT id FROM `job`
|
||||
WHERE
|
||||
@@ -349,9 +421,4 @@ class Job
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getPid()
|
||||
{
|
||||
return System::getPid();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
50
application/Espo/Core/Utils/Cron/JobTask.php
Normal file
50
application/Espo/Core/Utils/Cron/JobTask.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
|
||||
* Website: http://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\Cron;
|
||||
|
||||
class JobTask extends \Spatie\Async\Task
|
||||
{
|
||||
private $jobId;
|
||||
|
||||
public function __construct($jobId)
|
||||
{
|
||||
$this->jobId = $jobId;
|
||||
}
|
||||
|
||||
public function configure()
|
||||
{
|
||||
}
|
||||
|
||||
public function run()
|
||||
{
|
||||
$app = new \Espo\Core\Application();
|
||||
$app->runJob($this->jobId);
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,6 @@ class Crypt
|
||||
|
||||
public function generateKey()
|
||||
{
|
||||
return md5(uniqid());
|
||||
return \Espo\Core\Utils\Util::generateKey();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,16 +25,19 @@
|
||||
*
|
||||
* 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\DBAL\Driver\Mysqli;
|
||||
|
||||
class Driver extends \Doctrine\DBAL\Driver\Mysqli\Driver
|
||||
class Driver extends \Doctrine\DBAL\Driver\Mysqli\Driver
|
||||
{
|
||||
|
||||
public function getDatabasePlatform()
|
||||
{
|
||||
return new \Espo\Core\Utils\Database\DBAL\Platforms\MySqlPlatform();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function getSchemaManager(\Doctrine\DBAL\Connection $conn)
|
||||
{
|
||||
return new \Espo\Core\Utils\Database\DBAL\Schema\MySqlSchemaManager($conn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
namespace Espo\Core\Utils\Database\DBAL\FieldTypes;
|
||||
|
||||
class JsonArrayType extends \Doctrine\DBAL\Types\JsonArrayType
|
||||
class JsonArrayType extends \Doctrine\DBAL\Types\TextType
|
||||
{
|
||||
const JSON_ARRAY = 'jsonArray';
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
namespace Espo\Core\Utils\Database\DBAL\FieldTypes;
|
||||
|
||||
class JsonObjectType extends \Doctrine\DBAL\Types\ObjectType
|
||||
class JsonObjectType extends \Doctrine\DBAL\Types\TextType
|
||||
{
|
||||
const JSON_OBJECT = 'jsonObject';
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user